From 98c0a2e22d8727ff6e452079ba152ebf94f6bf1c Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:33:07 -0700 Subject: [PATCH] feat: Ktor download manager Using the system DownloadManager on some emulators causes a very odd SSL bug linked to compression, which is impossible to disable on Android's DownloadManager requests due to the Accept-Encoding header always being overwritten. This commit creates a generic download manager interface for which there is an Android DownloadManager wrapper (like before) in addition to a Ktor wrapper (which disables compression) --- .../aliucord/manager/ManagerApplication.kt | 10 +- .../manager/di/DownloadManagerProvider.kt | 40 +++++++ .../com/aliucord/manager/di/Managers.kt | 13 +-- .../manager/manager/PreferencesManager.kt | 5 +- .../AndroidDownloadManager.kt} | 103 ++++++------------ .../manager/download/IDownloadManager.kt | 64 +++++++++++ .../manager/download/KtorDownloadManager.kt | 81 ++++++++++++++ .../patcher/steps/base/DownloadStep.kt | 21 ++-- .../screens/patching/PatchingScreenModel.kt | 5 +- .../ui/widgets/updater/UpdaterViewModel.kt | 8 +- .../kotlin/com/aliucord/manager/util/Utils.kt | 59 ++++++++++ 11 files changed, 310 insertions(+), 99 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/di/DownloadManagerProvider.kt rename app/src/main/kotlin/com/aliucord/manager/manager/{DownloadManager.kt => download/AndroidDownloadManager.kt} (57%) create mode 100644 app/src/main/kotlin/com/aliucord/manager/manager/download/IDownloadManager.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt index 634679b..91e7ac0 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt @@ -6,6 +6,9 @@ import com.aliucord.manager.domain.repository.AliucordMavenRepository import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.installers.pm.PMInstaller import com.aliucord.manager.manager.InstallerManager +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.manager.download.AndroidDownloadManager +import com.aliucord.manager.manager.download.KtorDownloadManager import com.aliucord.manager.network.service.* import com.aliucord.manager.ui.screens.about.AboutModel import com.aliucord.manager.ui.screens.home.HomeModel @@ -62,9 +65,12 @@ class ManagerApplication : Application() { // Managers modules(module { single { providePreferences() } - single { provideDownloadManager() } - single { providePathManager() } + singleOf(::PathManager) singleOf(::InstallerManager) + + singleOf(::DownloadManagerProvider) + singleOf(::AndroidDownloadManager) + singleOf(::KtorDownloadManager) }) // Installers diff --git a/app/src/main/kotlin/com/aliucord/manager/di/DownloadManagerProvider.kt b/app/src/main/kotlin/com/aliucord/manager/di/DownloadManagerProvider.kt new file mode 100644 index 0000000..21c6efe --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/di/DownloadManagerProvider.kt @@ -0,0 +1,40 @@ +package com.aliucord.manager.di + +import com.aliucord.manager.manager.PreferencesManager +import com.aliucord.manager.manager.download.* +import com.aliucord.manager.patcher.util.Signer.getKoin +import com.aliucord.manager.util.IS_PROBABLY_EMULATOR +import org.koin.core.annotation.KoinInternalApi +import kotlin.reflect.KClass + +/** + * Handle providing the correct install manager based on preferences and device type. + */ +class DownloadManagerProvider(private val prefs: PreferencesManager) { + fun getActiveDownloader(): IDownloadManager = + getDownloader(prefs.downloader) + + @OptIn(KoinInternalApi::class) + fun getDownloader(type: DownloaderSetting): IDownloadManager = + getKoin().scopeRegistry.rootScope.get(clazz = type.downloaderClass) + + companion object { + fun getDefaultDownloader(): DownloaderSetting { + // Ktor downloader has specific fix for emulator + return if (IS_PROBABLY_EMULATOR) { + DownloaderSetting.Ktor + } else { + DownloaderSetting.Android + } + } + } +} + +enum class DownloaderSetting( + // @StringRes + // private val localizedName: Int, + val downloaderClass: KClass, +) { + Android(AndroidDownloadManager::class), + Ktor(KtorDownloadManager::class), +} diff --git a/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt b/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt index 52b2e6a..466963a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/Managers.kt @@ -1,21 +1,10 @@ package com.aliucord.manager.di -import android.app.Application import android.content.Context -import com.aliucord.manager.manager.* +import com.aliucord.manager.manager.PreferencesManager import org.koin.core.scope.Scope fun Scope.providePreferences(): PreferencesManager { val ctx: Context = get() return PreferencesManager(ctx.getSharedPreferences("preferences", Context.MODE_PRIVATE)) } - -fun Scope.provideDownloadManager(): DownloadManager { - val application: Application = get() - return DownloadManager(application) -} - -fun Scope.providePathManager(): PathManager { - val ctx: Context = get() - return PathManager(ctx) -} diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt index f57891b..39024b6 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PreferencesManager.kt @@ -1,6 +1,8 @@ package com.aliucord.manager.manager import android.content.SharedPreferences +import com.aliucord.manager.di.DownloadManagerProvider +import com.aliucord.manager.di.DownloaderSetting import com.aliucord.manager.manager.base.BasePreferenceManager import com.aliucord.manager.ui.components.Theme @@ -8,6 +10,7 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager var theme by enumPreference("theme", Theme.DARK) var dynamicColor by booleanPreference("dynamic_color", true) var devMode by booleanPreference("dev_mode", false) - var installer by enumPreference("installer", InstallerSetting.PM) + var downloader by enumPreference("downloader", DownloadManagerProvider.getDefaultDownloader()) + var installer by enumPreference("installer", InstallerSetting.PM) var keepPatchedApks by booleanPreference("keep_patched_apks", false) } diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/download/AndroidDownloadManager.kt similarity index 57% rename from app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt rename to app/src/main/kotlin/com/aliucord/manager/manager/download/AndroidDownloadManager.kt index 55b7993..b5abf7c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/download/AndroidDownloadManager.kt @@ -1,39 +1,35 @@ -package com.aliucord.manager.manager +package com.aliucord.manager.manager.download import android.app.Application import android.app.DownloadManager +import android.content.Context import android.database.Cursor import android.net.Uri -import android.os.Build -import android.util.Log -import androidx.annotation.StringRes import androidx.core.content.getSystemService import com.aliucord.manager.BuildConfig import com.aliucord.manager.R +import com.aliucord.manager.manager.download.IDownloadManager.ProgressListener +import com.aliucord.manager.manager.download.IDownloadManager.Result import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import java.io.File import kotlin.coroutines.cancellation.CancellationException /** - * Handle downloading remote urls to a path through the system [DownloadManager]. + * Handle downloading remote urls to a path through the system's [DownloadManager]. */ -class DownloadManager(application: Application) { +class AndroidDownloadManager(application: Application) : IDownloadManager { private val downloadManager = application.getSystemService() ?: throw IllegalStateException("DownloadManager service is not available") /** - * Start a cancellable download with the system [DownloadManager]. + * Start a cancellable download with the system [IDownloadManager]. * If the current [CoroutineScope] is cancelled, then the system download will be cancelled within 100ms. * @param url Remote src url * @param out Target path to download to. It is assumed that the application has write permissions to this path. * @param onProgressUpdate An optional [ProgressListener] */ - suspend fun download( - url: String, - out: File, - onProgressUpdate: ProgressListener? = null, - ): Result { + override suspend fun download(url: String, out: File, onProgressUpdate: ProgressListener?): Result { onProgressUpdate?.onUpdate(null) out.parentFile?.mkdirs() @@ -44,13 +40,6 @@ class DownloadManager(application: Application) { .setDestinationUri(Uri.fromFile(out)) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) .addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}") - .apply { - // Disable gzip on emulator due to https compression bug - if (Build.FINGERPRINT.contains("emulator")) { - Log.d(BuildConfig.TAG, "Disabling DownloadManager compression") - addRequestHeader("Accept-Encoding", null) - } - } .let(downloadManager::enqueue) // Repeatedly request download state until it is finished @@ -93,7 +82,7 @@ class DownloadManager(application: Application) { val reasonColumn = cursor.getColumnIndex(DownloadManager.COLUMN_REASON) val reason = cursor.getInt(reasonColumn) - return Result.Error(reason) + return Error(reason) } else -> throw Error("Unreachable") @@ -118,61 +107,29 @@ class DownloadManager(application: Application) { } /** - * A callback executed from a coroutine called every 100ms in order to provide - * info about the current download. This should not perform long-running tasks as the delay will be offset. + * Error returned by the system [DownloadManager]. + * @param reason The reason code returned by the [DownloadManager.COLUMN_REASON] column. */ - fun interface ProgressListener { + data class Error(private val reason: Int) : Result.Error() { /** - * @param progress The current download progress in a `[0,1]` range. If null, then the download is either - * paused, pending, or waiting to retry. + * Convert a [DownloadManager.COLUMN_REASON] code into its name. */ - fun onUpdate(progress: Float?) - } + override fun getDebugReason(): String = when (reason) { + DownloadManager.ERROR_UNKNOWN -> "Unknown" + DownloadManager.ERROR_FILE_ERROR -> "File Error" + DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code" + DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error" + DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects" + DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space" + DownloadManager.ERROR_DEVICE_NOT_FOUND -> "Target file's device not found" + DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume" + DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File exists" + /* DownloadManager.ERROR_BLOCKED */ 1010 -> "Network policy block" + else -> "Unknown code ($reason)" + } - /** - * The state of a download after execution has been completed and the system-level [DownloadManager] has been cleaned up. - */ - sealed interface Result { - /** - * The download succeeded successfully. - * @param file The path that the download was downloaded to. - */ - data class Success(val file: File) : Result - - /** - * This download was interrupted and the in-progress file has been deleted. - * @param systemTriggered Whether the cancellation happened from the system (ie. clicked cancel on the download notification) - * Otherwise, this was caused by a coroutine cancellation. - */ - data class Cancelled(val systemTriggered: Boolean) : Result - - /** - * Error returned by the system [DownloadManager]. - * @param reason The reason code returned by the [DownloadManager.COLUMN_REASON] column. - */ - data class Error(private val reason: Int) : Result { - /** - * Convert a [DownloadManager.COLUMN_REASON] code into its name. - */ - val debugReason = when (reason) { - DownloadManager.ERROR_UNKNOWN -> "Unknown" - DownloadManager.ERROR_FILE_ERROR -> "File Error" - DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code" - DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error" - DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects" - DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space" - DownloadManager.ERROR_DEVICE_NOT_FOUND -> "Target file's device not found" - DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume" - DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File exists" - /* DownloadManager.ERROR_BLOCKED */ 1010 -> "Network policy block" - else -> "Unknown code ($reason)" - } - - /** - * Simplified + translatable user facing errors - */ - @StringRes - val localizedReason = when (reason) { // @formatter:off + override fun getLocalizedReason(context: Context): String { + val string = when (reason) { // @formatter:off DownloadManager.ERROR_HTTP_DATA_ERROR, DownloadManager.ERROR_TOO_MANY_REDIRECTS, DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> @@ -186,6 +143,10 @@ class DownloadManager(application: Application) { else -> R.string.downloader_err_unknown } // @formatter:on + + return context.getString(string) } + + override fun toString(): String = getDebugReason() } } diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/download/IDownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/download/IDownloadManager.kt new file mode 100644 index 0000000..a00ec24 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/manager/download/IDownloadManager.kt @@ -0,0 +1,64 @@ +package com.aliucord.manager.manager.download + +import android.app.DownloadManager +import android.content.Context +import java.io.File + +/** + * Common interface for different implementations of starting and managing the lifetime of downloads. + */ +interface IDownloadManager { + /** + * Start a cancellable download. + * @param url Remote src url + * @param out Target path to download to. It is assumed that the application has write permissions to this path. + * @param onProgressUpdate An optional [ProgressListener] callback. + */ + suspend fun download(url: String, out: File, onProgressUpdate: ProgressListener? = null): Result + + /** + * A callback executed from a coroutine called every 100ms in order to provide + * info about the current download. This should not perform long-running tasks as the delay will be offset. + */ + fun interface ProgressListener { + /** + * @param progress The current download progress in a `[0,1]` range. If null, then the download is either + * paused, pending, or waiting to retry. + */ + fun onUpdate(progress: Float?) + } + + /** + * The state of a download after execution has been completed and the system-level [DownloadManager] has been cleaned up. + */ + sealed interface Result { + /** + * The download succeeded successfully. + * @param file The path that the download was downloaded to. + */ + data class Success(val file: File) : Result + + /** + * This download was interrupted and the in-progress file has been deleted. + * @param systemTriggered Whether the cancellation happened from the system (ie. clicked cancel on the download notification) + * Otherwise, this was caused by a coroutine cancellation. + */ + data class Cancelled(val systemTriggered: Boolean) : Result + + /** + * This download failed to complete due to an error. + */ + abstract class Error : Result { + /** + * The full internal error representation. + */ + abstract fun getDebugReason(): String + + /** + * Simplified + translatable user facing reason for the failure. + * If null is returned, then the [getDebugReason] will be used instead. + */ + open fun getLocalizedReason(context: Context): String? = null + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt new file mode 100644 index 0000000..890273a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt @@ -0,0 +1,81 @@ +package com.aliucord.manager.manager.download + +import com.aliucord.manager.manager.download.IDownloadManager.Result +import io.ktor.client.HttpClient +import io.ktor.client.request.* +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.HttpHeaders +import io.ktor.http.contentLength +import io.ktor.utils.io.core.isEmpty +import io.ktor.utils.io.core.readBytes +import kotlinx.coroutines.* +import java.io.File +import kotlin.math.min + +/** + * Handle downloading remote urls to a path with Ktor. + * This is used as an alternative downloader option due to some bugs with the + * system's DownloadManager that prevents its usage on some emulators and ROMs. + */ +class KtorDownloadManager(private val http: HttpClient) : IDownloadManager { + override suspend fun download(url: String, out: File, onProgressUpdate: IDownloadManager.ProgressListener?): Result { + onProgressUpdate?.onUpdate(null) + out.parentFile?.mkdirs() + + val tmpOut = out.resolveSibling(out.name + ".tmp") + .apply { exists() || createNewFile() } + + try { + val httpStmt = http.prepareGet(url) { + // Disable compression due to bug on emulators + // This header cannot be set with Android's DownloadManager + header(HttpHeaders.AcceptEncoding, null) + } + + httpStmt.execute { resp -> + val channel = resp.bodyAsChannel() + val total = resp.contentLength() ?: 0 + var retrieved = 0L + + tmpOut.outputStream().use { stream -> + while (!channel.isClosedForRead) { + channel.awaitContent() + val packet = channel.readRemaining(1024 * 1024 * 1) + + while (!packet.isEmpty) { + // TODO: reuse bytearray + val bytes = packet.readBytes() + stream.write(bytes) + stream.flush() + + retrieved += bytes.size + + if (total > 0) { + onProgressUpdate?.onUpdate(retrieved / total.toFloat()) + } else { + onProgressUpdate?.onUpdate(null) + } + } + } + } + } + } catch (_: CancellationException) { + tmpOut.delete() + return Result.Cancelled(systemTriggered = false) + } catch (t: Throwable) { + tmpOut.delete() + return Error(t) + } + + tmpOut.renameTo(out) + return Result.Success(out) + } + + /** + * Wrapper around an exception that occurred from invoking Ktor + */ + class Error(private val throwable: Throwable) : Result.Error() { + override fun getDebugReason(): String = throwable.message ?: "Unknown exception" + override fun toString(): String = throwable.stackTraceToString() + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt index 3b024c9..4d66fac 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt @@ -1,11 +1,13 @@ package com.aliucord.manager.patcher.steps.base import android.content.Context +import android.widget.Toast import androidx.compose.runtime.Stable import com.aliucord.manager.R +import com.aliucord.manager.di.DownloadManagerProvider +import com.aliucord.manager.manager.download.IDownloadManager import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.steps.StepGroup -import com.aliucord.manager.manager.DownloadManager import com.aliucord.manager.util.showToast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -16,7 +18,7 @@ import java.io.File @Stable abstract class DownloadStep : Step(), KoinComponent { private val context: Context by inject() - private val downloads: DownloadManager by inject() + private val downloaders: DownloadManagerProvider by inject() /** * The remote url to download @@ -53,12 +55,12 @@ abstract class DownloadStep : Step(), KoinComponent { targetFile.delete() } - val result = downloads.download(targetUrl, targetFile) { newProgress -> + val result = downloaders.getActiveDownloader().download(targetUrl, targetFile) { newProgress -> progress = newProgress ?: -1f } when (result) { - is DownloadManager.Result.Success -> { + is IDownloadManager.Result.Success -> { try { verify() } catch (t: Throwable) { @@ -70,15 +72,18 @@ abstract class DownloadStep : Step(), KoinComponent { } } - is DownloadManager.Result.Error -> { + is IDownloadManager.Result.Error -> { withContext(Dispatchers.Main) { - context.showToast(result.localizedReason) + val toastText = result.getLocalizedReason(context) + ?: context.getString(R.string.downloader_err_unknown) + + Toast.makeText(context, toastText, Toast.LENGTH_LONG).show() } - throw Error("Failed to download: ${result.debugReason}") + throw Error("Failed to download: $result") } - is DownloadManager.Result.Cancelled -> + is IDownloadManager.Result.Cancelled -> state = StepState.Error } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt index 3c57ec3..7ba211a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt @@ -11,14 +11,14 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.R +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.patcher.KotlinPatchRunner import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.patcher.steps.base.Step import com.aliucord.manager.patcher.steps.base.StepState import com.aliucord.manager.patcher.steps.install.InstallStep -import com.aliucord.manager.manager.PathManager -import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.ui.screens.patchopts.PatchOptions import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.util.* @@ -193,6 +193,7 @@ class PatchingScreenModel( Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges Android API: ${Build.VERSION.SDK_INT} + Emulator: $IS_PROBABLY_EMULATOR (guess) ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH}) Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE}) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt index e32a7ad..be857eb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/widgets/updater/UpdaterViewModel.kt @@ -5,8 +5,10 @@ import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aliucord.manager.BuildConfig +import com.aliucord.manager.di.DownloadManagerProvider import com.aliucord.manager.domain.repository.GithubRepository -import com.aliucord.manager.manager.* +import com.aliucord.manager.manager.InstallerManager +import com.aliucord.manager.manager.InstallerSetting import com.aliucord.manager.network.utils.SemVer import com.aliucord.manager.network.utils.getOrNull import kotlinx.coroutines.Dispatchers @@ -14,7 +16,7 @@ import kotlinx.coroutines.launch class UpdaterViewModel( private val github: GithubRepository, - private val downloadManager: DownloadManager, + private val downloaders: DownloadManagerProvider, private val installers: InstallerManager, private val application: Application, ) : ViewModel() { @@ -41,7 +43,7 @@ class UpdaterViewModel( application.externalCacheDir!!.resolve("manager.apk").also { it.delete() - downloadManager.download(url, it) + downloaders.getActiveDownloader().download(url, it) installers.getInstaller(InstallerSetting.PM).install( apks = listOf(it), silent = true, diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt b/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt index a48379f..4b7b3ff 100644 --- a/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt +++ b/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt @@ -1,7 +1,10 @@ package com.aliucord.manager.util +import android.os.Build import androidx.collection.ObjectList import com.aliucord.manager.BuildConfig +import java.io.BufferedReader +import java.io.IOException import kotlin.math.pow import kotlin.math.truncate @@ -31,3 +34,59 @@ val IS_CUSTOM_BUILD by lazy { || BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS } + +/** + * Whether this device is most likely an emulator. + * src: https://stackoverflow.com/a/21505193/13964629 + */ +val IS_PROBABLY_EMULATOR by lazy { + // Android SDK emulator + return@lazy ((Build.MANUFACTURER == "Google" && Build.BRAND == "google" && + ((Build.FINGERPRINT.startsWith("google/sdk_gphone_") + && Build.FINGERPRINT.endsWith(":user/release-keys") + && Build.PRODUCT.startsWith("sdk_gphone_") + && Build.MODEL.startsWith("sdk_gphone_")) + // Alternative + || (Build.FINGERPRINT.startsWith("google/sdk_gphone64_") + && (Build.FINGERPRINT.endsWith(":userdebug/dev-keys") || Build.FINGERPRINT.endsWith(":user/release-keys")) + && Build.PRODUCT.startsWith("sdk_gphone64_") + && Build.MODEL.startsWith("sdk_gphone64_")))) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + // Bluestacks + || "QC_Reference_Phone" == Build.BOARD && !"Xiaomi".equals(Build.MANUFACTURER, ignoreCase = true) + // Bluestacks + || Build.MANUFACTURER.contains("Genymotion") + || Build.HOST.startsWith("Build") + // MSI App Player + || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") + || Build.PRODUCT == "google_sdk" + // Another Android SDK emulator check + || getSystemProp("ro.kernel.qemu") == "1") +} + +/** + * Gets a system property from build.prop + */ +private fun getSystemProp(name: String): String? { + var reader: BufferedReader? = null + + return try { + val process = Runtime.getRuntime().exec("getprop $name") + reader = process.inputStream.bufferedReader() + reader.readLine() + } catch (_: Exception) { + null + } finally { + try { + reader?.close() + } catch (_: IOException) { + // ignore + } + } +} + +