mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-09 17:44:26 +02:00
feat: add dashboard notification for new plugins
This commit is contained in:
parent
2355dc62aa
commit
7ec3be460b
@ -59,9 +59,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
is Destination.Dashboard -> DashboardScreen(
|
is Destination.Dashboard -> DashboardScreen(
|
||||||
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
||||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
||||||
onUpdateClick = { navController.navigate(
|
onUpdateClick = {
|
||||||
Destination.Settings(SettingsDestination.Update())
|
navController.navigate(Destination.Settings(SettingsDestination.Update()))
|
||||||
) },
|
},
|
||||||
|
onDownloaderPluginClick = {
|
||||||
|
navController.navigate(Destination.Settings(SettingsDestination.Downloads))
|
||||||
|
},
|
||||||
onAppClick = { installedApp ->
|
onAppClick = { installedApp ->
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
Destination.InstalledApplicationInfo(
|
Destination.InstalledApplicationInfo(
|
||||||
|
@ -2,6 +2,7 @@ package app.revanced.manager.data.room.plugins
|
|||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@ -14,4 +15,8 @@ interface TrustedDownloaderPluginDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
suspend fun remove(packageName: String)
|
suspend fun remove(packageName: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
|
||||||
|
suspend fun removeAll(packageNames: Set<String>)
|
||||||
}
|
}
|
@ -26,4 +26,6 @@ class PreferencesManager(
|
|||||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
||||||
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||||
|
|
||||||
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,9 @@ abstract class BasePreferencesManager(private val context: Context, name: String
|
|||||||
protected fun stringPreference(key: String, default: String) =
|
protected fun stringPreference(key: String, default: String) =
|
||||||
StringPreference(dataStore, key, default)
|
StringPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected fun stringSetPreference(key: String, default: Set<String>) =
|
||||||
|
StringSetPreference(dataStore, key, default)
|
||||||
|
|
||||||
protected fun booleanPreference(key: String, default: Boolean) =
|
protected fun booleanPreference(key: String, default: Boolean) =
|
||||||
BooleanPreference(dataStore, key, default)
|
BooleanPreference(dataStore, key, default)
|
||||||
|
|
||||||
@ -52,6 +55,10 @@ class EditorContext(private val prefs: MutablePreferences) {
|
|||||||
var <T> Preference<T>.value
|
var <T> Preference<T>.value
|
||||||
get() = prefs.run { read() }
|
get() = prefs.run { read() }
|
||||||
set(value) = prefs.run { write(value) }
|
set(value) = prefs.run { write(value) }
|
||||||
|
|
||||||
|
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
|
||||||
|
write(read() + value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Preference<T>(
|
abstract class Preference<T>(
|
||||||
@ -65,10 +72,12 @@ abstract class Preference<T>(
|
|||||||
|
|
||||||
suspend fun get() = flow.first()
|
suspend fun get() = flow.first()
|
||||||
fun getBlocking() = runBlocking { get() }
|
fun getBlocking() = runBlocking { get() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
||||||
getBlocking()
|
getBlocking()
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun update(value: T) = dataStore.editor {
|
suspend fun update(value: T) = dataStore.editor {
|
||||||
this@Preference.value = value
|
this@Preference.value = value
|
||||||
}
|
}
|
||||||
@ -108,6 +117,14 @@ class StringPreference(
|
|||||||
override val key = stringPreferencesKey(key)
|
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(
|
class BooleanPreference(
|
||||||
dataStore: DataStore<Preferences>,
|
dataStore: DataStore<Preferences>,
|
||||||
key: String,
|
key: String,
|
||||||
|
@ -11,6 +11,7 @@ import androidx.paging.PagingState
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.data.room.AppDatabase
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
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.DownloaderPluginState
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderApp
|
import app.revanced.manager.network.downloader.ParceledDownloaderApp
|
||||||
@ -27,6 +28,7 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -35,6 +37,7 @@ import java.lang.reflect.Modifier
|
|||||||
class DownloaderPluginRepository(
|
class DownloaderPluginRepository(
|
||||||
private val pm: PM,
|
private val pm: PM,
|
||||||
private val fs: Filesystem,
|
private val fs: Filesystem,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
db: AppDatabase
|
db: AppDatabase
|
||||||
) {
|
) {
|
||||||
@ -45,6 +48,15 @@ class DownloaderPluginRepository(
|
|||||||
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
|
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() {
|
suspend fun reload() {
|
||||||
val pluginPackages =
|
val pluginPackages =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@ -55,6 +67,15 @@ class DownloaderPluginRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pluginStates.value = pluginPackages.associate { it.packageName to loadPlugin(it) }
|
_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> {
|
fun unwrapParceledApp(app: ParceledDownloaderApp): Pair<LoadedDownloaderPlugin, App> {
|
||||||
@ -157,12 +178,19 @@ class DownloaderPluginRepository(
|
|||||||
pm.getSignatures(packageInfo).first().toCharsString()
|
pm.getSignatures(packageInfo).first().toCharsString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
reload()
|
reload()
|
||||||
|
prefs.edit {
|
||||||
|
acknowledgedDownloaderPlugins += packageInfo.packageName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun revokeTrustForPackage(packageName: String) =
|
suspend fun revokeTrustForPackage(packageName: String) =
|
||||||
trustDao.remove(packageName).also { reload() }
|
trustDao.remove(packageName).also { reload() }
|
||||||
|
|
||||||
|
suspend fun acknowledgeAllNewPlugins() =
|
||||||
|
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
|
||||||
|
|
||||||
private suspend fun verify(packageInfo: PackageInfo): Boolean {
|
private suspend fun verify(packageInfo: PackageInfo): Boolean {
|
||||||
val expectedSignature =
|
val expectedSignature =
|
||||||
trustDao.getTrustedSignature(packageInfo.packageName)?.let(::Signature) ?: return false
|
trustDao.getTrustedSignature(packageInfo.packageName)?.let(::Signature) ?: return false
|
||||||
|
@ -5,7 +5,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.BackHandler
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.filled.Close
|
||||||
import androidx.compose.material.icons.outlined.Apps
|
import androidx.compose.material.icons.outlined.Apps
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
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.Refresh
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.Source
|
import androidx.compose.material.icons.outlined.Source
|
||||||
@ -73,17 +74,21 @@ enum class DashboardPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(
|
fun DashboardScreen(
|
||||||
vm: DashboardViewModel = koinViewModel(),
|
vm: DashboardViewModel = koinViewModel(),
|
||||||
onAppSelectorClick: () -> Unit,
|
onAppSelectorClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onUpdateClick: () -> Unit,
|
onUpdateClick: () -> Unit,
|
||||||
|
onDownloaderPluginClick: () -> Unit,
|
||||||
onAppClick: (InstalledApp) -> Unit
|
onAppClick: (InstalledApp) -> Unit
|
||||||
) {
|
) {
|
||||||
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
||||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
|
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||||
|
false
|
||||||
|
)
|
||||||
val androidContext = LocalContext.current
|
val androidContext = LocalContext.current
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(
|
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(
|
HorizontalPager(
|
||||||
|
@ -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.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
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.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
@ -28,6 +29,7 @@ import kotlinx.coroutines.launch
|
|||||||
class DashboardViewModel(
|
class DashboardViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
private val patchBundleRepository: PatchBundleRepository,
|
||||||
|
private val downloaderPluginRepository: DownloaderPluginRepository,
|
||||||
private val reVancedAPI: ReVancedAPI,
|
private val reVancedAPI: ReVancedAPI,
|
||||||
private val networkInfo: NetworkInfo,
|
private val networkInfo: NetworkInfo,
|
||||||
val prefs: PreferencesManager
|
val prefs: PreferencesManager
|
||||||
@ -39,6 +41,8 @@ class DashboardViewModel(
|
|||||||
val sources = patchBundleRepository.sources
|
val sources = patchBundleRepository.sources
|
||||||
val selectedSources = mutableStateListOf<PatchBundleSource>()
|
val selectedSources = mutableStateListOf<PatchBundleSource>()
|
||||||
|
|
||||||
|
val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||||
|
|
||||||
var updatedManagerVersion: String? by mutableStateOf(null)
|
var updatedManagerVersion: String? by mutableStateOf(null)
|
||||||
private set
|
private set
|
||||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||||
@ -52,6 +56,10 @@ class DashboardViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ignoreNewDownloaderPlugins() = viewModelScope.launch {
|
||||||
|
downloaderPluginRepository.acknowledgeAllNewPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
fun dismissUpdateDialog() {
|
fun dismissUpdateDialog() {
|
||||||
updatedManagerVersion = null
|
updatedManagerVersion = null
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
<string name="select_patches">Select patches</string>
|
<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="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_">Import</string>
|
||||||
<string name="import_bundle">Import patch bundle</string>
|
<string name="import_bundle">Import patch bundle</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user