xiongzhu 1 anno fa
parent
commit
c1e3514657

+ 1 - 0
app/build.gradle

@@ -148,4 +148,5 @@ dependencies {
 
     implementation("io.coil-kt.coil3:coil-compose:3.0.2")
     implementation("io.coil-kt.coil3:coil-network-ktor2:3.0.2")
+
 }

+ 15 - 4
app/src/main/java/com/example/modifier/MainActivity.kt

@@ -1,5 +1,6 @@
 package com.example.modifier
 
+import android.content.Context
 import android.content.DialogInterface
 import android.content.Intent
 import android.os.Bundle
@@ -10,6 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.ui.NavigationUI.setupWithNavController
 import com.example.modifier.databinding.ActivityMainBinding
+import com.example.modifier.service.ModifierService
 import com.example.modifier.utils.enableAccessibility
 import com.example.modifier.utils.enableOverlay
 import com.example.modifier.utils.getIPAddress
@@ -45,14 +47,23 @@ class MainActivity : AppCompatActivity() {
                 }
                 enableOverlay()
             } else {
+                if (ModifierService.instance != null) {
+                    return@launch
+                }
+
                 withContext(Dispatchers.Main) {
                     MaterialAlertDialogBuilder(this@MainActivity)
-                        .setTitle("No Root Access")
-                        .setMessage("Root access is required to run this app")
-                        .setCancelable(false)
-                        .setPositiveButton("Exit") { _: DialogInterface?, _: Int ->
+                        .setTitle("Access Required")
+                        .setMessage("Enable Accessibility Service")
+                        .setNegativeButton("Exit") { _: DialogInterface?, _: Int ->
                             System.exit(0)
                         }
+                        .setPositiveButton("Open Settings") { _: DialogInterface?, _: Int ->
+                            val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                            startActivity(intent)
+                        }
                         .show()
                 }
             }

+ 1 - 0
app/src/main/java/com/example/modifier/MyApplication.kt

@@ -29,6 +29,7 @@ class MyApplication : Application(), SingletonImageLoader.Factory {
         Log.i(TAG, "server=${AppPrefsRepo.instance.appPrefs.value.server}")
     }
 
+
     override fun newImageLoader(context: Context): ImageLoader {
         return ImageLoader.Builder(context)
             .crossfade(true)

+ 1 - 0
app/src/main/java/com/example/modifier/data/AppPreferences.kt

@@ -8,4 +8,5 @@ data class AppPreferences(
     val preventRequest: Boolean,
     val preventReset: Boolean,
     val storeNum: Int,
+    val adbPaired: Boolean
 )

+ 6 - 2
app/src/main/java/com/example/modifier/repo/AppPrefsRepo.kt

@@ -36,6 +36,7 @@ class AppPrefsRepo private constructor(private val context: Context) {
         val PREVENT_REQUEST = booleanPreferencesKey("prevent_request")
         val PREVENT_RESET = booleanPreferencesKey("prevent_reset")
         val STORE_NUM = intPreferencesKey("store_num")
+        val ADB_PAIRED = booleanPreferencesKey("adb_paired")
     }
 
     val appPrefs = MutableStateFlow(
@@ -46,7 +47,8 @@ class AppPrefsRepo private constructor(private val context: Context) {
             preventClean = false,
             preventRequest = false,
             preventReset = false,
-            storeNum = 20
+            storeNum = 20,
+            adbPaired = false
         )
     )
 
@@ -70,6 +72,7 @@ class AppPrefsRepo private constructor(private val context: Context) {
         val preventRequest = it[PreferencesKeys.PREVENT_REQUEST] ?: false
         val preventReset = it[PreferencesKeys.PREVENT_RESET] ?: false
         val storeNum = it[PreferencesKeys.STORE_NUM] ?: 20
+        val adbPaired = it[PreferencesKeys.ADB_PAIRED] ?: false
         AppPreferences(
             server = server,
             id = id,
@@ -77,7 +80,8 @@ class AppPrefsRepo private constructor(private val context: Context) {
             preventClean = preventClean,
             preventRequest = preventRequest,
             preventReset = preventReset,
-            storeNum = storeNum
+            storeNum = storeNum,
+            adbPaired = adbPaired
         )
     }
 

+ 9 - 2
app/src/main/java/com/example/modifier/service/ModifierService.kt

@@ -21,6 +21,7 @@ import android.widget.FrameLayout
 import androidx.annotation.MenuRes
 import androidx.appcompat.widget.PopupMenu
 import androidx.core.content.ContextCompat
+import com.draco.ladb.utils.ADB
 import com.example.modifier.BuildConfig
 import com.example.modifier.R
 import com.example.modifier.TraverseResult
@@ -36,6 +37,7 @@ import com.example.modifier.repo.SpoofedSimInfoRepo
 import com.example.modifier.service.TaskRunner.Companion
 import com.example.modifier.utils.checkPif
 import com.example.modifier.utils.clearConv
+import com.example.modifier.utils.findFirstByDesc
 import com.example.modifier.utils.hasRootAccess
 import com.example.modifier.utils.isRebooted
 import com.example.modifier.utils.killPhoneProcess
@@ -280,7 +282,7 @@ class ModifierService : AccessibilityService() {
 
             appStateRepo.updateRuntimeFlags(preparing = true)
             val hasRoot = run checkRoot@{
-                repeat(30) {
+                repeat(3) {
                     if (hasRootAccess()) {
                         return@checkRoot true
                     }
@@ -289,7 +291,11 @@ class ModifierService : AccessibilityService() {
                 false
             }
             if (!hasRoot) {
-                System.exit(0)
+                if (appPrefsRepo.appPrefs.value.adbPaired) {
+                    ADB.getInstance(applicationContext).initServer()
+                } else {
+                    screenController.adbPair()
+                }
             }
             if (isRebooted()) {
                 delay(2.minutes)
@@ -373,6 +379,7 @@ class ModifierService : AccessibilityService() {
                     R.id.inspect -> {
                         delay(1500)
                         screenInspector.traverseNode(TraverseResult(), true)
+                        rootInActiveWindow.findFirstByDesc("wireless debugging")
                     }
 
                     R.id.check_availability -> {

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

@@ -1,17 +1,32 @@
 package com.example.modifier.service
 
 import android.accessibilityservice.AccessibilityService
+import android.accessibilityservice.GestureDescription
+import android.content.Intent
+import android.graphics.Path
 import android.graphics.Rect
+import android.provider.Settings
+import android.util.Log
 import android.view.accessibility.AccessibilityNodeInfo
+import com.draco.ladb.utils.ADB
 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.currentActivity
+import com.example.modifier.utils.findAllScrollable
+import com.example.modifier.utils.findFirstByDesc
+import com.example.modifier.utils.findFirstByIdAndText
+import com.example.modifier.utils.findFirstScrollable
 import com.example.modifier.utils.shellRun
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.withTimeoutOrNull
 
 class ScreenController(val context: AccessibilityService, private val inspector: ScreenInspector) {
 
+    companion object {
+        private const val TAG = "ScreenController"
+    }
+
     suspend fun toggleRcsSwitch(state: Boolean, retry: Int = 3): Boolean {
         val res = TraverseResult()
 
@@ -87,4 +102,68 @@ class ScreenController(val context: AccessibilityService, private val inspector:
         }
         return success
     }
+
+    suspend fun adbPair(): Pair<String, String>? {
+        val i = Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
+        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+        context.startActivity(i)
+        delay(500)
+        withTimeoutOrNull(10000) {
+            while (true) {
+                val node = context.rootInActiveWindow.findFirstByIdAndText(
+                    "android:id/title",
+                    "Wireless debugging"
+                )
+                if (node != null) {
+                    Log.i(TAG, "found wireless debugging")
+                    delay(500)
+                    tapNode(node)
+                    delay(1000)
+                 return@withTimeoutOrNull   context.rootInActiveWindow.findFirstByIdAndText(
+                        "android:id/title",
+                        "Pair device with pairing code"
+                    )?.let {
+                        Log.i(TAG, "found pair device with pairing code")
+                        it.parent.parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+
+                        delay(1000)
+                        val code = context.rootInActiveWindow.findAccessibilityNodeInfosByViewId(
+                            "com.android.settings:id/pairing_code"
+                        ).firstOrNull()?.text?.toString()
+                        val ip = context.rootInActiveWindow.findAccessibilityNodeInfosByViewId(
+                            "com.android.settings:id/ip_addr"
+                        ).firstOrNull()?.text?.toString()
+                        Log.i(TAG, "pairing code: $code, ip: $ip")
+
+                        if (code != null && ip != null) {
+                            return@let Pair(code, ip)
+                        } else {
+
+                        }
+                    }
+                    return@withTimeoutOrNull
+                }
+                context.rootInActiveWindow.findAllScrollable().forEach {
+                    Log.i(TAG, "scrolling ${it.viewIdResourceName}")
+                    it.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
+                    it.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
+                }
+                delay(500)
+            }
+            null
+        }
+    }
+
+    fun tapNode(node: AccessibilityNodeInfo) {
+        val rect = Rect()
+        node.getBoundsInScreen(rect)
+        val path = Path()
+        path.moveTo(rect.centerX().toFloat(), rect.centerY().toFloat())
+        context.dispatchGesture(
+            GestureDescription.Builder().addStroke(
+                GestureDescription.StrokeDescription(path, 0, 100)
+            ).build(), null, null
+        )
+    }
 }

+ 264 - 0
app/src/main/java/com/example/modifier/utils/ADB.kt

@@ -0,0 +1,264 @@
+package com.draco.ladb.utils
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.provider.Settings
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.example.modifier.BuildConfig
+import java.io.File
+import java.io.PrintStream
+import java.util.concurrent.TimeUnit
+
+class ADB(private val context: Context) {
+    companion object {
+        const val MAX_OUTPUT_BUFFER_SIZE = 1024 * 16
+        const val OUTPUT_BUFFER_DELAY_MS = 100L
+
+        @SuppressLint("StaticFieldLeak")
+        @Volatile
+        private var instance: ADB? = null
+        fun getInstance(context: Context): ADB = instance ?: synchronized(this) {
+            instance ?: ADB(context).also { instance = it }
+        }
+    }
+
+
+    private val adbPath = "${context.applicationInfo.nativeLibraryDir}/libadb.so"
+    private val scriptPath = "${context.getExternalFilesDir(null)}/script.sh"
+
+    /**
+     * Is the shell ready to handle commands?
+     */
+    private val _started = MutableLiveData(false)
+    val started: LiveData<Boolean> = _started
+
+    private var tryingToPair = false
+
+    /**
+     * Is the shell closed for any reason?
+     */
+    private val _closed = MutableLiveData(false)
+    val closed: LiveData<Boolean> = _closed
+
+    /**
+     * Where shell output is stored
+     */
+    val outputBufferFile: File = File.createTempFile("buffer", ".txt").also {
+        it.deleteOnExit()
+    }
+
+    /**
+     * Single shell instance where we can pipe commands to
+     */
+    private var shellProcess: Process? = null
+
+    /**
+     * Returns the user buffer size if valid, else the default
+     */
+    fun getOutputBufferSize(): Int {
+        return 16384
+    }
+
+    /**
+     * Start the ADB server
+     */
+    fun initServer(): Boolean {
+        if (_started.value == true || tryingToPair)
+            return true
+
+        tryingToPair = true
+
+        val autoShell = true
+
+        val secureSettingsGranted =
+            context.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) == PackageManager.PERMISSION_GRANTED
+
+        if (autoShell) {
+            /* Only do wireless debugging steps on compatible versions */
+            if (secureSettingsGranted) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !isWirelessDebuggingEnabled()) {
+                    Settings.Global.putInt(
+                        context.contentResolver,
+                        "adb_wifi_enabled",
+                        1
+                    )
+
+                    Thread.sleep(2_000)
+                } else if (!isUSBDebuggingEnabled()) {
+                    Settings.Global.putInt(
+                        context.contentResolver,
+                        Settings.Global.ADB_ENABLED,
+                        1
+                    )
+
+                    Thread.sleep(2_000)
+                }
+            }
+
+            /* Check again... */
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !isWirelessDebuggingEnabled()) {
+                debug("Wireless debugging is not enabled!")
+                debug("Settings -> Developer options -> Wireless debugging")
+                debug("Waiting for wireless debugging...")
+
+                while (!isWirelessDebuggingEnabled()) {
+                    Thread.sleep(1_000)
+                }
+            } else if (!isUSBDebuggingEnabled()) {
+                debug("USB debugging is not enabled!")
+                debug("Settings -> Developer options -> USB debugging")
+                debug("Waiting for USB debugging...")
+
+                while (!isUSBDebuggingEnabled()) {
+                    Thread.sleep(1_000)
+                }
+            }
+
+            adb(false, listOf("start-server")).waitFor()
+            debug("Waiting for device to connect...")
+            debug("This may take a minute")
+            val waitProcess = adb(false, listOf("wait-for-device")).waitFor(1, TimeUnit.MINUTES)
+            if (!waitProcess) {
+                debug("Your device didn't connect to LADB")
+                debug("If a reboot doesn't work, please contact support")
+
+                tryingToPair = false
+                return false
+            }
+        }
+
+        shellProcess = if (autoShell) {
+            val argList = if (Build.SUPPORTED_ABIS[0] == "arm64-v8a")
+                listOf("-t", "1", "shell")
+            else
+                listOf("shell")
+
+            adb(true, argList)
+        } else {
+            shell(true, listOf("sh", "-l"))
+        }
+
+        sendToShellProcess("alias adb=\"$adbPath\"")
+
+        if (!secureSettingsGranted) {
+            sendToShellProcess("pm grant ${BuildConfig.APPLICATION_ID} android.permission.WRITE_SECURE_SETTINGS &> /dev/null")
+        }
+
+        if (autoShell)
+            sendToShellProcess("echo 'Entered adb shell'")
+        else
+            sendToShellProcess("echo 'Entered non-adb shell'")
+
+        val startupCommand = "echo 'Success! ※\\(^o^)/※'"
+        if (startupCommand.isNotEmpty())
+            sendToShellProcess(startupCommand)
+
+        _started.postValue(true)
+        tryingToPair = false
+
+        return true
+    }
+
+    private fun isWirelessDebuggingEnabled() =
+        Settings.Global.getInt(context.contentResolver, "adb_wifi_enabled", 0) == 1
+
+    private fun isUSBDebuggingEnabled() =
+        Settings.Global.getInt(context.contentResolver, Settings.Global.ADB_ENABLED, 0) == 1
+
+    /**
+     * Wait restart the shell once it dies
+     */
+    fun waitForDeathAndReset() {
+        while (true) {
+            shellProcess?.waitFor()
+            _started.postValue(false)
+            debug("Shell is dead, resetting")
+            adb(false, listOf("kill-server")).waitFor()
+            Thread.sleep(3_000)
+            initServer()
+        }
+    }
+
+    /**
+     * Ask the device to pair on Android 11+ devices
+     */
+    fun pair(port: String, pairingCode: String): Boolean {
+        val pairShell = adb(false, listOf("pair", "localhost:$port"))
+
+        /* Sleep to allow shell to catch up */
+        Thread.sleep(5000)
+
+        /* Pipe pairing code */
+        PrintStream(pairShell.outputStream).apply {
+            println(pairingCode)
+            flush()
+        }
+
+        /* Continue once finished pairing (or 10s elapses) */
+        pairShell.waitFor(10, TimeUnit.SECONDS)
+        pairShell.destroyForcibly().waitFor()
+
+        val killShell = adb(false, listOf("kill-server"))
+        killShell.waitFor(3, TimeUnit.SECONDS)
+        killShell.destroyForcibly()
+
+        return pairShell.exitValue() == 0
+    }
+
+    /**
+     * Send a raw ADB command
+     */
+    private fun adb(redirect: Boolean, command: List<String>): Process {
+        val commandList = command.toMutableList().also {
+            it.add(0, adbPath)
+        }
+        return shell(redirect, commandList)
+    }
+
+    /**
+     * Send a raw shell command
+     */
+    private fun shell(redirect: Boolean, command: List<String>): Process {
+        val processBuilder = ProcessBuilder(command)
+            .directory(context.filesDir)
+            .apply {
+                if (redirect) {
+                    redirectErrorStream(true)
+                    redirectOutput(outputBufferFile)
+                }
+
+                environment().apply {
+                    put("HOME", context.filesDir.path)
+                    put("TMPDIR", context.cacheDir.path)
+                }
+            }
+
+        return processBuilder.start()!!
+    }
+
+    /**
+     * Send commands directly to the shell process
+     */
+    fun sendToShellProcess(msg: String) {
+        if (shellProcess == null || shellProcess?.outputStream == null)
+            return
+        PrintStream(shellProcess!!.outputStream!!).apply {
+            println(msg)
+            flush()
+        }
+    }
+
+    /**
+     * Write a debug message to the user
+     */
+    fun debug(msg: String) {
+        synchronized(outputBufferFile) {
+            if (outputBufferFile.exists())
+                outputBufferFile.appendText("* $msg" + System.lineSeparator())
+        }
+    }
+}

+ 70 - 0
app/src/main/java/com/example/modifier/utils/AccessibilityNodeInfo.kt

@@ -0,0 +1,70 @@
+package com.example.modifier.utils
+
+import android.accessibilityservice.GestureDescription
+import android.graphics.Path
+import android.graphics.Rect
+import android.util.Log
+import android.view.accessibility.AccessibilityNodeInfo
+
+fun AccessibilityNodeInfo?.findFirstByDesc(desc: String): AccessibilityNodeInfo? {
+    if (this == null) {
+        return null
+    }
+    Log.i("findFirstByDesc", "findFirstByDesc: ${this.contentDescription}")
+    if (this.contentDescription?.toString()?.lowercase()?.contains(desc.lowercase()) == true) {
+        return this
+    }
+    for (i in 0 until this.childCount) {
+        val child = this.getChild(i)
+        val result = child.findFirstByDesc(desc)
+        if (result != null) {
+            return result
+        }
+    }
+    return null
+}
+
+fun AccessibilityNodeInfo?.findFirstByIdAndText(id: String, text: String): AccessibilityNodeInfo? {
+    if (this == null) {
+        return null
+    }
+    if (this.viewIdResourceName?.toString()?.lowercase()?.contains(id.lowercase()) == true
+        && this.text?.toString()?.lowercase()?.contains(text.lowercase()) == true
+    ) {
+        return this
+    }
+    for (i in 0 until this.childCount) {
+        val child = this.getChild(i)
+        val result = child.findFirstByIdAndText(id, text)
+        if (result != null) {
+            return result
+        }
+    }
+    return null
+}
+
+fun AccessibilityNodeInfo.findFirstScrollable(): AccessibilityNodeInfo? {
+    if (this.isScrollable) {
+        return this
+    }
+    for (i in 0 until this.childCount) {
+        val child = this.getChild(i)
+        val result = child.findFirstScrollable()
+        if (result != null) {
+            return result
+        }
+    }
+    return null
+}
+
+fun AccessibilityNodeInfo.findAllScrollable(): List<AccessibilityNodeInfo> {
+    val result = mutableListOf<AccessibilityNodeInfo>()
+    if (this.isScrollable) {
+        result.add(this)
+    }
+    for (i in 0 until this.childCount) {
+        val child = this.getChild(i)
+        result.addAll(child.findAllScrollable())
+    }
+    return result
+}

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

@@ -26,24 +26,17 @@ import com.example.modifier.http.response.SysConfigResponse
 import com.example.modifier.service.ModifierService
 import io.ktor.client.call.body
 import io.ktor.client.plugins.resources.get
-import io.ktor.client.request.get
 import io.ktor.client.request.head
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.withContext
 import org.apache.commons.io.FileUtils
 import org.json.JSONObject
-import java.io.BufferedInputStream
-import java.io.ByteArrayOutputStream
 import java.io.File
-import java.io.FileInputStream
-import java.io.IOException
-import java.net.InetAddress
 import java.net.NetworkInterface
 import java.time.ZoneId
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
-import java.util.Collections
 import java.util.Locale
 import java.util.UUID
 import kotlin.system.exitProcess

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

@@ -2,7 +2,7 @@
 <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
     android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
     android:accessibilityFeedbackType="feedbackSpoken"
-    android:accessibilityFlags="flagDefault|flagReportViewIds|flagIncludeNotImportantViews"
+    android:accessibilityFlags="flagDefault|flagReportViewIds|flagIncludeNotImportantViews|flagRequestFingerprintGestures|flagRequestTouchExplorationMode"
     android:canPerformGestures="true"
     android:canRetrieveWindowContent="true"
     android:description="@string/accessibility_service_description"

+ 0 - 1
gradle/libs.versions.toml

@@ -37,7 +37,6 @@ uiTooling = "1.6.8"
 [libraries]
 androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
 androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
-
 androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsExtended" }
 androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
 androidx-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3" }