x1ongzhu 1 ano atrás
pai
commit
b4320fce0e

+ 2 - 36
app/src/main/java/com/example/modifier/Global.kt

@@ -37,6 +37,7 @@ import java.io.File
 import java.nio.file.Files
 import java.util.Date
 import java.util.Optional
+import kotlin.system.exitProcess
 
 
 object Global {
@@ -255,41 +256,6 @@ object Global {
     }
 
 
-    @JvmStatic
-    suspend fun killPhoneProcess(force: Boolean = false): Boolean {
-        try {
-            if (!force) {
-                if (shellRun("getprop phonekilled").component1().contains("yes")) {
-                    return true
-                }
-            }
-            run kill@{
-                repeat(3) {
-                    val pid = shellRun("pidof com.android.phone").component1().trim()
-                    if (!Regex("[0-9]+").matches(pid)) {
-                        Log.e(com.example.modifier.TAG, "killPhoneProcess: pid not found")
-                        return true
-                    }
-                    Log.i(com.example.modifier.TAG, "killPhoneProcess: pid=$pid")
-                    shellRun("kill -9 $pid")
-                    delay(1000)
-
-                    val pidNew = shellRun("pidof com.android.phone").component1().trim()
-                    if (pidNew == pid) {
-                        Log.e(com.example.modifier.TAG, "killPhoneProcess: failed to kill phone process")
-                    } else {
-                        Log.i(com.example.modifier.TAG, "killPhoneProcess: success, new pid: $pidNew")
-                        shellRun("kill -9 $pid", "setprop phonekilled yes")
-                        return true
-                    }
-                }
-            }
-        } catch (e: Exception) {
-            Log.e(com.example.modifier.TAG, "Error Kill Phone", e)
-        }
-        return false
-    }
-
     @JvmStatic
     fun sendSmsIntent(sender: String, msg: String) {
         val intent = Intent()
@@ -476,6 +442,6 @@ object Global {
         )
         val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
         mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent)
-        System.exit(0)
+        exitProcess(0)
     }
 }

+ 119 - 0
app/src/main/java/com/example/modifier/data/GoogleMessageStateRepository.kt

