x1ongzhu 1 năm trước cách đây
mục cha
commit
1220ee0e5a
23 tập tin đã thay đổi với 1270 bổ sung229 xóa
  1. 5 3
      app/src/main/AndroidManifest.xml
  2. 1 1
      app/src/main/java/com/example/modifier/MainActivity.kt
  3. 20 0
      app/src/main/java/com/example/modifier/MainActivity2.kt
  4. 2 0
      app/src/main/java/com/example/modifier/adapter/BackupAdapter.kt
  5. 5 1
      app/src/main/java/com/example/modifier/data/AppRuntimeFlags.kt
  6. 5 1
      app/src/main/java/com/example/modifier/data/AppState.kt
  7. 6 0
      app/src/main/java/com/example/modifier/data/BackupItemDao.kt
  8. 0 16
      app/src/main/java/com/example/modifier/data/SocketClient.kt
  9. 10 0
      app/src/main/java/com/example/modifier/enums/RequestNumberState.kt
  10. 1 1
      app/src/main/java/com/example/modifier/repo/AppPreferencesRepository.kt
  11. 20 4
      app/src/main/java/com/example/modifier/repo/AppStateRepository.kt
  12. 4 0
      app/src/main/java/com/example/modifier/repo/BackupRepository.kt
  13. 164 117
      app/src/main/java/com/example/modifier/service/ModifierService.kt
  14. 118 0
      app/src/main/java/com/example/modifier/service/SocketClient.kt
  15. 747 1
      app/src/main/java/com/example/modifier/service/TaskRunner.kt
  16. 66 44
      app/src/main/java/com/example/modifier/ui/backup/BackupFragment.kt
  17. 2 8
      app/src/main/java/com/example/modifier/ui/settings/SettingsFragment.kt
  18. 1 1
      app/src/main/java/com/example/modifier/ui/utils/UtilsFragment.kt
  19. 11 0
      app/src/main/res/layout/action_bar_progress.xml
  20. 10 0
      app/src/main/res/layout/activity_main2.xml
  21. 16 1
      app/src/main/res/layout/floating_window.xml
  22. 42 30
      app/src/main/res/layout/fragment_backup.xml
  23. 14 0
      app/src/main/res/menu/search_bar_menu.xml

+ 5 - 3
app/src/main/AndroidManifest.xml

@@ -32,8 +32,8 @@
 
     <queries>
         <package android:name="com.github.metacubex.clash" />
-
     </queries>
+
     <application
         android:name=".MyApplication"
         android:allowBackup="true"
@@ -46,11 +46,14 @@
         android:theme="@style/AppTheme"
         android:usesCleartextTraffic="true"
         tools:targetApi="31">
+        <activity
+            android:name=".MainActivity2"
+            android:exported="false" />
+
         <receiver
             android:name=".receiver.MyReceiver"
             android:enabled="true"
             android:exported="true">
-
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
@@ -59,7 +62,6 @@
         <activity
             android:name=".ui.login.LoginActivity"
             android:exported="true" />
-
         <activity
             android:name=".MainActivity"
             android:exported="true"

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

@@ -23,7 +23,7 @@ class MainActivity : AppCompatActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        this.enableEdgeToEdge()
+        enableEdgeToEdge()
         mBinding = ActivityMainBinding.inflate(layoutInflater)
         setContentView(mBinding.root)
 

+ 20 - 0
app/src/main/java/com/example/modifier/MainActivity2.kt

@@ -0,0 +1,20 @@
+package com.example.modifier
+
+import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+
+class MainActivity2 : AppCompatActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enableEdgeToEdge()
+        setContentView(R.layout.activity_main2)
+        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+            insets
+        }
+    }
+}

+ 2 - 0
app/src/main/java/com/example/modifier/adapter/BackupAdapter.kt

@@ -11,6 +11,7 @@ import android.widget.Toast
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.recyclerview.widget.RecyclerView
 import com.example.modifier.R
+import com.example.modifier.TAG
 import com.example.modifier.Utils
 import com.example.modifier.adapter.BackupAdapter.BackupViewHolder
 import com.example.modifier.data.BackupItem
