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)
This commit is contained in:
parent
f736062a93
commit
98c0a2e22d
11 changed files with 310 additions and 99 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<out IDownloadManager>,
|
||||
) {
|
||||
Android(AndroidDownloadManager::class),
|
||||
Ktor(KtorDownloadManager::class),
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DownloaderSetting>("downloader", DownloadManagerProvider.getDefaultDownloader())
|
||||
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PM)
|
||||
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DownloadManager>()
|
||||
?: 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue