Files
openless/openless-all/app/android/kotlin/OpenLessShizukuUserService.kt

457 lines
17 KiB
Kotlin

package com.openless.app
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.Keep
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import org.json.JSONObject
/**
* Runs in a Shizuku UserService process with shell/root identity. Best-effort accessibility
* recovery — Secure Settings writes are not compare-and-set.
*/
@Keep
class OpenLessShizukuUserService @JvmOverloads constructor(private val appPackage: String = "") :
IOpenLessShizukuUserService.Stub() {
@Keep constructor(context: Context) : this(context.packageName)
override fun destroy() {
Log.i(TAG, "destroy")
System.exit(0)
}
override fun injectPasteKey(): Boolean {
return runPasteKeyInjection() is ShellResult.Success
}
override fun recoverAccessibilityService(serviceComponent: String): String {
if (!OpenLessShizukuBridge.isValidServiceComponent(serviceComponent)) {
return recoveryJson(
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"invalid_component",
)
}
for (attempt in 0 until MAX_RECOVERY_ATTEMPTS) {
when (val attemptResult = attemptRecovery(serviceComponent, attempt)) {
is RecoveryAttemptResult.Success -> {
return recoveryJson(
OpenLessShizukuBridge.RecoveryOutcome.Success,
"success",
)
}
is RecoveryAttemptResult.Retry -> {
// Try again after a concurrent settings change.
}
is RecoveryAttemptResult.Failure -> {
return recoveryJson(attemptResult.outcome, attemptResult.messageKey)
}
}
}
return recoveryJson(
OpenLessShizukuBridge.RecoveryOutcome.WriteRejected,
"concurrent_change",
)
}
private fun attemptRecovery(
serviceComponent: String,
attempt: Int,
): RecoveryAttemptResult {
val preWrite =
readSnapshot()
?: return RecoveryAttemptResult.Failure(
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"read_failed",
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && appPackage.isNotBlank()) {
if (!allowRestrictedSettingsForApp()) {
Log.w(TAG, "ACCESS_RESTRICTED_SETTINGS appops step failed attempt=$attempt")
}
}
val immediate =
readSnapshot()
?: return RecoveryAttemptResult.Failure(
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"read_failed",
)
if (OpenLessShizukuBridge.preWriteSnapshotChanged(preWrite, immediate)) {
Log.i(TAG, "pre-put snapshot changed attempt=$attempt")
return RecoveryAttemptResult.Retry
}
val prePut =
readSnapshot()
?: return RecoveryAttemptResult.Failure(
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"read_failed",
)
if (OpenLessShizukuBridge.preWriteSnapshotChanged(immediate, prePut)) {
Log.i(TAG, "immediate pre-put snapshot changed attempt=$attempt")
return RecoveryAttemptResult.Retry
}
if (OpenLessShizukuBridge.requiresManualRecovery(prePut, serviceComponent)) {
return RecoveryAttemptResult.Failure(
OpenLessShizukuBridge.RecoveryOutcome.WriteRejected,
"manual_required",
)
}
val merged =
OpenLessShizukuBridge.mergeEnabledAccessibilityServices(
prePut.services,
serviceComponent,
)
if (merged.isBlank()) {
return RecoveryAttemptResult.Failure(
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"merge_failed",
)
}
if (!putSecureSetting(KEY_ENABLED_SERVICES, merged)) {
return RecoveryAttemptResult.Failure(
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"write_services_failed",
)
}
val postServicesPut =
readEnabledServices()
?: run {
val rollback =
rollbackWrittenState(
WrittenState(merged, null),
prePut,
)
return failureAfterRollback(
rollback,
prePut,
WrittenState(merged, null),
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"readback_failed",
)
}
if (!OpenLessShizukuBridge.servicesListsEqual(postServicesPut, merged)) {
Log.w(TAG, "services changed immediately after write attempt=$attempt")
rollbackWrittenState(
WrittenState(merged, null),
prePut,
)
return RecoveryAttemptResult.Retry
}
val writtenEnabled = "1"
if (!putSecureSetting(KEY_ACCESSIBILITY_ENABLED, writtenEnabled)) {
val rollback =
rollbackWrittenState(
WrittenState(merged, writtenEnabled),
prePut,
)
return failureAfterRollback(
rollback,
prePut,
WrittenState(merged, writtenEnabled),
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"write_enabled_failed",
)
}
val readback =
readEnabledServices()
?: run {
val rollback =
rollbackWrittenState(
WrittenState(merged, writtenEnabled),
prePut,
)
return failureAfterRollback(
rollback,
prePut,
WrittenState(merged, writtenEnabled),
OpenLessShizukuBridge.RecoveryOutcome.ShellFailed,
"readback_failed",
)
}
if (!OpenLessShizukuBridge.verifyReadbackExact(readback, merged)) {
val rollback =
rollbackWrittenState(
WrittenState(merged, writtenEnabled),
prePut,
)
val failureCause =
if (!OpenLessShizukuBridge.readbackContainsComponent(readback, serviceComponent)) {
"oem_rollback"
} else {
"concurrent_change"
}
return failureAfterRollback(
rollback,
prePut,
WrittenState(merged, writtenEnabled),
OpenLessShizukuBridge.RecoveryOutcome.WriteRejected,
failureCause,
)
}
return RecoveryAttemptResult.Success
}
private fun rollbackWrittenState(
written: WrittenState,
snapshot: OpenLessShizukuBridge.AccessibilitySettingsSnapshot,
): RollbackOutcome {
val servicesResult = rollbackServicesIfUnchanged(written.services, snapshot.services)
val enabledResult =
if (written.enabled == null) {
OpenLessShizukuBridge.EnabledRollbackResult.Skipped
} else if (
OpenLessShizukuBridge.shouldRollbackEnabledAfterServices(
servicesResult,
written.enabled,
snapshot.enabled,
)
) {
rollbackEnabledIfUnchanged(
written.enabled,
snapshot.enabled,
snapshot.services,
)
} else {
Log.w(TAG, "skip enabled rollback: services rollback=${servicesResult.name}")
OpenLessShizukuBridge.EnabledRollbackResult.SkippedDueToServicesConflict
}
return RollbackOutcome(servicesResult, enabledResult)
}
private fun failureAfterRollback(
rollback: RollbackOutcome,
snapshot: OpenLessShizukuBridge.AccessibilitySettingsSnapshot,
written: WrittenState,
outcome: OpenLessShizukuBridge.RecoveryOutcome,
failureCause: String,
): RecoveryAttemptResult.Failure {
val messageKey =
OpenLessShizukuBridge.recoveryFailureMessageKey(
OpenLessShizukuBridge.RecoveryRollbackStatus(
rollback.services,
rollback.enabled,
),
wroteEnabled = written.enabled != null,
baselineEnabled = snapshot.enabled,
failureCause = failureCause,
)
return RecoveryAttemptResult.Failure(outcome, messageKey)
}
private fun rollbackServicesIfUnchanged(
writtenServices: String,
baselineServices: String,
): OpenLessShizukuBridge.ServicesRollbackResult {
val current =
readEnabledServices() ?: return OpenLessShizukuBridge.ServicesRollbackResult.ReadFailed
if (OpenLessShizukuBridge.servicesListsEqual(writtenServices, baselineServices)) {
return OpenLessShizukuBridge.evaluateUnchangedServicesWriteRollback(
current,
baselineServices,
)
}
if (!OpenLessShizukuBridge.servicesListsEqual(current, writtenServices)) {
Log.w(TAG, "skip services rollback: current differs from written")
return OpenLessShizukuBridge.ServicesRollbackResult.Conflict
}
return if (putSecureSetting(KEY_ENABLED_SERVICES, baselineServices)) {
OpenLessShizukuBridge.ServicesRollbackResult.Restored
} else {
OpenLessShizukuBridge.ServicesRollbackResult.WriteFailed
}
}
private fun rollbackEnabledIfUnchanged(
writtenEnabled: String,
baselineEnabled: String,
baselineServices: String,
): OpenLessShizukuBridge.EnabledRollbackResult {
if (writtenEnabled == baselineEnabled) {
return OpenLessShizukuBridge.EnabledRollbackResult.AlreadyBaseline
}
if (writtenEnabled == "1" && baselineEnabled != "1") {
Log.w(TAG, "skip enabled rollback: refusing to auto-disable global accessibility")
return OpenLessShizukuBridge.EnabledRollbackResult.SkippedDueToServicesConflict
}
val currentEnabled =
readAccessibilityEnabled()
?: return OpenLessShizukuBridge.EnabledRollbackResult.ReadFailed
if (currentEnabled != writtenEnabled) {
Log.w(TAG, "skip enabled rollback: current differs from written")
return OpenLessShizukuBridge.EnabledRollbackResult.Skipped
}
val currentServices =
readEnabledServices() ?: return OpenLessShizukuBridge.EnabledRollbackResult.ReadFailed
if (!OpenLessShizukuBridge.servicesListsEqual(currentServices, baselineServices)) {
Log.w(TAG, "skip enabled rollback: services changed before enabled put")
return OpenLessShizukuBridge.EnabledRollbackResult.SkippedDueToServicesConflict
}
return if (putSecureSetting(KEY_ACCESSIBILITY_ENABLED, baselineEnabled)) {
OpenLessShizukuBridge.EnabledRollbackResult.Restored
} else {
OpenLessShizukuBridge.EnabledRollbackResult.WriteFailed
}
}
private fun readSnapshot(): OpenLessShizukuBridge.AccessibilitySettingsSnapshot? {
val services = readEnabledServices() ?: return null
val enabled = readAccessibilityEnabled() ?: return null
return OpenLessShizukuBridge.AccessibilitySettingsSnapshot(services, enabled)
}
private fun readEnabledServices(): String? {
return when (val result = runSettingsGet(KEY_ENABLED_SERVICES)) {
is ShellResult.Failure -> null
is ShellResult.Success -> result.value
}
}
private fun readAccessibilityEnabled(): String? {
return when (val result = runSettingsGet(KEY_ACCESSIBILITY_ENABLED)) {
is ShellResult.Failure -> null
is ShellResult.Success -> result.value
}
}
private fun putSecureSetting(key: String, value: String): Boolean {
if (!isAllowedSecureKey(key)) {
return false
}
return runSettingsPut(key, value) is ShellResult.Success
}
private fun allowRestrictedSettingsForApp(): Boolean {
if (appPackage.isBlank() || !OpenLessShizukuBridge.isValidAndroidPackageName(appPackage)) {
return false
}
return runProcess(
listOf("cmd", "appops", "set", appPackage, "ACCESS_RESTRICTED_SETTINGS", "allow")
) is
ShellResult.Success
}
private fun runPasteKeyInjection(): ShellResult {
return runProcess(listOf("input", "keyevent", KEYCODE_PASTE))
}
private fun runSettingsGet(key: String): ShellResult {
if (!isAllowedSecureKey(key)) {
return ShellResult.Failure
}
return when (val result = runProcess(listOf("settings", "get", "secure", key))) {
is ShellResult.Failure -> ShellResult.Failure
is ShellResult.Success -> ShellResult.Success(normalizeSettingsOutput(result.value))
}
}
private fun runSettingsPut(key: String, value: String): ShellResult {
if (!isAllowedSecureKey(key)) {
return ShellResult.Failure
}
return runProcess(listOf("settings", "put", "secure", key, value))
}
private fun normalizeSettingsOutput(raw: String): String {
val trimmed = raw.trim()
if (trimmed.isEmpty() || trimmed == "null") {
return ""
}
return trimmed
}
private fun isAllowedSecureKey(key: String): Boolean {
return key == KEY_ENABLED_SERVICES || key == KEY_ACCESSIBILITY_ENABLED
}
private fun runProcess(command: List<String>): ShellResult {
if (command.isEmpty() || command.any { it.isBlank() }) {
return ShellResult.Failure
}
return try {
val process = ProcessBuilder(command).redirectErrorStream(true).start()
val reader = Executors.newSingleThreadExecutor()
val outputTask =
reader.submit(Callable { process.inputStream.bufferedReader().readText() })
try {
if (!process.waitFor(SHELL_TIMEOUT_SEC, TimeUnit.SECONDS)) {
process.destroyForcibly()
return ShellResult.Failure
}
if (process.exitValue() != 0) {
return ShellResult.Failure
}
ShellResult.Success(outputTask.get(1, TimeUnit.SECONDS).orEmpty())
} catch (_: TimeoutException) {
process.destroyForcibly()
ShellResult.Failure
} finally {
reader.shutdownNow()
}
} catch (error: Throwable) {
Log.w(TAG, "privileged process failed", error)
ShellResult.Failure
}
}
private fun recoveryJson(
outcome: OpenLessShizukuBridge.RecoveryOutcome,
messageKey: String,
): String {
return JSONObject().put("outcome", outcome.name).put("messageKey", messageKey).toString()
}
private data class WrittenState(
val services: String,
val enabled: String?,
)
private data class RollbackOutcome(
val services: OpenLessShizukuBridge.ServicesRollbackResult,
val enabled: OpenLessShizukuBridge.EnabledRollbackResult,
)
private sealed class RecoveryAttemptResult {
data object Success : RecoveryAttemptResult()
data object Retry : RecoveryAttemptResult()
data class Failure(
val outcome: OpenLessShizukuBridge.RecoveryOutcome,
val messageKey: String,
) : RecoveryAttemptResult()
}
private sealed class ShellResult {
data class Success(val value: String) : ShellResult()
data object Failure : ShellResult()
}
companion object {
private const val TAG = "OpenLessShizukuUserSvc"
private const val SHELL_TIMEOUT_SEC = 10L
private const val KEY_ENABLED_SERVICES = "enabled_accessibility_services"
private const val KEY_ACCESSIBILITY_ENABLED = "accessibility_enabled"
private const val MAX_RECOVERY_ATTEMPTS = 3
private const val KEYCODE_PASTE = "279"
}
}