mirror of
https://github.com/revanced/revanced-manager-compose
synced 2024-11-16 08:19:25 +01:00
feat: in-app updater (#25)
This commit is contained in:
parent
4584128f7d
commit
3de4d84484
@ -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)
|
||||
}
|
||||
|
@ -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()
|
@ -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 {
|
||||
|
@ -23,4 +23,7 @@ sealed interface SettingsDestination : Parcelable {
|
||||
@Parcelize
|
||||
object About : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
object UpdateProgress : SettingsDestination
|
||||
|
||||
}
|
@ -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(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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>
|
Loading…
Reference in New Issue
Block a user