feat: add dashboard notification for new plugins

This commit is contained in:
Ax333l 2024-07-15 00:39:09 +02:00
parent 2355dc62aa
commit 7ec3be460b
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
8 changed files with 89 additions and 6 deletions

View File

@ -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(

View File

@ -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<String>)
}

View File

@ -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())
}

View File

@ -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<String>) =
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 <T> Preference<T>.value
get() = prefs.run { read() }
set(value) = prefs.run { write(value) }
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
write(read() + value)
}
}
abstract class Preference<T>(
@ -65,10 +72,12 @@ abstract class Preference<T>(
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<Preferences>,
key: String,
default: Set<String>
) : BasePreference<Set<String>>(dataStore, default) {
override val key = stringSetPreferencesKey(key)
}
class BooleanPreference(
dataStore: DataStore<Preferences>,
key: String,

View File

@ -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<DownloaderPluginState.Loaded>().map { it.plugin }
}
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
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<LoadedDownloaderPlugin, App> {
@ -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

View File

@ -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(

View File

@ -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<PatchBundleSource>()
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
}

View File

@ -12,6 +12,7 @@
<string name="select_patches">Select patches</string>
<string name="unsupported_architecture_warning">Patching on ARMv7 devices is not yet supported and will most likely fail.</string>
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
<string name="import_">Import</string>
<string name="import_bundle">Import patch bundle</string>