feat(Update Screen): changelogs & handle states (#1464)

Co-authored-by: Ax333l <main@axelen.xyz>
This commit is contained in:
Ushie 2023-11-19 23:28:28 +03:00 committed by GitHub
parent 62a5fce66c
commit bd9778a3d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 545 additions and 273 deletions

View File

@ -158,4 +158,7 @@ dependencies {
// Markdown
implementation(libs.markdown.renderer)
// Fading Edges
implementation(libs.fading.edges)
}

View File

@ -73,7 +73,7 @@ class MainActivity : ComponentActivity() {
TextButton(
onClick = {
vm.dismissUpdateDialog()
navController.navigate(Destination.Settings(SettingsDestination.UpdateProgress))
navController.navigate(Destination.Settings(SettingsDestination.Update(false)))
}
) {
Text(stringResource(R.string.update))
@ -85,8 +85,8 @@ class MainActivity : ComponentActivity() {
}
},
icon = { Icon(Icons.Outlined.Update, null) },
title = { Text(stringResource(R.string.update_available)) },
text = { Text(stringResource(R.string.update_available_description, it)) }
title = { Text(stringResource(R.string.update_available_dialog_title)) },
text = { Text(stringResource(R.string.update_available_dialog_description, it)) }
)
}

View File

@ -14,7 +14,7 @@ val viewModelModule = module {
viewModelOf(::AppSelectorViewModel)
viewModelOf(::VersionSelectorViewModel)
viewModelOf(::InstallerViewModel)
viewModelOf(::UpdateProgressViewModel)
viewModelOf(::UpdateViewModel)
viewModelOf(::ChangelogsViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::ContributorViewModel)

View File

@ -29,6 +29,7 @@ class InstallService : Service() {
else -> {
sendBroadcast(Intent().apply {
action = APP_INSTALL_ACTION
`package` = packageName
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)

View File

@ -31,7 +31,7 @@ class UninstallService : Service() {
else -> {
sendBroadcast(Intent().apply {
action = APP_UNINSTALL_ACTION
`package` = packageName
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
})

View File

@ -0,0 +1,95 @@
package app.revanced.manager.ui.component.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarToday
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import app.revanced.manager.ui.component.Markdown
@Composable
fun Changelog(
markdown: String,
version: String,
downloadCount: String,
publishDate: String
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 0.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Campaign,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(32.dp)
)
Text(
version.removePrefix("v"),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
color = MaterialTheme.colorScheme.primary,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
) {
Tag(
Icons.Outlined.Sell,
version
)
Tag(
Icons.Outlined.FileDownload,
downloadCount
)
Tag(
Icons.Outlined.CalendarToday,
publishDate
)
}
}
Markdown(
markdown,
)
}
@Composable
private fun Tag(icon: ImageVector, text: String) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.outline,
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
)
}
}

View File

@ -27,7 +27,7 @@ sealed interface SettingsDestination : Parcelable {
object About : SettingsDestination
@Parcelize
object UpdateProgress : SettingsDestination
data class Update(val downloadOnScreenEntry: Boolean) : SettingsDestination
@Parcelize
object Changelogs : SettingsDestination
@ -37,5 +37,4 @@ sealed interface SettingsDestination : Parcelable {
@Parcelize
object Licenses: SettingsDestination
}

View File

@ -28,15 +28,17 @@ import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
import app.revanced.manager.ui.screen.settings.update.UpdateProgressScreen
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.getViewModel
import app.revanced.manager.ui.component.settings.SettingsListItem
import org.koin.core.parameter.parametersOf
import org.koin.androidx.compose.getViewModel as getComposeViewModel
@SuppressLint("BatteryLife")
@OptIn(ExperimentalMaterial3Api::class)
@ -96,7 +98,6 @@ fun SettingsScreen(
controller = navController
) { destination ->
when (destination) {
is SettingsDestination.General -> GeneralSettingsScreen(
onBackClick = backClick,
viewModel = viewModel
@ -109,7 +110,7 @@ fun SettingsScreen(
is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = backClick,
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
)
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
@ -126,8 +127,13 @@ fun SettingsScreen(
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) }
)
is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
is SettingsDestination.Update -> UpdateScreen(
onBackClick = backClick,
vm = getComposeViewModel {
parametersOf(
destination.downloadOnScreenEntry
)
}
)
is SettingsDestination.Changelogs -> ChangelogsScreen(

View File

@ -1,37 +1,27 @@
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarToday
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.Markdown
import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
import app.revanced.manager.util.formatNumber
import app.revanced.manager.util.relativeTime
@ -103,76 +93,4 @@ fun ChangelogItem(
)
}
}
}
@Composable
private fun Changelog(
markdown: String,
version: String,
downloadCount: String,
publishDate: String
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 0.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Campaign,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(32.dp)
)
Text(
version.removePrefix("v"),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
color = MaterialTheme.colorScheme.primary,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
) {
Tag(
Icons.Outlined.Sell,
version
)
Tag(
Icons.Outlined.FileDownload,
downloadCount
)
Tag(
Icons.Outlined.CalendarToday,
publishDate
)
}
}
Markdown(
markdown,
)
}
@Composable
private fun Tag(icon: ImageVector, text: String) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.outline,
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
)
}
}