@@ -0,0 +1,119 @@
+package com.example.modifier.data
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import com.example.modifier.enums.RcsConfigureState
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+import org.apache.commons.collections4.queue.CircularFifoQueue
+import kotlin.coroutines.resume
+import kotlin.time.Duration
+
+class GoogleMessageStateRepository(context: Context) {
+    private val handler = Handler(Looper.getMainLooper())
+    val logs = MutableLiveData<String>()
+    val rcsConfigureState = MutableLiveData(RcsConfigureState.CONFIGURED)
+
+    suspend fun startLogging() {
+        try {
+            val logsCache = CircularFifoQueue<String>(128)
+            val p = Runtime.getRuntime().exec("su")
+            p.outputStream.bufferedWriter().use { writer ->
+                writer.write("logcat -c")
+                writer.newLine()
+                writer.flush()
+                writer.write("logcat BugleRcsEngine:D *:S -v time")
+                writer.newLine()
+                writer.flush()
+            }
+            p.inputStream
+                .bufferedReader()
+                .useLines { lines ->
+                    lines.forEach { line ->
+                        if (line.contains("destState=CheckPreconditionsState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.NOT_CONFIGURED)
+                        } else if (line.contains("destState=ReadyState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.READY)
+                        } else if (line.contains("destState=WaitingForOtpState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.WAITING_FOR_OTP)
+                        } else if (line.contains("destState=VerifyOtpState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.VERIFYING_OTP)
+                        } else if (line.contains("destState=ConfiguredState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.CONFIGURED)
+                        } else if (line.contains("destState=WaitingForRcsDefaultOnState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.WAITING_FOR_DEFAULT_ON)
+                        } else if (line.contains("destState=WaitingForGoogleTosState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.WAITING_FOR_TOS)
+                        } else if (line.contains("destState=RetryState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.RETRY)
+                        } else if (line.contains("destState=ReplayRequestState")) {
+                            rcsConfigureState.postValue(RcsConfigureState.REPLAY_REQUEST)
+                        }
+                        Regex("(?<time>\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3}) I/BugleRcsEngine\\(\\W*\\d+\\): (?<log>.*)")
+                            .matchEntire(line)
+                            ?.apply {
+                                val time = groups["time"]?.value?.dropLast(4)
+                                val log = groups["log"]?.value
+                                    ?.replace(Regex("\\[\\w+-\\w+-\\w+-\\w+-\\w+]"), "")
+                                    ?.replace(Regex("\\[CONTEXT.*]"), "")
+                                    ?.trim()
+
+                                if (time != null && log != null) {
+                                    if (log.contains("destState=")) {
+                                        logsCache.add("$time: $log")
+                                        logs.postValue(logsCache.joinToString("\n"))
+                                        delay(100)
+                                        logs.postValue(logsCache.joinToString("\n"))
+                                    }
+                                }
+                            }
+                    }
+                }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    suspend fun waitForRcsState(
+        states: Array<RcsConfigureState>,
+        timeout: Duration
+    ): RcsConfigureState? {
+        var state: RcsConfigureState? = null
+        withTimeoutOrNull(timeout) {
+            withContext(Dispatchers.Main) {
+                suspendCancellableCoroutine { continuation ->
+                    val observer = Observer<RcsConfigureState> { value ->
+                        if (states.contains(value)) {
+                            state = value
+                            if (isActive)
+                                continuation.resume(Unit)
+                        }
+                    }
+
+                    rcsConfigureState.observeForever(observer)
+
+                    continuation.invokeOnCancellation {
+                        handler.post {
+                            Log.i(com.example.modifier.TAG, "removeObserver")
+                            rcsConfigureState.removeObserver(observer)
+                        }
+                    }
+                }
+            }
+            false
+        }
+        return state
+    }
+
+    fun updateRcsState(state: RcsConfigureState) {
+        rcsConfigureState.postValue(state)
+    }
+}

+ 4 - 0
app/src/main/java/com/example/modifier/http/api/SysConfigApi.kt

@@ -5,6 +5,10 @@ import io.ktor.resources.Resource
 @Resource("api/sys-config")
 class SysConfigApi {
 
+    companion object {
+        const val check_availability_numbers = "check_availability_numbers"
+    }
+
     @Resource("{id}")
     class Id(val parent: SysConfigApi = SysConfigApi(), val id: String) {
     }

+ 63 - 315
app/src/main/java/com/example/modifier/service/ModifierService.kt

@@ -27,9 +27,6 @@ import android.widget.FrameLayout
 import androidx.annotation.MenuRes
 import androidx.appcompat.widget.PopupMenu
 import androidx.core.content.ContextCompat
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Observer
-import androidx.lifecycle.liveData
 import com.example.modifier.BuildConfig
 import com.example.modifier.Global
 import com.example.modifier.Global.backup
@@ -49,33 +46,36 @@ import com.example.modifier.data.AppPreferencesRepository
 import com.example.modifier.data.AppState
 import com.example.modifier.data.AppStateRepository
 import com.example.modifier.data.BackupItemDao
+import com.example.modifier.data.GoogleMessageStateRepository
 import com.example.modifier.databinding.FloatingWindowBinding
 import com.example.modifier.enums.RcsConfigureState
 import com.example.modifier.enums.RcsConnectionStatus
 import com.example.modifier.extension.kill
-import com.example.modifier.utils.hasRootAccess
-import com.example.modifier.http.ktorClient
 import com.example.modifier.http.api.DeviceApi
 import com.example.modifier.http.api.RcsNumberApi
 import com.example.modifier.http.api.SysConfigApi
+import com.example.modifier.http.ktorClient
 import com.example.modifier.http.request.RcsNumberRequest
 import com.example.modifier.http.response.DeviceResponse
 import com.example.modifier.http.response.RcsNumberResponse
 import com.example.modifier.http.response.SysConfigResponse
 import com.example.modifier.model.InstallApkAction
+import com.example.modifier.model.SimInfo
 import com.example.modifier.model.SocketCallback
 import com.example.modifier.model.TaskAction
 import com.example.modifier.model.TaskConfig
 import com.example.modifier.model.TaskExecutionResult
-import com.example.modifier.model.SimInfo
 import com.example.modifier.serializer.Json
 import com.example.modifier.utils.changeClashProfile
 import com.example.modifier.utils.clearConv
+import com.example.modifier.utils.hasRootAccess
 import com.example.modifier.utils.isClashInstalled
 import com.example.modifier.utils.isOldVersion
 import com.example.modifier.utils.isRebooted
+import com.example.modifier.utils.killPhoneProcess
 import com.example.modifier.utils.optimize
 import com.example.modifier.utils.resetAll
+import com.example.modifier.utils.setBatteryLevel
 import com.example.modifier.utils.shellRun
 import com.example.modifier.utils.smsIntent
 import com.example.modifier.utils.stopClash
@@ -105,14 +105,11 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
 import kotlinx.coroutines.withTimeoutOrNull
 import kotlinx.serialization.encodeToString
-import org.apache.commons.collections4.queue.CircularFifoQueue
 import org.apache.commons.lang3.RandomStringUtils
 import org.json.JSONException
 import org.json.JSONObject
@@ -123,10 +120,8 @@ import java.util.Optional
 import java.util.Timer
 import java.util.TimerTask
 import java.util.concurrent.atomic.AtomicReference
-import kotlin.coroutines.resume
 import kotlin.math.max
 import kotlin.math.min
-import kotlin.time.Duration
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.Duration.Companion.seconds
@@ -150,67 +145,6 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
     private var currentTaskId = 0
     private var lastSend = 0L
 
-    private val rcsConfigureState = MutableLiveData(RcsConfigureState.CONFIGURED)
-    private val logcat = liveData(Dispatchers.IO) {
-        try {
-            val logs = CircularFifoQueue<String>(128)
-            val p = Runtime.getRuntime().exec("su")
-            p.outputStream.bufferedWriter().use { writer ->
-                writer.write("logcat -c")
-                writer.newLine()
-                writer.flush()
-                writer.write("logcat BugleRcsEngine:D *:S -v time")
-                writer.newLine()
-                writer.flush()
-            }
-            p.inputStream
-                .bufferedReader()
-                .useLines { lines ->
-                    lines.forEach { line ->
-                        if (line.contains("destState=CheckPreconditionsState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.NOT_CONFIGURED)
-                        } else if (line.contains("destState=ReadyState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.READY)
-                        } else if (line.contains("destState=WaitingForOtpState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.WAITING_FOR_OTP)
-                        } else if (line.contains("destState=VerifyOtpState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.VERIFYING_OTP)
-                        } else if (line.contains("destState=ConfiguredState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.CONFIGURED)
-                        } else if (line.contains("destState=WaitingForRcsDefaultOnState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.WAITING_FOR_DEFAULT_ON)
-                        } else if (line.contains("destState=WaitingForGoogleTosState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.WAITING_FOR_TOS)
-                        } else if (line.contains("destState=RetryState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.RETRY)
-                        } else if (line.contains("destState=ReplayRequestState")) {
-                            rcsConfigureState.postValue(RcsConfigureState.REPLAY_REQUEST)
-                        }
-                        Regex("(?<time>\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3}) I/BugleRcsEngine\\(\\W*\\d+\\): (?<log>.*)")
-                            .matchEntire(line)
-                            ?.apply {
-                                val time = groups["time"]?.value?.dropLast(4)
-                                val log = groups["log"]?.value
-                                    ?.replace(Regex("\\[\\w+-\\w+-\\w+-\\w+-\\w+]"), "")
-                                    ?.replace(Regex("\\[CONTEXT.*]"), "")
-                                    ?.trim()
-
-                                if (time != null && log != null) {
-                                    if (log.contains("destState=")) {
-                                        logs.add("$time: $log")
-                                        emit(logs.joinToString("\n"))
-                                        delay(100)
-                                        emit(logs.joinToString("\n"))
-                                    }
-                                }
-                            }
-                    }
-                }
-        } catch (e: Exception) {
-            e.printStackTrace()
-        }
-    }
-
     private val backupItemDao: BackupItemDao by lazy {
         AppDatabase.getDatabase(this).itemDao()
     }
@@ -222,8 +156,17 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         AppStateRepository(this)
     }
     private lateinit var appState: StateFlow<AppState>
+    private val googleMessageStateRepository: GoogleMessageStateRepository by lazy {
+        GoogleMessageStateRepository(this)
+    }
     private var requestMode = 1;
     private var currentActivity = ""
+    private val screenInspector: ScreenInspector by lazy {
+        ScreenInspector(this)
+    }
+    private val screenController: ScreenController by lazy {
+        ScreenController(this, screenInspector)
+    }
 
     fun connect() {
         try {
@@ -278,6 +221,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             if (!hasRoot) {
                 System.exit(0)
             }
+            googleMessageStateRepository.startLogging()
             if (isRebooted()) {
                 delay(2.minutes)
             } else {
@@ -286,7 +230,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             optimize()
             syncTime()
             if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) {
-                Global.killPhoneProcess(force = false)
+                killPhoneProcess(force = false)
             }
             appStateRepository.updateRuntimeFlags(preparing = false)
 
@@ -297,11 +241,19 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     reportDeviceStatues()
                 }
             }, 0, 3.seconds.inWholeMilliseconds)
-            timer.schedule(object : TimerTask() {
-                override fun run() {
-
-                }
-            }, 0, 30.minutes.inWholeMilliseconds)
+            if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) {
+                timer.schedule(object : TimerTask() {
+                    override fun run() {
+                        CoroutineScope(Dispatchers.IO).launch {
+                            try {
+                                setBatteryLevel(100)
+                            } catch (e: Exception) {
+                                e.printStackTrace()
+                            }
+                        }
+                    }
+                }, 0, 30.minutes.inWholeMilliseconds)
+            }
         }
     }
 
@@ -556,18 +508,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             var traverseResult = TraverseResult()
             withTimeoutOrNull(taskConfig.rcsWait) {
                 while (true) {
-                    val root = rootInActiveWindow
                     traverseResult = TraverseResult()
-                    traverseNode(root, traverseResult)
-                    if (traverseResult.isRcsCapable) {
-                        if (traverseResult.sendBtn != null) {
-                            if (taskConfig.endToEndEncryption) {
-                                if (traverseResult.encrypted) {
-                                    break
-                                }
-                            } else {
-                                break
-                            }
+                    screenInspector.traverseNode(traverseResult)
+                    if (traverseResult.isRcsCapable && traverseResult.sendBtn != null) {
+                        if (!taskConfig.endToEndEncryption || traverseResult.encrypted) {
+                            break
                         }
                     }
                     delay(200)
@@ -604,110 +549,12 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         return false
     }
 
-    fun getNodes(node: AccessibilityNodeInfo? = null): List<AccessibilityNodeInfo> {
-        fun traverseChildren(node: AccessibilityNodeInfo): List<AccessibilityNodeInfo> {
-            val result = mutableListOf<AccessibilityNodeInfo>()
-            for (i in 0 until node.childCount) {
-                val child = node.getChild(i)
-                result.add(child)
-                result.addAll(traverseChildren(child))
-            }
-            return result
-        }
-        return traverseChildren(node ?: rootInActiveWindow)
-    }
-
-    private fun traverseNode(node: AccessibilityNodeInfo?, result: TraverseResult) {
-        if (node == null) {
-            return
-        }
-
-        val packageInfo = packageManager.getPackageInfo(
-            node.packageName.toString(), 0
-        )
-
-        val className = node.className.toString()
-        val name = node.viewIdResourceName
-        val text = Optional.ofNullable(node.text).map { obj: CharSequence -> obj.toString() }
-            .orElse(null)
-        val id = node.viewIdResourceName
-
-        Log.d(com.example.modifier.TAG, "Node: class=$className, text=$text, name=$name, id=$id")
-
-        if ("Compose:Draft:Send" == name) {
-            result.sendBtn = node
-        }
-
-        if ("com.google.android.apps.messaging:id/send_message_button_icon" == id) {
-            result.sendBtn = node
-        }
-
-        if (text != null && (text.contains("RCS 聊天") || text.contains("RCS chat"))) {
-            result.isRcsCapable = true
-        }
-
-        if ("com.google.android.apps.messaging:id/tombstone_message" == id) {
-            result.isRcsCapable = text.contains("聊天") || text.contains("Chatting with")
-        }
-
-        if (text != null && (text.contains("Turn on RCS chats") || text.contains("开启 RCS 聊天功能")
-                    || text.contains("Enable chat features") || text.contains("启用聊天功能"))
-        ) {
-            fun findSwitch(node: AccessibilityNodeInfo): Boolean {
-                if ("com.google.android.apps.messaging:id/switchWidget" == node.viewIdResourceName) {
-                    result.rcsSwitch = node
-                    return true
-                }
-                for (i in 0 until node.childCount) {
-                    val child = node.getChild(i)
-                    if (findSwitch(child)) {
-                        return true
-                    }
-                }
-                return false
-            }
-            findSwitch(node.parent.parent)
-        }
-
-        if ("com.google.android.apps.messaging:id/rcs_sim_status_status_text" == id) {
-            if (text.lowercase().contains("connected") || text.lowercase().contains("已连接")) {
-                result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
-            }
-        }
-
-        if ("android:id/title" == id) {
-            when (text) {
-                "状态:已连接" -> result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
-                "Status: Connected" -> result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
-            }
-        }
-
-        if (text != null && (text.contains("end-to-end encrypted") || text.contains("已经过端到端加密"))) {
-            result.encrypted = true
-        }
-
-        if (node.childCount != 0) {
-            for (i in 0 until node.childCount) {
-                traverseNode(node.getChild(i), result)
-            }
-        }
-    }
-
     @SuppressLint("ClickableViewAccessibility")
     override fun onServiceConnected() {
         super.onServiceConnected()
 
         instance = this
 
-        val info = AccessibilityServiceInfo()
-        info.flags = AccessibilityServiceInfo.DEFAULT or
-                AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or
-                AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
-        info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
-        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN
-        info.notificationTimeout = 100
-        this.serviceInfo = info
-
         val displayMetrics = DisplayMetrics()
         val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
         windowManager.defaultDisplay.getMetrics(displayMetrics)
@@ -801,7 +648,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             }
         }
 
-        logcat.observeForever {
+        googleMessageStateRepository.logs.observeForever {
             binding.tvLog.text = it
             binding.scroll.fullScroll(View.FOCUS_DOWN)
         }
@@ -835,7 +682,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                 when (item.itemId) {
                     R.id.inspect -> {
                         delay(1500)
-                        traverseNode(rootInActiveWindow, TraverseResult())
+                        screenInspector.traverseNode(TraverseResult())
                     }
 
                     R.id.check_availability -> {
@@ -843,11 +690,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     }
 
                     R.id.toggle_on -> {
-                        toggleRcsSwitch(true)
+                        screenController.toggleRcsSwitch(true)
                     }
 
                     R.id.toggle_off -> {
-                        toggleRcsSwitch(false)
+                        screenController.toggleRcsSwitch(false)
                     }
 
                     R.id.clear_conv -> {
@@ -905,104 +752,6 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         }
     }
 
-    suspend fun waitForRcsState(
-        states: Array<RcsConfigureState>,
-        timeout: Duration
-    ): RcsConfigureState? {
-        var state: RcsConfigureState? = null
-        withTimeoutOrNull(timeout) {
-            withContext(Dispatchers.Main) {
-                suspendCancellableCoroutine { continuation ->
-                    val observer = Observer<RcsConfigureState> { value ->
-                        if (states.contains(value)) {
-                            state = value
-                            if (isActive)
-                                continuation.resume(Unit)
-                        }
-                    }
-
-                    rcsConfigureState.observeForever(observer)
-
-                    continuation.invokeOnCancellation {
-                        handler.post {
-                            Log.i(com.example.modifier.TAG, "removeObserver")
-                            rcsConfigureState.removeObserver(observer)
-                        }
-                    }
-                }
-            }
-            false
-        }
-        return state
-    }
-
-    suspend fun toggleRcsSwitch(state: Boolean, retry: Int = 3): Boolean {
-        val res = TraverseResult()
-
-        shellRun(CMD_RCS_SETTINGS_ACTIVITY, "sleep 2")
-        val success = run repeatBlock@{
-            repeat(retry) {
-                res.rcsSwitch = null
-                traverseNode(rootInActiveWindow, res)
-                if (res.rcsSwitch == null) {
-                    shellRun(CMD_BACK, "sleep 0.5", CMD_RCS_SETTINGS_ACTIVITY, "sleep 0.5")
-                } else {
-                    if (res.rcsSwitch!!.isChecked == state) {
-                        return@repeatBlock true
-                    }
-                    val rect = Rect()
-                    res.rcsSwitch!!.getBoundsInScreen(rect)
-                    if (state) {
-                        shellRun(
-                            "input tap ${rect.centerX()} ${rect.centerY()}", "sleep 1",
-                        )
-                        val btn =
-                            rootInActiveWindow.findAccessibilityNodeInfosByViewId("android:id/button1")
-                                .firstOrNull()
-                        if (btn != null) {
-                            btn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
-                            delay(1000)
-                        }
-                        while (shellRun("dumpsys activity activities | grep topResumedActivity")
-                                .first.contains("RcsSettingsActivity")
-                        ) {
-                            shellRun(CMD_BACK)
-                            delay(500)
-                        }
-                        shellRun(CMD_RCS_SETTINGS_ACTIVITY, "sleep 1")
-                    } else {
-                        shellRun(
-                            "input tap ${rect.centerX()} ${rect.centerY()}", "sleep 1",
-                        )
-                        rootInActiveWindow.findAccessibilityNodeInfosByViewId("android:id/button1")
-                            .firstOrNull()?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
-                        delay(1000)
-                        while (shellRun("dumpsys activity activities | grep topResumedActivity")
-                                .first.contains("RcsSettingsActivity")
-                        ) {
-                            shellRun(CMD_BACK)
-                            delay(500)
-                        }
-                        shellRun(CMD_RCS_SETTINGS_ACTIVITY, "sleep 1")
-                    }
-                    res.rcsSwitch = null
-                    traverseNode(rootInActiveWindow, res)
-                    if (res.rcsSwitch?.isChecked == state) {
-                        return@repeatBlock true
-                    }
-                }
-            }
-            false
-        }
-        while (shellRun("dumpsys activity activities | grep topResumedActivity")
-                .first.contains("RcsSettingsActivity")
-        ) {
-            shellRun(CMD_BACK)
-            delay(500)
-        }
-        return success
-    }
-
     private suspend fun reset() {
         if (isOldVersion(this)) {
             withTimeout(1.hours) {
@@ -1011,10 +760,10 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     withContext(Dispatchers.Main) {
                         binding.tvLog.text = "Waiting for RCS switch on..."
                     }
-                    rcsConfigureState.postValue(RcsConfigureState.NOT_CONFIGURED)
+                    googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED)
                     Global.saveMock()
                     resetAll()
-                    var switchAppear = waitForRcsState(
+                    var switchAppear = googleMessageStateRepository.waitForRcsState(
                         arrayOf(RcsConfigureState.WAITING_FOR_TOS),
                         2.minutes
                     )?.let {
@@ -1025,7 +774,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             PACKAGE_GMS.kill(), PACKAGE_MESSAGING.kill(), "sleep 1",
                             CMD_MESSAGING_APP
                         )
-                        switchAppear = waitForRcsState(
+                        switchAppear = googleMessageStateRepository.waitForRcsState(
                             arrayOf(RcsConfigureState.WAITING_FOR_TOS),
                             5.minutes
                         )?.let {
@@ -1039,24 +788,24 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             continue
                         }
                     }
-                    if (!toggleRcsSwitch(false)) {
+                    if (!screenController.toggleRcsSwitch(false)) {
                         Log.e(com.example.modifier.TAG, "RCS switch not turned off, retrying...")
                         continue
                     }
-                    if (!toggleRcsSwitch(true)) {
+                    if (!screenController.toggleRcsSwitch(true)) {
                         Log.e(com.example.modifier.TAG, "RCS switch not turned on, retrying...")
                         continue
                     }
-                    var resetSuccess = waitForRcsState(
+                    var resetSuccess = googleMessageStateRepository.waitForRcsState(
                         arrayOf(
                             RcsConfigureState.READY
                         ), 30.seconds
                     ).let { it == RcsConfigureState.READY }
                     if (!resetSuccess) {
-                        toggleRcsSwitch(false)
+                        screenController.toggleRcsSwitch(false)
                         delay(1000)
-                        toggleRcsSwitch(true)
-                        resetSuccess = waitForRcsState(
+                        screenController.toggleRcsSwitch(true)
+                        resetSuccess = googleMessageStateRepository.waitForRcsState(
                             arrayOf(
                                 RcsConfigureState.READY
                             ), 1.minutes
@@ -1077,10 +826,10 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     withContext(Dispatchers.Main) {
                         binding.tvLog.text = "Waiting for RCS switch on..."
                     }
-                    rcsConfigureState.postValue(RcsConfigureState.NOT_CONFIGURED)
+                    googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED)
                     Global.saveMock()
                     resetAll()
-                    var switchAppear = waitForRcsState(
+                    var switchAppear = googleMessageStateRepository.waitForRcsState(
                         arrayOf(RcsConfigureState.WAITING_FOR_DEFAULT_ON),
                         1.minutes
                     )?.let {
@@ -1091,7 +840,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             PACKAGE_GMS.kill(), PACKAGE_MESSAGING.kill(), "sleep 1",
                             CMD_MESSAGING_APP
                         )
-                        switchAppear = waitForRcsState(
+                        switchAppear = googleMessageStateRepository.waitForRcsState(
                             arrayOf(RcsConfigureState.WAITING_FOR_DEFAULT_ON),
                             2.minutes
                         )?.let {
@@ -1105,21 +854,21 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             continue
                         }
                     }
-                    val switchOn = toggleRcsSwitch(true)
+                    val switchOn = screenController.toggleRcsSwitch(true)
                     if (!switchOn) {
                         Log.e(com.example.modifier.TAG, "RCS switch not turned on, retrying...")
                         continue
                     }
-                    var resetSuccess = waitForRcsState(
+                    var resetSuccess = googleMessageStateRepository.waitForRcsState(
                         arrayOf(
                             RcsConfigureState.READY
                         ), 30.seconds
                     ).let { it == RcsConfigureState.READY }
                     if (!resetSuccess) {
-                        toggleRcsSwitch(false)
+                        screenController.toggleRcsSwitch(false)
                         delay(1000)
-                        toggleRcsSwitch(true)
-                        resetSuccess = waitForRcsState(
+                        screenController.toggleRcsSwitch(true)
+                        resetSuccess = googleMessageStateRepository.waitForRcsState(
                             arrayOf(
                                 RcsConfigureState.READY
                             ), 1.minutes
@@ -1222,7 +971,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         needRest = false
                     }
 
-                    rcsConfigureState.postValue(RcsConfigureState.NOT_CONFIGURED)
+                    googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED)
                     withContext(Dispatchers.Main) {
                         binding.tvLog.text = "Requesting number..."
                     }
@@ -1289,15 +1038,15 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     if (sendOtpTimeout > 2.minutes) {
                         sendOtpTimeout = 2.minutes
                     }
-                    if (waitForRcsState(
+                    if (googleMessageStateRepository.waitForRcsState(
                             arrayOf(RcsConfigureState.WAITING_FOR_OTP),
                             sendOtpTimeout
                         ) != RcsConfigureState.WAITING_FOR_OTP
                     ) {
-                        if (!toggleRcsSwitch(true)) {
+                        if (!screenController.toggleRcsSwitch(true)) {
                             needRest = true
                         }
-                        if (RcsConfigureState.REPLAY_REQUEST == rcsConfigureState.value) {
+                        if (RcsConfigureState.REPLAY_REQUEST == googleMessageStateRepository.rcsConfigureState.value) {
                             Log.e(
                                 com.example.modifier.TAG,
                                 "REPLAY_REQUEST detected, may reset after 3 retry ($retry)"
@@ -1369,7 +1118,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             repeat(2) {
                                 Global.sendSmsIntent(sender, msg)
                                 val state =
-                                    waitForRcsState(
+                                    googleMessageStateRepository.waitForRcsState(
                                         arrayOf(
                                             RcsConfigureState.CONFIGURED,
                                             RcsConfigureState.RETRY
@@ -1381,7 +1130,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                                     }
 
                                     RcsConfigureState.RETRY -> {
-                                        waitForRcsState(
+                                        googleMessageStateRepository.waitForRcsState(
                                             arrayOf(RcsConfigureState.WAITING_FOR_OTP),
                                             60.seconds
                                         )
@@ -1458,7 +1207,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                 "sleep 1",
             )
             val res = TraverseResult()
-            traverseNode(rootInActiveWindow, res)
+            screenInspector.traverseNode(res)
             if (res.rcsConnectionStatus == RcsConnectionStatus.CONNECTED) {
                 Log.i(com.example.modifier.TAG, "RCS is connected")
                 shellRun(CMD_BACK)
@@ -1512,9 +1261,8 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                 startActivity(smsIntent(it, ""))
                 val s = withTimeoutOrNull(5.seconds) {
                     while (true) {
-                        val root = rootInActiveWindow
                         val traverseResult = TraverseResult()
-                        traverseNode(root, traverseResult)
+                        screenInspector.traverseNode(traverseResult)
                         if (traverseResult.isRcsCapable) {
                             return@withTimeoutOrNull true
                         } else {

+ 80 - 0
app/src/main/java/com/example/modifier/service/ScreenController.kt

@@ -0,0 +1,80 @@
+package com.example.modifier.service
+
+import android.accessibilityservice.AccessibilityService
+import android.graphics.Rect
+import android.view.accessibility.AccessibilityNodeInfo
+import com.example.modifier.TraverseResult
+import com.example.modifier.constants.CMD_BACK
+import com.example.modifier.constants.CMD_RCS_SETTINGS_ACTIVITY
+import com.example.modifier.utils.shellRun
+import kotlinx.coroutines.delay
+
+class ScreenController(val context: AccessibilityService, val inspector: ScreenInspector) {
+
+    suspend fun toggleRcsSwitch(state: Boolean, retry: Int = 3): Boolean {
+        val res = TraverseResult()
+
+        shellRun(CMD_RCS_SETTINGS_ACTIVITY, "sleep 2")
+        val success = run repeatBlock@{
+            repeat(retry) {
+                res.rcsSwitch = null
+                inspector.traverseNode(res)
+                if (res.rcsSwitch == null) {
+                    shellRun(CMD_BACK, "sleep 0.5", CMD_RCS_SETTINGS_ACTIVITY, "sleep 0.5")
+                } else {
+                    if (res.rcsSwitch!!.isChecked == state) {
+                        return@repeatBlock true
+                    }
+                    val rect = Rect()
+                    res.rcsSwitch!!.getBoundsInScreen(rect)
+                    if (state) {
+                        shellRun(
+                            "input tap ${rect.centerX()} ${rect.centerY()}", "sleep 1",
+                        )
+                        val btn =
+                            context.rootInActiveWindow.findAccessibilityNodeInfosByViewId("android:id/button1")
+                                .firstOrNull()
+                        if (btn != null) {
+                            btn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+                            delay(1000)
+                        }
+                        while (shellRun("dumpsys activity activities | grep topResumedActivity")
+                                .first.contains("RcsSettingsActivity")
+                        ) {
+                            shellRun(CMD_BACK)
+                            delay(500)
+                        }
+                        shellRun(CMD_RCS_SETTINGS_ACTIVITY, "sleep 1")
+                    } else {
+                        shellRun(
+                            "input tap ${rect.centerX()} ${rect.centerY()}", "sleep 1",
+                        )
+                        context.rootInActiveWindow.findAccessibilityNodeInfosByViewId("android:id/button1")
+                            .firstOrNull()?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+                        delay(1000)
+                        while (shellRun("dumpsys activity activities | grep topResumedActivity")
+                                .first.contains("RcsSettingsActivity")
+                        ) {
+                            shellRun(CMD_BACK)
+                            delay(500)
+                        }
+                        shellRun(CMD_RCS_SETTINGS_ACTIVITY, "sleep 1")
+                    }
+                    res.rcsSwitch = null
+                    inspector.traverseNode(res)
+                    if (res.rcsSwitch?.isChecked == state) {
+                        return@repeatBlock true
+                    }
+                }
+            }
+            false
+        }
+        while (shellRun("dumpsys activity activities | grep topResumedActivity")
+                .first.contains("RcsSettingsActivity")
+        ) {
+            shellRun(CMD_BACK)
+            delay(500)
+        }
+        return success
+    }
+}

+ 91 - 0
app/src/main/java/com/example/modifier/service/ScreenInspector.kt

@@ -0,0 +1,91 @@
+package com.example.modifier.service
+
+import android.accessibilityservice.AccessibilityService
+import android.util.Log
+import android.view.accessibility.AccessibilityNodeInfo
+import com.example.modifier.TraverseResult
+import com.example.modifier.enums.RcsConnectionStatus
+import java.util.Optional
+
+class ScreenInspector(val context: AccessibilityService) {
+
+    private fun traverse(node: AccessibilityNodeInfo?, result: TraverseResult) {
+        if (node == null) {
+            return
+        }
+
+        val packageInfo = context.packageManager.getPackageInfo(
+            node.packageName.toString(), 0
+        )
+
+        val className = node.className.toString()
+        val name = node.viewIdResourceName
+        val text = Optional.ofNullable(node.text).map { obj: CharSequence -> obj.toString() }
+            .orElse(null)
+        val id = node.viewIdResourceName
+
+        Log.d(com.example.modifier.TAG, "Node: class=$className, text=$text, name=$name, id=$id")
+
+        if ("Compose:Draft:Send" == name) {
+            result.sendBtn = node
+        }
+
+        if ("com.google.android.apps.messaging:id/send_message_button_icon" == id) {
+            result.sendBtn = node
+        }
+
+        if (text != null && (text.contains("RCS 聊天") || text.contains("RCS chat"))) {
+            result.isRcsCapable = true
+        }
+
+        if ("com.google.android.apps.messaging:id/tombstone_message" == id) {
+            result.isRcsCapable = text.contains("聊天") || text.contains("Chatting with")
+        }
+
+        if (text != null && (text.contains("Turn on RCS chats") || text.contains("开启 RCS 聊天功能")
+                    || text.contains("Enable chat features") || text.contains("启用聊天功能"))
+        ) {
+            fun findSwitch(node: AccessibilityNodeInfo): Boolean {
+                if ("com.google.android.apps.messaging:id/switchWidget" == node.viewIdResourceName) {
+                    result.rcsSwitch = node
+                    return true
+                }
+                for (i in 0 until node.childCount) {
+                    val child = node.getChild(i)
+                    if (findSwitch(child)) {
+                        return true
+                    }
+                }
+                return false
+            }
+            findSwitch(node.parent.parent)
+        }
+
+        if ("com.google.android.apps.messaging:id/rcs_sim_status_status_text" == id) {
+            if (text.lowercase().contains("connected") || text.lowercase().contains("已连接")) {
+                result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
+            }
+        }
+
+        if ("android:id/title" == id) {
+            when (text) {
+                "状态:已连接" -> result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
+                "Status: Connected" -> result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
+            }
+        }
+
+        if (text != null && (text.contains("end-to-end encrypted") || text.contains("已经过端到端加密"))) {
+            result.encrypted = true
+        }
+
+        if (node.childCount != 0) {
+            for (i in 0 until node.childCount) {
+                traverse(node.getChild(i), result)
+            }
+        }
+    }
+
+    fun traverseNode(result: TraverseResult) {
+        traverse(context.rootInActiveWindow, result)
+    }
+}

+ 4 - 0
app/src/main/java/com/example/modifier/service/TaskRunner.kt

@@ -0,0 +1,4 @@
+package com.example.modifier.service
+
+class TaskRunner() {
+}

+ 20 - 4
app/src/main/java/com/example/modifier/ui/utils/UtilsFragment.kt

@@ -28,6 +28,7 @@ import com.example.modifier.http.response.SysConfigResponse
 import com.example.modifier.service.ModifierService
 import com.example.modifier.utils.clear
 import com.example.modifier.utils.clearConv
+import com.example.modifier.utils.killPhoneProcess
 import com.example.modifier.utils.resetAll
 import com.example.modifier.utils.resumePackage
 import com.example.modifier.utils.shellRun
@@ -213,7 +214,7 @@ class UtilsFragment : Fragment() {
             Utils.makeLoadingButton(context, binding.btnKillPhone)
             lifecycleScope.launch {
                 val success = withContext(Dispatchers.IO) {
-                    Global.killPhoneProcess(force = true)
+                    killPhoneProcess(force = true)
                 }
                 if (success) {
                     binding.btnKillPhone.setIconResource(R.drawable.ic_done)
@@ -249,7 +250,12 @@ class UtilsFragment : Fragment() {
         binding.btnUpdateModifier.setOnClickListener {
             lifecycleScope.launch {
                 try {
-                    val config = ktorClient(appPreferences.value.server).get(SysConfigApi.Id(SysConfigApi(), "modifier_apk"))
+                    val config = ktorClient(appPreferences.value.server).get(
+                        SysConfigApi.Id(
+                            SysConfigApi(),
+                            "modifier_apk"
+                        )
+                    )
                         .body<SysConfigResponse>()
                     installApk(config.value)
                 } catch (e: Exception) {
@@ -261,7 +267,12 @@ class UtilsFragment : Fragment() {
         binding.btnUpdateMessage.setOnClickListener {
             lifecycleScope.launch {
                 try {
-                    val config = ktorClient(appPreferences.value.server).get(SysConfigApi.Id(SysConfigApi(), "message_apk"))
+                    val config = ktorClient(appPreferences.value.server).get(
+                        SysConfigApi.Id(
+                            SysConfigApi(),
+                            "message_apk"
+                        )
+                    )
                         .body<SysConfigResponse>()
                     installApk(config.value)
                 } catch (e: Exception) {
@@ -273,7 +284,12 @@ class UtilsFragment : Fragment() {
         binding.btnUpdateGms.setOnClickListener {
             lifecycleScope.launch {
                 try {
-                    val config = ktorClient(appPreferences.value.server).get(SysConfigApi.Id(SysConfigApi(), "gms_apk"))
+                    val config = ktorClient(appPreferences.value.server).get(
+                        SysConfigApi.Id(
+                            SysConfigApi(),
+                            "gms_apk"
+                        )
+                    )
                         .body<SysConfigResponse>()
                     installApk(config.value)
                 } catch (e: Exception) {

+ 4 - 4
app/src/main/java/com/example/modifier/utils/Root.kt

@@ -21,16 +21,16 @@ suspend fun shellRun(vararg commands: String): Pair<String, String> {
         }
         coroutineScope {
             launch {
-                p.inputStream.bufferedReader().useLines {
-                    it.forEach {
+                p.inputStream.bufferedReader().useLines { line ->
+                    line.forEach {
                         output += it + "\n"
                         Log.i(com.example.modifier.TAG, "shellRunOut: $it")
                     }
                 }
             }
             launch {
-                p.errorStream.bufferedReader().useLines {
-                    it.forEach {
+                p.errorStream.bufferedReader().useLines { line ->
+                    line.forEach {
                         error += it + "\n"
                         Log.e(com.example.modifier.TAG, "shellRunErr: $it")
                     }

+ 35 - 0
app/src/main/java/com/example/modifier/utils/System.kt

@@ -14,6 +14,7 @@ import com.example.modifier.Utils
 import com.example.modifier.http.ktorClient
 import com.example.modifier.service.ModifierService
 import io.ktor.client.request.head
+import kotlinx.coroutines.delay
 import org.apache.commons.lang3.StringUtils
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -184,4 +185,38 @@ suspend fun isRebooted(): Boolean {
 
 suspend fun setBatteryLevel(level: Int) {
     shellRun("dumpsys battery set level $level")
+}
+
+suspend fun killPhoneProcess(force: Boolean = false): Boolean {
+    try {
+        if (!force) {
+            if (shellRun("getprop phonekilled").component1().contains("yes")) {
+                return true
+            }
+        }
+        run kill@{
+            repeat(3) {
+                val pid = shellRun("pidof com.android.phone").component1().trim()
+                if (!Regex("[0-9]+").matches(pid)) {
+                    Log.e(com.example.modifier.TAG, "killPhoneProcess: pid not found")
+                    return true
+                }
+                Log.i(com.example.modifier.TAG, "killPhoneProcess: pid=$pid")
+                shellRun("kill -9 $pid")
+                delay(1000)
+
+                val pidNew = shellRun("pidof com.android.phone").component1().trim()
+                if (pidNew == pid) {
+                    Log.e(com.example.modifier.TAG, "killPhoneProcess: failed to kill phone process")
+                } else {
+                    Log.i(com.example.modifier.TAG, "killPhoneProcess: success, new pid: $pidNew")
+                    shellRun("kill -9 $pid", "setprop phonekilled yes")
+                    return true
+                }
+            }
+        }
+    } catch (e: Exception) {
+        Log.e(com.example.modifier.TAG, "Error Kill Phone", e)
+    }
+    return false
 }

+ 3 - 3
app/src/main/res/xml/accessibility_service_config.xml

@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
-    android:accessibilityEventTypes="typeAllMask"
-    android:accessibilityFeedbackType="feedbackGeneric"
-    android:accessibilityFlags="flagDefault|flagReportViewIds"
+    android:accessibilityEventTypes="typeWindowStateChanged"
+    android:accessibilityFeedbackType="feedbackSpoken"
+    android:accessibilityFlags="flagDefault|flagReportViewIds|flagIncludeNotImportantViews"
     android:canPerformGestures="true"
     android:canRetrieveWindowContent="true"
     android:description="@string/accessibility_service_description"