diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt index 43f86e72..9b6d1d60 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt @@ -10,8 +10,10 @@ import java.nio.file.StandardCopyOption class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) { suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) { withContext(Dispatchers.IO) { - patches?.let { - Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + patches?.let { inputStream -> + patchBundleOutputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } } integrations?.let { Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index f8b8e74c..dedbbf5d 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOf import java.io.File +import java.io.OutputStream /** * A [PatchBundle] source. @@ -23,6 +24,16 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) */ fun hasInstalled() = patchesFile.exists() + protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) { + // Android 14+ requires dex containers to be readonly. + try { + setWritable(true, true) + outputStream() + } finally { + setReadOnly() + } + } + private fun load(): State { if (!hasInstalled()) return State.Missing @@ -40,7 +51,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) sealed interface State { fun patchBundleOrNull(): PatchBundle? = null - object Missing : State + data object Missing : State data class Failed(val throwable: Throwable) : State data class Loaded(val bundle: PatchBundle) : State { override fun patchBundleOrNull() = bundle diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt index 93c945a8..240347af 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -33,16 +33,19 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) { val (patches, integrations) = info coroutineScope { - mapOf( - patches.url to patchesFile, - integrations.url to integrationsFile - ).forEach { (asset, file) -> - launch { - http.download(file) { - url(asset) + launch { + patchBundleOutputStream().use { + http.streamTo(it) { + url(patches.url) } } } + + launch { + http.download(integrationsFile) { + url(integrations.url) + } + } } saveVersion(patches.version, integrations.version) diff --git a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt index 3781c3b4..e0b69aa6 100644 --- a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt +++ b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt @@ -18,8 +18,11 @@ import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.core.isNotEmpty import io.ktor.utils.io.core.readBytes import it.skrape.core.htmlDocument +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File +import java.io.OutputStream /** * @author Aliucord Authors, DiamondMiner88 @@ -49,7 +52,10 @@ class HttpService( null } - Log.e(tag, "Failed to fetch: API error, http status: ${response.status}, body: $body") + Log.e( + tag, + "Failed to fetch: API error, http status: ${response.status}, body: $body" + ) APIResponse.Error(APIError(response.status, body)) } } catch (t: Throwable) { @@ -59,20 +65,19 @@ class HttpService( return response } - suspend fun download( - saveLocation: File, + suspend fun streamTo( + outputStream: OutputStream, builder: HttpRequestBuilder.() -> Unit ) { http.prepareGet(builder).execute { httpResponse -> if (httpResponse.status.isSuccess()) { - - saveLocation.outputStream().use { stream -> - val channel: ByteReadChannel = httpResponse.body() + val channel: ByteReadChannel = httpResponse.body() + withContext(Dispatchers.IO) { while (!channel.isClosedForRead) { val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) while (packet.isNotEmpty) { val bytes = packet.readBytes() - stream.write(bytes) + outputStream.write(bytes) } } } @@ -83,6 +88,11 @@ class HttpService( } } + suspend fun download( + saveLocation: File, + builder: HttpRequestBuilder.() -> Unit + ) = saveLocation.outputStream().use { streamTo(it, builder) } + suspend fun getHtml(builder: HttpRequestBuilder.() -> Unit) = htmlDocument( html = http.get(builder).bodyAsText() )