diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/AKnownPkg.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/AKnownPkg.kt index d54b8c4a5..c111d6270 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/AKnownPkg.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/AKnownPkg.kt @@ -38,37 +38,24 @@ sealed class AKnownPkg(override val id: Pkg.Id) : Pkg { ContextCompat.getDrawable(context, R.drawable.ic_default_app_icon_24)!! } - data object AndroidSystem : AKnownPkg("android") { - override val labelRes: Int = R.string.apps_known_android_system_label - } + data object AndroidSystem : AKnownPkg("android") data object GooglePlay : AKnownPkg("com.android.vending"), AppStore { - override val labelRes: Int = R.string.apps_known_installer_gplay_label override val iconRes: Int = R.drawable.ic_baseline_gplay_24 override val urlGenerator: ((Pkg.Id) -> String) = { "https://play.google.com/store/apps/details?id=${it.name}" } } - data object VivoAppStore : AKnownPkg("com.vivo.appstore"), AppStore { - override val labelRes: Int = R.string.apps_known_installer_vivo_label - } + data object VivoAppStore : AKnownPkg("com.vivo.appstore"), AppStore - data object OppoMarket : AKnownPkg("com.oppo.market"), AppStore { - override val labelRes: Int = R.string.apps_known_installer_oppo_label - } + data object OppoMarket : AKnownPkg("com.oppo.market"), AppStore - data object HuaweiAppGallery : AKnownPkg("com.huawei.appmarket"), AppStore { - override val labelRes: Int = R.string.apps_known_installer_huawei_label - } + data object HuaweiAppGallery : AKnownPkg("com.huawei.appmarket"), AppStore - data object SamsungAppStore : AKnownPkg("com.sec.android.app.samsungapps"), AppStore { - override val labelRes: Int = R.string.apps_known_installer_samsung_label - } + data object SamsungAppStore : AKnownPkg("com.sec.android.app.samsungapps"), AppStore - data object XiaomiAppStore : AKnownPkg("com.xiaomi.mipicks"), AppStore { - override val labelRes: Int = R.string.apps_known_installer_xiaomi_label - } + data object XiaomiAppStore : AKnownPkg("com.xiaomi.mipicks"), AppStore companion object { val values: List = listOf( diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/pkgops/IllegalPkgDataException.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/pkgops/IllegalPkgDataException.kt deleted file mode 100644 index 37c7f2745..000000000 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/pkgs/pkgops/IllegalPkgDataException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package eu.darken.sdmse.common.pkgs.pkgops - -class IllegalPkgDataException( - override val message: String -) : IllegalStateException() \ No newline at end of file diff --git a/app-common-io/src/main/res/values/strings.xml b/app-common-io/src/main/res/values/strings.xml deleted file mode 100644 index 91fb6ace4..000000000 --- a/app-common-io/src/main/res/values/strings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - Google Play Store - Vivo V-AppStore - Oppo App Market - Huawei AppGallery - Samsung Galaxy Store - Xiaomi GetApps - Android System - \ No newline at end of file diff --git a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/InvalidPkgInventoryException.kt b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/InvalidPkgInventoryException.kt new file mode 100644 index 000000000..139efcd20 --- /dev/null +++ b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/InvalidPkgInventoryException.kt @@ -0,0 +1,16 @@ +package eu.darken.sdmse.common.pkgs + +import eu.darken.sdmse.common.ca.toCaString +import eu.darken.sdmse.common.error.HasLocalizedError +import eu.darken.sdmse.common.error.LocalizedError + +class InvalidPkgInventoryException( + override val message: String +) : IllegalStateException(), HasLocalizedError { + + override fun getLocalizedError(): LocalizedError = LocalizedError( + throwable = this, + label = R.string.pkgrepo_invalid_inventory_error_title.toCaString(), + description = R.string.pkgrepo_invalid_inventory_error_description.toCaString(), + ) +} \ No newline at end of file diff --git a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepo.kt b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepo.kt index c7f50f95c..ef862d6ca 100644 --- a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepo.kt +++ b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepo.kt @@ -2,13 +2,17 @@ package eu.darken.sdmse.common.pkgs import eu.darken.sdmse.common.collections.mutate import eu.darken.sdmse.common.coroutine.AppScope +import eu.darken.sdmse.common.coroutine.DispatcherProvider import eu.darken.sdmse.common.debug.Bugs import eu.darken.sdmse.common.debug.logging.Logging.Priority.DEBUG +import eu.darken.sdmse.common.debug.logging.Logging.Priority.ERROR import eu.darken.sdmse.common.debug.logging.Logging.Priority.INFO import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.sdmse.common.debug.logging.asLog import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.files.GatewaySwitch +import eu.darken.sdmse.common.flow.DynamicStateFlow import eu.darken.sdmse.common.flow.replayingShare import eu.darken.sdmse.common.pkgs.features.Installed import eu.darken.sdmse.common.pkgs.pkgops.PkgOps @@ -21,13 +25,9 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.plus import javax.inject.Inject import javax.inject.Singleton import kotlin.reflect.KClass @@ -35,6 +35,7 @@ import kotlin.reflect.KClass @Singleton class PkgRepo @Inject constructor( @AppScope private val appScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, pkgEventListener: PackageEventListener, private val pkgSources: Set<@JvmSuppressWildcards PkgDataSource>, private val gatewaySwitch: GatewaySwitch, @@ -42,30 +43,32 @@ class PkgRepo @Inject constructor( private val userManager: UserManager2, ) { - data class CacheContainer( - val isSetupDone: Boolean = false, - val isInitialized: Boolean = false, - val pkgData: Map = emptyMap(), + private val cache = DynamicStateFlow(TAG, appScope + dispatcherProvider.IO) { + log(TAG, INFO) { "Initializing pkg cache" } + generateCacheContainer().also { + isPreInitInternal.value = false + } + } + + data class PkgData( + internal val pkgMap: Map = emptyMap(), + val error: Exception? = null, ) { + + val pkgs: Collection + get() { + if (error != null) throw error + return pkgMap.values.mapNotNull { it.data } + } + internal val pkgCount: Int - get() = pkgData.count { it.value.data != null } + get() = pkgMap.count { it.value.data != null } } - private val cacheLock = Mutex() - private val pkgCache = MutableStateFlow(CacheContainer()) - - val pkgs: Flow> = pkgCache - .filter { it.isInitialized } - .map { it.pkgData } - .map { cachedInfo -> cachedInfo.values.mapNotNull { it.data } } - .onStart { - cacheLock.withLock { - if (!pkgCache.value.isInitialized) { - log(TAG, INFO) { "Init due to pkgs subscription" } - load() - } - } - } + private val isPreInitInternal = MutableStateFlow(true) + val isPreInit: Flow = isPreInitInternal + + val data: Flow = cache.flow .replayingShare(appScope) init { @@ -78,15 +81,17 @@ class PkgRepo @Inject constructor( .launchIn(appScope) } - private suspend fun load() { - log(TAG) { "load()" } - pkgCache.value = CacheContainer( - isInitialized = true, - pkgData = generatePkgcache() - ) + private suspend fun generateCacheContainer(): PkgData { + log(TAG) { "generateCacheContainer()..." } + return try { + PkgData(pkgMap = gatherPkgData()) + } catch (e: Exception) { + log(TAG, ERROR) { "Failed to load pkg data: ${e.asLog()}" } + PkgData(error = e) + } } - private suspend fun generatePkgcache(): Map { + private suspend fun gatherPkgData(): Map { log(TAG, INFO) { "generatePkgcache()..." } val start = System.currentTimeMillis() return gatewaySwitch.useRes { @@ -142,33 +147,49 @@ class PkgRepo @Inject constructor( } } + suspend fun refresh( + id: Pkg.Id, + userHandle: UserHandle2? = null + ): Collection { + log(TAG) { "refresh(): $id" } + // TODO refreshing the whole cache is inefficient, implement single target refresh? + refresh() + return queryCache(id, userHandle).mapNotNull { it.data } + } + + suspend fun refresh(): Collection { + val before = cache.value() + log(TAG) { "refresh()... (before=${before.pkgCount})" } + val after = cache.updateBlocking { generateCacheContainer() } + log(TAG, INFO) { "...refresh()ed (after=${after.pkgCount})" } + return after.pkgs + } + private suspend fun queryCache( pkgId: Pkg.Id, userHandle: UserHandle2?, - ): Set = cacheLock.withLock { - if (!pkgCache.value.isInitialized) { - log(TAG) { "Package cache doesn't exist yet..." } - load() - } + ): Set { val systemHandle = userManager.systemUser().handle - val infos = pkgCache.value.pkgData.values.filter { + val infos = cache.value().pkgMap.filter { it.key.pkgId == pkgId && (userHandle == null || userHandle == systemHandle || it.key.userHandle == userHandle) } - if (infos.isNotEmpty()) return@withLock infos.toSet() + if (infos.isNotEmpty()) return infos.values.toSet() log(TAG, VERBOSE) { "Cache miss for $pkgId:$userHandle" } - // We didn't have any cache matches - if (userHandle != null) { + // Save the cache miss for better performance + return if (userHandle != null) { val key = CacheKey(pkgId, userHandle) val cacheInfo = CachedInfo(key, null) - pkgCache.value = pkgCache.value.copy( - pkgData = pkgCache.value.pkgData.mutate { - this[key] = cacheInfo - } - ) + cache.updateBlocking { + this.copy( + pkgMap = this.pkgMap.mutate { + this[key] = cacheInfo + } + ) + } setOf(cacheInfo) } else { @@ -181,23 +202,6 @@ class PkgRepo @Inject constructor( userHandle: UserHandle2?, ): Collection = queryCache(pkgId, userHandle).mapNotNull { it.data } - suspend fun refresh( - id: Pkg.Id, - userHandle: UserHandle2? = null - ): Collection { - log(TAG) { "refresh(): $id" } - // TODO refreshing the whole cache is inefficient, implement single target refresh? - cacheLock.withLock { load() } - return queryCache(id, userHandle).mapNotNull { it.data } - } - - suspend fun refresh(): Collection = cacheLock.withLock { - log(TAG) { "refresh()... (before=${pkgCache.value.pkgCount})" } - load() - log(TAG, INFO) { "...refresh()ed (after=${pkgCache.value.pkgCount})" } - pkgCache.value.pkgData.mapNotNull { it.value.data } - } - data class CacheKey( val pkgId: Pkg.Id, val userHandle: UserHandle2, diff --git a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepoExtensions.kt b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepoExtensions.kt index 559fb1ff2..dc044ea98 100644 --- a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepoExtensions.kt +++ b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/PkgRepoExtensions.kt @@ -2,22 +2,24 @@ package eu.darken.sdmse.common.pkgs import eu.darken.sdmse.common.pkgs.features.Installed import eu.darken.sdmse.common.user.UserHandle2 +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +fun PkgRepo.pkgs(): Flow> = data.map { it.pkgs } -suspend fun PkgRepo.currentPkgs(): Collection = this.pkgs.first() +suspend fun PkgRepo.current(): Collection = pkgs().first() -suspend fun PkgRepo.getPkg( +suspend fun PkgRepo.get( pkgId: Pkg.Id, ): Collection = query(pkgId, null) -suspend fun PkgRepo.getPkg( +suspend fun PkgRepo.get( pkgId: Pkg.Id, userHandle: UserHandle2?, ): Installed? = query(pkgId, userHandle).singleOrNull() - -suspend fun PkgRepo.getPkg( +suspend fun PkgRepo.get( installId: Installed.InstallId ): Installed? = query(installId.pkgId, installId.userHandle).singleOrNull() diff --git a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/sources/NormalPkgsSource.kt b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/sources/NormalPkgsSource.kt index 3a07d2d03..8ba5271c4 100644 --- a/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/sources/NormalPkgsSource.kt +++ b/app-common-pkgs/src/main/java/eu/darken/sdmse/common/pkgs/sources/NormalPkgsSource.kt @@ -16,11 +16,11 @@ import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.hasApiLevel import eu.darken.sdmse.common.permissions.Permission +import eu.darken.sdmse.common.pkgs.InvalidPkgInventoryException import eu.darken.sdmse.common.pkgs.PkgDataSource import eu.darken.sdmse.common.pkgs.container.NormalPkg import eu.darken.sdmse.common.pkgs.features.Installed import eu.darken.sdmse.common.pkgs.features.InstallerInfo -import eu.darken.sdmse.common.pkgs.pkgops.IllegalPkgDataException import eu.darken.sdmse.common.pkgs.pkgops.PkgOps import eu.darken.sdmse.common.root.RootManager import eu.darken.sdmse.common.root.canUseRootNow @@ -76,10 +76,10 @@ class NormalPkgsSource @Inject constructor( // FYI: MATCH_ALL does not include MATCH_UNINSTALLED_PACKAGES val pkgInfos = pkgOps.queryPkgs(PackageManager.MATCH_ALL) if (pkgInfos.isEmpty()) { - throw IllegalPkgDataException("No installed packages") + throw InvalidPkgInventoryException("Could not retrieve list of installed packages") } if (pkgInfos.none { it.packageName == BuildConfigWrap.APPLICATION_ID }) { - throw IllegalPkgDataException("Returned package data didn't contain us") + throw InvalidPkgInventoryException("Returned package data didn't contain us") } val currentHandle = userManager.currentUser().handle diff --git a/app-common-pkgs/src/main/res/values/strings.xml b/app-common-pkgs/src/main/res/values/strings.xml new file mode 100644 index 000000000..7ffb71a83 --- /dev/null +++ b/app-common-pkgs/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Invalid app inventory + The system provided an invalid list of installed apps to SD Maid. + \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/sdmse/common/WebpageTool.kt b/app-common/src/main/java/eu/darken/sdmse/common/WebpageTool.kt index 3b8bc0659..80979e6e1 100644 --- a/app-common/src/main/java/eu/darken/sdmse/common/WebpageTool.kt +++ b/app-common/src/main/java/eu/darken/sdmse/common/WebpageTool.kt @@ -16,18 +16,23 @@ class WebpageTool @Inject constructor( ) { fun open(address: String) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(address)).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - try { - context.startActivity(intent) - } catch (e: ActivityNotFoundException) { - log(ERROR) { "Failed to launch. No compatible activity!" } - } catch (e: SecurityException) { - // Permission Denial: starting Intent { act=android.intent.action.VIEW dat=https://github.com/... - // flg=0x10000000 cmp=com.mxtech.videoplayer.pro/com.mxtech.videoplayer.ActivityWebBrowser } - log(ERROR) { "Failed to launch activity due to $e" } - } + open(context, address) } + companion object { + fun open(context: Context, address: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(address)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + log(ERROR) { "Failed to launch. No compatible activity!" } + } catch (e: SecurityException) { + // Permission Denial: starting Intent { act=android.intent.action.VIEW dat=https://github.com/... + // flg=0x10000000 cmp=com.mxtech.videoplayer.pro/com.mxtech.videoplayer.ActivityWebBrowser } + log(ERROR) { "Failed to launch activity due to $e" } + } + } + } } \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/sdmse/common/error/LocalizedError.kt b/app-common/src/main/java/eu/darken/sdmse/common/error/LocalizedError.kt index d17a14c2a..0db806b60 100644 --- a/app-common/src/main/java/eu/darken/sdmse/common/error/LocalizedError.kt +++ b/app-common/src/main/java/eu/darken/sdmse/common/error/LocalizedError.kt @@ -26,7 +26,7 @@ fun Throwable.localized(c: Context): LocalizedError = when { this is HasLocalizedError -> this.getLocalizedError() this is ActivityNotFoundException -> LocalizedError( throwable = this, - label = caString { "${c.getString(R.string.general_error_label)} - ${this::class.simpleName!!}" }, + label = caString { "${c.getString(R.string.general_error_label)} - ${this@localized::class.simpleName!!}" }, description = caString { "${it.getString(R.string.general_error_no_compatible_app_found_msg)}\n$localizedMessage" } @@ -34,13 +34,13 @@ fun Throwable.localized(c: Context): LocalizedError = when { localizedMessage != null -> LocalizedError( throwable = this, - label = caString { "${c.getString(R.string.general_error_label)} - ${this::class.simpleName!!}" }, + label = caString { "${c.getString(R.string.general_error_label)} - ${this@localized::class.simpleName!!}" }, description = caString { localizedMessage ?: getStackTracePeek() } ) else -> LocalizedError( throwable = this, - label = caString { "${c.getString(R.string.general_error_label)} - ${this::class.simpleName!!}" }, + label = caString { "${c.getString(R.string.general_error_label)} - ${this@localized::class.simpleName!!}" }, description = caString { getStackTracePeek() } ) } diff --git a/app/src/main/java/eu/darken/sdmse/analyzer/core/storage/StorageScanner.kt b/app/src/main/java/eu/darken/sdmse/analyzer/core/storage/StorageScanner.kt index c807e1103..356dd7d3a 100644 --- a/app/src/main/java/eu/darken/sdmse/analyzer/core/storage/StorageScanner.kt +++ b/app/src/main/java/eu/darken/sdmse/analyzer/core/storage/StorageScanner.kt @@ -28,7 +28,7 @@ import eu.darken.sdmse.common.flow.throttleLatest import eu.darken.sdmse.common.forensics.FileForensics import eu.darken.sdmse.common.forensics.OwnerInfo import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import eu.darken.sdmse.common.progress.Progress import eu.darken.sdmse.common.progress.increaseProgress import eu.darken.sdmse.common.progress.updateProgressCount @@ -165,7 +165,7 @@ class StorageScanner @Inject constructor( updateProgressPrimary(R.string.analyzer_progress_scanning_apps) - val targetPkgs = pkgRepo.currentPkgs() + val targetPkgs = pkgRepo.current() .filter { it.packageName != "android" } .filter { it.applicationInfo != null } diff --git a/app/src/main/java/eu/darken/sdmse/appcleaner/core/automation/ClearCacheModule.kt b/app/src/main/java/eu/darken/sdmse/appcleaner/core/automation/ClearCacheModule.kt index 5ea43a24c..593e2b2bb 100644 --- a/app/src/main/java/eu/darken/sdmse/appcleaner/core/automation/ClearCacheModule.kt +++ b/app/src/main/java/eu/darken/sdmse/appcleaner/core/automation/ClearCacheModule.kt @@ -47,7 +47,7 @@ import eu.darken.sdmse.common.device.DeviceDetective import eu.darken.sdmse.common.funnel.IPCFunnel import eu.darken.sdmse.common.pkgs.PkgRepo import eu.darken.sdmse.common.pkgs.features.Installed -import eu.darken.sdmse.common.pkgs.getPkg +import eu.darken.sdmse.common.pkgs.get import eu.darken.sdmse.common.progress.Progress import eu.darken.sdmse.common.progress.increaseProgress import eu.darken.sdmse.common.progress.updateProgressCount @@ -131,7 +131,7 @@ class ClearCacheModule @AssistedInject constructor( throw UnsupportedOperationException("ACS based deletion is not support for other users ($target)") } - val installed = pkgRepo.getPkg(target.pkgId, target.userHandle) + val installed = pkgRepo.get(target.pkgId, target.userHandle) if (installed == null) { log(TAG, WARN) { "$target is not in package repo" } diff --git a/app/src/main/java/eu/darken/sdmse/appcleaner/core/scanner/AppScanner.kt b/app/src/main/java/eu/darken/sdmse/appcleaner/core/scanner/AppScanner.kt index cd345e5e1..b1cef7423 100644 --- a/app/src/main/java/eu/darken/sdmse/appcleaner/core/scanner/AppScanner.kt +++ b/app/src/main/java/eu/darken/sdmse/appcleaner/core/scanner/AppScanner.kt @@ -45,7 +45,7 @@ import eu.darken.sdmse.common.forensics.identifyArea import eu.darken.sdmse.common.pkgs.Pkg import eu.darken.sdmse.common.pkgs.PkgRepo import eu.darken.sdmse.common.pkgs.container.NormalPkg -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import eu.darken.sdmse.common.pkgs.features.Installed import eu.darken.sdmse.common.pkgs.getPrivateDataDirs import eu.darken.sdmse.common.pkgs.isEnabled @@ -133,7 +133,7 @@ class AppScanner @Inject constructor( val currentUser = userManager.currentUser() val allUsers = userManager.allUsers() - val allCurrentPkgs = pkgRepo.currentPkgs() + val allCurrentPkgs = pkgRepo.current() .filter { includeOtherUsers || it.userHandle == currentUser.handle } .filter { includeSystemApps || !it.isSystemApp } .filter { includeRunningApps || !pkgOps.isRunning(it.installId) } diff --git a/app/src/main/java/eu/darken/sdmse/appcontrol/core/AppControl.kt b/app/src/main/java/eu/darken/sdmse/appcontrol/core/AppControl.kt index cc0ecb704..419907dac 100644 --- a/app/src/main/java/eu/darken/sdmse/appcontrol/core/AppControl.kt +++ b/app/src/main/java/eu/darken/sdmse/appcontrol/core/AppControl.kt @@ -28,7 +28,7 @@ import eu.darken.sdmse.common.flow.replayingShare import eu.darken.sdmse.common.pkgs.Pkg import eu.darken.sdmse.common.pkgs.PkgRepo import eu.darken.sdmse.common.pkgs.container.NormalPkg -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import eu.darken.sdmse.common.pkgs.features.Installed import eu.darken.sdmse.common.pkgs.features.SourceAvailable import eu.darken.sdmse.common.pkgs.isEnabled @@ -151,7 +151,7 @@ class AppControl @Inject constructor( } val currentUserHandle = userManager.currentUser().handle - val pkgs = if (task.refreshPkgCache) pkgRepo.refresh() else pkgRepo.currentPkgs() + val pkgs = if (task.refreshPkgCache) pkgRepo.refresh() else pkgRepo.current() val appInfos = pkgs .filter { it.userHandle == currentUserHandle } .map { it.toAppInfo() } diff --git a/app/src/main/java/eu/darken/sdmse/appcontrol/core/automation/AppControlAutomation.kt b/app/src/main/java/eu/darken/sdmse/appcontrol/core/automation/AppControlAutomation.kt index d9bd877af..39ccc93cc 100644 --- a/app/src/main/java/eu/darken/sdmse/appcontrol/core/automation/AppControlAutomation.kt +++ b/app/src/main/java/eu/darken/sdmse/appcontrol/core/automation/AppControlAutomation.kt @@ -36,7 +36,7 @@ import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.device.DeviceDetective import eu.darken.sdmse.common.pkgs.PkgRepo import eu.darken.sdmse.common.pkgs.features.Installed -import eu.darken.sdmse.common.pkgs.getPkg +import eu.darken.sdmse.common.pkgs.get import eu.darken.sdmse.common.progress.Progress import eu.darken.sdmse.common.progress.updateProgressCount import eu.darken.sdmse.common.progress.updateProgressPrimary @@ -124,7 +124,7 @@ class AppControlAutomation @AssistedInject constructor( throw UnsupportedOperationException("ACS based force-stop is not support for other users ($target)") } - val installed = pkgRepo.getPkg(target.pkgId, target.userHandle) + val installed = pkgRepo.get(target.pkgId, target.userHandle) if (installed == null) { log(TAG, WARN) { "$target is not in package repo" } diff --git a/app/src/main/java/eu/darken/sdmse/appcontrol/core/uninstall/Uninstaller.kt b/app/src/main/java/eu/darken/sdmse/appcontrol/core/uninstall/Uninstaller.kt index cd92cf8e0..a297e2f97 100644 --- a/app/src/main/java/eu/darken/sdmse/appcontrol/core/uninstall/Uninstaller.kt +++ b/app/src/main/java/eu/darken/sdmse/appcontrol/core/uninstall/Uninstaller.kt @@ -15,6 +15,7 @@ import eu.darken.sdmse.common.debug.logging.asLog import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.pkgs.PkgRepo +import eu.darken.sdmse.common.pkgs.pkgs import eu.darken.sdmse.common.root.RootManager import eu.darken.sdmse.common.root.canUseRootNow import eu.darken.sdmse.common.sharedresource.HasSharedResource @@ -105,7 +106,7 @@ class Uninstaller @Inject constructor( log(TAG) { "Waiting for system uninstall process (timeout=$timeoutSeconds)" } // Wait until the app is no longer installed withTimeout(timeoutSeconds * 1000) { - pkgRepo.pkgs.first { pkgs -> + pkgRepo.pkgs().first { pkgs -> pkgs.none { it.installId == installId } } } diff --git a/app/src/main/java/eu/darken/sdmse/common/clutter/manual/ManualMarkerSource.kt b/app/src/main/java/eu/darken/sdmse/common/clutter/manual/ManualMarkerSource.kt index 2b9ce4a1b..1bc63d2b4 100644 --- a/app/src/main/java/eu/darken/sdmse/common/clutter/manual/ManualMarkerSource.kt +++ b/app/src/main/java/eu/darken/sdmse/common/clutter/manual/ManualMarkerSource.kt @@ -9,7 +9,7 @@ import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.pkgs.Pkg import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import eu.darken.sdmse.common.pkgs.features.Installed import eu.darken.sdmse.common.pkgs.toPkgId import kotlinx.coroutines.sync.Mutex @@ -78,7 +78,7 @@ open class ManualMarkerSource( log(TAG, VERBOSE) { "buildDatabase(entries=${clutterEntries.size})..." } val startTimeMarkerGeneration = System.currentTimeMillis() - val installedApps: Collection = pkgRepo.currentPkgs() + val installedApps: Collection = pkgRepo.current() var markerCount: Long = 0 val clutterMap: HashMap> = LinkedHashMap() var counter = 0 diff --git a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/CustomDexOptCheck.kt b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/CustomDexOptCheck.kt index e5abb1a36..443980e93 100644 --- a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/CustomDexOptCheck.kt +++ b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/CustomDexOptCheck.kt @@ -6,7 +6,7 @@ import eu.darken.sdmse.common.forensics.AreaInfo import eu.darken.sdmse.common.forensics.Owner import eu.darken.sdmse.common.forensics.csi.dalvik.DalvikCheck import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import java.io.File import javax.inject.Inject @@ -19,7 +19,7 @@ class CustomDexOptCheck @Inject constructor( areaInfo: AreaInfo, ): Pair { val owners = mutableSetOf() - val currentPkgs = pkgRepo.currentPkgs() + val currentPkgs = pkgRepo.current() var extraPathToCheck: LocalPath? = null // Custom apk/jar subfile that has been optimized manually diff --git a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/SourceDirCheck.kt b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/SourceDirCheck.kt index f040dbcea..800f15d9c 100644 --- a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/SourceDirCheck.kt +++ b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/dalvik/tools/SourceDirCheck.kt @@ -6,7 +6,7 @@ import eu.darken.sdmse.common.forensics.AreaInfo import eu.darken.sdmse.common.forensics.Owner import eu.darken.sdmse.common.forensics.csi.dalvik.DalvikCheck import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import eu.darken.sdmse.common.pkgs.features.SourceAvailable import javax.inject.Inject @@ -16,7 +16,7 @@ class SourceDirCheck @Inject constructor( ) : DalvikCheck { suspend fun check(areaInfo: AreaInfo, candidates: Collection): DalvikCheck.Result { - val ownerPkg = pkgRepo.currentPkgs() + val ownerPkg = pkgRepo.current() .filterIsInstance() .filter { it.sourceDir != null } .firstOrNull { pkg -> candidates.any { it.path == pkg.sourceDir?.path } } diff --git a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/AppSourceLibCSI.kt b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/AppSourceLibCSI.kt index ffbef04d9..08f834af7 100644 --- a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/AppSourceLibCSI.kt +++ b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/AppSourceLibCSI.kt @@ -21,7 +21,7 @@ import eu.darken.sdmse.common.forensics.Owner import eu.darken.sdmse.common.forensics.csi.LocalCSIProcessor import eu.darken.sdmse.common.forensics.csi.toOwners import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.currentPkgs +import eu.darken.sdmse.common.pkgs.current import eu.darken.sdmse.common.pkgs.isInstalled import eu.darken.sdmse.common.pkgs.toPkgId import javax.inject.Inject @@ -61,7 +61,7 @@ class AppSourceLibCSI @Inject constructor( ?.let { owners.add(Owner(it, userHandle)) } if (owners.isEmpty()) { - pkgRepo.currentPkgs() + pkgRepo.current() .filter { it.applicationInfo != null } .filter { pkg -> val nativLibDir = pkg.applicationInfo?.nativeLibraryDir?.let { diff --git a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/tools/SimilarityFilter.kt b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/tools/SimilarityFilter.kt index 8b33bc437..600300d0d 100644 --- a/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/tools/SimilarityFilter.kt +++ b/app/src/main/java/eu/darken/sdmse/common/forensics/csi/source/tools/SimilarityFilter.kt @@ -8,7 +8,7 @@ import eu.darken.sdmse.common.forensics.AreaInfo import eu.darken.sdmse.common.forensics.Owner import eu.darken.sdmse.common.pkgs.PkgRepo import eu.darken.sdmse.common.pkgs.features.SourceAvailable -import eu.darken.sdmse.common.pkgs.getPkg +import eu.darken.sdmse.common.pkgs.get import javax.inject.Inject @@ -37,7 +37,7 @@ class SimilarityFilter @Inject constructor( return toCheck.filter { candidate -> val userHandle = areaInfo.userHandle - val sourceDir = pkgRepo.getPkg(candidate.pkgId, userHandle) + val sourceDir = pkgRepo.get(candidate.pkgId, userHandle) ?.let { it as? SourceAvailable } ?.sourceDir ?: return@filter true diff --git a/app/src/main/java/eu/darken/sdmse/common/ui/TextViewExtensions.kt b/app/src/main/java/eu/darken/sdmse/common/ui/TextViewExtensions.kt new file mode 100644 index 000000000..1d1f23386 --- /dev/null +++ b/app/src/main/java/eu/darken/sdmse/common/ui/TextViewExtensions.kt @@ -0,0 +1,27 @@ +package eu.darken.sdmse.common.ui + +import android.content.res.ColorStateList +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.widget.TextViewCompat +import com.google.android.material.textview.MaterialTextView +import eu.darken.sdmse.common.getColorForAttr + +fun MaterialTextView.setLeftIcon( + @DrawableRes iconRes: Int, + @AttrRes tintRes: Int? = null, +) { + setCompoundDrawablesRelativeWithIntrinsicBounds( + ContextCompat.getDrawable(context, iconRes), + null, + null, + null, + ) + if (tintRes != null) { + TextViewCompat.setCompoundDrawableTintList( + this, + ColorStateList.valueOf(context.getColorForAttr(tintRes)) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/sdmse/exclusion/ui/editor/pkg/PkgExclusionViewModel.kt b/app/src/main/java/eu/darken/sdmse/exclusion/ui/editor/pkg/PkgExclusionViewModel.kt index 7a23bf84d..681a73d76 100644 --- a/app/src/main/java/eu/darken/sdmse/exclusion/ui/editor/pkg/PkgExclusionViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/exclusion/ui/editor/pkg/PkgExclusionViewModel.kt @@ -11,7 +11,7 @@ import eu.darken.sdmse.common.flow.DynamicStateFlow import eu.darken.sdmse.common.navigation.navArgs import eu.darken.sdmse.common.pkgs.Pkg import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.getPkg +import eu.darken.sdmse.common.pkgs.get import eu.darken.sdmse.common.uix.ViewModel3 import eu.darken.sdmse.exclusion.core.ExclusionManager import eu.darken.sdmse.exclusion.core.current @@ -52,7 +52,7 @@ class PkgExclusionViewModel @Inject constructor( State( original = origExclusion, current = newExcl, - pkg = pkgRepo.getPkg(newExcl.pkgId).firstOrNull(), + pkg = pkgRepo.get(newExcl.pkgId).firstOrNull(), ) } diff --git a/app/src/main/java/eu/darken/sdmse/exclusion/ui/list/ExclusionListViewModel.kt b/app/src/main/java/eu/darken/sdmse/exclusion/ui/list/ExclusionListViewModel.kt index 7cf3231bc..4a0606031 100644 --- a/app/src/main/java/eu/darken/sdmse/exclusion/ui/list/ExclusionListViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/exclusion/ui/list/ExclusionListViewModel.kt @@ -19,6 +19,7 @@ import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.files.GatewaySwitch import eu.darken.sdmse.common.pkgs.PkgRepo +import eu.darken.sdmse.common.pkgs.pkgs import eu.darken.sdmse.common.readAsText import eu.darken.sdmse.common.uix.ViewModel3 import eu.darken.sdmse.common.upgrade.UpgradeRepo @@ -82,7 +83,7 @@ class ExclusionListViewModel @Inject constructor( val state = combine( exclusionManager.exclusions, - pkgRepo.pkgs.onStart { emit(emptySet()) }, + pkgRepo.pkgs().onStart { emit(emptySet()) }, lookups.onStart { emit(emptyMap()) }, showDefaults, ) { holders, pkgs, lookups, showDefaults -> diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardAdapter.kt b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardAdapter.kt index 894af2ba0..de2bc0570 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardAdapter.kt @@ -16,8 +16,8 @@ import eu.darken.sdmse.common.lists.differ.setupDiffer import eu.darken.sdmse.common.lists.modular.ModularAdapter import eu.darken.sdmse.common.lists.modular.mods.DataBinderMod import eu.darken.sdmse.common.lists.modular.mods.TypedVHCreatorMod -import eu.darken.sdmse.main.ui.dashboard.items.DataAreaCardVH import eu.darken.sdmse.main.ui.dashboard.items.DebugCardVH +import eu.darken.sdmse.main.ui.dashboard.items.ErrorDataAreaVH import eu.darken.sdmse.main.ui.dashboard.items.MotdCardVH import eu.darken.sdmse.main.ui.dashboard.items.ReviewCardVH import eu.darken.sdmse.main.ui.dashboard.items.SetupCardVH @@ -46,7 +46,6 @@ class DashboardAdapter @Inject constructor( addMod(TypedVHCreatorMod({ data[it] is SetupCardVH.Item }) { SetupCardVH(it) }) addMod(TypedVHCreatorMod({ data[it] is UpgradeCardVH.Item }) { UpgradeCardVH(it) }) addMod(TypedVHCreatorMod({ data[it] is UpdateCardVH.Item }) { UpdateCardVH(it) }) - addMod(TypedVHCreatorMod({ data[it] is DataAreaCardVH.Item }) { DataAreaCardVH(it) }) addMod(TypedVHCreatorMod({ data[it] is DashboardToolCard.Item }) { DashboardToolCard(it) }) addMod(TypedVHCreatorMod({ data[it] is AppControlDashCardVH.Item }) { AppControlDashCardVH(it) }) addMod(TypedVHCreatorMod({ data[it] is AnalyzerDashCardVH.Item }) { AnalyzerDashCardVH(it) }) @@ -55,6 +54,7 @@ class DashboardAdapter @Inject constructor( addMod(TypedVHCreatorMod({ data[it] is MotdCardVH.Item }) { MotdCardVH(it) }) addMod(TypedVHCreatorMod({ data[it] is ReviewCardVH.Item }) { ReviewCardVH(activity, it) }) addMod(TypedVHCreatorMod({ data[it] is StatsDashCardVH.Item }) { StatsDashCardVH(it) }) + addMod(TypedVHCreatorMod({ data[it] is ErrorDataAreaVH.Item }) { ErrorDataAreaVH(it) }) } abstract class BaseVH( diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardEvents.kt b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardEvents.kt index 46f90ee56..471432c80 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardEvents.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardEvents.kt @@ -1,5 +1,6 @@ package eu.darken.sdmse.main.ui.dashboard +import android.content.Intent import eu.darken.sdmse.appcleaner.core.tasks.AppCleanerProcessingTask import eu.darken.sdmse.corpsefinder.core.tasks.CorpseFinderDeleteTask import eu.darken.sdmse.deduplicator.core.Duplicate @@ -32,4 +33,6 @@ sealed interface DashboardEvents { data class TaskResult( val result: SDMTool.Task.Result ) : DashboardEvents + + data class OpenIntent(val intent: Intent) : DashboardEvents } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardFragment.kt b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardFragment.kt index a15928f09..abe00055f 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardFragment.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardFragment.kt @@ -1,5 +1,6 @@ package eu.darken.sdmse.main.ui.dashboard +import android.content.ActivityNotFoundException import android.content.res.ColorStateList import android.os.Bundle import android.text.format.Formatter @@ -13,6 +14,7 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import eu.darken.sdmse.R import eu.darken.sdmse.common.easterEggProgressMsg +import eu.darken.sdmse.common.error.asErrorDialogBuilder import eu.darken.sdmse.common.getColorForAttr import eu.darken.sdmse.common.lists.differ.update import eu.darken.sdmse.common.lists.setupDefaults @@ -229,6 +231,12 @@ class DashboardFragment : Fragment3(R.layout.dashboard_fragment) { is DashboardEvents.TodoHint -> MaterialAlertDialogBuilder(requireContext()).apply { setMessage(eu.darken.sdmse.common.R.string.general_todo_msg) }.show() + + is DashboardEvents.OpenIntent -> try { + startActivity(event.intent) + } catch (e: ActivityNotFoundException) { + e.asErrorDialogBuilder(requireActivity()).show() + } } } diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardViewModel.kt index 2582989e6..feb1366bb 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardViewModel.kt @@ -1,8 +1,10 @@ package eu.darken.sdmse.main.ui.dashboard +import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.sdmse.App import eu.darken.sdmse.MainDirections import eu.darken.sdmse.analyzer.core.Analyzer @@ -37,6 +39,7 @@ import eu.darken.sdmse.common.flow.intervalFlow import eu.darken.sdmse.common.flow.replayingShare import eu.darken.sdmse.common.flow.setupCommonEventHandlers import eu.darken.sdmse.common.flow.throttleLatest +import eu.darken.sdmse.common.pkgs.PkgRepo import eu.darken.sdmse.common.review.ReviewTool import eu.darken.sdmse.common.rngString import eu.darken.sdmse.common.uix.ViewModel3 @@ -63,8 +66,8 @@ import eu.darken.sdmse.main.core.motd.MotdRepo import eu.darken.sdmse.main.core.release.ReleaseManager import eu.darken.sdmse.main.core.taskmanager.TaskManager import eu.darken.sdmse.main.core.taskmanager.getLatestResult -import eu.darken.sdmse.main.ui.dashboard.items.DataAreaCardVH import eu.darken.sdmse.main.ui.dashboard.items.DebugCardVH +import eu.darken.sdmse.main.ui.dashboard.items.ErrorDataAreaVH import eu.darken.sdmse.main.ui.dashboard.items.MotdCardVH import eu.darken.sdmse.main.ui.dashboard.items.ReviewCardVH import eu.darken.sdmse.main.ui.dashboard.items.SetupCardVH @@ -103,6 +106,7 @@ class DashboardViewModel @Inject constructor( @Suppress("unused") private val handle: SavedStateHandle, dispatcherProvider: DispatcherProvider, private val areaManager: DataAreaManager, + private val pkgRepo: PkgRepo, private val taskManager: TaskManager, private val setupManager: SetupManager, private val corpseFinder: CorpseFinder, @@ -122,6 +126,7 @@ class DashboardViewModel @Inject constructor( private val releaseManager: ReleaseManager, private val reviewTool: ReviewTool, private val statsRepo: StatsRepo, + @ApplicationContext private val context: Context, ) : ViewModel3(dispatcherProvider = dispatcherProvider) { init { @@ -348,17 +353,17 @@ class DashboardViewModel @Inject constructor( ) } - private val dataAreaItem: Flow = areaManager.latestState + private val dataAreaItem: Flow = areaManager.latestState .map { if (it == null) return@map null if (it.areas.isNotEmpty()) return@map null - DataAreaCardVH.Item( + ErrorDataAreaVH.Item( state = it, onReload = { launch { areaManager.reload() } - } + }, ) } @@ -467,7 +472,7 @@ class DashboardViewModel @Inject constructor( upgradeInfo: UpgradeRepo.Info?, updateInfo: UpdateCardVH.Item?, setupItem: SetupCardVH.Item?, - dataAreaInfo: DataAreaCardVH.Item?, + dataAreaError: ErrorDataAreaVH.Item?, corpseFinderItem: DashboardToolCard.Item?, systemCleanerItem: DashboardToolCard.Item?, appCleanerItem: DashboardToolCard.Item?, @@ -481,7 +486,9 @@ class DashboardViewModel @Inject constructor( _ -> val items = mutableListOf(titleInfo) - if (motdItem == null && updateInfo == null && setupItem == null && dataAreaInfo == null && reviewItem != null) { + val noError = dataAreaError == null + + if (motdItem == null && updateInfo == null && setupItem == null && noError && reviewItem != null) { log(TAG, INFO) { "Showing review item" } items.add(reviewItem) } else if (reviewItem != null) { @@ -491,7 +498,7 @@ class DashboardViewModel @Inject constructor( motdItem?.let { items.add(it) } updateInfo?.let { items.add(it) } setupItem?.let { items.add(it) } - dataAreaInfo?.let { items.add(it) } + dataAreaError?.let { items.add(it) } corpseFinderItem?.let { items.add(it) } systemCleanerItem?.let { items.add(it) } diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/items/DataAreaCardVH.kt b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/items/ErrorDataAreaVH.kt similarity index 61% rename from app/src/main/java/eu/darken/sdmse/main/ui/dashboard/items/DataAreaCardVH.kt rename to app/src/main/java/eu/darken/sdmse/main/ui/dashboard/items/ErrorDataAreaVH.kt index a93154ca2..9ce826ef9 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/items/DataAreaCardVH.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/dashboard/items/ErrorDataAreaVH.kt @@ -4,19 +4,19 @@ import android.view.ViewGroup import eu.darken.sdmse.R import eu.darken.sdmse.common.areas.DataAreaManager import eu.darken.sdmse.common.lists.binding -import eu.darken.sdmse.databinding.DashboardDataareaItemBinding +import eu.darken.sdmse.databinding.DashboardErrorDataareaItemBinding import eu.darken.sdmse.main.ui.dashboard.DashboardAdapter -class DataAreaCardVH(parent: ViewGroup) : - DashboardAdapter.BaseVH( - R.layout.dashboard_dataarea_item, +class ErrorDataAreaVH(parent: ViewGroup) : + DashboardAdapter.BaseVH( + R.layout.dashboard_error_dataarea_item, parent ) { - override val viewBinding = lazy { DashboardDataareaItemBinding.bind(itemView) } + override val viewBinding = lazy { DashboardErrorDataareaItemBinding.bind(itemView) } - override val onBindData: DashboardDataareaItemBinding.( + override val onBindData: DashboardErrorDataareaItemBinding.( item: Item, payloads: List ) -> Unit = binding { item -> diff --git a/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt b/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt index ed4d54dcf..bcdff1a3e 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt @@ -212,12 +212,10 @@ class SetupViewModel @Inject constructor( is SetupModule.State.Current -> InventorySetupCardVH.Item( state = state as InventorySetupModule.Result, onGrantAction = { - state.missingPermission.firstOrNull()?.let { - events.postValue(SetupEvents.ShowOurDetailsPage(state.settingsIntent)) - } + events.postValue(SetupEvents.ShowOurDetailsPage(state.settingsIntent)) }, onHelp = { - webpageTool.open("https://github.com/d4rken-org/sdmaid-se/wiki/Setup#app-inventory") + webpageTool.open(InventorySetupModule.INFO_URL) } ) diff --git a/app/src/main/java/eu/darken/sdmse/setup/automation/AutomationSetupCardVH.kt b/app/src/main/java/eu/darken/sdmse/setup/automation/AutomationSetupCardVH.kt index d05b92c20..5379e8529 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/automation/AutomationSetupCardVH.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/automation/AutomationSetupCardVH.kt @@ -1,13 +1,11 @@ package eu.darken.sdmse.setup.automation -import android.content.res.ColorStateList import android.view.ViewGroup -import androidx.core.content.ContextCompat import androidx.core.view.isVisible -import androidx.core.widget.TextViewCompat import eu.darken.sdmse.R import eu.darken.sdmse.common.getColorForAttr import eu.darken.sdmse.common.lists.binding +import eu.darken.sdmse.common.ui.setLeftIcon import eu.darken.sdmse.databinding.SetupAutomationItemBinding import eu.darken.sdmse.setup.SetupAdapter @@ -27,30 +25,21 @@ class AutomationSetupCardVH(parent: ViewGroup) : val state = item.state enabledState.apply { isVisible = state.hasConsent == true - setCompoundDrawablesRelativeWithIntrinsicBounds( - ContextCompat.getDrawable( - context, when { - state.isServiceEnabled -> R.drawable.ic_check_circle - state.canSelfEnable -> R.drawable.ic_baseline_access_time_filled_24 - else -> R.drawable.ic_cancel - } - ), - null, - null, - null, - ) - TextViewCompat.setCompoundDrawableTintList( - this, - ColorStateList.valueOf( - context.getColorForAttr( - when { - state.isServiceEnabled -> androidx.appcompat.R.attr.colorPrimary - state.canSelfEnable -> com.google.android.material.R.attr.colorSecondary - else -> androidx.appcompat.R.attr.colorError - } - ) + + when { + state.isServiceEnabled -> setLeftIcon( + R.drawable.ic_check_circle, + androidx.appcompat.R.attr.colorPrimary ) - ) + + state.canSelfEnable -> setLeftIcon( + R.drawable.ic_baseline_access_time_filled_24, + com.google.android.material.R.attr.colorSecondary + ) + + else -> setLeftIcon(R.drawable.ic_cancel, androidx.appcompat.R.attr.colorError) + } + setTextColor( context.getColorForAttr( when { @@ -82,20 +71,19 @@ class AutomationSetupCardVH(parent: ViewGroup) : runningState.apply { isVisible = state.isServiceEnabled && state.hasConsent == true - setCompoundDrawablesRelativeWithIntrinsicBounds( - ContextCompat.getDrawable( - context, if (state.isServiceRunning) R.drawable.ic_check_circle else R.drawable.ic_cancel - ), - null, - null, - null, - ) - TextViewCompat.setCompoundDrawableTintList( - this, - ColorStateList.valueOf( - context.getColorForAttr(if (state.isServiceRunning) androidx.appcompat.R.attr.colorPrimary else androidx.appcompat.R.attr.colorError) + + when { + state.isServiceRunning -> setLeftIcon( + R.drawable.ic_check_circle, + androidx.appcompat.R.attr.colorPrimary ) - ) + + else -> setLeftIcon( + R.drawable.ic_cancel, + androidx.appcompat.R.attr.colorError + ) + } + setTextColor( context.getColorForAttr( if (state.isServiceRunning) androidx.appcompat.R.attr.colorPrimary else androidx.appcompat.R.attr.colorError diff --git a/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupCardVH.kt b/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupCardVH.kt index 69c5ebf7c..08dcb6fa2 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupCardVH.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupCardVH.kt @@ -4,6 +4,7 @@ import android.view.ViewGroup import androidx.core.view.isGone import eu.darken.sdmse.R import eu.darken.sdmse.common.lists.binding +import eu.darken.sdmse.common.ui.setLeftIcon import eu.darken.sdmse.databinding.SetupInventoryItemBinding import eu.darken.sdmse.setup.SetupAdapter @@ -20,12 +21,51 @@ class InventorySetupCardVH(parent: ViewGroup) : item: Item, payloads: List ) -> Unit = binding { item -> - grantState.isGone = item.state.missingPermission.isNotEmpty() + val state = item.state + + grantState.apply { + setTextColor( + getColorForAttr( + when { + state.isAccessFaked -> com.google.android.material.R.attr.colorError + else -> com.google.android.material.R.attr.colorPrimary + } + ) + ) + text = when { + state.isAccessFaked -> getString(R.string.setup_permission_error_label) + else -> getString(R.string.setup_permission_granted_label) + } + + when { + state.isAccessFaked -> setLeftIcon( + R.drawable.ic_error_onsurface, + com.google.android.material.R.attr.colorError + ) + + else -> setLeftIcon( + R.drawable.ic_check_circle, + com.google.android.material.R.attr.colorPrimary + ) + } + + isGone = item.state.missingPermission.isNotEmpty() + } + + grantHint.apply { + text = getString(R.string.setup_inventory_invalid_message) + isGone = !state.isAccessFaked + } grantAction.apply { + text = when { + state.isAccessFaked -> getString(eu.darken.sdmse.common.R.string.general_open_system_settings_action) + else -> getString(eu.darken.sdmse.common.R.string.general_grant_access_action) + } isGone = item.state.isComplete setOnClickListener { item.onGrantAction() } } + helpAction.setOnClickListener { item.onHelp() } } diff --git a/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupModule.kt b/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupModule.kt index 8c9e83f0c..748acea35 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupModule.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/inventory/InventorySetupModule.kt @@ -2,6 +2,7 @@ package eu.darken.sdmse.setup.inventory import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -9,12 +10,15 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet import eu.darken.sdmse.common.coroutine.AppScope +import eu.darken.sdmse.common.debug.logging.Logging.Priority.ERROR +import eu.darken.sdmse.common.debug.logging.asLog import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.flow.replayingShare import eu.darken.sdmse.common.hasApiLevel import eu.darken.sdmse.common.permissions.Permission import eu.darken.sdmse.common.pkgs.getSettingsIntent +import eu.darken.sdmse.common.pkgs.pkgops.PkgOps import eu.darken.sdmse.common.pkgs.toPkgId import eu.darken.sdmse.common.rngString import eu.darken.sdmse.setup.SetupModule @@ -31,6 +35,7 @@ import javax.inject.Singleton class InventorySetupModule @Inject constructor( @AppScope private val appScope: CoroutineScope, @ApplicationContext private val context: Context, + private val pkgOps: PkgOps, ) : SetupModule { private val refreshTrigger = MutableStateFlow(rngString) @@ -44,9 +49,30 @@ class InventorySetupModule @Inject constructor( !isGranted }.toSet() + val isAccessFaked = when { + missingPermission.isEmpty() -> { + run { + val pkgs = try { + pkgOps.queryPkgs(PackageManager.MATCH_ALL).map { it.packageName } + } catch (e: Exception) { + log(TAG, ERROR) { "Check for fake access failed: ${e.asLog()}" } + null + } + when { + pkgs == null -> false + pkgs.isEmpty() -> true + else -> !pkgs.contains(context.packageName) + } + } + } + + else -> false + } + @Suppress("USELESS_CAST") Result( missingPermission = missingPermission, + isAccessFaked = isAccessFaked, settingsIntent = context.packageName.toPkgId().getSettingsIntent(context) ) as SetupModule.State } @@ -71,13 +97,14 @@ class InventorySetupModule @Inject constructor( data class Result( val missingPermission: Set, + val isAccessFaked: Boolean, val settingsIntent: Intent, ) : SetupModule.State.Current { override val type: SetupModule.Type get() = SetupModule.Type.INVENTORY - override val isComplete: Boolean = missingPermission.isEmpty() + override val isComplete: Boolean = !isAccessFaked && missingPermission.isEmpty() } @@ -88,5 +115,6 @@ class InventorySetupModule @Inject constructor( companion object { private val TAG = logTag("Setup", "Inventory", "Module") + const val INFO_URL = "https://github.com/d4rken-org/sdmaid-se/wiki/Setup#app-inventory" } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/sdmse/stats/ui/pkgs/AffectedPkgsViewModel.kt b/app/src/main/java/eu/darken/sdmse/stats/ui/pkgs/AffectedPkgsViewModel.kt index 87b3c8648..3e574af9f 100644 --- a/app/src/main/java/eu/darken/sdmse/stats/ui/pkgs/AffectedPkgsViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/stats/ui/pkgs/AffectedPkgsViewModel.kt @@ -6,7 +6,7 @@ import eu.darken.sdmse.common.coroutine.DispatcherProvider import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.getPkg +import eu.darken.sdmse.common.pkgs.get import eu.darken.sdmse.common.uix.ViewModel3 import eu.darken.sdmse.stats.core.AffectedPkg import eu.darken.sdmse.stats.core.Report @@ -58,7 +58,7 @@ class AffectedPkgsViewModel @Inject constructor( affectedPkgs .sortedBy { it.pkgId.name } - .map { pkg -> AffectedPkgVH.Item(pkg, pkgRepo.getPkg(pkg.pkgId).firstOrNull()) } + .map { pkg -> AffectedPkgVH.Item(pkg, pkgRepo.get(pkg.pkgId).firstOrNull()) } .run { elements.addAll(this) } State(elements) diff --git a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt index 23a5d4e56..1d0c3a6f6 100644 --- a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt +++ b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt @@ -29,7 +29,7 @@ import eu.darken.sdmse.common.files.read import eu.darken.sdmse.common.files.segs import eu.darken.sdmse.common.hashing.Hasher import eu.darken.sdmse.common.pkgs.PkgRepo -import eu.darken.sdmse.common.pkgs.getPkg +import eu.darken.sdmse.common.pkgs.get import eu.darken.sdmse.common.pkgs.pkgops.PkgOps import eu.darken.sdmse.systemcleaner.core.SystemCleanerSettings import eu.darken.sdmse.systemcleaner.core.filter.BaseSystemCleanerFilter @@ -149,7 +149,7 @@ class SuperfluousApksFilter @Inject constructor( log(TAG) { "Checking status for ${apkInfo.packageName} (${apkInfo.versionCode})" } // TODO Multiple profiles can't have different versions of the same APK, right? - val installed = pkgRepo.getPkg(apkInfo.id).firstOrNull() ?: return null + val installed = pkgRepo.get(apkInfo.id).firstOrNull() ?: return null val superfluos = installed.versionCode >= apkInfo.versionCode if (superfluos) { diff --git a/app/src/main/res/layout/dashboard_dataarea_item.xml b/app/src/main/res/layout/dashboard_error_dataarea_item.xml similarity index 98% rename from app/src/main/res/layout/dashboard_dataarea_item.xml rename to app/src/main/res/layout/dashboard_error_dataarea_item.xml index 3987920da..3a40b67d0 100644 --- a/app/src/main/res/layout/dashboard_dataarea_item.xml +++ b/app/src/main/res/layout/dashboard_error_dataarea_item.xml @@ -4,7 +4,7 @@ style="@style/DashboardCardItem" android:layout_width="match_parent" android:layout_height="wrap_content" - android:backgroundTint="?colorTertiaryContainer"> + android:backgroundTint="?colorErrorContainer"> - + diff --git a/app/src/main/res/layout/setup_inventory_item.xml b/app/src/main/res/layout/setup_inventory_item.xml index a68e6106e..08d6cb995 100644 --- a/app/src/main/res/layout/setup_inventory_item.xml +++ b/app/src/main/res/layout/setup_inventory_item.xml @@ -57,11 +57,25 @@ android:gravity="center" android:text="@string/setup_permission_granted_label" android:textColor="?colorPrimary" - app:layout_constraintBottom_toTopOf="@id/grant_action" + app:layout_constraintBottom_toTopOf="@id/grant_hint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/body" /> + + + app:layout_constraintTop_toBottomOf="@id/grant_hint" /> You can find the setup screen again via the settings menu. Permission granted + Permission error Usage statistics The usage statistics permission grants access to more app details. SD Maid uses it to determine additional cache sizes. @@ -176,6 +177,8 @@ SD Maid needs to know which apps you have to find files that don\'t belong to any installed apps. Look for a permission called \"App list\" or \"Query all packages\". + The system provided an invalid list of installed apps (e.g. too few or some were missing). Go to system settings and allow SD Maid access to \"App list\". + SD Maid didn\'t find any data areas that can be scanned. Did you complete the setup? Data areas Reload data areas diff --git a/crowdin.yml b/crowdin.yml index 383a19f05..0dfb8891b 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -100,6 +100,12 @@ "translation": "/app-common/src/main/res/values-%android_code%/strings.xml", # "update_option" : "update_without_changes", "languages_mapping": *stringmapping + }, { + "source": "/app-common-pkgs/src/main/res/values/strings.xml", + "dest": "/strings-app-common-pkgs.xml", + "translation": "/app-common-pkgs/src/main/res/values-%android_code%/strings.xml", + # "update_option" : "update_without_changes", + "languages_mapping": *stringmapping }, { "source": "/fastlane/metadata/android/en-US/full_description.txt", "dest": "/store-description.txt",