View File

@ -1,98 +0,0 @@
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.foundation.layout.Arrangement
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
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.viewmodel.UpdateProgressViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Stable
fun UpdateProgressScreen(
onBackClick: () -> Unit,
vm: UpdateProgressViewModel = 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)
.verticalScroll(rememberScrollState()),
) {
Text(
text = if (vm.isInstalling) stringResource(R.string.installing_manager_update) else stringResource(
R.string.downloading_manager_update
), style = MaterialTheme.typography.headlineMedium
)
LinearProgressIndicator(
progress = vm.downloadProgress,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth()
)
Text(
text = if (!vm.isInstalling) "${vm.downloadedSize.div(1000000)} MB / ${
vm.totalSize.div(
1000000
)
} MB (${vm.downloadProgress.times(100).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 = onBackClick,
) {
Text(text = stringResource(R.string.cancel))
}
Button(onClick = vm::installUpdate, enabled = vm.finished) {
Text(text = stringResource(R.string.update))
}
}
}
}
}

View File

@ -0,0 +1,239 @@
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel
import app.revanced.manager.ui.viewmodel.UpdateViewModel.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
import app.revanced.manager.util.formatNumber
import app.revanced.manager.util.relativeTime
import com.gigamole.composefadingedges.content.FadingEdgesContentType
import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig
import com.gigamole.composefadingedges.fill.FadingEdgesFillType
import com.gigamole.composefadingedges.verticalFadingEdges
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Stable
fun UpdateScreen(
onBackClick: () -> Unit,
vm: UpdateViewModel = getViewModel()
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.updates),
onBackClick = onBackClick
)
}
) { paddingValues ->
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
MeteredDownloadConfirmationDialog(
onDismiss = { vm.showInternetCheckDialog = false },
onDownloadAnyways = { vm.downloadUpdate(true) }
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
Header(
vm.state,
vm.changelog,
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
)
vm.changelog?.let { changelog ->
Divider()
Changelog(changelog)
} ?: Spacer(modifier = Modifier.weight(1f))
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick)
}
}
}
@Composable
private fun MeteredDownloadConfirmationDialog(
onDismiss: () -> Unit,
onDownloadAnyways: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
TextButton(onDismiss) {
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
onDismiss()
onDownloadAnyways()
}
) {
Text(stringResource(R.string.download))
}
},
title = { Text(stringResource(R.string.download_update_confirmation)) },
icon = { Icon(Icons.Outlined.Update, null) },
text = { Text(stringResource(R.string.download_confirmation_metered)) }
)
}
@Composable
private fun Header(state: State, changelog: Changelog?, downloadData: DownloadData) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
text = stringResource(state.title),
style = MaterialTheme.typography.headlineMedium
)
if (state == State.CAN_DOWNLOAD) {
Column {
Text(
text = stringResource(
id = R.string.current_version,
BuildConfig.VERSION_NAME
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
changelog?.let { changelog ->
Text(
text = stringResource(
id = R.string.new_version,
changelog.version.replace("v", "")
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else if (state == State.DOWNLOADING) {
LinearProgressIndicator(
progress = downloadData.downloadProgress,
modifier = Modifier.fillMaxWidth()
)
Text(
text =
"${downloadData.downloadedSize.div(1000000)} MB / ${
downloadData.totalSize.div(
1000000
)
} MB (${
downloadData.downloadProgress.times(
100
).toInt()
}%)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
}
@Composable
private fun ColumnScope.Changelog(changelog: Changelog) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(scrollState)
.verticalFadingEdges(
fillType = FadingEdgesFillType.FadeColor(
color = MaterialTheme.colorScheme.background,
fillStops = Triple(0F, 0.55F, 1F),
secondStopAlpha = 1F
),
contentType = FadingEdgesContentType.Dynamic.Scroll(
state = scrollState,
scrollConfig = FadingEdgesScrollConfig.Dynamic(
animationSpec = spring(),
isLerpByDifferenceForPartialContent = true,
scrollFactor = 1.25F
)
),
length = 350.dp
)
) {
Changelog(
markdown = changelog.body.replace("`", ""),
version = changelog.version,
downloadCount = changelog.downloadCount.formatNumber(),
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
)
}
}
@Composable
private fun Buttons(
state: State,
onDownloadClick: () -> Unit,
onInstallClick: () -> Unit,
onBackClick: () -> Unit
) {
Row(modifier = Modifier.fillMaxWidth()) {
if (state.showCancel) {
TextButton(
onClick = onBackClick,
) {
Text(text = stringResource(R.string.cancel))
}
}
Spacer(modifier = Modifier.weight(1f))
if (state == State.CAN_DOWNLOAD) {
Button(onClick = onDownloadClick) {
Text(text = stringResource(R.string.update))
}
} else if (state == State.CAN_INSTALL) {
Button(
onClick = onInstallClick
) {
Text(text = stringResource(R.string.install_app))
}
}
}
}
data class DownloadData(
val downloadProgress: Float,
val downloadedSize: Long,
val totalSize: Long
)

View File

@ -1,75 +0,0 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.APK_MIMETYPE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import app.revanced.manager.util.PM
import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import kotlinx.coroutines.withContext
import java.io.File
class UpdateProgressViewModel(
app: Application,
private val reVancedAPI: ReVancedAPI,
private val http: HttpService,
private val pm: PM
) : ViewModel() {
var downloadedSize by mutableStateOf(0L)
private set
var totalSize by mutableStateOf(0L)
private set
val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
downloadedSize.toFloat() / totalSize.toFloat()
}
val isInstalling by derivedStateOf { downloadProgress >= 1 }
var finished by mutableStateOf(false)
private set
private val location = File.createTempFile("updater", ".apk", app.cacheDir)
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
withContext(Dispatchers.IO) {
val asset = reVancedAPI
.getLatestRelease("revanced-manager")
.getOrThrow()
.findAssetByType(APK_MIMETYPE)
http.download(location) {
url(asset.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}
}
finished = true
}
}
fun installUpdate() = viewModelScope.launch {
pm.installApp(listOf(location))
}
override fun onCleared() {
super.onCleared()
job.cancel()
location.delete()
}
}

View File

@ -0,0 +1,170 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.dto.ReVancedRelease
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.PM
import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
class UpdateViewModel(
private val downloadOnScreenEntry: Boolean
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val reVancedAPI: ReVancedAPI by inject()
private val http: HttpService by inject()
private val pm: PM by inject()
private val networkInfo: NetworkInfo by inject()
var downloadedSize by mutableStateOf(0L)
private set
var totalSize by mutableStateOf(0L)
private set
val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
downloadedSize.toFloat() / totalSize.toFloat()
}
var showInternetCheckDialog by mutableStateOf(false)
var state by mutableStateOf(State.CAN_DOWNLOAD)
private set
var installError by mutableStateOf("")
var changelog: Changelog? by mutableStateOf(null)
private val location = File.createTempFile("updater", ".apk", app.cacheDir)
private var release: ReVancedRelease? = null
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
withContext(Dispatchers.IO) {
val response = reVancedAPI
.getLatestRelease("revanced-manager")
.getOrThrow()
release = response
changelog = Changelog(
response.metadata.tag,
response.findAssetByType(APK_MIMETYPE).downloadCount,
response.metadata.publishedAt,
response.metadata.body
)
}
if (downloadOnScreenEntry) {
downloadUpdate()
} else {
state = State.CAN_DOWNLOAD
}
}
}
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
withContext(Dispatchers.IO) {
if (!networkInfo.isSafe() && !ignoreInternetCheck) {
showInternetCheckDialog = true
} else {
state = State.DOWNLOADING
val asset = release?.findAssetByType(APK_MIMETYPE)
?: throw Exception("couldn't find asset to download")
http.download(location) {
url(asset.downloadUrl)
onDownload { bytesSentTotal, contentLength ->
downloadedSize = bytesSentTotal
totalSize = contentLength
}
}
state = State.CAN_INSTALL
}
}
}
}
fun installUpdate() = viewModelScope.launch {
state = State.INSTALLING
pm.installApp(listOf(location))
}
private val installBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
val extra =
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
app.toast(app.getString(R.string.install_app_success))
state = State.SUCCESS
} else {
state = State.FAILED
// TODO: handle install fail with a popup
installError = extra
app.toast(app.getString(R.string.install_app_fail, extra))
}
}
}
}
init {
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
}, ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(installBroadcastReceiver)
job.cancel()
location.delete()
}
data class Changelog(
val version: String,
val downloadCount: Int,
val publishDate: String,
val body: String,
)
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update, true),
CAN_INSTALL(R.string.ready_to_install_update, true),
INSTALLING(R.string.installing_manager_update),
FAILED(R.string.install_update_manager_failed),
SUCCESS(R.string.update_completed)
}
}

