diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 4c8d9ef7..095945e0 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -59,9 +59,12 @@ class MainActivity : ComponentActivity() { is Destination.Dashboard -> DashboardScreen( onSettingsClick = { navController.navigate(Destination.Settings()) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, - onUpdateClick = { navController.navigate( - Destination.Settings(SettingsDestination.Update()) - ) }, + onUpdateClick = { + navController.navigate(Destination.Settings(SettingsDestination.Update())) + }, + onDownloaderPluginClick = { + navController.navigate(Destination.Settings(SettingsDestination.Downloads)) + }, onAppClick = { installedApp -> navController.navigate( Destination.InstalledApplicationInfo( diff --git a/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt index 5bfc6624..f04bd4f5 100644 --- a/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt @@ -2,6 +2,7 @@ package app.revanced.manager.data.room.plugins import androidx.room.Dao import androidx.room.Query +import androidx.room.Transaction import androidx.room.Upsert @Dao @@ -14,4 +15,8 @@ interface TrustedDownloaderPluginDao { @Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName") suspend fun remove(packageName: String) + + @Transaction + @Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)") + suspend fun removeAll(packageNames: Set) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt index 8e87879c..87127e42 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -26,4 +26,6 @@ class PreferencesManager( val disableSelectionWarning = booleanPreference("disable_selection_warning", false) val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false) val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true) + + val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet()) } diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt index 57810a05..e78d26a9 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt @@ -26,6 +26,9 @@ abstract class BasePreferencesManager(private val context: Context, name: String protected fun stringPreference(key: String, default: String) = StringPreference(dataStore, key, default) + protected fun stringSetPreference(key: String, default: Set) = + StringSetPreference(dataStore, key, default) + protected fun booleanPreference(key: String, default: Boolean) = BooleanPreference(dataStore, key, default) @@ -52,6 +55,10 @@ class EditorContext(private val prefs: MutablePreferences) { var Preference.value get() = prefs.run { read() } set(value) = prefs.run { write(value) } + + operator fun Preference>.plusAssign(value: String) = prefs.run { + write(read() + value) + } } abstract class Preference( @@ -65,10 +72,12 @@ abstract class Preference( suspend fun get() = flow.first() fun getBlocking() = runBlocking { get() } + @Composable fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember { getBlocking() }) + suspend fun update(value: T) = dataStore.editor { this@Preference.value = value } @@ -108,6 +117,14 @@ class StringPreference( override val key = stringPreferencesKey(key) } +class StringSetPreference( + dataStore: DataStore, + key: String, + default: Set +) : BasePreference>(dataStore, default) { + override val key = stringSetPreferencesKey(key) +} + class BooleanPreference( dataStore: DataStore, key: String, diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt index 67fdbe97..b4f9583f 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt @@ -11,6 +11,7 @@ import androidx.paging.PagingState import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin +import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.network.downloader.DownloaderPluginState import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.ParceledDownloaderApp @@ -27,6 +28,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import java.io.File @@ -35,6 +37,7 @@ import java.lang.reflect.Modifier class DownloaderPluginRepository( private val pm: PM, private val fs: Filesystem, + private val prefs: PreferencesManager, private val context: Context, db: AppDatabase ) { @@ -45,6 +48,15 @@ class DownloaderPluginRepository( states.values.filterIsInstance().map { it.plugin } } + private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins + private val installedPluginPackageNames = MutableStateFlow(emptySet()) + val newPluginPackageNames = combine( + installedPluginPackageNames, + acknowledgedDownloaderPlugins.flow + ) { installed, acknowledged -> + installed subtract acknowledged + } + suspend fun reload() { val pluginPackages = withContext(Dispatchers.IO) { @@ -55,6 +67,15 @@ class DownloaderPluginRepository( } _pluginStates.value = pluginPackages.associate { it.packageName to loadPlugin(it) } + installedPluginPackageNames.value = pluginPackages.map { it.packageName }.toSet() + + val acknowledgedPlugins = acknowledgedDownloaderPlugins.get() + val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value + if (uninstalledPlugins.isNotEmpty()) { + Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}") + acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins) + trustDao.removeAll(uninstalledPlugins) + } } fun unwrapParceledApp(app: ParceledDownloaderApp): Pair { @@ -157,12 +178,19 @@ class DownloaderPluginRepository( pm.getSignatures(packageInfo).first().toCharsString() ) ) + reload() + prefs.edit { + acknowledgedDownloaderPlugins += packageInfo.packageName + } } suspend fun revokeTrustForPackage(packageName: String) = trustDao.remove(packageName).also { reload() } + suspend fun acknowledgeAllNewPlugins() = + acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value) + private suspend fun verify(packageInfo: PackageInfo): Boolean { val expectedSignature = trustDao.getTrustedSignature(packageInfo.packageName)?.let(::Signature) ?: return false diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 11681824..933f2f35 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -5,7 +5,7 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.activity.compose.BackHandler -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.BatteryAlert import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Source @@ -73,17 +74,21 @@ enum class DashboardPage( } @SuppressLint("BatteryLife") -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DashboardScreen( vm: DashboardViewModel = koinViewModel(), onAppSelectorClick: () -> Unit, onSettingsClick: () -> Unit, onUpdateClick: () -> Unit, + onDownloaderPluginClick: () -> Unit, onAppClick: (InstalledApp) -> Unit ) { val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) + val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle( + false + ) val androidContext = LocalContext.current val composableScope = rememberCoroutineScope() val pagerState = rememberPagerState( @@ -246,7 +251,21 @@ fun DashboardScreen( } ) } - } + }, + if (showNewDownloaderPluginsNotification) { + { + NotificationCard( + text = stringResource(R.string.new_downloader_plugins_notification), + icon = Icons.Outlined.Download, + modifier = Modifier.clickable(onClick = onDownloaderPluginClick), + actions = { + TextButton(onClick = vm::ignoreNewDownloaderPlugins) { + Text(stringResource(R.string.dismiss)) + } + } + ) + } + } else null ) HorizontalPager( diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index 9d2e1224..1d3e96a5 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -17,6 +17,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloaderPluginRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.util.toast @@ -28,6 +29,7 @@ import kotlinx.coroutines.launch class DashboardViewModel( private val app: Application, private val patchBundleRepository: PatchBundleRepository, + private val downloaderPluginRepository: DownloaderPluginRepository, private val reVancedAPI: ReVancedAPI, private val networkInfo: NetworkInfo, val prefs: PreferencesManager @@ -39,6 +41,8 @@ class DashboardViewModel( val sources = patchBundleRepository.sources val selectedSources = mutableStateListOf() + val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() } + var updatedManagerVersion: String? by mutableStateOf(null) private set var showBatteryOptimizationsWarning by mutableStateOf(false) @@ -52,6 +56,10 @@ class DashboardViewModel( } } + fun ignoreNewDownloaderPlugins() = viewModelScope.launch { + downloaderPluginRepository.acknowledgeAllNewPlugins() + } + fun dismissUpdateDialog() { updatedManagerVersion = null } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06b0a417..af9cfd7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Select patches Patching on ARMv7 devices is not yet supported and will most likely fail. + New downloader plugins available. Click here to configure them. Import Import patch bundle