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

View File

@ -1,12 +1,14 @@
package app.revanced.manager.compose.network.api package app.revanced.manager.compose.network.api
import android.app.Application import android.app.Application
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import app.revanced.manager.compose.domain.repository.ReVancedRepository import app.revanced.manager.compose.domain.repository.ReVancedRepository
import app.revanced.manager.compose.util.ghIntegrations 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.ghPatches
import app.revanced.manager.compose.util.tag import app.revanced.manager.compose.util.tag
import app.revanced.manager.compose.util.toast import app.revanced.manager.compose.util.toast
@ -24,11 +26,15 @@ class ManagerAPI(
private val revancedRepository: ReVancedRepository private val revancedRepository: ReVancedRepository
) { ) {
var downloadProgress: Float? by mutableStateOf(null) 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) { private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
client.get(downloadUrl) { client.get(downloadUrl) {
onDownload { bytesSentTotal, contentLength -> onDownload { bytesSentTotal, contentLength, ->
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat()) downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
downloadedSize = bytesSentTotal
totalSize = contentLength
} }
}.bodyAsChannel().copyAndClose(saveLocation.writeChannel()) }.bodyAsChannel().copyAndClose(saveLocation.writeChannel())
downloadProgress = null downloadProgress = null
@ -65,10 +71,20 @@ class ManagerAPI(
return null 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() class MissingAssetException : Exception()

View File

@ -1,7 +1,7 @@
package app.revanced.manager.compose.network.service package app.revanced.manager.compose.network.service
import app.revanced.manager.compose.network.api.MissingAssetException 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.ReVancedReleases
import app.revanced.manager.compose.network.dto.ReVancedRepositories import app.revanced.manager.compose.network.dto.ReVancedRepositories
import app.revanced.manager.compose.network.utils.APIResponse 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 releases = getAssets().getOrNull() ?: throw Exception("Cannot retrieve assets")
val asset = releases.tools.find { asset -> val asset = releases.tools.find { asset ->
(asset.name.contains(file) && asset.repository.contains(repo)) (asset.name.contains(file) && asset.repository.contains(repo))
} ?: throw MissingAssetException() } ?: 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 { private companion object {

View File

@ -23,4 +23,7 @@ sealed interface SettingsDestination : Parcelable {
@Parcelize @Parcelize
object About : SettingsDestination 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.destination.SettingsDestination
import app.revanced.manager.compose.ui.screen.settings.* import app.revanced.manager.compose.ui.screen.settings.*
import app.revanced.manager.compose.ui.viewmodel.SettingsViewModel import app.revanced.manager.compose.ui.viewmodel.SettingsViewModel
import app.revanced.manager.compose.ui.viewmodel.UpdateSettingsViewModel
import dev.olshevski.navigation.reimagined.* import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@ -99,7 +100,8 @@ fun SettingsScreen(
) )
is SettingsDestination.Updates -> UpdatesSettingsScreen( is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = { navController.pop() } onBackClick = { navController.pop() },
navController = navController
) )
is SettingsDestination.Downloads -> DownloadsSettingsScreen( is SettingsDestination.Downloads -> DownloadsSettingsScreen(
@ -114,6 +116,10 @@ fun SettingsScreen(
onBackClick = { navController.pop() } onBackClick = { navController.pop() }
) )
is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
{ navController.pop() },
)
is SettingsDestination.Settings -> { is SettingsDestination.Settings -> {
Scaffold( Scaffold(
topBar = { topBar = {
@ -136,7 +142,8 @@ fun SettingsScreen(
context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}") data = Uri.parse("package:${context.packageName}")
}) })
showBatteryButton = !pm.isIgnoringBatteryOptimizations(context.packageName) showBatteryButton =
!pm.isIgnoringBatteryOptimizations(context.packageName)
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -151,16 +158,36 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Icon(imageVector = Icons.Default.BatteryAlert, contentDescription = null, tint = MaterialTheme.colorScheme.onTertiaryContainer, modifier = Modifier.size(24.dp)) Icon(
Text(text = stringResource(R.string.battery_optimization_notification), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onTertiaryContainer) 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) -> settingsSections.forEach { (titleDescIcon, destination) ->
ListItem( ListItem(
modifier = Modifier.clickable { navController.navigate(destination) }, modifier = Modifier.clickable { navController.navigate(destination) },
headlineContent = { Text(stringResource(titleDescIcon.first), style = MaterialTheme.typography.titleLarge) }, headlineContent = {
supportingContent = { Text(stringResource(titleDescIcon.second), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline) }, 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) } 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 package app.revanced.manager.compose.ui.screen.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -14,33 +16,57 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppTopBar 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 @Composable
fun UpdatesSettingsScreen( fun UpdatesSettingsScreen(
onBackClick: () -> Unit onBackClick: () -> Unit,
navController: NavController<SettingsDestination>,
) { ) {
val listItems = listOf( val listItems = listOf(
Triple(stringResource(R.string.update_channel), stringResource(R.string.update_channel_description), third = { /*TODO*/ }), Triple(
Triple(stringResource(R.string.update_notifications), stringResource(R.string.update_notifications_description), third = { /*TODO*/ }), stringResource(R.string.update_channel),
Triple(stringResource(R.string.changelog), stringResource(R.string.changelog_description), third = { /*TODO*/ }), 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( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
@ -55,15 +81,31 @@ fun UpdatesSettingsScreen(
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
UpdateNotification() UpdateNotification(
onClick = {
navController.navigate(SettingsDestination.UpdateProgress)
}
)
listItems.forEach { (title, description, onClick) -> listItems.forEach { (title, description, onClick) ->
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
.clickable { onClick() }, .clickable { onClick() },
headlineContent = { Text(title, style = MaterialTheme.typography.titleLarge) }, headlineContent = {
supportingContent = { Text(description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline) } Text(
title,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = {
Text(
description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
) )
} }
} }
@ -71,13 +113,16 @@ fun UpdatesSettingsScreen(
} }
@Composable @Composable
fun UpdateNotification() { fun UpdateNotification(
onClick: () -> Unit
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
.background(MaterialTheme.colorScheme.secondaryContainer) .background(MaterialTheme.colorScheme.secondaryContainer)
.clickable { onClick() },
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -87,7 +132,79 @@ fun UpdateNotification() {
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Icon(imageVector = Icons.Default.Update, contentDescription = null) 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">Changelog</string>
<string name="changelog_description">Check out the latest changes in this update</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="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> </resources>