Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automation: Improve reliability one Android 14 devices (OnePlus & Redmi) #1152

Merged
merged 10 commits into from
May 1, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import android.content.res.Configuration
import android.os.Build
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
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.isInstalled
import javax.inject.Inject
Expand Down Expand Up @@ -69,8 +67,6 @@ class DeviceDetective @Inject constructor(
checkManufactor("vivo") -> RomType.VIVO
checkManufactor("HONOR") -> RomType.HONOR
else -> RomType.AOSP
}.also {
log(TAG, VERBOSE) { "getROMType(): $it" }
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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.common.ScreenUnavailableException
import eu.darken.sdmse.automation.core.errors.ScreenUnavailableException
import eu.darken.sdmse.automation.core.errors.UserCancelledAutomationException
import eu.darken.sdmse.automation.core.specs.AutomationExplorer
import eu.darken.sdmse.automation.core.specs.AutomationSpec
Expand Down Expand Up @@ -96,7 +96,6 @@ class ClearCacheModule @AssistedInject constructor(

host.changeOptions { old ->
old.copy(
showOverlay = true,
accessibilityServiceInfo = AccessibilityServiceInfo().apply {
flags = (
AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
Expand All @@ -105,6 +104,7 @@ class ClearCacheModule @AssistedInject constructor(
)
eventTypes = AccessibilityEvent.TYPES_ALL_MASK
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
notificationTimeout = 250L
},
controlPanelTitle = R.string.appcleaner_automation_title.toCaString(),
controlPanelSubtitle = R.string.appcleaner_automation_subtitle_default_caches.toCaString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.sdmse.automation.core.common.AutomationLabelSource
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.common.device.DeviceDetective
import eu.darken.sdmse.common.isInstalled
import eu.darken.sdmse.common.pkgs.toPkgId
import javax.inject.Inject

class LabelDebugger @Inject constructor(
@ApplicationContext private val context: Context,
private val deviceDetective: DeviceDetective,
) : AutomationLabelSource {

suspend fun logAllLabels() {
log(TAG) { "logAllStorageLabels()" }
val romType = deviceDetective.getROMType()
log(TAG) { "ROMTYPE is $romType" }
SETTINGS_PKGS
.filter { context.isInstalled(it.name) }
.forEach { pkgId ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import eu.darken.sdmse.R
import eu.darken.sdmse.appcleaner.core.automation.specs.AppCleanerSpecGenerator
import eu.darken.sdmse.appcleaner.core.automation.specs.OnTheFlyLabler
import eu.darken.sdmse.appcleaner.core.automation.specs.aosp.AOSPLabels
import eu.darken.sdmse.automation.core.common.StepAbortException
import eu.darken.sdmse.automation.core.common.StepProcessor
import eu.darken.sdmse.automation.core.common.clickableParent
import eu.darken.sdmse.automation.core.common.crawl
Expand All @@ -29,6 +28,7 @@ import eu.darken.sdmse.automation.core.common.pkgId
import eu.darken.sdmse.automation.core.common.textEndsWithAny
import eu.darken.sdmse.automation.core.common.textMatchesAny
import eu.darken.sdmse.automation.core.common.windowCriteriaAppIdentifier
import eu.darken.sdmse.automation.core.errors.StepAbortException
import eu.darken.sdmse.automation.core.specs.AutomationExplorer
import eu.darken.sdmse.automation.core.specs.AutomationSpec
import eu.darken.sdmse.common.ca.toCaString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import eu.darken.sdmse.appcontrol.core.forcestop.ForceStopAutomationTask
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.common.ScreenUnavailableException
import eu.darken.sdmse.automation.core.errors.ScreenUnavailableException
import eu.darken.sdmse.automation.core.specs.AutomationExplorer
import eu.darken.sdmse.automation.core.specs.AutomationSpec
import eu.darken.sdmse.common.ca.CaString
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
package eu.darken.sdmse.automation.core

import android.view.accessibility.AccessibilityNodeInfo
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 kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive

suspend fun AutomationManager.canUseAcsNow(): Boolean = useAcs.first()
suspend fun AutomationManager.canUseAcsNow(): Boolean = useAcs.first()

suspend fun AutomationHost.waitForWindowRoot(delayMs: Long = 250): AccessibilityNodeInfo {
var root: AccessibilityNodeInfo? = null

while (currentCoroutineContext().isActive) {
root = windowRoot()
if (root != null) break

if (Bugs.isDebug) log(VERBOSE) { "Waiting for windowRoot..." }
delay(delayMs)
}

return root ?: throw CancellationException("Cancelled while waiting for windowRoot")
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface AutomationHost : Progress.Client {

val scope: CoroutineScope

suspend fun windowRoot(): AccessibilityNodeInfo
suspend fun windowRoot(): AccessibilityNodeInfo?

suspend fun changeOptions(action: (Options) -> Options)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import androidx.annotation.Keep
import androidx.appcompat.view.ContextThemeWrapper
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.sdmse.R
import eu.darken.sdmse.automation.core.common.AutomationException
import eu.darken.sdmse.automation.core.common.getRoot
import eu.darken.sdmse.automation.core.common.toStringShort
import eu.darken.sdmse.automation.core.errors.AutomationNoConsentException
import eu.darken.sdmse.automation.core.errors.UserCancelledAutomationException
import eu.darken.sdmse.automation.ui.AutomationControlView
Expand Down Expand Up @@ -200,7 +199,7 @@ class AutomationService : AccessibilityService(), AutomationHost, Progress.Host,
}

override fun onAccessibilityEvent(event: AccessibilityEvent) {
log(TAG) { "onAccessibilityEvent(eventType=${event.eventType})" }
log(TAG, VERBOSE) { "onAccessibilityEvent(eventType=${event.eventType})" }
if (!checkLaunch()) return

if (generalSettings.hasAcsConsent.valueBlocking != true) {
Expand All @@ -223,45 +222,17 @@ class AutomationService : AccessibilityService(), AutomationHost, Progress.Host,
}
}

if (Bugs.isDebug) log(TAG, VERBOSE) { "New automation event: $eventCopy" }

serviceScope.launch {
try {
eventCopy.source
?.getRoot(maxNesting = Int.MAX_VALUE)
?.let {
fallbackMutex.withLock {
if (!hasApiLevel(30)) {
@Suppress("DEPRECATION")
fallbackRoot?.recycle()
}
fallbackRoot = it
}
}
.also { log(TAG, VERBOSE) { "Fallback root was $fallbackRoot, now is $it" } }
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to get fallbackRoot from $event: $e" }
}
}

// TODO use a queue here?
serviceScope.launch {
// If we need fallbackRoot, don't race it
delay(50)
log(TAG, VERBOSE) { "Providing event: $eventCopy" }
if (Bugs.isDebug) log(TAG) { "Providing event: $eventCopy" }
automationEvents.emit(eventCopy)
}
}

private val fallbackMutex = Mutex()
private var fallbackRoot: AccessibilityNodeInfo? = null

override suspend fun windowRoot(): AccessibilityNodeInfo = suspendCancellableCoroutine {
val maybeRootNode: AccessibilityNodeInfo? = rootInActiveWindow ?: fallbackRoot?.also {
log(TAG, WARN) { "Using fallback rootNode: $it" }
}

log(TAG, VERBOSE) { "Providing window root: $maybeRootNode" }
it.resume(maybeRootNode ?: throw AutomationException("Root node is currently null"))
override suspend fun windowRoot(): AccessibilityNodeInfo? = suspendCancellableCoroutine {
val rootNode: AccessibilityNodeInfo? = rootInActiveWindow
log(TAG, VERBOSE) { "Providing windowRoot: ${rootNode?.toStringShort()}" }
it.resume(rootNode)
}

private val controlLp: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
Expand All @@ -276,6 +247,7 @@ class AutomationService : AccessibilityService(), AutomationHost, Progress.Host,
override suspend fun changeOptions(action: (AutomationHost.Options) -> AutomationHost.Options) {
val newOptions = action(currentOptions)
currentOptions = newOptions
serviceInfo = newOptions.accessibilityServiceInfo

mainThread.post {
controlView?.let { acv ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eu.darken.sdmse.automation.core.common

import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import eu.darken.sdmse.automation.core.errors.AutomationException
import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.asLog
Expand All @@ -13,8 +14,10 @@ import java.util.concurrent.LinkedBlockingDeque

private val TAG: String = logTag("Automation", "Crawler", "Common")

fun AccessibilityNodeInfo.toStringShort() =
"className=${this.className}, text='${this.text}', isClickable=${this.isClickable}, isEnabled=${this.isEnabled}, viewIdResourceName=${this.viewIdResourceName}, pkgName=${this.packageName}"
fun AccessibilityNodeInfo.toStringShort(): String {
val identity = Integer.toHexString(System.identityHashCode(this))
return "className=${this.className}, text='${this.text}', isClickable=${this.isClickable}, isEnabled=${this.isEnabled}, viewIdResourceName=${this.viewIdResourceName}, pkgName=${this.packageName}, identity=$identity"
}

val AccessibilityNodeInfo.textVariants: Set<String>
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import eu.darken.sdmse.automation.core.errors.AutomationException
import eu.darken.sdmse.automation.core.errors.DisabledTargetException
import eu.darken.sdmse.automation.core.specs.AutomationExplorer
import eu.darken.sdmse.common.debug.Bugs
import eu.darken.sdmse.common.debug.logging.Logging.Priority.INFO
Expand Down Expand Up @@ -37,9 +39,13 @@ fun AutomationExplorer.Context.defaultWindowIntent(

fun AutomationExplorer.Context.defaultWindowFilter(
pkgId: Pkg.Id
): (AccessibilityEvent) -> Boolean {
return fun(event: AccessibilityEvent): Boolean {
return event.pkgId == pkgId
): (AccessibilityEvent) -> Boolean = fun(event: AccessibilityEvent): Boolean {
// We want to know that the settings window is open now
if (event.pkgId != pkgId) return false
return when (event.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> true
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> true
else -> false
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import eu.darken.sdmse.automation.core.AutomationHost
import eu.darken.sdmse.automation.core.ScreenState
import eu.darken.sdmse.automation.core.errors.AutomationException
import eu.darken.sdmse.automation.core.errors.PlanAbortException
import eu.darken.sdmse.automation.core.errors.ScreenUnavailableException
import eu.darken.sdmse.automation.core.errors.StepAbortException
import eu.darken.sdmse.automation.core.waitForWindowRoot
import eu.darken.sdmse.common.R
import eu.darken.sdmse.common.ca.CaDrawable
import eu.darken.sdmse.common.ca.CaString
Expand All @@ -35,6 +40,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeout
import kotlin.system.measureTimeMillis


class StepProcessor @AssistedInject constructor(
Expand Down Expand Up @@ -70,7 +76,12 @@ class StepProcessor @AssistedInject constructor(
try {
while (currentCoroutineContext().isActive) {
try {
withTimeout(5 * 1000) { doCrawl(step, attempts++) }
withTimeout(5 * 1000) {
val stepTime = measureTimeMillis {
doCrawl(step, attempts++)
}
log(TAG) { "Step took ${stepTime}ms to execute" }
}
// Step was successful :))
break
} catch (e: PlanAbortException) {
Expand Down Expand Up @@ -119,24 +130,23 @@ class StepProcessor @AssistedInject constructor(
}

// avg delay between activity launch and acs event
delay(200)
delay(50)

// Wait for correct window
val targetWindowRoot: AccessibilityNodeInfo = withTimeout(4000) {
// Condition for the right window, e.g. check title
// Wait for correct window
if (step.windowIntent != null && step.windowEventFilter != null) {
log(TAG, VERBOSE) { "Waiting for window event filter to pass..." }
host.events.filter {
log(TAG, VERBOSE) { "Testing window event $it" }
step.windowEventFilter.invoke(it)
}.first()
log(TAG, VERBOSE) { "Waiting for window event filter passed!" }
log(TAG, VERBOSE) { "Window event filter passed!" }
}

// Condition for the right window, e.g. check title
var currentRoot: AccessibilityNodeInfo? = null

while (step.windowNodeTest != null && currentCoroutineContext().isActive) {
currentRoot = host.windowRoot().apply {
currentRoot = host.waitForWindowRoot().apply {
if (Bugs.isDebug) {
log(TAG, VERBOSE) { "Looking for viable window root, current nodes:" }
crawl().forEach { log(TAG, VERBOSE) { it.infoShort } }
Expand All @@ -147,11 +157,12 @@ class StepProcessor @AssistedInject constructor(
break
} else {
log(TAG) { "Not a viable root node: $currentRoot (spec=$step)" }
delay(200)
delay(500)
}
}

currentRoot ?: host.windowRoot()
// There was no windowNodeTest, so we continue with the first thing we got
currentRoot ?: host.waitForWindowRoot()
}
log(TAG, VERBOSE) { "Current window root node is ${targetWindowRoot.toStringShort()}" }

Expand Down Expand Up @@ -185,12 +196,12 @@ class StepProcessor @AssistedInject constructor(
delay(100)
}
// Let's try a new one
currentRootNode = host.windowRoot()
currentRootNode = host.waitForWindowRoot()
}
target!!
}

else -> host.windowRoot()
else -> host.waitForWindowRoot()
}
log(TAG, VERBOSE) { "Target node is ${targetNode.toStringShort()}" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.common.crawl
import eu.darken.sdmse.automation.core.waitForWindowRoot
import eu.darken.sdmse.common.ca.caString
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
Expand Down Expand Up @@ -60,7 +61,7 @@ class DebugTaskModule @AssistedInject constructor(
val eventJob = host.events
.onEach {
log(TAG) { "Event: $it" }
val crawled = host.windowRoot().crawl(debug = true).toList()
val crawled = host.waitForWindowRoot().crawl(debug = true).toList()
updateProgressSecondary("Event: ${it.eventType} (depth: ${crawled.last().level})")
}
.launchIn(moduleScope)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
package eu.darken.sdmse.automation.core.errors

open class AutomationException : Exception()
open class AutomationException(
message: String? = null,
cause: Throwable? = null
) : Exception(message, cause) {
constructor(message: String?) : this(message, null)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package eu.darken.sdmse.automation.core.common
package eu.darken.sdmse.automation.core.errors

open class DisabledTargetException(message: String?, cause: Throwable?) : AutomationException(message, cause) {
constructor(message: String?) : this(message, null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package eu.darken.sdmse.automation.core.common
package eu.darken.sdmse.automation.core.errors

open class PlanAbortException(
message: String,
Expand Down
Loading
Loading