diff --git a/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt index a0ad114..a11ec2e 100644 --- a/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt @@ -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) } diff --git a/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt b/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt index 1f19ffb..2d5e766 100644 --- a/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt +++ b/app/src/main/java/app/revanced/manager/compose/network/api/ManagerAPI.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt b/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt index e6d6560..a05ff25 100644 --- a/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt +++ b/app/src/main/java/app/revanced/manager/compose/network/service/ReVancedService.kt @@ -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 { diff --git a/app/src/main/java/app/revanced/manager/compose/ui/destination/SettingsDestination.kt b/app/src/main/java/app/revanced/manager/compose/ui/destination/SettingsDestination.kt index 358d84e..39eb5dd 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/destination/SettingsDestination.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/destination/SettingsDestination.kt @@ -23,4 +23,7 @@ sealed interface SettingsDestination : Parcelable { @Parcelize object About : SettingsDestination + @Parcelize + object UpdateProgress : SettingsDestination + } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/compose/ui/screen/SettingsScreen.kt index 232b266..16dea6a 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/screen/SettingsScreen.kt @@ -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( } } } -} + } } diff --git a/app/src/main/java/app/revanced/manager/compose/ui/screen/settings/UpdatesSettingsScreen.kt b/app/src/main/java/app/revanced/manager/compose/ui/screen/settings/UpdatesSettingsScreen.kt index 3227a13..f46a9dd 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/screen/settings/UpdatesSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/screen/settings/UpdatesSettingsScreen.kt @@ -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, ) { - 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)) + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt new file mode 100644 index 0000000..afc9ede --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e5359a..aa218a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,4 +92,9 @@ Changelog Check out the latest changes in this update Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Tap here to turn off. + Installing update… + Downloading update… + Cancel + Update + Tap on Update when prompted. \n ReVanced Manager will close when updating. \ No newline at end of file