@@ -46,6 +47,7 @@ class BackupAdapter(
 
     @SuppressLint("DefaultLocale")
     override fun onBindViewHolder(holder: BackupViewHolder, position: Int) {
+        Log.i("$TAG/BackupAdapter", "onBindViewHolder")
         val backup = backups[position]
         holder.binding.tvNumber.text = "+${backup.code} ${backup.number}"
         holder.binding.tvTime.text =

+ 5 - 1
app/src/main/java/com/example/modifier/data/AppRuntimeFlags.kt

@@ -1,8 +1,12 @@
 package com.example.modifier.data
 
+import com.example.modifier.enums.RequestNumberState
+
 data class AppRuntimeFlags(
     val running: Boolean = false,
     val requesting: Boolean = false,
     val preparing: Boolean = false,
-    val checkingConnection: Boolean = false
+    val checkingConnection: Boolean = false,
+    val suspended: Boolean = false,
+    val requestNumberState: RequestNumberState = RequestNumberState.IDLE,
 )

+ 5 - 1
app/src/main/java/com/example/modifier/data/AppState.kt

@@ -1,5 +1,7 @@
 package com.example.modifier.data
 
+import com.example.modifier.enums.RequestNumberState
+
 data class AppState(
     val send: Boolean,
     val executedNum: Int,
@@ -9,5 +11,7 @@ data class AppState(
     val requesting: Boolean,
     val preparing: Boolean,
     val checkingConnection: Boolean,
-    val busy: Boolean
+    val busy: Boolean,
+    val suspended: Boolean,
+    val requestNumberState: RequestNumberState
 )

+ 6 - 0
app/src/main/java/com/example/modifier/data/BackupItemDao.kt

@@ -27,6 +27,12 @@ interface BackupItemDao {
     @Query("SELECT * FROM backupitem ORDER BY id DESC")
     suspend fun getAllR(): List<BackupItem>
 
+    @Query("SELECT * FROM backupitem WHERE number LIKE :number ORDER BY id")
+    suspend fun getAll(number:String): List<BackupItem>
+
+    @Query("SELECT * FROM backupitem WHERE number LIKE :number ORDER BY id DESC")
+    suspend fun getAllR(number:String): List<BackupItem>
+
     @Query("SELECT * FROM backupitem WHERE country = :country AND number = :number limit 1")
     suspend fun findBackupForNumber(country: String, number: String): BackupItem?
 

+ 0 - 16
app/src/main/java/com/example/modifier/data/SocketClient.kt

@@ -1,16 +0,0 @@
-package com.example.modifier.data
-
-import io.socket.client.IO
-import io.socket.client.Socket
-
-class SocketClient {
-    private val mSocketOpts = IO.Options()
-    private lateinit var mSocket: Socket
-    init {
-
-    }
-
-    suspend fun connect() {
-
-    }
-}

+ 10 - 0
app/src/main/java/com/example/modifier/enums/RequestNumberState.kt

@@ -0,0 +1,10 @@
+package com.example.modifier.enums
+
+enum class RequestNumberState {
+    IDLE,
+    RESET,
+    REQUEST,
+    OTP_1,
+    OTP_2,
+    CONFIG
+}

+ 1 - 1
app/src/main/java/com/example/modifier/repo/AppPreferencesRepository.kt

@@ -57,7 +57,7 @@ class AppPreferencesRepository(private val context: Context) {
         )
     }
 
-    suspend fun getAppPreferences() = appPreferencesFlow.stateIn(CoroutineScope(coroutineContext))
+    suspend fun stateFlow() = appPreferencesFlow.stateIn(CoroutineScope(coroutineContext))
 
     suspend fun updateServer(server: String) {
         context.appPreferencesDataStore.edit { preferences ->

+ 20 - 4
app/src/main/java/com/example/modifier/repo/AppStateRepository.kt

@@ -10,9 +10,12 @@ import androidx.lifecycle.asFlow
 import com.example.modifier.BuildConfig
 import com.example.modifier.data.AppRuntimeFlags
 import com.example.modifier.data.AppState
+import com.example.modifier.enums.RequestNumberState
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import kotlin.coroutines.coroutineContext
 
 val Context.appStateDataStore by preferencesDataStore(name = "${BuildConfig.APPLICATION_ID}.appState")
@@ -45,11 +48,20 @@ class AppStateRepository(
                 requesting = runtimeFlags.requesting,
                 preparing = runtimeFlags.preparing,
                 checkingConnection = runtimeFlags.checkingConnection,
-                busy = runtimeFlags.running || runtimeFlags.requesting || runtimeFlags.preparing || runtimeFlags.checkingConnection
+                busy = runtimeFlags.running || runtimeFlags.requesting || runtimeFlags.preparing || runtimeFlags.checkingConnection,
+                suspended = runtimeFlags.suspended,
+                requestNumberState = runtimeFlags.requestNumberState
             )
         }
 
-    suspend fun getAppState() = appStateFlow.stateIn(CoroutineScope(coroutineContext))
+    init {
+
+        MainScope().launch {
+
+        }
+    }
+
+    suspend fun stateFlow() = appStateFlow.stateIn(CoroutineScope(coroutineContext))
 
     suspend fun incrementExecutedNum(success: Boolean) {
         context.appStateDataStore.edit { preferences ->
@@ -97,7 +109,9 @@ class AppStateRepository(
         running: Boolean? = null,
         requesting: Boolean? = null,
         preparing: Boolean? = null,
-        checkingConnection: Boolean? = null
+        checkingConnection: Boolean? = null,
+        suspended: Boolean? = null,
+        requestNumberState: RequestNumberState? = null
     ) {
         val value = appRuntimeFlags.value ?: AppRuntimeFlags()
         appRuntimeFlags.postValue(
@@ -105,7 +119,9 @@ class AppStateRepository(
                 running = running ?: value.running,
                 requesting = requesting ?: value.requesting,
                 preparing = preparing ?: value.preparing,
-                checkingConnection = checkingConnection ?: value.checkingConnection
+                checkingConnection = checkingConnection ?: value.checkingConnection,
+                suspended = suspended ?: value.suspended,
+                requestNumberState = requestNumberState ?: value.requestNumberState
             )
         )
     }

+ 4 - 0
app/src/main/java/com/example/modifier/repo/BackupRepository.kt

@@ -182,4 +182,8 @@ class BackupRepository(
     suspend fun delete(backupItem: BackupItem) {
         dao.delete(backupItem)
     }
+
+    suspend fun findBackupForRestore(number: String, time: Long): BackupItem? {
+        return dao.findBackupForRestore(number, time)
+    }
 }

+ 164 - 117
app/src/main/java/com/example/modifier/service/ModifierService.kt

@@ -16,7 +16,9 @@ import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
+import android.view.View.GONE
 import android.view.View.OnTouchListener
+import android.view.View.VISIBLE
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
@@ -27,6 +29,7 @@ import androidx.appcompat.widget.PopupMenu
 import androidx.core.content.ContextCompat
 import com.example.modifier.BuildConfig
 import com.example.modifier.R
+import com.example.modifier.TAG
 import com.example.modifier.TraverseResult
 import com.example.modifier.constants.CMD_BACK
 import com.example.modifier.constants.CMD_CONVERSATION_LIST_ACTIVITY
@@ -43,6 +46,7 @@ import com.example.modifier.repo.GoogleMessageStateRepository
 import com.example.modifier.databinding.FloatingWindowBinding
 import com.example.modifier.enums.RcsConfigureState
 import com.example.modifier.enums.RcsConnectionStatus
+import com.example.modifier.enums.RequestNumberState
 import com.example.modifier.extension.kill
 import com.example.modifier.http.api.DeviceApi
 import com.example.modifier.http.api.RcsNumberApi
@@ -103,6 +107,7 @@ import io.socket.client.Socket
 import io.socket.emitter.Emitter
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
@@ -118,6 +123,7 @@ import java.time.temporal.ChronoUnit
 import java.util.Timer
 import java.util.TimerTask
 import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.coroutineContext
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.time.Duration.Companion.hours
@@ -181,21 +187,21 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             mSocketOpts.query =
                 "model=${Build.MODEL}&name=${appPreferences.value.name}&id=${uniqueId}&version=${BuildConfig.VERSION_CODE}"
             mSocketOpts.transports = arrayOf("websocket")
-            Log.i(com.example.modifier.TAG, "Connection query: ${mSocketOpts.query}")
+            Log.i(TAG, "Connection query: ${mSocketOpts.query}")
             mSocket = IO.socket(appPreferences.value.server, mSocketOpts)
             mSocket.on("message", this@ModifierService)
             mSocket.on(Socket.EVENT_CONNECT) {
-                Log.i(com.example.modifier.TAG, "Connected to server")
+                Log.i(TAG, "Connected to server")
                 CoroutineScope(Dispatchers.IO).launch {
                     delay(500)
                     reportDeviceStatues()
                 }
             }
             mSocket.on(Socket.EVENT_DISCONNECT) {
-                Log.i(com.example.modifier.TAG, "Disconnected from server")
+                Log.i(TAG, "Disconnected from server")
             }
             mSocket.on(Socket.EVENT_CONNECT_ERROR) { args ->
-                Log.i(com.example.modifier.TAG, "Connection error: " + args[0])
+                Log.i(TAG, "Connection error: " + args[0])
                 if (args[0] is Exception) {
                     val e = args[0] as Exception
                     e.printStackTrace()
@@ -207,67 +213,61 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         }
     }
 
-    override fun onCreate() {
-        super.onCreate()
-        Log.i(com.example.modifier.TAG, "Starting ModifierService")
-        CoroutineScope(Dispatchers.IO).launch {
-            appState = appStateRepository.getAppState()
-            appPreferences = appPreferencesRepository.getAppPreferences()
-            spoofedSimInfo = spoofedSimInfoRepository.stateFlow()
-            appStateRepository.updateRuntimeFlags(preparing = true)
-            val hasRoot = run checkRoot@{
-                repeat(30) {
-                    if (hasRootAccess()) {
-                        return@checkRoot true
-                    }
-                    delay(1000)
+    suspend fun init() {
+        Log.i("$TAG/AccessibilityService", "init")
+        appStateRepository.updateRuntimeFlags(preparing = true)
+        val hasRoot = run checkRoot@{
+            repeat(30) {
+                if (hasRootAccess()) {
+                    return@checkRoot true
                 }
-                false
+                delay(1000)
             }
-            if (!hasRoot) {
-                System.exit(0)
-            }
-            launch {
-                googleMessageStateRepository.startLogging()
-            }
-            if (isRebooted()) {
-                delay(2.minutes)
-            } else {
-                delay(5000)
-            }
-            optimize()
-            syncTime()
-            if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) {
-                killPhoneProcess(force = false)
-            }
-            appStateRepository.updateRuntimeFlags(preparing = false)
+            false
+        }
+        if (!hasRoot) {
+            System.exit(0)
+        }
+        CoroutineScope(coroutineContext).launch {
+            googleMessageStateRepository.startLogging()
+        }
+        if (isRebooted()) {
+            delay(2.minutes)
+        } else {
+            delay(5000)
+        }
+        optimize()
+        syncTime()
+        if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) {
+            killPhoneProcess(force = false)
+        }
+        appStateRepository.updateRuntimeFlags(preparing = false)
 
-            connect()
-            val timer = Timer()
+        connect()
+        val timer = Timer()
+        timer.schedule(object : TimerTask() {
+            override fun run() {
+                reportDeviceStatues()
+            }
+        }, 0, 3.seconds.inWholeMilliseconds)
+        if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) {
             timer.schedule(object : TimerTask() {
                 override fun run() {
-                    reportDeviceStatues()
-                }
-            }, 0, 3.seconds.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()
-                            }
+                    CoroutineScope(Dispatchers.IO).launch {
+                        try {
+                            setBatteryLevel(100)
+                        } catch (e: Exception) {
+                            e.printStackTrace()
                         }
                     }
-                }, 0, 30.minutes.inWholeMilliseconds)
-            }
+                }
+            }, 0, 30.minutes.inWholeMilliseconds)
         }
     }
 
     override fun onAccessibilityEvent(event: AccessibilityEvent) {
         Log.d(
-            com.example.modifier.TAG,
+            TAG,
             "eventType: ${event.eventType}, packageName: ${event.packageName}, className: ${event.className}"
         )
         if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
@@ -279,7 +279,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                 try {
                     packageManager.getActivityInfo(componentName, 0)
                     currentActivity = componentName.flattenToShortString()
-                    Log.d(com.example.modifier.TAG, "Activity: $currentActivity")
+                    Log.d(TAG, "Activity: $currentActivity")
                 } catch (_: Exception) {
                 }
 
@@ -292,7 +292,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
 
     override fun call(vararg args: Any) {
         if (args.isNotEmpty()) {
-            Log.i(com.example.modifier.TAG, "Received message: " + args[0])
+            Log.i(TAG, "Received message: " + args[0])
             if (args[0] is JSONObject) {
                 val json = args[0] as JSONObject
                 val action = json.optString("action")
@@ -360,7 +360,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         }
                     }
                 }
-            Log.i(com.example.modifier.TAG, "Apk file saved to ${file.path}")
+            Log.i(TAG, "Apk file saved to ${file.path}")
             shellRun("pm install -d -r ${file.path}")
             mSocket.emit(
                 "callback",
@@ -451,7 +451,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         fail.add(taskItem.id)
                     }
                 } catch (e: Exception) {
-                    Log.e(com.example.modifier.TAG, "runTaskError: ${e.message}", e)
+                    Log.e(TAG, "runTaskError: ${e.message}", e)
                     fail.add(taskItem.id)
                 }
             }
