feat: better download manager
This commit is contained in:
parent
b47a1b4488
commit
e5b0307ef9
12 changed files with 202 additions and 125 deletions
|
|
@ -25,7 +25,8 @@ android {
|
|||
|
||||
buildConfigField("String", "TAG", "\"AliucordManager\"")
|
||||
buildConfigField("String", "SUPPORT_SERVER", "\"EsNDvBaHVU\"")
|
||||
buildConfigField("boolean", "RN_ENABLED", "true")
|
||||
|
||||
buildConfigField("String", "BACKEND_URL", "\"https://aliucord.com/\"")
|
||||
|
||||
buildConfigField("String", "GIT_BRANCH", "\"${getCurrentBranch()}\"")
|
||||
buildConfigField("String", "GIT_COMMIT", "\"${getLatestCommit()}\"")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.transitions.FadeTransition
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.components.*
|
||||
import com.aliucord.manager.ui.components.dialogs.StoragePermissionsDialog
|
||||
import com.aliucord.manager.ui.screens.home.HomeScreen
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.aliucord.manager.di
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.DownloadManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
fun Scope.providePreferences(): PreferencesManager {
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
package com.aliucord.manager.domain.manager
|
||||
|
||||
import android.app.Application
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.network.service.AliucordGithubService
|
||||
import java.io.File
|
||||
import kotlin.coroutines.*
|
||||
|
||||
class DownloadManager(
|
||||
private val application: Application,
|
||||
) {
|
||||
private val downloadManager = application.getSystemService<DownloadManager>()!!
|
||||
|
||||
// Discord APK downloading
|
||||
suspend fun downloadDiscordApk(version: String, out: File): File =
|
||||
download("$BACKEND_HOST/download/discord?v=$version", out)
|
||||
|
||||
// Aliucord Kotlin downloads
|
||||
suspend fun downloadKtInjector(out: File): File =
|
||||
download(AliucordGithubService.KT_INJECTOR_URL, out)
|
||||
|
||||
suspend fun downloadAliuhook(version: String, out: File): File =
|
||||
download(AliucordMavenRepository.getAliuhookUrl(version), out)
|
||||
|
||||
suspend fun downloadKotlinDex(out: File): File =
|
||||
download(AliucordGithubService.KOTLIN_DEX_URL, out)
|
||||
|
||||
suspend fun download(url: String, out: File): File {
|
||||
out.parentFile?.mkdirs()
|
||||
|
||||
return suspendCoroutine { continuation ->
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
var downloadId = 0L
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||
|
||||
if (downloadId != id) return
|
||||
|
||||
val (status, reason) = DownloadManager.Query().run {
|
||||
setFilterById(downloadId)
|
||||
|
||||
val cursor = downloadManager.query(this)
|
||||
.apply { moveToFirst() }
|
||||
|
||||
// Notification's "cancel" was clicked
|
||||
if (cursor.count == 0) {
|
||||
-1 to -1
|
||||
} else {
|
||||
val status = cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_STATUS)) }
|
||||
val reason = cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_REASON)) }
|
||||
status to reason
|
||||
}
|
||||
}
|
||||
|
||||
when (status) {
|
||||
// Cancelled
|
||||
-1 -> {
|
||||
context.unregisterReceiver(this)
|
||||
continuation.resumeWithException(Error("Download was cancelled manually"))
|
||||
}
|
||||
|
||||
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||
context.unregisterReceiver(this)
|
||||
continuation.resume(out)
|
||||
}
|
||||
|
||||
DownloadManager.STATUS_FAILED -> {
|
||||
context.unregisterReceiver(this)
|
||||
continuation.resumeWithException(Error("Failed to download $url because of: ${reasonToString(reason)}"))
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
application.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||
|
||||
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
|
||||
setTitle("Aliucord Manager: Downloading ${out.name}")
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||
setDestinationUri(out.toUri())
|
||||
addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}")
|
||||
downloadManager.enqueue(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reasonToString(int: Int): String {
|
||||
return when (int) {
|
||||
DownloadManager.ERROR_UNKNOWN -> "Unknown Error"
|
||||
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 -> "Device not found"
|
||||
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
|
||||
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
|
||||
else -> "Unknown error"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BACKEND_HOST = "https://aliucord.com/"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package com.aliucord.manager.manager
|
||||
|
||||
import android.app.Application
|
||||
import android.app.DownloadManager
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.network.service.AliucordGithubService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.File
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* Handle downloading remote urls to a file through the system [DownloadManager].
|
||||
*/
|
||||
class DownloadManager(application: Application) {
|
||||
private val downloadManager = application.getSystemService<DownloadManager>()
|
||||
?: throw IllegalStateException("DownloadManager service is not available")
|
||||
|
||||
// Discord APK downloading
|
||||
suspend fun downloadDiscordApk(version: String, out: File): Result =
|
||||
download("${BuildConfig.BACKEND_URL}/download/discord?v=$version", out)
|
||||
|
||||
// Aliucord Kotlin downloads
|
||||
suspend fun downloadKtInjector(out: File): Result =
|
||||
download(AliucordGithubService.KT_INJECTOR_URL, out)
|
||||
|
||||
suspend fun downloadAliuhook(version: String, out: File): Result =
|
||||
download(AliucordMavenRepository.getAliuhookUrl(version), out)
|
||||
|
||||
suspend fun downloadKotlinDex(out: File): Result =
|
||||
download(AliucordGithubService.KOTLIN_DEX_URL, out)
|
||||
|
||||
/**
|
||||
* Start a cancellable download with the system [DownloadManager].
|
||||
* 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
|
||||
* @param onProgressUpdate Download progress update in a `[0,1]` range, and if null then the download is currently in a pending state.
|
||||
* This is called every 100ms, and should not perform long-running tasks.
|
||||
*/
|
||||
suspend fun download(
|
||||
url: String,
|
||||
out: File,
|
||||
onProgressUpdate: ((Float?) -> Unit)? = null,
|
||||
): Result {
|
||||
out.parentFile?.mkdirs()
|
||||
|
||||
// Create and start a download in the system DownloadManager
|
||||
val downloadId = DownloadManager.Request(Uri.parse(url))
|
||||
.setTitle("Aliucord Manager")
|
||||
.setDescription("Downloading ${out.name}...")
|
||||
.setDestinationUri(Uri.fromFile(out))
|
||||
.setAllowedOverMetered(true)
|
||||
.setAllowedOverRoaming(true)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||
.addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}")
|
||||
.let(downloadManager::enqueue)
|
||||
|
||||
// Repeatedly request download state until it is finished
|
||||
while (true) {
|
||||
try {
|
||||
// Hand over control to a suspend function to check for cancellation
|
||||
// At the same time, delay 100ms to slow down the potentially infinite loop
|
||||
delay(100)
|
||||
} catch (_: CancellationException) {
|
||||
// If the running CoroutineScope has been cancelled, then gracefully cancel download
|
||||
downloadManager.remove(downloadId)
|
||||
return Result.Cancelled(systemTriggered = false)
|
||||
}
|
||||
|
||||
// Request download status
|
||||
val cursor = DownloadManager.Query()
|
||||
.setFilterById(downloadId)
|
||||
.let(downloadManager::query)
|
||||
|
||||
cursor.use {
|
||||
// No results in cursor, download was cancelled
|
||||
if (!cursor.moveToFirst()) {
|
||||
return Result.Cancelled(systemTriggered = true)
|
||||
}
|
||||
|
||||
val statusColumn = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
val status = cursor.getInt(statusColumn)
|
||||
|
||||
when (status) {
|
||||
DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED ->
|
||||
onProgressUpdate?.invoke(null)
|
||||
|
||||
DownloadManager.STATUS_RUNNING ->
|
||||
onProgressUpdate?.invoke(getDownloadProgress(cursor))
|
||||
|
||||
DownloadManager.STATUS_SUCCESSFUL ->
|
||||
return Result.Success
|
||||
|
||||
DownloadManager.STATUS_FAILED -> {
|
||||
val reasonColumn = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
|
||||
val reason = cursor.getInt(reasonColumn)
|
||||
|
||||
return Result.Error(reason)
|
||||
}
|
||||
|
||||
else -> throw Error("Unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download progress of the current row in a [DownloadManager.Query].
|
||||
* @return Download progress in the range of `[0,1]`
|
||||
*/
|
||||
private fun getDownloadProgress(queryCursor: Cursor): Float {
|
||||
val bytesColumn = queryCursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||
val bytes = queryCursor.getLong(bytesColumn)
|
||||
|
||||
val totalBytesColumn = queryCursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||
val totalBytes = queryCursor.getLong(totalBytesColumn)
|
||||
|
||||
if (totalBytes <= 0) return 0f
|
||||
return bytes.toFloat() / totalBytes
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of a download after execution has been completed and the system-level [DownloadManager] has been cleaned up.
|
||||
*/
|
||||
sealed interface Result {
|
||||
data object Success : 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
|
||||
DownloadManager.ERROR_HTTP_DATA_ERROR,
|
||||
DownloadManager.ERROR_TOO_MANY_REDIRECTS,
|
||||
DownloadManager.ERROR_UNHANDLED_HTTP_CODE ->
|
||||
R.string.downloader_err_response
|
||||
|
||||
DownloadManager.ERROR_INSUFFICIENT_SPACE ->
|
||||
R.string.downloader_err_storage_space
|
||||
|
||||
DownloadManager.ERROR_FILE_ALREADY_EXISTS ->
|
||||
R.string.downloader_err_file_exists
|
||||
|
||||
else -> R.string.downloader_err_unknown
|
||||
} // @formatter:on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.aliucord.manager.domain.manager
|
||||
package com.aliucord.manager.manager
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.aliucord.manager.domain.manager.base.BasePreferenceManager
|
||||
import com.aliucord.manager.manager.base.BasePreferenceManager
|
||||
import com.aliucord.manager.ui.components.Theme
|
||||
|
||||
class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.aliucord.manager.domain.manager.base
|
||||
package com.aliucord.manager.manager.base
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -8,7 +8,7 @@ import cafe.adriel.voyager.core.model.ScreenModel
|
|||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.installer.util.uninstallApk
|
||||
import com.aliucord.manager.network.utils.fold
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import cafe.adriel.voyager.core.model.ScreenModel
|
|||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.DownloadManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.domain.repository.AliucordMavenRepository
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.installer.util.*
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.runtime.*
|
|||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.components.Theme
|
||||
import com.aliucord.manager.util.showToast
|
||||
import com.aliucord.manager.util.throttle
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.installer.util.installApks
|
||||
import com.aliucord.manager.network.utils.SemVer
|
||||
|
|
|
|||
|
|
@ -127,4 +127,9 @@
|
|||
|
||||
<string name="updater_body">A new update has been released for Aliucord Manager! It may be required in order to function properly. Would you like to update?</string>
|
||||
<string name="updater_title">Update to %1$s</string>
|
||||
|
||||
<string name="downloader_err_unknown">Download failed (Unknown)</string>
|
||||
<string name="downloader_err_response">Download failed (Invalid response)</string>
|
||||
<string name="downloader_err_file_exists">Download failed (File exists)</string>
|
||||
<string name="downloader_err_storage_space">Download failed (Insufficient space)</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue