xiongzhu 1 anno fa
parent
commit
c9795315f3

+ 1 - 1
app/build.gradle

@@ -24,7 +24,7 @@ android {
         applicationId "com.example.modifier"
         minSdk 29
         targetSdk 34
-        versionCode 141
+        versionCode 142
         versionName "1.0.1"
         archivesBaseName = "modifier-${versionCode}"
 

+ 7 - 0
app/src/main/java/com/example/modifier/model/BaseAction.kt

@@ -0,0 +1,7 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+open class BaseAction(open val id: String, open val action: String) {
+}

+ 14 - 0
app/src/main/java/com/example/modifier/model/CancelStoreNumberAction.kt

@@ -0,0 +1,14 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class CancelStoreNumberAction : BaseAction {
+    companion object {
+        const val NAME = "cancelStoreNumber"
+    }
+
+
+    constructor(id: String) : super(id, NAME) {
+    }
+}

+ 11 - 2
app/src/main/java/com/example/modifier/model/InstallApkAction.kt

@@ -3,10 +3,19 @@ package com.example.modifier.model
 import kotlinx.serialization.Serializable
 
 @Serializable
-class InstallApkAction(val id: String, val action: String, val data: InstallApkData) {
+class InstallApkAction : BaseAction {
+    companion object {
+        const val NAME = "installApk"
+    }
+
+    val data: InstallApkData
+
+    constructor(id: String, data: InstallApkData) : super(id, NAME) {
+        this.data = data
+    }
 
 }
 
 @Serializable
-class InstallApkData(val apkUrl: String) {
+class InstallApkData(val apkUrl: String, val interrupt: Boolean?) {
 }

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

@@ -0,0 +1,20 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class ResumeAction : BaseAction {
+    companion object {
+        const val NAME = "resume"
+    }
+
+    val data: ResumeData?
+
+    constructor(id: String, data: ResumeData?) : super(id, NAME) {
+        this.data = data
+    }
+}
+
+@Serializable
+data class ResumeData(val reset: Boolean?, val request: Boolean?) {
+}

+ 10 - 1
app/src/main/java/com/example/modifier/model/RunScriptAction.kt

@@ -3,7 +3,16 @@ package com.example.modifier.model
 import kotlinx.serialization.Serializable
 
 @Serializable
-class RunScriptAction(val id: String, val action: String, val data: RunScriptData) {
+class RunScriptAction : BaseAction {
+    companion object {
+        const val NAME = "runScript"
+    }
+
+    val data: RunScriptData
+
+    constructor(id: String, data: RunScriptData) : super(id, NAME) {
+        this.data = data
+    }
 
 }
 

+ 19 - 0
app/src/main/java/com/example/modifier/model/StoreNumberAction.kt

@@ -0,0 +1,19 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class StoreNumberAction : BaseAction {
+    companion object {
+        const val NAME = "storeNumber"
+    }
+
+    val data: StoreNumberData?
+
+    constructor(id: String, data: StoreNumberData?) : super(id, NAME) {
+        this.data = data
+    }
+}
+
+@Serializable
+data class StoreNumberData(val num: Int?)

+ 9 - 1
app/src/main/java/com/example/modifier/model/TaskAction.kt

@@ -5,8 +5,16 @@ import kotlinx.serialization.Serializable
 import java.time.LocalDateTime
 
 @Serializable
-class TaskAction(val id: String, val action: String, val data: TaskData) {
+class TaskAction : BaseAction {
+    companion object {
+        const val NAME = "task"
+    }
 
+    val data: TaskData
+
+    constructor(id: String, data: TaskData) : super(id, NAME) {
+        this.data = data
+    }
 }
 
 @Serializable

+ 10 - 1
app/src/main/java/com/example/modifier/model/UpdateDeviceAction.kt

@@ -3,7 +3,16 @@ package com.example.modifier.model
 import kotlinx.serialization.Serializable
 
 @Serializable
-class UpdateDeviceAction(val id: String, val action: String, val data: UpdateDeviceData) {
+class UpdateDeviceAction : BaseAction {
+    companion object {
+        const val NAME = "updateDevice"
+    }
+
+    val data: UpdateDeviceData
+
+    constructor(id: String, data: UpdateDeviceData) : super(id, NAME) {
+        this.data = data
+    }
 }
 
 @Serializable

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

@@ -13,6 +13,7 @@ import com.example.modifier.constants.PACKAGE_MESSAGING
 import com.example.modifier.data.BackupItem
 import com.example.modifier.data.BackupItemDao
 import com.example.modifier.enums.RcsConfigureState
+import com.example.modifier.enums.ReqState
 import com.example.modifier.extension.clear
 import com.example.modifier.extension.disable
 import com.example.modifier.extension.enable
@@ -38,6 +39,7 @@ class BackupRepository(
     }
 
     suspend fun backup(type: String, sendCount: Int, stock: Int = 0): BackupItem {
+        AppStateRepo.instance.updateRuntimeFlags(reqState = ReqState.BACKUP)
         val spoofedSimInfoRepo = SpoofedSimInfoRepo.instance
         clearConv()
         delay(3000)
@@ -114,6 +116,7 @@ class BackupRepository(
 
     @SuppressLint("SdCardPath")
     suspend fun restore(backup: BackupItem): Boolean {
+        AppStateRepo.instance.updateRuntimeFlags(reqState = ReqState.RESTORE)
         val spoofedSimInfoRepo = SpoofedSimInfoRepo.instance
         val simInfo = SpoofedSimInfo(
             numberId = backup.numberId,

+ 10 - 50
app/src/main/java/com/example/modifier/service/ModifierService.kt

@@ -348,10 +348,13 @@ class ModifierService : AccessibilityService() {
     private fun showMenu(context: Context, v: View, @MenuRes menuRes: Int) {
         val popup = PopupMenu(context, v)
         popup.menuInflater.inflate(menuRes, popup.menu)
-        popup.menu.findItem(R.id.store_numbers).isEnabled =
+        popup.menu.findItem(R.id.store_numbers).isVisible =
             !appStateRepo.appState.value.storing
+        popup.menu.findItem(R.id.cancel_store_numbers).isVisible =
+            appStateRepo.appState.value.storing
         popup.menu.findItem(R.id.check_availability).isEnabled =
             !appStateRepo.appState.value.checkingConnection
+
         popup.setOnMenuItemClickListener { item ->
             CoroutineScope(Dispatchers.IO).launch {
                 when (item.itemId) {
@@ -380,11 +383,11 @@ class ModifierService : AccessibilityService() {
                     }
 
                     R.id.store_numbers -> {
-                        if (!appStateRepo.appState.value.storing) {
-                            launch {
-                                storeNumbers()
-                            }
-                        }
+                        taskRunner.storeNumbers()
+                    }
+
+                    R.id.cancel_store_numbers -> {
+                        taskRunner.cancelStoreNumber()
                     }
 
                     R.id.change_profile -> {
@@ -413,48 +416,5 @@ class ModifierService : AccessibilityService() {
         popup.show()
     }
 
-    private suspend fun storeNumbers() {
-        val num = appPrefsRepo.appPrefs.value.storeNum
-        if (num <= 0) {
-            return
-        }
-        appStateRepo.updateRuntimeFlags(storing = true)
-        var success = true
-        var retry = 0
-        repeat(num) {
-            Log.i(TAG, "storeNumbers: $it")
-            try {
-                if (success || retry > 3) {
-                    taskRunner.reset()
-                }
-                taskRunner.requestNumberAtomic()
-                spoofedSimInfoRepo.updateSpoofedSimInfo(
-                    spoofedSimInfo = spoofedSimInfoRepo.spoofedSimInfo.value.copy(
-                        available = true
-                    ),
-                    suspend = false
-                )
-                appStateRepo.updateRuntimeFlags(reqState = ReqState.BACKUP)
-                taskRunner.screenController.toggleRcsSwitch(false)
-                backupRepository.backup(type = "auto", sendCount = 0, stock = 1)
-                RcsNumberApi.updateStockFlag(
-                    id = spoofedSimInfoRepo.spoofedSimInfo.value.numberId,
-                    flag = 1
-                )
-                success = true
-                retry = 0
-            } catch (e: Exception) {
-                success = false
-                retry++
-                Log.i(TAG, "Error store number, retry: $retry", e)
-            }
-        }
-        if (success) {
-            taskRunner.reset()
-        }
-        appStateRepo.updateRuntimeFlags(
-            reqState = ReqState.NONE,
-            storing = false
-        )
-    }
+
 }

+ 152 - 97
app/src/main/java/com/example/modifier/service/SocketClient.kt

@@ -5,12 +5,14 @@ import android.util.Log
 import com.example.modifier.BuildConfig
 import com.example.modifier.baseTag
 import com.example.modifier.enums.ReqState
+import com.example.modifier.model.CancelStoreNumberAction
 import com.example.modifier.model.InstallApkAction
+import com.example.modifier.model.ResumeAction
 import com.example.modifier.model.RunScriptAction
 import com.example.modifier.model.RunScriptResult
 import com.example.modifier.model.SocketCallback
+import com.example.modifier.model.StoreNumberAction
 import com.example.modifier.model.TaskAction
-import com.example.modifier.model.TaskConfig
 import com.example.modifier.model.TaskExecutionResult
 import com.example.modifier.model.UpdateDeviceAction
 import com.example.modifier.repo.AppPrefsRepo
@@ -29,9 +31,7 @@ import kotlinx.serialization.encodeToString
 import org.json.JSONException
 import org.json.JSONObject
 import java.util.Timer
-import java.util.TimerTask
 import kotlin.concurrent.schedule
-import kotlin.time.Duration.Companion.seconds
 
 class SocketClient(
     private val taskRunner: TaskRunner
@@ -78,107 +78,158 @@ class SocketClient(
         mSocket.disconnect()
     }
 
+    private fun throwIfBusy() {
+        if (AppStateRepo.instance.appState.value.busy) {
+            throw Exception("Device is busy")
+        }
+    }
+
     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")
-                val id = json.optString("id")
-                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 {
-                            taskRunner.send(
-                                to, body, TaskConfig(
-                                    rcsWait = 3000,
-                                    rcsInterval = 1000,
-                                    cleanCount = 10,
-                                    requestNumberInterval = 50,
-                                    checkConnection = true,
-                                    useBackup = false,
-                                    e2ee = 0
-                                )
+        if (args.isEmpty()) {
+            return
+        }
+        Log.i(TAG, "Received message: " + args[0])
+        if (args[0] !is JSONObject) return
+        val json = args[0] as JSONObject
+        val action = json.optString("action")
+        val id = json.optString("id")
+        val jsonStr = json.toString()
+        when (action) {
+            TaskAction.NAME -> {
+                try {
+                    throwIfBusy()
+                    val taskAction = Json.decodeFromString<TaskAction>(jsonStr)
+                    CoroutineScope(Dispatchers.IO).launch {
+                        taskRunner.runTask(taskAction, onSuccess = { res ->
+                            socketCallback(taskAction.id, res)
+                        }, onError = { error ->
+                            socketCallback(
+                                id = taskAction.id,
+                                status = -1,
+                                error = error.message
                             )
-                        }
+                        })
                     }
-                } else if ("task" == action) {
-                    try {
-                        val taskAction = Json.decodeFromString<TaskAction>(json.toString())
-                        CoroutineScope(Dispatchers.IO).launch {
-                            taskRunner.runTask(taskAction, onSuccess = { res ->
-                                socketCallback(taskAction.id, res)
-                            }, onError = { error ->
-                                socketCallback(
-                                    id = taskAction.id,
-                                    status = -1,
-                                    error = error.message
-                                )
-                            })
-                        }
-                    } catch (e: Exception) {
-                        Log.e(TAG, "taskAction error", e)
-                        socketCallback(id = id, status = -1, error = e.message)
+                } catch (e: Exception) {
+                    Log.e(TAG, "taskAction error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
+                }
+            }
+
+            InstallApkAction.NAME -> {
+                try {
+                    val installApkAction = Json.decodeFromString<InstallApkAction>(jsonStr)
+                    if (true != installApkAction.data.interrupt) {
+                        throwIfBusy()
                     }
-                } else if ("installApk" == action) {
-                    try {
-                        val installApkAction =
-                            Json.decodeFromString<InstallApkAction>(json.toString())
-                        CoroutineScope(Dispatchers.IO).launch {
-                            taskRunner.installApk(installApkAction, onSuccess = {
-                                socketCallback(id = installApkAction.id, status = 0, data = "OK")
-                            }, onError = { error ->
-                                socketCallback(
-                                    id = installApkAction.id,
-                                    status = -1,
-                                    error = error.message
-                                )
-                            })
-                        }
-                    } catch (e: Exception) {
-                        Log.e(TAG, "installApk error", e)
-                        socketCallback(id = id, status = -1, error = e.message)
+                    CoroutineScope(Dispatchers.IO).launch {
+                        taskRunner.installApk(installApkAction, onSuccess = {
+                            socketCallback(
+                                id = installApkAction.id,
+                                status = 0,
+                                data = "OK"
+                            )
+                        }, onError = { error ->
+                            socketCallback(
+                                id = installApkAction.id,
+                                status = -1,
+                                error = error.message
+                            )
+                        })
                     }
-                } else if ("runScript" == action) {
-                    try {
-                        val runScriptAction =
-                            Json.decodeFromString<RunScriptAction>(json.toString())
-                        CoroutineScope(Dispatchers.IO).launch {
-                            taskRunner.runScript(runScriptAction, onSuccess = { out, err ->
-                                socketCallback(runScriptAction.id, RunScriptResult(out, err))
-                            }, onError = { error ->
-                                socketCallback(
-                                    id = runScriptAction.id,
-                                    status = -1,
-                                    error = error.message
-                                )
-                            })
-                        }
-                    } catch (e: Exception) {
-                        Log.e(TAG, "runScript error", e)
-                        socketCallback(id = id, status = -1, error = e.message)
+                } catch (e: Exception) {
+                    Log.e(TAG, "installApk error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
+                }
+            }
+
+            RunScriptAction.NAME -> {
+                try {
+                    val runScriptAction = Json.decodeFromString<RunScriptAction>(jsonStr)
+                    CoroutineScope(Dispatchers.IO).launch {
+                        taskRunner.runScript(runScriptAction, onSuccess = { out, err ->
+                            socketCallback(runScriptAction.id, RunScriptResult(out, err))
+                        }, onError = { error ->
+                            socketCallback(
+                                id = runScriptAction.id,
+                                status = -1,
+                                error = error.message
+                            )
+                        })
                     }
-                } else if ("updateDevice" == action) {
-                    try {
-                        val updateDeviceAction =
-                            Json.decodeFromString<UpdateDeviceAction>(json.toString())
-                        CoroutineScope(Dispatchers.IO).launch {
-                            taskRunner.updateDevice(updateDeviceAction, onSuccess = {
-                                socketCallback(id = updateDeviceAction.id, status = 0, data = "OK")
-                            }, onError = { error ->
-                                socketCallback(
-                                    id = updateDeviceAction.id,
-                                    status = -1,
-                                    error = error.message
-                                )
-                            })
+                } catch (e: Exception) {
+                    Log.e(TAG, "runScript error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
+                }
+            }
+
+            UpdateDeviceAction.NAME -> {
+                try {
+                    val updateDeviceAction = Json.decodeFromString<UpdateDeviceAction>(jsonStr)
+                    CoroutineScope(Dispatchers.IO).launch {
+                        taskRunner.updateDevice(updateDeviceAction, onSuccess = {
+                            socketCallback(
+                                id = updateDeviceAction.id,
+                                status = 0,
+                                data = "OK"
+                            )
+                        }, onError = { error ->
+                            socketCallback(
+                                id = updateDeviceAction.id,
+                                status = -1,
+                                error = error.message
+                            )
+                        })
+                    }
+                } catch (e: Exception) {
+                    Log.e(TAG, "updateDevice error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
+                }
+            }
+
+            StoreNumberAction.NAME -> {
+                try {
+                    throwIfBusy()
+                    val storeNumberAction = Json.decodeFromString<StoreNumberAction>(jsonStr)
+                    CoroutineScope(Dispatchers.IO).launch {
+                        taskRunner.storeNumbers(storeNumberAction.data?.num)
+                        socketCallback(id = storeNumberAction.id, status = 0, data = "OK")
+                    }
+                } catch (e: Exception) {
+                    Log.e(TAG, "storeNumber error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
+                }
+            }
+
+            CancelStoreNumberAction.NAME -> {
+                try {
+                    CoroutineScope(Dispatchers.IO).launch {
+                        taskRunner.cancelStoreNumber()
+                        socketCallback(id = id, status = 0, data = "OK")
+                    }
+                } catch (e: Exception) {
+                    Log.e(TAG, "cancelStoreNumber error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
+                }
+            }
+
+            ResumeAction.NAME -> {
+                try {
+                    throwIfBusy()
+                    val resumeAction = Json.decodeFromString<ResumeAction>(jsonStr)
+                    CoroutineScope(Dispatchers.IO).launch {
+                        AppStateRepo.instance.updateRuntimeFlags(suspended = false)
+                        AppStateRepo.instance.updateSend(true)
+                        if (resumeAction.data?.request == true) {
+                            taskRunner.requestNumberOnTask(
+                                reset = resumeAction.data.reset ?: false
+                            )
                         }
-                    } catch (e: Exception) {
-                        Log.e(TAG, "updateDevice error", e)
-                        socketCallback(id = id, status = -1, error = e.message)
                     }
+                    socketCallback(id = id, status = 0, data = "OK")
+                } catch (e: Exception) {
+                    Log.e(TAG, "resume error", e)
+                    socketCallback(id = id, status = -1, error = e.message)
                 }
             }
         }
@@ -206,8 +257,12 @@ class SocketClient(
             dataObj.put("busy", AppStateRepo.instance.appState.value.busy)
             dataObj.put("currentCountry", SpoofedSimInfoRepo.instance.spoofedSimInfo.value.country)
             dataObj.put("ip", getIPAddress().joinToString(","))
-            dataObj.put("requesting", AppStateRepo.instance.appState.value.reqState == ReqState.REQUEST)
+            dataObj.put(
+                "requesting",
+                AppStateRepo.instance.appState.value.reqState == ReqState.REQUEST
+            )
             dataObj.put("suspended", AppStateRepo.instance.appState.value.suspended)
+            dataObj.put("storing", AppStateRepo.instance.appState.value.storing)
             data.put("data", dataObj)
             mSocket.emit("message", data)
         } catch (e: JSONException) {

+ 93 - 5
app/src/main/java/com/example/modifier/service/TaskRunner.kt

@@ -34,6 +34,7 @@ import com.example.modifier.repo.AppStateRepo
 import com.example.modifier.repo.BackupRepository
 import com.example.modifier.repo.GmsgStateRepository
 import com.example.modifier.repo.SpoofedSimInfoRepo
+import com.example.modifier.service.ModifierService.Companion
 import com.example.modifier.utils.clearConv
 import com.example.modifier.utils.genICCID
 import com.example.modifier.utils.genIMEI
@@ -53,10 +54,16 @@ import io.ktor.client.request.prepareGet
 import io.ktor.utils.io.ByteReadChannel
 import io.ktor.utils.io.core.isEmpty
 import io.ktor.utils.io.core.readBytes
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
 import kotlinx.coroutines.withTimeoutOrNull
@@ -85,6 +92,7 @@ class TaskRunner(
     private var lastSend = 0L
     private var currentTaskId = 0
     private var requestMode = 1
+    private var storeNumberJob: Job? = null
 
     suspend fun send(
         to: String,
@@ -168,9 +176,11 @@ class TaskRunner(
             currentTaskId = taskAction.data.taskId
             requestMode = if (taskAction.data.config.useBackup) 2 else 1
 
-            if (taskAction.data.config.checkConnection && appStateRepo.appState.value.executedNum == 0) {
+            if (!spoofedSimInfoRepo.spoofedSimInfo.value.available ||
+                (taskAction.data.config.checkConnection && appStateRepo.appState.value.executedNum == 0)
+            ) {
                 appStateRepo.updateRuntimeFlags(checkingConnection = true)
-                if (!checkRcsA10y()) {
+                if (!spoofedSimInfoRepo.spoofedSimInfo.value.available || !checkRcsA10y()) {
                     onError(Exception("RCS not available"))
                     requestNumberOnTask()
                     appStateRepo.updateRuntimeFlags(checkingConnection = false)
@@ -481,7 +491,6 @@ class TaskRunner(
         appStateRepo.updateRuntimeFlags(reqState = ReqState.CLEAN)
         clearConv()
         if (spoofedSimInfoRepo.spoofedSimInfo.value.available) {
-            appStateRepo.updateRuntimeFlags(reqState = ReqState.BACKUP)
             backupRepository.backup(
                 type = "auto",
                 sendCount = appStateRepo.appState.value.successNum
@@ -494,7 +503,6 @@ class TaskRunner(
                 delay(200)
                 try {
                     if (requestMode == 2 && !noBackup) {
-                        appStateRepo.updateRuntimeFlags(reqState = ReqState.RESTORE)
                         val backup = backupRepository.findBackupForRestore(
                             spoofedSimInfoRepo.spoofedSimInfo.value.number
                         )
@@ -506,7 +514,6 @@ class TaskRunner(
                             } else {
                                 backup.stock = 3
                                 backupRepository.update(backup)
-                                appStateRepo.updateRuntimeFlags(reqState = ReqState.BACKUP)
                                 continue
                             }
                         }
@@ -691,4 +698,85 @@ class TaskRunner(
         }
     }
 
+    suspend fun storeNumbers(n: Int? = null) {
+        if (storeNumberJob != null) {
+            Log.e(TAG, "storeNumbers: another store number job is running")
+            return
+        }
+        val num = n ?: appPrefsRepo.appPrefs.value.storeNum
+        if (num <= 0) {
+            Log.e(TAG, "storeNumbers: storeNum is 0")
+            return
+        }
+        storeNumberJob = CoroutineScope(coroutineContext).launch {
+            try {
+                appStateRepo.updateRuntimeFlags(storing = true)
+                if (spoofedSimInfoRepo.spoofedSimInfo.value.available && appStateRepo.appState.value.successNum <= 5) {
+                    backupRepository.backup(
+                        type = "auto",
+                        sendCount = appStateRepo.appState.value.successNum,
+                        stock = 1
+                    )
+                }
+                var success = true
+                var retry = 0
+                repeat(num) {
+                    if (!isActive) {
+                        Log.e(TAG, "storeNumbers: job is cancelled, stopping repeat")
+                        return@launch
+                    }
+                    Log.i(TAG, "storeNumbers: $it")
+                    try {
+                        if (success || retry > 3) {
+                            reset()
+                        }
+                        requestNumberAtomic()
+                        spoofedSimInfoRepo.updateSpoofedSimInfo(
+                            spoofedSimInfo = spoofedSimInfoRepo.spoofedSimInfo.value.copy(
+                                available = true
+                            ),
+                            suspend = false
+                        )
+                        screenController.toggleRcsSwitch(false)
+                        backupRepository.backup(type = "auto", sendCount = 0, stock = 1)
+                        RcsNumberApi.updateStockFlag(
+                            id = spoofedSimInfoRepo.spoofedSimInfo.value.numberId,
+                            flag = 1
+                        )
+                        success = true
+                        retry = 0
+                    } catch (e: CancellationException) {
+                        Log.e(TAG, "storeNumbers: job is cancelled", e)
+                    } catch (e: Exception) {
+                        success = false
+                        retry++
+                        Log.e(TAG, "Error store number, retry: $retry", e)
+                    }
+                }
+                if (success) {
+                    reset()
+                }
+                appStateRepo.updateRuntimeFlags(
+                    reqState = ReqState.NONE,
+                    storing = false
+                )
+                storeNumberJob = null
+            } catch (e: CancellationException) {
+                Log.e(TAG, "storeNumbers: job is cancelled", e)
+            }
+        }
+    }
+
+    suspend fun cancelStoreNumber() {
+        if (storeNumberJob != null) {
+            if (storeNumberJob!!.isActive) {
+                storeNumberJob!!.cancel()
+            }
+            appStateRepo.updateRuntimeFlags(
+                reqState = ReqState.NONE,
+                storing = false
+            )
+            storeNumberJob = null
+        }
+    }
 }

+ 11 - 4
app/src/main/res/menu/more.xml

@@ -2,25 +2,32 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
     <item
         android:id="@+id/inspect"
-        android:title="Inspect" />
+        android:title="Inspect"
+        android:visible="false" />
     <item
         android:id="@+id/check_availability"
         android:title="检查可用性" />
     <item
         android:id="@+id/toggle_on"
-        android:title="开开关" />
+        android:title="开开关"
+        android:visible="false" />
     <item
         android:id="@+id/toggle_off"
-        android:title="关开关" />
+        android:title="关开关"
+        android:visible="false" />
     <item
         android:id="@+id/clear_conv"
         android:title="清对话" />
     <item
         android:id="@+id/store_numbers"
         android:title="存号码" />
+    <item
+        android:id="@+id/cancel_store_numbers"
+        android:title="取消存号码" />
     <item
         android:id="@+id/change_profile"
-        android:title="换Clash配置" />
+        android:title="换Clash配置"
+        android:visible="false" />
     <item
         android:id="@+id/restart_modifier"
         android:title="重启Modifier" />