feat: in-app updater (#25)

This commit is contained in:
Aunali321 2023-05-23 14:32:22 +05:30 committed by GitHub
parent 4584128f7d
commit 3de4d84484
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 238 additions and 27 deletions

View File

@ -4,6 +4,7 @@ import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.compose.ui.viewmodel.InstallerScreenViewModel
import app.revanced.manager.compose.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.compose.ui.viewmodel.SettingsViewModel
import app.revanced.manager.compose.ui.viewmodel.UpdateSettingsViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
@ -25,4 +26,5 @@ val viewModelModule = module {
signerService = get(),
)
}
viewModelOf(::UpdateSettingsViewModel)
}

View File

@ -1,12 +1,14 @@
package app.revanced.manager.compose.network.api
import android.app.Application
import android.os.Environment
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.revanced.manager.compose.domain.repository.ReVancedRepository
import app.revanced.manager.compose.util.ghIntegrations
import app.revanced.manager.compose.util.ghManager
import app.revanced.manager.compose.util.ghPatches
import app.revanced.manager.compose.util.tag
import app.revanced.manager.compose.util.toast
@ -24,11 +26,15 @@ class ManagerAPI(
private val revancedRepository: ReVancedRepository
) {
var downloadProgress: Float? by mutableStateOf(null)
var downloadedSize: Long? by mutableStateOf(null)
var totalSize: Long? by mutableStateOf(null)
private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
client.get(downloadUrl) {
onDownload { bytesSentTotal, contentLength ->
onDownload { bytesSentTotal, contentLength, ->
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}.bodyAsChannel().copyAndClose(saveLocation.writeChannel())
downloadProgress = null
@ -65,10 +71,20 @@ class ManagerAPI(
return null
}
suspend fun downloadManager(): File? {
try {
val managerAsset = revancedRepository.findAsset(ghManager, ".apk")
val managerFile = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).also { it.mkdirs() }
.resolve("revanced-manager.apk")
downloadAsset(managerAsset.downloadUrl, managerFile)
println("Downloaded manager at ${managerFile.absolutePath}")
return managerFile
} catch (e: Exception) {
Log.e(tag, "Failed to download manager", e)
app.toast("Failed to download manager")
}
return null
}
}
data class PatchesAsset(
val downloadUrl: String, val name: String
)
class MissingAssetException : Exception()

View File

@ -1,7 +1,7 @@
package app.revanced.manager.compose.network.service
import app.revanced.manager.compose.network.api.MissingAssetException
import app.revanced.manager.compose.network.api.PatchesAsset
import app.revanced.manager.compose.network.dto.Assets
import app.revanced.manager.compose.network.dto.ReVancedReleases
import app.revanced.manager.compose.network.dto.ReVancedRepositories
import app.revanced.manager.compose.network.utils.APIResponse
@ -30,12 +30,12 @@ class ReVancedService(
}
}
suspend fun findAsset(repo: String, file: String): PatchesAsset {
suspend fun findAsset(repo: String, file: String): Assets {
val releases = getAssets().getOrNull() ?: throw Exception("Cannot retrieve assets")
val asset = releases.tools.find { asset ->
(asset.name.contains(file) && asset.repository.contains(repo))
} ?: throw MissingAssetException()
return PatchesAsset(asset.downloadUrl, asset.name)
return Assets(asset.repository, asset.version, asset.timestamp, asset.name,asset.size, asset.downloadUrl, asset.content_type)
}
private companion object {

View File

@ -23,4 +23,7 @@ sealed interface SettingsDestination : Parcelable {
@Parcelize
object About : SettingsDestination
@Parcelize
object UpdateProgress : SettingsDestination
}

View File

@ -41,6 +41,7 @@ import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.destination.SettingsDestination
import app.revanced.manager.compose.ui.screen.settings.*
import app.revanced.manager.compose.ui.viewmodel.SettingsViewModel
import app.revanced.manager.compose.ui.viewmodel.UpdateSettingsViewModel
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.getViewModel
@ -99,7 +100,8 @@ fun SettingsScreen(
)
is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = { navController.pop() }
onBackClick = { navController.pop() },
navController = navController
)
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
@ -114,6 +116,10 @@ fun SettingsScreen(
onBackClick = { navController.pop() }
)
is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
{ navController.pop() },
)
is SettingsDestination.Settings -> {
Scaffold(
topBar = {
@ -136,7 +142,8 @@ fun SettingsScreen(
context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
})
showBatteryButton = !pm.isIgnoringBatteryOptimizations(context.packageName)
showBatteryButton =
!pm.isIgnoringBatteryOptimizations(context.packageName)
},
modifier = Modifier
.fillMaxWidth()
@ -151,16 +158,36 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(imageVector = Icons.Default.BatteryAlert, contentDescription = null, tint = MaterialTheme.colorScheme.onTertiaryContainer, modifier = Modifier.size(24.dp))
Text(text = stringResource(R.string.battery_optimization_notification), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onTertiaryContainer)
Icon(
imageVector = Icons.Default.BatteryAlert,
contentDescription = null,
tint = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.size(24.dp)
)
Text(
text = stringResource(R.string.battery_optimization_notification),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}
settingsSections.forEach { (titleDescIcon, destination) ->
ListItem(
modifier = Modifier.clickable { navController.navigate(destination) },
headlineContent = { Text(stringResource(titleDescIcon.first), style = MaterialTheme.typography.titleLarge) },
supportingContent = { Text(stringResource(titleDescIcon.second), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline) },
headlineContent = {
Text(
stringResource(titleDescIcon.first),
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = {
Text(
stringResource(titleDescIcon.second),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
},
leadingContent = { Icon(titleDescIcon.third, null) }
)
}
@ -168,5 +195,5 @@ fun SettingsScreen(
}
}
}
}
}
}