@@ -482,7 +482,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             }
             appStateRepository.updateRuntimeFlags(running = false)
         } catch (e: Exception) {
-            Log.e(com.example.modifier.TAG, "runTaskError: ${e.message}", e)
+            Log.e(TAG, "runTaskError: ${e.message}", e)
             mSocket.emit(
                 "callback",
                 JSONObject(
@@ -504,11 +504,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         body: String,
         taskConfig: TaskConfig
     ): Boolean {
-        Log.i(com.example.modifier.TAG, "Sending SMS to $to: $body")
+        Log.i(TAG, "Sending SMS to $to: $body")
         startActivity(smsIntent(to, body))
         try {
             Log.i(
-                com.example.modifier.TAG,
+                TAG,
                 "Command executed successfully, waiting for app to open..."
             )
             delay(1000)
@@ -528,13 +528,13 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             }
             if (traverseResult.isRcsCapable) {
                 if (traverseResult.sendBtn == null) {
-                    Log.i(com.example.modifier.TAG, "Send button not found")
+                    Log.i(TAG, "Send button not found")
                 } else {
-                    Log.i(com.example.modifier.TAG, "Clicking send button")
+                    Log.i(TAG, "Clicking send button")
 
                     val dt = System.currentTimeMillis() - lastSend
                     if (taskConfig.rcsInterval > 0 && dt < taskConfig.rcsInterval) {
-                        Log.i(com.example.modifier.TAG, "Waiting for RCS interval")
+                        Log.i(TAG, "Waiting for RCS interval")
                         delay(taskConfig.rcsInterval - dt)
                     }
                     traverseResult.sendBtn!!.performAction(AccessibilityNodeInfo.ACTION_CLICK)
@@ -542,11 +542,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     success = true
                 }
             } else {
-                Log.i(com.example.modifier.TAG, "RCS not detected")
+                Log.i(TAG, "RCS not detected")
             }
             appStateRepository.incrementExecutedNum(success)
             Log.i(
-                com.example.modifier.TAG,
+                TAG,
                 "executedNum: ${appState.value.executedNum}, successNum: ${appState.value.successNum}"
             )
             delay(1000)
@@ -560,7 +560,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
     @SuppressLint("ClickableViewAccessibility")
     override fun onServiceConnected() {
         super.onServiceConnected()
-
+        Log.i("$TAG/AccessibilityService", "onServiceConnected")
         instance = this
 
         val displayMetrics = DisplayMetrics()
@@ -594,7 +594,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         binding.root.post {
             maxX = width - binding.root.measuredWidth
             maxY = height - binding.root.measuredHeight
-            Log.i(com.example.modifier.TAG, "measured: $maxX, $maxY")
+            Log.i(TAG, "measured: $maxX, $maxY")
             layoutParams.x = maxX
             windowManager.updateViewLayout(mLayout, layoutParams)
         }
@@ -633,21 +633,73 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             }
             false
         }
+        CoroutineScope(Dispatchers.IO).launch {
+            appPreferences = appPreferencesRepository.stateFlow()
+            appState = appStateRepository.stateFlow()
+            spoofedSimInfo = spoofedSimInfoRepository.stateFlow()
 
-        CoroutineScope(Dispatchers.Main).launch {
-            appState.collect {
-                binding.swSend.isChecked = it.send
-                binding.btnReq.isEnabled = !it.requesting
-                binding.tvCount.text = "${it.successNum} / ${it.executedNum}"
-                withContext(Dispatchers.IO) {
-                    reportDeviceStatues()
+            launch {
+                appState.collect {
+                    withContext(Dispatchers.Main) {
+                        binding.swSend.isChecked = it.send
+                        binding.btnReq.isEnabled = !it.requesting
+                        binding.tvCount.text = "${it.successNum} / ${it.executedNum}"
+                        if (it.suspended) {
+                            binding.btnReq.backgroundTintList = ContextCompat.getColorStateList(
+                                binding.root.context,
+                                R.color.btn_color_error
+                            )
+                        } else {
+                            binding.btnReq.backgroundTintList = ContextCompat.getColorStateList(
+                                binding.root.context,
+                                R.color.btn_color
+                            )
+                        }
+                        when (it.requestNumberState) {
+                            RequestNumberState.IDLE -> {
+                                binding.tvStatus.visibility = GONE
+                                binding.btnReq.text = ""
+                            }
+
+                            RequestNumberState.RESET -> {
+                                binding.tvStatus.visibility = VISIBLE
+                                binding.btnReq.text = "Resetting GMS"
+                            }
+
+                            RequestNumberState.REQUEST -> {
+                                binding.tvStatus.visibility = VISIBLE
+                                binding.btnReq.text = "Requesting Number"
+                            }
+
+                            RequestNumberState.OTP_1 -> {
+                                binding.tvStatus.visibility = VISIBLE
+                                binding.btnReq.text = "Waiting for OTP Sent"
+                            }
+
+                            RequestNumberState.OTP_2 -> {
+                                binding.tvStatus.visibility = VISIBLE
+                                binding.btnReq.text = "Waiting for OTP Received"
+                            }
+
+                            RequestNumberState.CONFIG -> {
+                                binding.tvStatus.visibility = VISIBLE
+                                binding.btnReq.text = "Waiting for Configuration"
+                            }
+                        }
+                    }
+                    withContext(Dispatchers.IO) {
+                        reportDeviceStatues()
+                    }
                 }
             }
-        }
-        CoroutineScope(Dispatchers.Main).launch {
-            appPreferences.collect {
-                binding.swSend.text = it.name
+            launch {
+                appPreferences.collect {
+                    withContext(Dispatchers.Main) {
+                        binding.swSend.text = it.name
+                    }
+                }
             }
+            init()
         }
         binding.swSend.setOnTouchListener(touchListener)
         binding.swSend.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
@@ -790,18 +842,18 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         }
                         if (switchAppear != true) {
                             Log.e(
-                                com.example.modifier.TAG,
+                                TAG,
                                 "RCS not entered default on state, retrying..."
                             )
                             continue
                         }
                     }
                     if (!screenController.toggleRcsSwitch(false)) {
-                        Log.e(com.example.modifier.TAG, "RCS switch not turned off, retrying...")
+                        Log.e(TAG, "RCS switch not turned off, retrying...")
                         continue
                     }
                     if (!screenController.toggleRcsSwitch(true)) {
-                        Log.e(com.example.modifier.TAG, "RCS switch not turned on, retrying...")
+                        Log.e(TAG, "RCS switch not turned on, retrying...")
                         continue
                     }
                     var resetSuccess = googleMessageStateRepository.waitForRcsState(
@@ -819,7 +871,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             ), 1.minutes
                         ).let { it == RcsConfigureState.READY }
                     }
-                    Log.i(com.example.modifier.TAG, "waitForRcsState: $resetSuccess")
+                    Log.i(TAG, "waitForRcsState: $resetSuccess")
                     appStateRepository.resetRequestedNum()
                     if (resetSuccess) {
                         delay(3000)
@@ -856,7 +908,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         }
                         if (switchAppear != true) {
                             Log.e(
-                                com.example.modifier.TAG,
+                                TAG,
                                 "RCS not entered default on state, retrying..."
                             )
                             continue
@@ -864,7 +916,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     }
                     val switchOn = screenController.toggleRcsSwitch(true)
                     if (!switchOn) {
-                        Log.e(com.example.modifier.TAG, "RCS switch not turned on, retrying...")
+                        Log.e(TAG, "RCS switch not turned on, retrying...")
                         continue
                     }
                     var resetSuccess = googleMessageStateRepository.waitForRcsState(
@@ -882,7 +934,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             ), 1.minutes
                         ).let { it == RcsConfigureState.READY }
                     }
-                    Log.i(com.example.modifier.TAG, "waitForRcsState: $resetSuccess")
+                    Log.i(TAG, "waitForRcsState: $resetSuccess")
                     appStateRepository.resetRequestedNum()
                     if (resetSuccess) {
                         delay(3000)
@@ -898,8 +950,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         noBackup: Boolean = false,
         fresh: Boolean = false
     ) {
-        val color = ContextCompat.getColorStateList(binding.root.context, R.color.btn_color)
-        binding.btnReq.backgroundTintList = color
+        appStateRepository.updateRuntimeFlags(suspended = false)
         if (appPreferences.value.preventRequest) {
             return
         }
@@ -1006,7 +1057,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         }
                     }
                     var rcsNumber = response.body<RcsNumberResponse>()
-                    Log.i(com.example.modifier.TAG, "requestNumber response: $rcsNumber")
+                    Log.i(TAG, "requestNumber response: $rcsNumber")
 
                     withContext(Dispatchers.Main) {
                         binding.tvLog.text = "Requesting success, waiting for logs..."
@@ -1034,7 +1085,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     }
 
                     if (rcsNumber.expiryTime.isBefore(LocalDateTime.now())) {
-                        Log.e(com.example.modifier.TAG, "RCS number expired, retrying...")
+                        Log.e(TAG, "RCS number expired, retrying...")
                         continue
                     }
                     var sendOtpTimeout = ChronoUnit.SECONDS.between(
@@ -1042,7 +1093,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         rcsNumber.expiryTime
                     ).seconds
                     if (sendOtpTimeout < 60.seconds) {
-                        Log.e(com.example.modifier.TAG, "OTP timeout too short, retrying...")
+                        Log.e(TAG, "OTP timeout too short, retrying...")
                         continue
                     }
                     if (sendOtpTimeout > 2.minutes) {
@@ -1058,13 +1109,13 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         }
                         if (RcsConfigureState.REPLAY_REQUEST == googleMessageStateRepository.rcsConfigureState.value) {
                             Log.e(
-                                com.example.modifier.TAG,
+                                TAG,
                                 "REPLAY_REQUEST detected, may reset after 3 retry ($retry)"
                             )
                             retry++
                         }
                         Log.e(
-                            com.example.modifier.TAG,
+                            TAG,
                             "RCS not entered waiting for OTP state, retrying..."
                         )
                         continue
@@ -1081,12 +1132,12 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                                 )
                             )
                         } catch (e: Exception) {
-                            Log.e(com.example.modifier.TAG, "Send OtpState Error: ${e.message}", e)
+                            Log.e(TAG, "Send OtpState Error: ${e.message}", e)
                         }
                     }
 
                     if (rcsNumber.expiryTime.isBefore(LocalDateTime.now())) {
-                        Log.e(com.example.modifier.TAG, "RCS number expired, retrying...")
+                        Log.e(TAG, "RCS number expired, retrying...")
                         continue
                     }
 
