From ae8c42f5b11839afbc4431fee70bf36a5e338e99 Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 20 Jan 2025 17:52:50 +0100 Subject: [PATCH 1/5] Remove unused error --- .../sdmse/automation/core/AutomationSupportException.kt | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/src/main/java/eu/darken/sdmse/automation/core/AutomationSupportException.kt diff --git a/app/src/main/java/eu/darken/sdmse/automation/core/AutomationSupportException.kt b/app/src/main/java/eu/darken/sdmse/automation/core/AutomationSupportException.kt deleted file mode 100644 index 43275332f..000000000 --- a/app/src/main/java/eu/darken/sdmse/automation/core/AutomationSupportException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package eu.darken.sdmse.automation.core - -class AutomationSupportException( - message: String, - cause: Throwable -) : UnsupportedOperationException(message, cause) \ No newline at end of file From 3978ff3c545b678e8ebf1aee058a579eef602eee Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 20 Jan 2025 18:28:38 +0100 Subject: [PATCH 2/5] Reduce general ACS step timeout to 15s Overall timeout was already long, no step should take 20s, 15s might still be excessive. --- .../eu/darken/sdmse/automation/core/common/StepProcessor.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/darken/sdmse/automation/core/common/StepProcessor.kt b/app/src/main/java/eu/darken/sdmse/automation/core/common/StepProcessor.kt index 74bf4c6fa..fc246a9e5 100644 --- a/app/src/main/java/eu/darken/sdmse/automation/core/common/StepProcessor.kt +++ b/app/src/main/java/eu/darken/sdmse/automation/core/common/StepProcessor.kt @@ -60,7 +60,7 @@ class StepProcessor @AssistedInject constructor( progressPub.value = update(progressPub.value) } - suspend fun process(step: Step): Unit = withTimeout(20 * 1000) { + suspend fun process(step: Step): Unit = withTimeout(step.timeout) { log(TAG) { "crawl(): $step" } updateProgressPrimary(step.label) var attempts = 0 @@ -236,7 +236,8 @@ class StepProcessor @AssistedInject constructor( val nodeTest: (suspend (node: AccessibilityNodeInfo) -> Boolean)? = null, val nodeRecovery: (suspend (node: AccessibilityNodeInfo) -> Boolean)? = null, val nodeMapping: (suspend (node: AccessibilityNodeInfo) -> AccessibilityNodeInfo)? = null, - val action: (suspend (node: AccessibilityNodeInfo, retryCount: Int) -> Boolean)? = null + val action: (suspend (node: AccessibilityNodeInfo, retryCount: Int) -> Boolean)? = null, + val timeout: Long = 15 * 1000, ) { override fun toString(): String = "Spec(source=$source, description=$descriptionInternal)" } From 1a863c9f7d352b31d9167f1ff76101eb76cbad00 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 21 Jan 2025 00:02:24 +0100 Subject: [PATCH 3/5] Detect ROM compatibility issues and abort ACS actions early with an explanation dialog. --- .../core/automation/ClearCacheModule.kt | 10 ++++++ .../AutomationCompatibilityException.kt | 18 +++++++++++ .../eu/darken/sdmse/main/ui/MainActivity.kt | 9 ++++++ .../eu/darken/sdmse/main/ui/MainViewModel.kt | 31 ++++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/darken/sdmse/automation/core/errors/AutomationCompatibilityException.kt 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 c7ef89199..f26987ef9 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 @@ -31,6 +31,7 @@ import eu.darken.sdmse.appcleaner.core.automation.specs.vivo.VivoSpecs import eu.darken.sdmse.automation.core.AutomationHost import eu.darken.sdmse.automation.core.AutomationModule import eu.darken.sdmse.automation.core.AutomationTask +import eu.darken.sdmse.automation.core.errors.AutomationCompatibilityException import eu.darken.sdmse.automation.core.errors.ScreenUnavailableException import eu.darken.sdmse.automation.core.errors.UserCancelledAutomationException import eu.darken.sdmse.automation.core.finishAutomation @@ -38,6 +39,7 @@ import eu.darken.sdmse.automation.core.specs.AutomationExplorer import eu.darken.sdmse.automation.core.specs.AutomationSpec import eu.darken.sdmse.common.ca.CaString import eu.darken.sdmse.common.ca.toCaString +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.Logging.Priority.WARN @@ -128,6 +130,8 @@ class ClearCacheModule @AssistedInject constructor( var cancelledByUser = false val currentUserHandle = userManager2.currentUser().handle + var timeoutCount = 0 + for (target in task.targets) { if (target.userHandle != currentUserHandle) { throw UnsupportedOperationException("ACS based deletion is not support for other users ($target)") @@ -156,6 +160,7 @@ class ClearCacheModule @AssistedInject constructor( } catch (e: TimeoutCancellationException) { log(TAG, WARN) { "Timeout while processing $installed" } failed.add(target) + if (timeoutCount > 2) break else timeoutCount++ } catch (e: CancellationException) { log(TAG, WARN) { "We were cancelled: ${e.asLog()}" } updateProgressPrimary(eu.darken.sdmse.common.R.string.general_cancel_action) @@ -182,6 +187,11 @@ class ClearCacheModule @AssistedInject constructor( deviceDetective = deviceDetective, ) + if (timeoutCount != 0 && successful.isEmpty()) { + log(TAG, ERROR) { "Continued timeout errors, no successes so far, possible compatbility issue?" } + throw AutomationCompatibilityException() + } + return ClearCacheTask.Result( successful = successful, failed = failed, diff --git a/app/src/main/java/eu/darken/sdmse/automation/core/errors/AutomationCompatibilityException.kt b/app/src/main/java/eu/darken/sdmse/automation/core/errors/AutomationCompatibilityException.kt new file mode 100644 index 000000000..fb437d7ec --- /dev/null +++ b/app/src/main/java/eu/darken/sdmse/automation/core/errors/AutomationCompatibilityException.kt @@ -0,0 +1,18 @@ +package eu.darken.sdmse.automation.core.errors + +import eu.darken.sdmse.R +import eu.darken.sdmse.common.ca.toCaString +import eu.darken.sdmse.common.error.HasLocalizedError +import eu.darken.sdmse.common.error.LocalizedError + +open class AutomationCompatibilityException( + override val message: String = "SD Maid couldn’t figure out the screen layout. If this keeps happening, your language or setup might not be fully supported. Check for updates or reach out to me so I can fix it." +) : AutomationException(), HasLocalizedError { + + override fun getLocalizedError(): LocalizedError = LocalizedError( + throwable = this, + label = R.string.automation_error_no_consent_title.toCaString(), + description = R.string.automation_error_compatibility_body.toCaString(), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/MainActivity.kt b/app/src/main/java/eu/darken/sdmse/main/ui/MainActivity.kt index 48731dd5c..f45afcbac 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/MainActivity.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/MainActivity.kt @@ -7,7 +7,10 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint import eu.darken.sdmse.R import eu.darken.sdmse.common.debug.Bugs +import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.recorder.core.RecorderModule +import eu.darken.sdmse.common.error.asErrorDialogBuilder import eu.darken.sdmse.common.navigation.findNavController import eu.darken.sdmse.common.theming.Theming import eu.darken.sdmse.common.uix.Activity2 @@ -56,11 +59,17 @@ class MainActivity : Activity2() { navController.addOnDestinationChangedListener { _, destination, bundle -> Bugs.leaveBreadCrumb("Navigated to $destination with args $bundle") } + + vm.errorEvents.observe2 { + log(tag, VERBOSE) { "Error event: $it" } + it.asErrorDialogBuilder(this).show() + } } override fun onResume() { super.onResume() vm.checkUpgrades() + vm.checkErrors() } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/MainViewModel.kt b/app/src/main/java/eu/darken/sdmse/main/ui/MainViewModel.kt index 9337d797d..35d95ce9c 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/MainViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/MainViewModel.kt @@ -2,17 +2,24 @@ package eu.darken.sdmse.main.ui import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.sdmse.automation.core.errors.AutomationException import eu.darken.sdmse.common.BuildConfigWrap +import eu.darken.sdmse.common.SingleLiveEvent import eu.darken.sdmse.common.coroutine.DispatcherProvider import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.uix.ViewModel2 import eu.darken.sdmse.common.upgrade.UpgradeRepo +import eu.darken.sdmse.main.core.SDMTool import eu.darken.sdmse.main.core.taskmanager.TaskManager +import eu.darken.sdmse.main.core.taskmanager.getLatestTask import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import java.time.Duration +import java.time.Instant import javax.inject.Inject @@ -21,7 +28,7 @@ class MainViewModel @Inject constructor( dispatcherProvider: DispatcherProvider, @Suppress("unused") private val handle: SavedStateHandle, private val upgradeRepo: UpgradeRepo, - taskManager: TaskManager, + private val taskManager: TaskManager, ) : ViewModel2(dispatcherProvider = dispatcherProvider) { private val stateFlow = MutableStateFlow(State()) @@ -29,6 +36,8 @@ class MainViewModel @Inject constructor( .onEach { log(VERBOSE) { "New state: $it" } } .asLiveData2() + val errorEvents = SingleLiveEvent() + private val readyStateInternal = MutableStateFlow(true) val readyState = readyStateInternal.asLiveData2() @@ -45,6 +54,26 @@ class MainViewModel @Inject constructor( upgradeRepo.refresh() } + private var handledErrors: Set + get() = handle["handledErrors"] ?: emptySet() + set(value) { + handle["handledErrors"] = value + } + + fun checkErrors() = launch { + log(TAG) { "checkErrors()" } + val state = taskManager.state.first() + + state.getLatestTask(SDMTool.Type.APPCLEANER) + ?.takeIf { !handledErrors.contains(it.id) } + ?.takeIf { Duration.between(it.completedAt!!, Instant.now()) < Duration.ofSeconds(10) } + ?.let { task -> + val error = task.error as? AutomationException ?: return@let + handledErrors = handledErrors + task.id + errorEvents.postValue(error) + } + } + data class State( val ready: Boolean = false ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c57833855..5e2980b4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -649,6 +649,7 @@ Accessibility service is not running. Try rebooting the device. Screen unavailable The operation was aborted because the screen became unavailable (e.g. display off or lockscreen active). + SD Maid couldn’t figure out the screen layout. If this keeps happening, your language or setup might not be fully supported. Check for updates or reach out to me so I can fix it. History A chronological list of recent actions and affected data. From 928706f3f093bddc2cddb271efa61e7191ba0bf6 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 21 Jan 2025 00:13:47 +0100 Subject: [PATCH 4/5] Only abort early if there are constant failures up till now --- .../sdmse/appcleaner/core/automation/ClearCacheModule.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 f26987ef9..bd9fe342a 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 @@ -160,7 +160,7 @@ class ClearCacheModule @AssistedInject constructor( } catch (e: TimeoutCancellationException) { log(TAG, WARN) { "Timeout while processing $installed" } failed.add(target) - if (timeoutCount > 2) break else timeoutCount++ + if (timeoutCount > 2 && successful.isEmpty()) break else timeoutCount++ } catch (e: CancellationException) { log(TAG, WARN) { "We were cancelled: ${e.asLog()}" } updateProgressPrimary(eu.darken.sdmse.common.R.string.general_cancel_action) @@ -176,8 +176,6 @@ class ClearCacheModule @AssistedInject constructor( } catch (e: Exception) { log(TAG, WARN) { "Failure for $target: ${e.asLog()}" } failed.add(target) - } finally { - increaseProgress() } } From 7b42d6a33efba4447bc418919044fffa1bc5151d Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 21 Jan 2025 00:13:58 +0100 Subject: [PATCH 5/5] Only increase progress for successes --- .../darken/sdmse/appcleaner/core/automation/ClearCacheModule.kt | 1 + 1 file changed, 1 insertion(+) 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 bd9fe342a..f1c9f005d 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 @@ -153,6 +153,7 @@ class ClearCacheModule @AssistedInject constructor( log(TAG, INFO) { "Successfully cleared cache for for $target" } task.onSuccess(target) successful.add(target) + increaseProgress() } catch (e: ScreenUnavailableException) { log(TAG, WARN) { "Cancelled because screen become unavailable: ${e.asLog()}" } // TODO We don't have to abort here, but this is not a normal state and should show an error?