View File

@ -1,11 +1,13 @@
package app.revanced.manager.compose.ui.screen.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -14,33 +16,57 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.destination.SettingsDestination
import app.revanced.manager.compose.ui.viewmodel.UpdateSettingsViewModel
import dev.olshevski.navigation.reimagined.NavController
import dev.olshevski.navigation.reimagined.navigate
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun UpdatesSettingsScreen(
onBackClick: () -> Unit
onBackClick: () -> Unit,
navController: NavController<SettingsDestination>,
) {
val listItems = listOf(
Triple(stringResource(R.string.update_channel), stringResource(R.string.update_channel_description), third = { /*TODO*/ }),
Triple(stringResource(R.string.update_notifications), stringResource(R.string.update_notifications_description), third = { /*TODO*/ }),
Triple(stringResource(R.string.changelog), stringResource(R.string.changelog_description), third = { /*TODO*/ }),
Triple(
stringResource(R.string.update_channel),
stringResource(R.string.update_channel_description),
third = { /*TODO*/ }),
Triple(
stringResource(R.string.update_notifications),
stringResource(R.string.update_notifications_description),
third = { /*TODO*/ }),
Triple(
stringResource(R.string.changelog),
stringResource(R.string.changelog_description),
third = { /*TODO*/ }),
)
Scaffold(
topBar = {
AppTopBar(
@ -55,15 +81,31 @@ fun UpdatesSettingsScreen(
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
UpdateNotification()
UpdateNotification(
onClick = {
navController.navigate(SettingsDestination.UpdateProgress)
}
)
listItems.forEach { (title, description, onClick) ->
ListItem(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onClick() },
headlineContent = { Text(title, style = MaterialTheme.typography.titleLarge) },
supportingContent = { Text(description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline) }
headlineContent = {
Text(
title,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = {
Text(
description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
)
}
}
@ -71,13 +113,16 @@ fun UpdatesSettingsScreen(
}
@Composable
fun UpdateNotification() {
fun UpdateNotification(
onClick: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clip(RoundedCornerShape(24.dp))
.background(MaterialTheme.colorScheme.secondaryContainer)
.clickable { onClick() },
) {
Row(
modifier = Modifier
@ -87,7 +132,79 @@ fun UpdateNotification() {
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(imageVector = Icons.Default.Update, contentDescription = null)
Text(text = stringResource(R.string.update_notification), style = MaterialTheme.typography.bodyMedium)
Text(
text = stringResource(R.string.update_notification),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Stable
fun UpdateProgressScreen(
onBackClick: () -> Unit,
vm: UpdateSettingsViewModel = getViewModel()
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.updates),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp),
) {
var isInstalling by remember { mutableStateOf(false) }
isInstalling = vm.downloadProgress >= 100
Text(
text = if (isInstalling) stringResource(R.string.installing_manager_update) else stringResource(
R.string.downloading_manager_update
), style = MaterialTheme.typography.headlineMedium
)
LinearProgressIndicator(
progress = vm.downloadProgress / 100f,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth()
)
Text(
text = if (!isInstalling) "${vm.downloadedSize.div(1000000)} MB / ${vm.totalSize.div(1000000)} MB (${vm.downloadProgress.toInt()}%)" else stringResource(R.string.installing_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center
)
Text(
text = "This update adds many functionality and fixes many issues in Manager. New experiment toggles are also added, they can be found in Settings > Advanced. Please submit some feedback in Settings > About > Submit issues or feedback. Thank you, everyone!",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 32.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.weight(1f))
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
TextButton(
onClick = { /*TODO*/ },
) {
Text(text = stringResource(R.string.cancel))
}
Button(onClick = {
vm.installUpdate()
}) {
Text(text = stringResource(R.string.update))
}
}
}
}
}

View File

@ -0,0 +1,41 @@
package app.revanced.manager.compose.ui.viewmodel
import android.app.Application
import android.os.Environment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.compose.network.api.ManagerAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import app.revanced.manager.compose.util.PM
import java.io.File
class UpdateSettingsViewModel(
private val managerAPI: ManagerAPI,
private val app: Application,
) : ViewModel() {
val downloadProgress get() = (managerAPI.downloadProgress?.times(100)) ?: 0f
val downloadedSize get() = managerAPI.downloadedSize ?: 0L
val totalSize get() = managerAPI.totalSize ?: 0L
private fun downloadLatestManager() {
viewModelScope.launch(Dispatchers.IO) {
managerAPI.downloadManager()
}
}
fun installUpdate() {
PM.installApp(
apks = listOf(
File(
(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + "/revanced-manager.apk")
.toString())
),
),
context = app,
)
}
init {
downloadLatestManager()
}
}

View File

@ -92,4 +92,9 @@
<string name="changelog">Changelog</string>
<string name="changelog_description">Check out the latest changes in this update</string>
<string name="battery_optimization_notification">Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Tap here to turn off.</string>
<string name="installing_manager_update">Installing update…</string>
<string name="downloading_manager_update">Downloading update…</string>
<string name="cancel">Cancel</string>
<string name="update">Update</string>
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
</resources>