View File

@ -32,10 +32,10 @@
<string name="patch_selector_item">Patch selection and options</string>
<string name="patch_selector_item_description">%d patches selected</string>
<string name="no_patches_selected">No patches selected</string>
<string name="version_selector_item">Change version</string>
<string name="version_selector_item_description">%s selected</string>
<string name="legacy_import_failed">Could not import legacy settings</string>
<string name="auto_updates_dialog_title">Select updates to receive</string>
@ -273,6 +273,12 @@
<string name="bundle_type_description">Choose the type of bundle you want</string>
<string name="about_revanced_manager">About ReVanced Manager</string>
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
<string name="update_available">An update is available</string>
<string name="current_version">Current version: %s</string>
<string name="new_version">New version: %s</string>
<string name="ready_to_install_update">Ready to install update</string>
<string name="update_completed">Update installed</string>
<string name="install_update_manager_failed">Failed to install update</string>
<string name="update_notification">A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!</string>
<string name="update_channel">Update channel</string>
<string name="update_channel_description">Stable</string>
@ -300,7 +306,11 @@
<string name="failed_to_check_updates">Failed to check for updates</string>
<string name="dismiss_temporary">Not now</string>
<string name="update_available">New update available</string>
<string name="update_available_description">A new version (%s) is available for download.</string>
<string name="update_available_dialog_title">New update available</string>
<string name="update_available_dialog_description">A new version (%s) is available for download.</string>
<string name="failed_to_download_update">Failed to download update: %s</string>
<string name="download">Download</string>
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>
<string name="download_update_confirmation">Download update?</string>
<string name="no_contributors_found">No contributors found</string>
</resources>

View File

@ -18,6 +18,7 @@ koin-version-compose = "3.4.6"
reimagined-navigation = "1.5.0"
ktor = "2.3.3"
markdown-renderer = "0.8.0"
fading-edges = "1.0.4"
androidGradlePlugin = "8.1.2"
kotlinGradlePlugin = "1.9.10"
devToolsGradlePlugin = "1.9.10-1.0.13"
@ -95,6 +96,9 @@ skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.
# Markdown
markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-android", version.ref = "markdown-renderer" }
# Fading Edges
fading-edges = { group = "com.github.GIGAMOLE", name = "ComposeFadingEdges", version.ref = "fading-edges"}
# LibSU
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }