feat: always use Ktor downloader

Android's DownloadManager has too many issues at this point, it's become unbearable. SecurityException here, random failure there, horrible logging, etc.

It's much more reliable to directly use an HTTP client to download our stuff. Maybe I'll reimplement the download progress notification separately.

Also turns out you can't use Android's DownloadManager to download to the app's internal data directory. very funny.
This commit is contained in:
rushiiMachine 2025-02-19 22:47:28 -08:00
parent b2e5353abc
commit a1e7594698
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
11 changed files with 29 additions and 180 deletions

View file

@ -70,7 +70,6 @@ class ManagerApplication : Application() {
singleOf(::InstallerManager)
singleOf(::OverlayManager)
singleOf(::DownloadManagerProvider)
singleOf(::AndroidDownloadManager)
singleOf(::KtorDownloadManager)
})

View file

@ -1,40 +0,0 @@
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),
}

View file

@ -37,7 +37,7 @@ class PathManager(
* Use the external cache directory (`/storage/emulated/0/Android/data/com.aliucord.manager/cache`)
* when dev mode or preserving APKs is enabled. Otherwise, default to internal app cache.
*/
private val externalCacheDir
private val cacheDir
get() = when (prefs.devMode || prefs.keepPatchedApks) {
false -> context.cacheDir
true -> context.externalCacheDir
@ -48,16 +48,16 @@ class PathManager(
* Delete the entire cache dir and recreate it.
*/
fun clearCache() {
if (!externalCacheDir.deleteRecursively())
if (!cacheDir.deleteRecursively())
throw IllegalStateException("Failed to delete cache")
externalCacheDir.mkdirs()
cacheDir.mkdirs()
}
/**
* Create a new subfolder in the Discord APK cache for a specific version.
*/
fun discordApkVersionCache(version: Int): File = externalCacheDir
fun discordApkVersionCache(version: Int): File = cacheDir
.resolve("discord")
.resolve(version.toString())
.apply { mkdirs() }
@ -65,45 +65,45 @@ class PathManager(
/**
* Resolve a specific path for a cached injector.
*/
fun cachedInjectorDex(version: SemVer, custom: Boolean = false) = externalCacheDir
fun cachedInjectorDex(version: SemVer, custom: Boolean = false) = cacheDir
.resolve("injector").apply { mkdirs() }
.resolve("$version${if (custom) ".custom" else ""}.dex")
/**
* Get all the versions of custom injector builds.
*/
fun customInjectorDexs() = listCustomFiles(externalCacheDir.resolve("injector"))
fun customInjectorDexs() = listCustomFiles(cacheDir.resolve("injector"))
/**
* Resolve a specific path for a versioned cached Aliuhook build
*/
fun cachedAliuhookAAR(version: SemVer) = externalCacheDir
fun cachedAliuhookAAR(version: SemVer) = cacheDir
.resolve("aliuhook").apply { mkdirs() }
.resolve("$version.aar")
/**
* Resolve a specific path for a versioned smali patches archive.
*/
fun cachedSmaliPatches(version: SemVer, custom: Boolean = false) = externalCacheDir
fun cachedSmaliPatches(version: SemVer, custom: Boolean = false) = cacheDir
.resolve("patches").apply { mkdirs() }
.resolve("$version${if (custom) ".custom" else ""}.zip")
/**
* Get all the versions of custom smali bundles.
*/
fun customSmaliPatches() = listCustomFiles(externalCacheDir.resolve("patches"))
fun customSmaliPatches() = listCustomFiles(cacheDir.resolve("patches"))
/**
* Singular Kotlin file of the most up-to-date version
* since the stdlib is backwards compatible.
*/
fun cachedKotlinDex() = externalCacheDir
fun cachedKotlinDex() = cacheDir
.resolve("kotlin.dex")
/**
* The temporary working directory of a currently executing patching process.
*/
fun patchingWorkingDir() = externalCacheDir
fun patchingWorkingDir() = cacheDir
.resolve("patched")
private companion object {

View file

@ -1,8 +1,6 @@
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
@ -10,7 +8,6 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
var theme by enumPreference("theme", Theme.DARK)
var dynamicColor by booleanPreference("dynamic_color", false)
var devMode by booleanPreference("dev_mode", false)
var downloader by enumPreference<DownloaderSetting>("downloader", DownloadManagerProvider.getDefaultDownloader())
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PM)
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
}

View file

@ -6,6 +6,7 @@ import android.content.Context
import android.database.Cursor
import android.net.Uri
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.manager.download.IDownloadManager.ProgressListener
@ -34,7 +35,7 @@ class AndroidDownloadManager(application: Application) : IDownloadManager {
out.parentFile?.mkdirs()
// Create and start a download in the system DownloadManager
val downloadId = DownloadManager.Request(Uri.parse(url))
val downloadId = DownloadManager.Request(url.toUri())
.setTitle("Aliucord Manager")
.setDescription("Downloading ${out.name}...")
.setDestinationUri(Uri.fromFile(out))

View file

@ -1,6 +1,7 @@
package com.aliucord.manager.manager.download
import com.aliucord.manager.manager.download.IDownloadManager.Result
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
import io.ktor.client.HttpClient
import io.ktor.client.request.header
import io.ktor.client.request.prepareGet
@ -28,7 +29,9 @@ class KtorDownloadManager(private val http: HttpClient) : IDownloadManager {
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)
if (IS_PROBABLY_EMULATOR) {
header(HttpHeaders.AcceptEncoding, null)
}
}
httpStmt.execute { resp ->

View file

@ -1,22 +1,14 @@
package com.aliucord.manager.patcher.steps.base
import android.app.DownloadManager
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.compose.runtime.Stable
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.di.DownloadManagerProvider
import com.aliucord.manager.di.DownloaderSetting
import com.aliucord.manager.manager.OverlayManager
import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.manager.download.AndroidDownloadManager
import com.aliucord.manager.manager.download.IDownloadManager
import com.aliucord.manager.manager.download.KtorDownloadManager
import com.aliucord.manager.patcher.StepRunner
import com.aliucord.manager.patcher.steps.StepGroup
import com.aliucord.manager.ui.components.dialogs.AlternativeDownloaderDialog
import com.aliucord.manager.util.showToast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -27,9 +19,7 @@ import java.io.File
@Stable
abstract class DownloadStep : Step(), KoinComponent {
private val context: Context by inject()
private val overlays: OverlayManager by inject()
private val prefs: PreferencesManager by inject()
private val downloaders: DownloadManagerProvider by inject()
private val downloader: KtorDownloadManager by inject()
/**
* The remote url to download
@ -67,7 +57,7 @@ abstract class DownloadStep : Step(), KoinComponent {
targetFile.delete()
}
val result = downloaders.getActiveDownloader().download(targetUrl, targetFile) { newProgress ->
val result = downloader.download(targetUrl, targetFile) { newProgress ->
progress = newProgress ?: -1f
}
@ -96,29 +86,6 @@ abstract class DownloadStep : Step(), KoinComponent {
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()
}
// If this is a specific Android DownloadManager error, then prompt to use a different downloader
if (result is AndroidDownloadManager.Error
&& result.reason in arrayOf(DownloadManager.ERROR_HTTP_DATA_ERROR, DownloadManager.ERROR_CANNOT_RESUME)
&& prefs.downloader != DownloaderSetting.Ktor
) {
state = StepState.Error
val confirmed = overlays.startComposableForResult { callback ->
AlternativeDownloaderDialog(
onConfirm = { callback(true) },
onDismiss = { callback(false) },
)
}
if (confirmed) {
Log.i(BuildConfig.TAG, "Changing to alternative downloader after failure")
prefs.downloader = DownloaderSetting.Ktor
state = StepState.Running
return execute(container)
}
}
throw Error("Failed to download: $result")
}
}

View file

@ -1,56 +0,0 @@
package com.aliucord.manager.ui.components.dialogs
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
@Composable
fun AlternativeDownloaderDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
FilledTonalButton(
onClick = onConfirm,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
),
) {
Text(stringResource(R.string.action_retry))
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error,
),
) {
Text(stringResource(R.string.action_cancel))
}
},
title = { Text(stringResource(R.string.alt_downloader_dialog_title)) },
text = {
Text(
text = stringResource(R.string.alt_downloader_dialog_desc),
textAlign = TextAlign.Center,
)
},
icon = {
Icon(
painter = painterResource(R.drawable.ic_download),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
},
)
}

View file

@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.aliucord.manager.R
import com.aliucord.manager.di.DownloaderSetting
import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.components.settings.*
import com.aliucord.manager.ui.screens.settings.components.ThemeDialog
@ -102,20 +101,6 @@ class SettingsScreen : Screen, Parcelable {
onPrefChange = { preferences.keepPatchedApks = it },
)
SettingsSwitch(
label = stringResource(R.string.setting_alt_downloader),
secondaryLabel = stringResource(R.string.setting_alt_downloader_desc),
icon = { Icon(painterResource(R.drawable.ic_download), null) },
pref = preferences.downloader == DownloaderSetting.Ktor,
onPrefChange = {
preferences.downloader = if (it) {
DownloaderSetting.Ktor
} else {
DownloaderSetting.Android
}
}
)
var clearedCache by rememberSaveable { mutableStateOf(false) }
Button(
shape = ShapeDefaults.Large,

View file

@ -5,10 +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.InstallerManager
import com.aliucord.manager.manager.InstallerSetting
import com.aliucord.manager.manager.download.KtorDownloadManager
import com.aliucord.manager.network.utils.SemVer
import com.aliucord.manager.network.utils.getOrNull
import kotlinx.coroutines.Dispatchers
@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
class UpdaterViewModel(
private val github: GithubRepository,
private val downloaders: DownloadManagerProvider,
private val downloader: KtorDownloadManager,
private val installers: InstallerManager,
private val application: Application,
) : ViewModel() {
@ -40,17 +40,15 @@ class UpdaterViewModel(
viewModelScope.launch {
isWorking = true
application.externalCacheDir!!.resolve("manager.apk").also {
it.delete()
val apkFile = application.cacheDir.resolve("manager.apk").apply { delete() }
downloaders.getActiveDownloader().download(url, it)
installers.getInstaller(InstallerSetting.PM).install(
apks = listOf(it),
silent = true,
)
downloader.download(url, apkFile)
installers.getInstaller(InstallerSetting.PM).install(
apks = listOf(apkFile),
silent = true,
)
it.delete()
}
apkFile.delete()
}
}

View file

@ -49,8 +49,6 @@
<string name="setting_dynamic_color_desc">Enables Material You theming on Android 12+</string>
<string name="setting_keep_patched_apks">Keep patched APKs</string>
<string name="setting_keep_patched_apks_desc">Keep all patched files and APKs after installing for debugging.</string>
<string name="setting_alt_downloader">Alternative downloader</string>
<string name="setting_alt_downloader_desc">Use a custom HTTP client instead of the system DownloadManager. May fix download errors on specific devices.</string>
<string name="theme_system">System</string>
<string name="theme_dark">Dark</string>
@ -171,9 +169,6 @@
<string name="play_protect_warning_ok">Continue</string>
<string name="play_protect_warning_open_gpp">Open Play Protect</string>
<string name="alt_downloader_dialog_title">Alternative Downloader</string>
<string name="alt_downloader_dialog_desc">This download failed with a specific error that may indicate issues with the system\'s DownloadManager. Would you like to retry with the the alternative downloader implementation?</string>
<string name="fun_fact_prefix">Fun Fact: %s</string>
<string name="fun_fact_1">Did you know that Google Play Protect is useless?</string>
<string name="fun_fact_2">Did you know Aliucord with 150+ plugins somehow runs better than Discord\'s new app?</string>