@@ -1096,13 +1147,13 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                                 rcsNumber =
                                     ktorClient(appPreferences.value.server).get(RcsNumberApi.Id(id = rcsNumber.id))
                                         .body<RcsNumberResponse>()
-                                Log.i(com.example.modifier.TAG, "wait for otp response: $rcsNumber")
+                                Log.i(TAG, "wait for otp response: $rcsNumber")
                                 if (rcsNumber.status == RcsNumberResponse.STATUS_SUCCESS || rcsNumber.status == RcsNumberResponse.STATUS_EXPIRED) {
                                     break
                                 }
                             } catch (exception: Exception) {
                                 Log.e(
-                                    com.example.modifier.TAG,
+                                    TAG,
                                     "wait for otp Error: ${exception.stackTrace}"
                                 )
                             }
@@ -1111,7 +1162,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     }
 
                     if (rcsNumber.status != RcsNumberResponse.STATUS_SUCCESS) {
-                        Log.e(com.example.modifier.TAG, "OTP not received, retrying...")
+                        Log.e(TAG, "OTP not received, retrying...")
                         continue
                     }
 
@@ -1120,7 +1171,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             .find(rcsNumber.message!!)
                     if (match != null) {
                         val otp = match.groupValues[1]
-                        Log.i(com.example.modifier.TAG, "OTP: $otp")
+                        Log.i(TAG, "OTP: $otp")
                         val sender = "3538"
                         val msg = "Your Messenger verification code is G-$otp"
 
@@ -1148,7 +1199,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
 
                                     else -> {
                                         Log.e(
-                                            com.example.modifier.TAG,
+                                            TAG,
                                             "verifyOtp fail, retrying..."
                                         )
                                     }
@@ -1157,7 +1208,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             false
                         }
                         if (!configured) {
-                            Log.e(com.example.modifier.TAG, "RCS not configured, retrying...")
+                            Log.e(TAG, "RCS not configured, retrying...")
                             continue
                         } else {
                             launch {
@@ -1172,7 +1223,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                                     )
                                 } catch (e: Exception) {
                                     Log.e(
-                                        com.example.modifier.TAG,
+                                        TAG,
                                         "Send ConfiguredState Error: ${e.message}",
                                         e
                                     )
@@ -1183,7 +1234,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         }
                     }
                 } catch (e: Exception) {
-                    Log.e(com.example.modifier.TAG, "requestNumberError: ${e.message}", e)
+                    Log.e(TAG, "requestNumberError: ${e.message}", e)
                 }
             }
         }
@@ -1195,25 +1246,21 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             )
             appStateRepository.resetSuccessNum()
             appStateRepository.resetExecutedNum()
-            Log.i(com.example.modifier.TAG, "requestNumber success")
+            Log.i(TAG, "requestNumber success")
             delay(5000)
             shellRun(PACKAGE_MESSAGING.kill(), "sleep 1", CMD_MESSAGING_APP)
             delay(2000)
         } else {
-            Log.e(com.example.modifier.TAG, "requestNumber failed")
+            Log.e(TAG, "requestNumber failed")
             appStateRepository.updateSend(false)
-            withContext(Dispatchers.Main) {
-                binding.swSend.isChecked = false
-                binding.btnReq.backgroundTintList =
-                    ContextCompat.getColorStateList(binding.root.context, R.color.btn_color_error)
-            }
+            appStateRepository.updateRuntimeFlags(suspended = true)
         }
         appStateRepository.updateRuntimeFlags(requesting = false)
     }
 
     private suspend fun checkRcsConnectivity(): Boolean = run checkRcsConnection@{
         repeat(3) {
-            Log.i(com.example.modifier.TAG, "Checking RCS status...")
+            Log.i(TAG, "Checking RCS status...")
             shellRun(
                 CMD_CONVERSATION_LIST_ACTIVITY,
                 CMD_RCS_SETTINGS_ACTIVITY,
@@ -1222,11 +1269,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             val res = TraverseResult()
             screenInspector.traverseNode(res)
             if (res.rcsConnectionStatus == RcsConnectionStatus.CONNECTED) {
-                Log.i(com.example.modifier.TAG, "RCS is connected")
+                Log.i(TAG, "RCS is connected")
                 shellRun(CMD_BACK)
                 return@checkRcsConnection true
             } else {
-                Log.i(com.example.modifier.TAG, "RCS not connected, retrying...")
+                Log.i(TAG, "RCS not connected, retrying...")
             }
             shellRun(CMD_BACK, "sleep ${it * 2}")
         }
@@ -1252,13 +1299,13 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                         )
                     )
                         .body<SysConfigResponse>()
-                    Log.i(com.example.modifier.TAG, "sysConfig response: $config")
+                    Log.i(TAG, "sysConfig response: $config")
                     checkRcsAvailabilityNumbers.addAll(
                         config.value.split(",").map { it.trim() })
 
                 } catch (exception: Exception) {
                     Log.e(
-                        com.example.modifier.TAG,
+                        TAG,
                         "sysConfig Error: ${exception.message}",
                         exception
                     )
@@ -1266,7 +1313,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             }
 
             if (checkRcsAvailabilityNumbers.isEmpty()) {
-                Log.e(com.example.modifier.TAG, "checkRcsAvailabilityNumbers is empty")
+                Log.e(TAG, "checkRcsAvailabilityNumbers is empty")
                 return true
             }
 
@@ -1280,7 +1327,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                             return@withTimeoutOrNull true
                         } else {
                             Log.i(
-                                com.example.modifier.TAG,
+                                TAG,
                                 "checkRcsAvailability: RCS not detected"
                             )
                         }
@@ -1288,7 +1335,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     }
                 }
                 if (s == true) {
-                    Log.i(com.example.modifier.TAG, "checkRcsAvailability: $it success")
+                    Log.i(TAG, "checkRcsAvailability: $it success")
                     delay(1000)
                     return@checkAvailability true
                 }

+ 118 - 0
app/src/main/java/com/example/modifier/service/SocketClient.kt

@@ -0,0 +1,118 @@
+package com.example.modifier.service
+
+import android.os.Build
+import android.util.Log
+import com.example.modifier.BuildConfig
+import com.example.modifier.TAG
+import com.example.modifier.model.InstallApkAction
+import com.example.modifier.model.TaskAction
+import com.example.modifier.model.TaskConfig
+import com.example.modifier.serializer.Json
+import com.example.modifier.utils.uniqueId
+import io.socket.client.IO
+import io.socket.client.Socket
+import io.socket.emitter.Emitter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.json.JSONException
+import org.json.JSONObject
+
+class SocketClient(private val server: String, private val name: String) : Emitter.Listener {
+    private val tag = "$TAG/SocketClient"
+    private val mSocketOpts: IO.Options = IO.Options()
+    private val mSocket: Socket
+
+    init {
+        mSocketOpts.query =
+            "model=${Build.MODEL}&name=${name}&id=$uniqueId&version=${BuildConfig.VERSION_CODE}"
+        mSocketOpts.transports = arrayOf("websocket")
+
+        mSocket = IO.socket(server, mSocketOpts)
+
+        mSocket.on("message", this)
+        mSocket.on(Socket.EVENT_CONNECT) { this.onConnected() }
+        mSocket.on(Socket.EVENT_DISCONNECT) { this.onDisconnected() }
+        mSocket.on(Socket.EVENT_CONNECT_ERROR) { this.onConnectError() }
+        try {
+            mSocket.connect()
+        } catch (e: Exception) {
+            Log.e(tag, "connect error", e)
+        }
+    }
+
+    override fun call(vararg args: Any?) {
+        if (args.isNotEmpty()) {
+            Log.i(TAG, "Received message: " + args[0])
+            if (args[0] is JSONObject) {
+                val json = args[0] as JSONObject
+                val action = json.optString("action")
+                if ("send" == action) {
+                    val data = json.optJSONObject("data")
+                    if (data != null) {
+                        val to = data.optString("to")
+                        val body = data.optString("body")
+                        CoroutineScope(Dispatchers.IO).launch {
+                            send(
+                                to, body, TaskConfig(
+                                    rcsWait = 3000,
+                                    rcsInterval = 1000,
+                                    cleanCount = 10,
+                                    requestNumberInterval = 50,
+                                    checkConnection = true,
+                                    useBackup = false,
+                                    endToEndEncryption = true
+                                )
+                            )
+                        }
+                    }
+                } else if ("task" == action) {
+                    val taskAction = Json.decodeFromString<TaskAction>(json.toString())
+
+                    CoroutineScope(Dispatchers.IO).launch {
+                        runTask(taskAction)
+                    }
+                } else if ("installApk" == action) {
+                    val installApkAction = Json.decodeFromString<InstallApkAction>(json.toString())
+
+                    CoroutineScope(Dispatchers.IO).launch {
+                        installApk(installApkAction)
+                    }
+                }
+            }
+        }
+    }
+
+    fun onConnected() {
+        Log.i(tag, "Connected to server")
+        CoroutineScope(Dispatchers.IO).launch {
+            delay(500)
+        }
+    }
+
+    fun onDisconnected() {}
+
+    fun onConnectError(vararg args: Any) {
+        Log.i(TAG, "Connection error: " + args[0])
+        if (args[0] is Exception) {
+            val e = args[0] as Exception
+            e.printStackTrace()
+        }
+    }
+
+    fun reportDeviceStatues(canSend: Boolean, busy: Boolean, currentCountry: String) {
+        val data = JSONObject()
+        try {
+            data.put("action", "updateDevice")
+            val dataObj = JSONObject()
+            dataObj.put("canSend", canSend)
+            dataObj.put("busy", busy)
+            dataObj.put("currentCountry", currentCountry)
+            data.put("data", dataObj)
+            mSocket.emit("message", data)
+        } catch (e: JSONException) {
+            e.printStackTrace()
+        }
+    }
+}

