Skip to content

Commit

Permalink
feat: better download manager
Browse files Browse the repository at this point in the history
  • Loading branch information
rushiiMachine committed Jan 19, 2024
1 parent b47a1b4 commit e5b0307
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 125 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}\"")
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/kotlin/com/aliucord/manager/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/kotlin/com/aliucord/manager/di/Managers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

This file was deleted.

184 changes: 184 additions & 0 deletions app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

0 comments on commit e5b0307

Please sign in to comment.