+ 747 - 1
app/src/main/java/com/example/modifier/service/TaskRunner.kt

@@ -1,4 +1,750 @@
 package com.example.modifier.service
 
-class TaskRunner() {
+import android.content.Context
+import android.text.TextUtils
+import android.util.Base64
+import android.util.Log
+import android.view.accessibility.AccessibilityNodeInfo
+import com.example.modifier.TAG
+import com.example.modifier.TraverseResult
+import com.example.modifier.constants.CMD_BACK
+import com.example.modifier.constants.CMD_CONVERSATION_LIST_ACTIVITY
+import com.example.modifier.constants.CMD_MESSAGING_APP
+import com.example.modifier.constants.CMD_RCS_SETTINGS_ACTIVITY
+import com.example.modifier.constants.PACKAGE_GMS
+import com.example.modifier.constants.PACKAGE_MESSAGING
+import com.example.modifier.data.AppPreferences
+import com.example.modifier.data.AppState
+import com.example.modifier.enums.RcsConfigureState
+import com.example.modifier.enums.RcsConnectionStatus
+import com.example.modifier.enums.RequestNumberState
+import com.example.modifier.extension.kill
+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.SpoofedSimInfo
+import com.example.modifier.model.TaskAction
+import com.example.modifier.model.TaskConfig
+import com.example.modifier.model.TaskExecutionResult
+import com.example.modifier.repo.AppPreferencesRepository
+import com.example.modifier.repo.AppStateRepository
+import com.example.modifier.repo.BackupRepository
+import com.example.modifier.repo.GoogleMessageStateRepository
+import com.example.modifier.repo.SpoofedSimInfoRepository
+import com.example.modifier.utils.changeClashProfile
+import com.example.modifier.utils.clearConv
+import com.example.modifier.utils.genICCID
+import com.example.modifier.utils.genIMEI
+import com.example.modifier.utils.genIMSI
+import com.example.modifier.utils.isClashInstalled
+import com.example.modifier.utils.isOldVersion
+import com.example.modifier.utils.resetAll
+import com.example.modifier.utils.shellRun
+import com.example.modifier.utils.smsIntent
+import com.example.modifier.utils.spoofSmsIntent
+import com.example.modifier.utils.stopClash
+import com.example.modifier.utils.uniqueId
+import io.ktor.client.call.body
+import io.ktor.client.plugins.resources.get
+import io.ktor.client.plugins.resources.post
+import io.ktor.client.plugins.resources.put
+import io.ktor.client.plugins.timeout
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
+import java.time.LocalDateTime
+import java.time.temporal.ChronoUnit
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+class TaskRunner(
+    private val context: Context,
+    private val screenInspector: ScreenInspector,
+    private val screenController: ScreenController,
+    private val appStateRepository: AppStateRepository,
+    private val appPreferencesRepository: AppPreferencesRepository,
+    private val spoofedSimInfoRepository: SpoofedSimInfoRepository,
+    private val googleMessageStateRepository: GoogleMessageStateRepository,
+    private val backupRepository: BackupRepository
+) {
+    private var lastSend = 0L
+    private var currentTaskId = 0
+    private var requestMode = 1
+    private lateinit var appState: StateFlow<AppState>
+    private lateinit var appPreferences: StateFlow<AppPreferences>
+    private lateinit var spoofedSimInfo: StateFlow<SpoofedSimInfo>
+
+    init {
+        CoroutineScope(Dispatchers.IO).launch {
+            appState = appStateRepository.stateFlow()
+            appPreferences = appPreferencesRepository.stateFlow()
+            spoofedSimInfo = spoofedSimInfoRepository.stateFlow()
+        }
+    }
+
+    private suspend fun send(
+        to: String,
+        body: String,
+        taskConfig: TaskConfig
+    ): Boolean {
+        Log.i(TAG, "Sending SMS to $to: $body")
+        context.startActivity(smsIntent(to, body))
+        try {
+            Log.i(
+                TAG,
+                "Command executed successfully, waiting for app to open..."
+            )
+            delay(1000)
+            var success = false
+            var traverseResult = TraverseResult()
+            withTimeoutOrNull(taskConfig.rcsWait) {
+                while (true) {
+                    traverseResult = TraverseResult()
+                    screenInspector.traverseNode(traverseResult)
+                    if (traverseResult.isRcsCapable && traverseResult.sendBtn != null) {
+                        if (!taskConfig.endToEndEncryption || traverseResult.encrypted) {
+                            break
+                        }
+                    }
+                    delay(200)
+                }
+            }
+            if (traverseResult.isRcsCapable) {
+                if (traverseResult.sendBtn == null) {
+                    Log.i(TAG, "Send button not found")
+                } else {
+                    Log.i(TAG, "Clicking send button")
+
+                    val dt = System.currentTimeMillis() - lastSend
+                    if (taskConfig.rcsInterval > 0 && dt < taskConfig.rcsInterval) {
+                        Log.i(TAG, "Waiting for RCS interval")
+                        delay(taskConfig.rcsInterval - dt)
+                    }
+                    traverseResult.sendBtn!!.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+                    lastSend = System.currentTimeMillis()
+                    success = true
+                }
+            } else {
+                Log.i(TAG, "RCS not detected")
+            }
+            appStateRepository.incrementExecutedNum(success)
+            Log.i(
+                TAG,
+                "executedNum: ${appState.value.executedNum}, successNum: ${appState.value.successNum}"
+            )
+            delay(1000)
+            return success
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+        return false
+    }
+
+    private suspend fun runTask(
+        taskAction: TaskAction,
+        onSuccess: (TaskExecutionResult) -> Unit,
+        onError: (Exception) -> Unit
+    ) {
+        if (appState.value.busy) {
+            onError(Exception("device busy"))
+            return
+        }
+
+        try {
+            val taskConfig = taskAction.data.config
+            currentTaskId = taskAction.data.taskId
+            requestMode = if (taskAction.data.config.useBackup) 2 else 1
+
+            if (taskAction.data.config.checkConnection) {
+                appStateRepository.updateRuntimeFlags(checkingConnection = true)
+                if (!checkRcsAvailability()) {
+                    onError(Exception("RCS not available"))
+                    requestNumber()
+                    appStateRepository.updateRuntimeFlags(checkingConnection = false)
+                    return
+                }
+                appStateRepository.updateRuntimeFlags(checkingConnection = false)
+            }
+
+            appStateRepository.updateRuntimeFlags(running = true)
+            val success = ArrayList<Int>()
+            val fail = ArrayList<Int>()
+            for (i in 0 until taskAction.data.tasks.size) {
+                val taskItem = taskAction.data.tasks[i]
+                try {
+                    if (send(
+                            taskItem.number,
+                            taskItem.message,
+                            taskConfig
+                        )
+                    ) {
+                        success.add(taskItem.id)
+                    } else {
+                        fail.add(taskItem.id)
+                    }
+                } catch (e: Exception) {
+                    Log.e(TAG, "runTaskError: ${e.message}", e)
+                    fail.add(taskItem.id)
+                }
+            }
+            shellRun(CMD_BACK)
+            onSuccess(TaskExecutionResult(success, fail))
+            if (taskConfig.requestNumberInterval in 1..appState.value.successNum) {
+                delay(3000)
+                requestNumber()
+            } else if (taskConfig.cleanCount in 1..appState.value.executedNum && !appPreferences.value.preventClean) {
+                delay(3000)
+                clearConv();
+                shellRun(CMD_MESSAGING_APP)
+                delay(3000)
+                appStateRepository.resetExecutedNum()
+            } else {
+                delay(2000)
+            }
+            appStateRepository.updateRuntimeFlags(running = false)
+        } catch (e: Exception) {
+            Log.e(TAG, "runTaskError: ${e.message}", e)
+            onError(e)
+            appStateRepository.updateRuntimeFlags(running = false)
+        }
+    }
+
+    private suspend fun reset() {
+        if (isOldVersion(context)) {
+            withTimeout(1.hours) {
+                while (true) {
+                    delay(100)
+                    appStateRepository.updateRuntimeFlags(requestNumberState = RequestNumberState.RESET)
+                    googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED)
+                    spoofedSimInfoRepository.mock()
+                    resetAll()
+                    var switchAppear = googleMessageStateRepository.waitForRcsState(
+                        arrayOf(RcsConfigureState.WAITING_FOR_TOS),
+                        2.minutes
+                    )?.let {
+                        it == RcsConfigureState.WAITING_FOR_TOS
+                    }
+                    if (switchAppear != true) {
+                        shellRun(
+                            PACKAGE_GMS.kill(), PACKAGE_MESSAGING.kill(), "sleep 1",
+                            CMD_MESSAGING_APP
+                        )
+                        switchAppear = googleMessageStateRepository.waitForRcsState(
+                            arrayOf(RcsConfigureState.WAITING_FOR_TOS),
+                            5.minutes
+                        )?.let {
+                            it == RcsConfigureState.WAITING_FOR_TOS
+                        }
+                        if (switchAppear != true) {
+                            Log.e(
+                                TAG,
+                                "RCS not entered default on state, retrying..."
+                            )
+                            continue
+                        }
+                    }
+                    if (!screenController.toggleRcsSwitch(false)) {
+                        Log.e(TAG, "RCS switch not turned off, retrying...")
+                        continue
+                    }
+                    if (!screenController.toggleRcsSwitch(true)) {
+                        Log.e(TAG, "RCS switch not turned on, retrying...")
+                        continue
+                    }
+                    var resetSuccess = googleMessageStateRepository.waitForRcsState(
+                        arrayOf(
+                            RcsConfigureState.READY
+                        ), 30.seconds
+                    ).let { it == RcsConfigureState.READY }
+                    if (!resetSuccess) {
+                        screenController.toggleRcsSwitch(false)
+                        delay(1000)
+                        screenController.toggleRcsSwitch(true)
+                        resetSuccess = googleMessageStateRepository.waitForRcsState(
+                            arrayOf(
+                                RcsConfigureState.READY
+                            ), 1.minutes
+                        ).let { it == RcsConfigureState.READY }
+                    }
+                    Log.i(TAG, "waitForRcsState: $resetSuccess")
+                    appStateRepository.resetRequestedNum()
+                    if (resetSuccess) {
+                        delay(3000)
+                        break
+                    }
+                }
+            }
+        } else {
+            withTimeout(1.hours) {
+                while (true) {
+                    delay(100)
+                    appStateRepository.updateRuntimeFlags(requestNumberState = RequestNumberState.RESET)
+                    googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED)
+                    spoofedSimInfoRepository.mock()
+                    resetAll()
+                    var switchAppear = googleMessageStateRepository.waitForRcsState(
+                        arrayOf(RcsConfigureState.WAITING_FOR_DEFAULT_ON),
+                        1.minutes
+                    )?.let {
+                        it == RcsConfigureState.WAITING_FOR_DEFAULT_ON
+                    }
+                    if (switchAppear != true) {
+                        shellRun(
+                            PACKAGE_GMS.kill(), PACKAGE_MESSAGING.kill(), "sleep 1",
+                            CMD_MESSAGING_APP
+                        )
+                        switchAppear = googleMessageStateRepository.waitForRcsState(
+                            arrayOf(RcsConfigureState.WAITING_FOR_DEFAULT_ON),
+                            2.minutes
+                        )?.let {
+                            it == RcsConfigureState.WAITING_FOR_DEFAULT_ON
+                        }
+                        if (switchAppear != true) {
+                            Log.e(
+                                TAG,
+                                "RCS not entered default on state, retrying..."
+                            )
+                            continue
+                        }
+                    }
+                    val switchOn = screenController.toggleRcsSwitch(true)
+                    if (!switchOn) {
+                        Log.e(TAG, "RCS switch not turned on, retrying...")
+                        continue
+                    }
+                    var resetSuccess = googleMessageStateRepository.waitForRcsState(
+                        arrayOf(
+                            RcsConfigureState.READY
+                        ), 30.seconds
+                    ).let { it == RcsConfigureState.READY }
+                    if (!resetSuccess) {
+                        screenController.toggleRcsSwitch(false)
+                        delay(1000)
+                        screenController.toggleRcsSwitch(true)
+                        resetSuccess = googleMessageStateRepository.waitForRcsState(
+                            arrayOf(
+                                RcsConfigureState.READY
+                            ), 1.minutes
+                        ).let { it == RcsConfigureState.READY }
+                    }
+                    Log.i(TAG, "waitForRcsState: $resetSuccess")
+                    appStateRepository.resetRequestedNum()
+                    if (resetSuccess) {
+                        delay(3000)
+                        break
+                    }
+                }
+            }
+        }
+    }
+
+    private suspend fun requestNumber(
+        reset: Boolean = false,
+        noBackup: Boolean = false,
+        fresh: Boolean = false
+    ) {
+        appStateRepository.updateRuntimeFlags(suspended = false)
+        if (appPreferences.value.preventRequest) {
+            return
+        }
+        if (appState.value.requesting) {
+            return
+        }
+
+        appStateRepository.updateRuntimeFlags(requesting = true)
+
+        if (spoofedSimInfo.value.available) {
+            backupRepository.backup(
+                spoofedSimInfo = spoofedSimInfo.value,
+                type = "auto",
+                sendCount = appState.value.executedNum,
+                fresh = fresh
+            )
+        } else {
+            clearConv();
+        }
+
+        appStateRepository.incrementRequestedNum()
+        var requestSuccess = false
+        var retry = 0
+        var needRest = reset
+        withTimeoutOrNull(1.hours) {
+            while (true) {
+                delay(200)
+                needRest = needRest || retry > 2 || appState.value.requestedNum > 5
+                try {
+                    val device =
+                        ktorClient(appPreferences.value.server).get(DeviceApi.Id(id = uniqueId))
+                            .body<DeviceResponse>()
+                    if (isClashInstalled(context)) {
+
+                        val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+                        if (TextUtils.isEmpty(device.clashProfile)) {
+                            prefs.edit()
+                                .remove("clash_profile")
+                                .apply()
+                            stopClash()
+                        } else {
+                            val oldProfile = prefs.getString("clash_profile", "")
+                            if (oldProfile != device.clashProfile) {
+                                prefs.edit()
+                                    .putString("clash_profile", device.clashProfile)
+                                    .apply()
+                                changeClashProfile(
+                                    device.pinCountry!!, Base64.encodeToString(
+                                        device.clashProfile!!.toByteArray(),
+                                        Base64.DEFAULT
+                                    )
+                                )
+                                delay(5000)
+                            }
+                        }
+                    }
+
+                    if (requestMode == 2 && !noBackup) {
+                        val backup = backupRepository.findBackupForRestore(
+                            spoofedSimInfo.value.number,
+                            System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000
+                        )
+                        if (backup != null) {
+                            if (backupRepository.restore(backup)) {
+                                requestSuccess = true
+                                break
+                            } else {
+                                backupRepository.backup(
+                                    spoofedSimInfo = spoofedSimInfo.value,
+                                    type = "auto",
+                                    sendCount = 0
+                                )
+                                continue
+                            }
+                        }
+                    }
+
+                    if (needRest && !appPreferences.value.preventReset) {
+                        reset()
+                        retry = 0
+                        needRest = false
+                    }
+
+                    googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED)
+                    appStateRepository.updateRuntimeFlags(requestNumberState = RequestNumberState.REQUEST)
+
+                    val req = RcsNumberRequest(
+                        deviceId = uniqueId,
+                        taskId = currentTaskId
+                    )
+                    if (!TextUtils.isEmpty(device.pinCountry)) {
+                        req.country = device.pinCountry
+                    }
+                    val response = ktorClient(appPreferences.value.server).put(
+                        RcsNumberApi()
+                    ) {
+                        contentType(ContentType.Application.Json)
+                        setBody(req)
+                        timeout {
+                            requestTimeoutMillis = 60 * 1000
+                            socketTimeoutMillis = 60 * 1000
+                        }
+                    }
+                    var rcsNumber = response.body<RcsNumberResponse>()
+                    Log.i(TAG, "requestNumber response: $rcsNumber")
+
+                    appStateRepository.updateRuntimeFlags(requestNumberState = RequestNumberState.OTP_1)
+
+                    spoofedSimInfoRepository.updateSpoofedSimInfo(
+                        SpoofedSimInfo(
+                            number = rcsNumber.number,
+                            mcc = rcsNumber.mcc,
+                            mnc = rcsNumber.mnc,
+                            iccid = genICCID(rcsNumber.mnc, rcsNumber.areaCode),
+                            imsi = genIMSI(rcsNumber.mcc + rcsNumber.mnc),
+                            imei = genIMEI(),
+                            country = rcsNumber.country,
+                            areaCode = rcsNumber.areaCode,
+                            available = false,
+                            carrierId = rcsNumber.carrierId,
+                            carrierName = rcsNumber.carrierName,
+                        )
+                    )
+
+                    shellRun(CMD_MESSAGING_APP)
+
+                    if (rcsNumber.expiryTime.isBefore(LocalDateTime.now())) {
+                        Log.e(TAG, "RCS number expired, retrying...")
+                        continue
+                    }
+                    var sendOtpTimeout = ChronoUnit.SECONDS.between(
+                        LocalDateTime.now(),
+                        rcsNumber.expiryTime
+                    ).seconds
+                    if (sendOtpTimeout < 60.seconds) {
+                        Log.e(TAG, "OTP timeout too short, retrying...")
+                        continue
+                    }
+                    if (sendOtpTimeout > 2.minutes) {
+                        sendOtpTimeout = 2.minutes
+                    }
+                    if (googleMessageStateRepository.waitForRcsState(
+                            arrayOf(RcsConfigureState.WAITING_FOR_OTP),
+                            sendOtpTimeout
+                        ) != RcsConfigureState.WAITING_FOR_OTP
+                    ) {
+                        if (!screenController.toggleRcsSwitch(true)) {
+                            needRest = true
+                        }
+                        if (RcsConfigureState.REPLAY_REQUEST == googleMessageStateRepository.rcsConfigureState.value) {
+                            Log.e(
+                                TAG,
+                                "REPLAY_REQUEST detected, may reset after 3 retry ($retry)"
+                            )
+                            retry++
+                        }
+                        Log.e(
+                            TAG,
+                            "RCS not entered waiting for OTP state, retrying..."
+                        )
+                        continue
+                    }
+
+                    launch {
+                        try {
+                            ktorClient(appPreferences.value.server).post(
+                                RcsNumberApi.Id.OtpState(
+                                    RcsNumberApi.Id(
+                                        RcsNumberApi(),
+                                        rcsNumber.id
+                                    )
+                                )
+                            )
+                        } catch (e: Exception) {
+                            Log.e(TAG, "Send OtpState Error: ${e.message}", e)
+                        }
+                    }
+
+                    if (rcsNumber.expiryTime.isBefore(LocalDateTime.now())) {
+                        Log.e(TAG, "RCS number expired, retrying...")
+                        continue
+                    }
+
+                    appStateRepository.updateRuntimeFlags(requestNumberState = RequestNumberState.OTP_2)
+                    withTimeoutOrNull(60.seconds) {
+                        while (true) {
+                            try {
+                                rcsNumber =
+                                    ktorClient(appPreferences.value.server).get(RcsNumberApi.Id(id = rcsNumber.id))
+                                        .body<RcsNumberResponse>()
+                                Log.i(TAG, "wait for otp response: $rcsNumber")
+                                if (rcsNumber.status == RcsNumberResponse.STATUS_SUCCESS || rcsNumber.status == RcsNumberResponse.STATUS_EXPIRED) {
+                                    break
+                                }
+                            } catch (exception: Exception) {
+                                Log.e(
+                                    TAG,
+                                    "wait for otp Error: ${exception.stackTrace}"
+                                )
+                            }
+                            delay(2.seconds)
+                        }
+                    }
+
+                    if (rcsNumber.status != RcsNumberResponse.STATUS_SUCCESS) {
+                        Log.e(TAG, "OTP not received, retrying...")
+                        continue
+                    }
+
+                    val match =
+                        Regex("Your Messenger verification code is G-(\\d{6})")
+                            .find(rcsNumber.message!!)
+                    if (match != null) {
+                        appStateRepository.updateRuntimeFlags(requestNumberState = RequestNumberState.CONFIG)
+
+                        val otp = match.groupValues[1]
+                        Log.i(TAG, "OTP: $otp")
+                        val sender = "3538"
+                        val msg = "Your Messenger verification code is G-$otp"
+
+                        val configured = run configuring@{
+                            repeat(2) {
+                                spoofSmsIntent(sender, msg)
+                                val state =
+                                    googleMessageStateRepository.waitForRcsState(
+                                        arrayOf(
+                                            RcsConfigureState.CONFIGURED,
+                                            RcsConfigureState.RETRY
+                                        ), 60.seconds
+                                    )
+                                when (state) {
+                                    RcsConfigureState.CONFIGURED -> {
+                                        return@configuring true
+                                    }
+
+                                    RcsConfigureState.RETRY -> {
+                                        googleMessageStateRepository.waitForRcsState(
+                                            arrayOf(RcsConfigureState.WAITING_FOR_OTP),
+                                            60.seconds
+                                        )
+                                    }
+
+                                    else -> {
+                                        Log.e(
+                                            TAG,
+                                            "verifyOtp fail, retrying..."
+                                        )
+                                    }
+                                }
+                            }
+                            false
+                        }
+                        if (!configured) {
+                            Log.e(TAG, "RCS not configured, retrying...")
+                            continue
+                        } else {
+                            launch {
+                                try {
+                                    ktorClient(appPreferences.value.server).post(
+                                        RcsNumberApi.Id.Configured(
+                                            RcsNumberApi.Id(
+                                                RcsNumberApi(),
+                                                rcsNumber.id
+                                            )
+                                        )
+                                    )
+                                } catch (e: Exception) {
+                                    Log.e(
+                                        TAG,
+                                        "Send ConfiguredState Error: ${e.message}",
+                                        e
+                                    )
+                                }
+                            }
+                            requestSuccess = true
+                            break
+                        }
+                    }
+                } catch (e: Exception) {
+                    Log.e(TAG, "requestNumberError: ${e.message}", e)
+                }
+            }
+        }
+        if (requestSuccess) {
+            spoofedSimInfoRepository.updateSpoofedSimInfo(
+                spoofedSimInfo = spoofedSimInfo.value.copy(
+                    available = true
+                )
+            )
+            appStateRepository.resetSuccessNum()
+            appStateRepository.resetExecutedNum()
+            Log.i(TAG, "requestNumber success")
+            delay(5000)
+            shellRun(PACKAGE_MESSAGING.kill(), "sleep 1", CMD_MESSAGING_APP)
+            delay(2000)
+        } else {
+            Log.e(TAG, "requestNumber failed")
+            appStateRepository.updateSend(false)
+            appStateRepository.updateRuntimeFlags(suspended = true)
+        }
+        appStateRepository.updateRuntimeFlags(requesting = false, requestNumberState = RequestNumberState.IDLE)
+    }
+
+    private suspend fun checkRcsConnectivity(): Boolean = run checkRcsConnection@{
+        repeat(3) {
+            Log.i(TAG, "Checking RCS status...")
+            shellRun(
+                CMD_CONVERSATION_LIST_ACTIVITY,
+                CMD_RCS_SETTINGS_ACTIVITY,
+                "sleep 1",
+            )
+            val res = TraverseResult()
+            screenInspector.traverseNode(res)
+            if (res.rcsConnectionStatus == RcsConnectionStatus.CONNECTED) {
+                Log.i(TAG, "RCS is connected")
+                shellRun(CMD_BACK)
+                return@checkRcsConnection true
+            } else {
+                Log.i(TAG, "RCS not connected, retrying...")
+            }
+            shellRun(CMD_BACK, "sleep ${it * 2}")
+        }
+        false
+    }
+
+    suspend fun checkRcsAvailability(): Boolean {
+        appStateRepository.updateRuntimeFlags(checkingConnection = true)
+        val availability = run checkAvailability@{
+            val rcsConnected = checkRcsConnectivity()
+            if (!rcsConnected) {
+                return@checkAvailability false
+            }
+
+            var config: SysConfigResponse
+            val checkRcsAvailabilityNumbers = mutableListOf<String>()
+            withTimeoutOrNull(60.seconds) {
+                try {
+                    config = ktorClient(appPreferences.value.server).get(
+                        SysConfigApi.Id(
+                            SysConfigApi(),
+                            "check_availability_numbers"
+                        )
+                    )
+                        .body<SysConfigResponse>()
+                    Log.i(TAG, "sysConfig response: $config")
+                    checkRcsAvailabilityNumbers.addAll(
+                        config.value.split(",").map { it.trim() })
+
+                } catch (exception: Exception) {
+                    Log.e(
+                        TAG,
+                        "sysConfig Error: ${exception.message}",
+                        exception
+                    )
+                }
+            }
+
+            if (checkRcsAvailabilityNumbers.isEmpty()) {
+                Log.e(TAG, "checkRcsAvailabilityNumbers is empty")
+                return true
+            }
+
+            checkRcsAvailabilityNumbers.forEach {
+                context.startActivity(smsIntent(it, ""))
+                val s = withTimeoutOrNull(5.seconds) {
+                    while (true) {
+                        val traverseResult = TraverseResult()
+                        screenInspector.traverseNode(traverseResult)
+                        if (traverseResult.isRcsCapable) {
+                            return@withTimeoutOrNull true
+                        } else {
+                            Log.i(
+                                TAG,
+                                "checkRcsAvailability: RCS not detected"
+                            )
+                        }
+                        delay(200)
+                    }
+                }
+                if (s == true) {
+                    Log.i(TAG, "checkRcsAvailability: $it success")
+                    delay(1000)
+                    return@checkAvailability true
+                }
+            }
+            false
+        }
+        appStateRepository.updateRuntimeFlags(checkingConnection = false)
+        return availability
+    }
 }

+ 66 - 44
app/src/main/java/com/example/modifier/ui/backup/BackupFragment.kt

@@ -2,16 +2,24 @@ package com.example.modifier.ui.backup
 
 import android.content.Context
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.widget.doAfterTextChanged
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asFlow
 import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.liveData
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.example.modifier.R
+import com.example.modifier.TAG
 import com.example.modifier.Utils
 import com.example.modifier.adapter.BackupAdapter
 import com.example.modifier.data.AppDatabase
@@ -21,7 +29,10 @@ import com.example.modifier.model.SpoofedSimInfo
 import com.example.modifier.repo.BackupRepository
 import com.example.modifier.repo.SpoofedSimInfoRepository
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -48,75 +59,86 @@ class BackupFragment : Fragment() {
             requireContext().getSharedPreferences("backup", Context.MODE_PRIVATE).edit()
                 .putBoolean("sort", value).apply()
         }
+    private val searchText = MutableLiveData<String>()
 
+    @OptIn(FlowPreview::class)
     override fun onCreateView(
         inflater: LayoutInflater, container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View {
-        if (this::binding.isInitialized) {
-            return binding.root
-        }
-        lifecycleScope.launch {
-            spoofedSimInfo = spoofedSimInfoRepository.stateFlow()
-        }
-        binding = FragmentBackupBinding.inflate(inflater, container, false)
-        adapter = BackupAdapter(requireContext(), list, backupRepository)
-        binding.rvBackup.adapter = adapter
-        binding.rvBackup.layoutManager = LinearLayoutManager(requireContext())
-        binding.fabBackup.setOnClickListener {
-            Utils.makeLoadingButton(requireContext(), binding.fabBackup)
-            lifecycleScope.launch {
-                withContext(Dispatchers.IO) {
-                    val backup = backupRepository.backup(spoofedSimInfo.value, "manual", 0)
-                    list.add(0, backup)
+        if (!this::binding.isInitialized) {
+            binding = FragmentBackupBinding.inflate(inflater, container, false)
+            adapter = BackupAdapter(requireContext(), list, backupRepository)
+            binding.rvBackup.adapter = adapter
+            binding.rvBackup.layoutManager = LinearLayoutManager(requireContext())
+            binding.btnSort.setOnClickListener {
+                sort = !sort
+                lifecycleScope.launch {
+                    withContext(Dispatchers.IO) {
+                        refresh()
+                    }
                 }
-
-                adapter.notifyItemInserted(0)
-                binding.fabBackup.isEnabled = true
-                binding.fabBackup.icon =
-                    AppCompatResources.getDrawable(requireContext(), R.drawable.ic_add)
             }
-        }
-        binding.refresh.setOnRefreshListener {
-            lifecycleScope.launch {
-                withContext(Dispatchers.IO) {
-                    refresh()
+            binding.btnAdd.setOnClickListener {
+                lifecycleScope.launch {
+                    withContext(Dispatchers.IO) {
+                        val backup = backupRepository.backup(spoofedSimInfo.value, "manual", 0)
+                        list.add(0, backup)
+                    }
+                    adapter.notifyItemInserted(0)
                 }
             }
+            binding.etSearch.doAfterTextChanged {
+                searchText.postValue(it.toString())
+            }
+            ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
+                binding.searchBar.setPadding(
+                    binding.searchBar.paddingLeft,
+                    insets.getInsets(WindowInsetsCompat.Type.systemBars()).top,
+                    binding.searchBar.paddingRight,
+                    binding.searchBar.paddingBottom,
+                )
+                insets
+            }
         }
-        binding.fabSort.setOnClickListener {
-            sort = !sort
-            lifecycleScope.launch {
-                withContext(Dispatchers.IO) {
-                    refresh()
-                }
+        lifecycleScope.launch {
+            spoofedSimInfo = spoofedSimInfoRepository.stateFlow()
+        }
+        lifecycleScope.launch {
+            withContext(Dispatchers.IO) {
+                refresh()
             }
         }
-
         lifecycleScope.launch {
-            // repeatOnLifecycle launches the block in a new coroutine every time the
-            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                withContext(Dispatchers.IO) {
-                    refresh()
-                }
+            searchText.asFlow().debounce(500).collect {
+                Log.i("$TAG/BackupFragment", "searchText: $it")
+                refresh(it)
             }
         }
-
         return binding.root
     }
 
-    private suspend fun refresh() {
+    private suspend fun refresh(number: String? = null) {
         val all = when (sort) {
-            true -> backupItemDao.getAll()
-            false -> backupItemDao.getAllR()
+            true -> {
+                when (number) {
+                    null -> backupItemDao.getAll()
+                    else -> backupItemDao.getAll("%$number%")
+                }
+            }
+
+            false -> {
+                when (number) {
+                    null -> backupItemDao.getAllR()
+                    else -> backupItemDao.getAllR("%$number%")
+                }
+            }
         }
         list.clear()
         list.addAll(all)
         withContext(Dispatchers.Main) {
             adapter.notifyDataSetChanged()
         }
-        binding.refresh.isRefreshing = false
     }
 
 }

+ 2 - 8
app/src/main/java/com/example/modifier/ui/settings/SettingsFragment.kt

@@ -20,9 +20,7 @@ import androidx.activity.result.contract.ActivityResultContracts
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
 import com.example.modifier.R
 import com.example.modifier.TAG
 import com.example.modifier.Utils
@@ -52,10 +50,6 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.single
-import kotlinx.coroutines.flow.subscribe
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.apache.commons.lang3.RandomStringUtils
@@ -86,7 +80,7 @@ class SettingsFragment : Fragment() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         CoroutineScope(Dispatchers.IO).launch {
-            appPreferences = appPreferencesRepository.getAppPreferences()
+            appPreferences = appPreferencesRepository.stateFlow()
             spoofedSimInfo = spoofedSimInfoRepository.stateFlow()
         }
         requestPermissionLauncher =
@@ -323,7 +317,7 @@ class SettingsFragment : Fragment() {
         }
         viewLifecycleOwner.lifecycleScope.launch {
             launch {
-                appPreferencesRepository.getAppPreferences().collect {
+                appPreferencesRepository.stateFlow().collect {
                     Log.i(TAG, "appPreferencesRepository.collect")
                     binding.etServer.setText(it.server)
                     binding.etDeviceLabel.setText(it.name)

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

@@ -61,7 +61,7 @@ class UtilsFragment : Fragment() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         CoroutineScope(Dispatchers.IO).launch {
-            appPreferences = appPreferencesRepository.getAppPreferences()
+            appPreferences = appPreferencesRepository.stateFlow()
         }
     }
 

+ 11 - 0
app/src/main/res/layout/action_bar_progress.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.google.android.material.progressindicator.CircularProgressIndicator
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:indeterminate="true" />
+</FrameLayout>

+ 10 - 0
app/src/main/res/layout/activity_main2.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity2">
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 16 - 1
app/src/main/res/layout/floating_window.xml

@@ -62,6 +62,8 @@
                     android:id="@+id/scroll"
                     android:layout_width="match_parent"
                     android:layout_height="0dp"
+                    android:clipToPadding="false"
+                    android:paddingBottom="20dp"
                     app:layout_constraintBottom_toTopOf="@id/btns"
                     app:layout_constraintTop_toBottomOf="@id/sw_send">
 
@@ -74,6 +76,16 @@
                         android:textSize="12sp" />
                 </ScrollView>
 
+                <TextView
+                    android:id="@+id/tv_status"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="Waiting for Configuration"
+                    android:textAlignment="center"
+                    android:textColor="?colorPrimary"
+                    android:textSize="12sp"
+                    app:layout_constraintBottom_toTopOf="@id/btns" />
+
                 <GridLayout
                     android:id="@+id/btns"
                     android:layout_width="wrap_content"
@@ -91,6 +103,7 @@
                         android:layout_marginStart="2dp"
                         android:layout_marginEnd="2dp"
                         android:backgroundTint="@color/btn_color"
+                        android:padding="0dp"
                         app:icon="@drawable/ic_autorenew"
                         app:iconGravity="textStart"
                         app:iconPadding="0dp" />
@@ -102,6 +115,7 @@
                         android:layout_height="34dp"
                         android:layout_marginStart="2dp"
                         android:layout_marginEnd="2dp"
+                        android:padding="0dp"
                         app:icon="@drawable/ic_refresh"
                         app:iconGravity="textStart"
                         app:iconPadding="0dp" />
@@ -124,13 +138,14 @@
                         android:layout_height="34dp"
                         android:layout_marginStart="2dp"
                         android:layout_marginEnd="2dp"
+                        android:padding="0dp"
                         app:icon="@drawable/ic_more_horiz"
                         app:iconGravity="textStart"
                         app:iconPadding="0dp" />
 
                     <TextView
                         android:id="@+id/tv_count"
-                        android:layout_width="40dp"
+                        android:layout_width="48dp"
                         android:layout_height="wrap_content"
                         android:text="32"
                         android:textAlignment="center"

+ 42 - 30
app/src/main/res/layout/fragment_backup.xml

@@ -6,44 +6,56 @@
     <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+
         tools:context=".ui.backup.BackupFragment">
 
-        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-            android:id="@+id/refresh"
+        <LinearLayout
+            android:id="@+id/search_bar"
             android:layout_width="match_parent"
-            android:layout_height="match_parent">
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:orientation="horizontal"
+            android:paddingLeft="16dp"
+            android:paddingRight="16dp"
+            android:paddingBottom="8dp"
+            app:layout_constraintTop_toTopOf="parent">
 
-            <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/rv_backup"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false"
-                android:paddingTop="40dp"
-                android:paddingBottom="150dp">
+            <com.google.android.material.textfield.TextInputEditText
+                android:id="@+id/et_search"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:background="@null"
+                android:drawableStart="@drawable/ic_search"
+                android:drawablePadding="16dp"
+                android:hint="搜索"
+                android:textSize="15sp" />
 
-            </androidx.recyclerview.widget.RecyclerView>
-        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/btn_sort"
+                style="?attr/materialIconButtonStyle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:icon="@drawable/ic_sort" />
 
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/btn_add"
+                style="?attr/materialIconButtonStyle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:icon="@drawable/ic_add" />
+        </LinearLayout>
 
-        <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
-            android:id="@+id/fab_backup"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="bottom|end"
-            android:layout_margin="16dp"
-            app:icon="@drawable/ic_add"
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_backup"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:clipToPadding="false"
+            android:paddingBottom="16dp"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent" />
+            app:layout_constraintTop_toBottomOf="@id/search_bar" />
 
-        <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
-            android:id="@+id/fab_sort"
-            style="?attr/floatingActionButtonSmallStyle"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="bottom|end"
-            android:layout_marginBottom="16dp"
-            app:icon="@drawable/ic_sort"
-            app:layout_constraintBottom_toTopOf="@id/fab_backup"
-            app:layout_constraintStart_toStartOf="@id/fab_backup" />
     </androidx.constraintlayout.widget.ConstraintLayout>
+
+
 </layout>

+ 14 - 0
app/src/main/res/menu/search_bar_menu.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item
+        android:id="@+id/action_sort"
+        android:icon="@drawable/ic_sort"
+        android:title="排序"
+        app:showAsAction="always" />
+    <item
+        android:id="@+id/action_create_backup"
+        android:icon="@drawable/ic_add"
+        android:title="新建备份"
+        app:showAsAction="always" />
+</menu>