package com.example.modifier.service import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.annotation.SuppressLint import android.content.ComponentName import android.content.Context import android.graphics.PixelFormat import android.graphics.Rect import android.os.Build import android.os.Handler import android.os.Looper import android.text.TextUtils import android.util.Base64 import android.util.DisplayMetrics import android.util.Log import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.View.OnTouchListener import android.view.WindowManager import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.widget.CompoundButton import android.widget.FrameLayout import androidx.annotation.MenuRes import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import com.example.modifier.BuildConfig import com.example.modifier.Global import com.example.modifier.Global.backup import com.example.modifier.Global.restartModifier import com.example.modifier.R import com.example.modifier.TraverseResult import com.example.modifier.Utils 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.AppDatabase import com.example.modifier.data.AppPreferences import com.example.modifier.data.AppPreferencesRepository import com.example.modifier.data.AppState import com.example.modifier.data.AppStateRepository import com.example.modifier.data.BackupItemDao import com.example.modifier.data.GoogleMessageStateRepository import com.example.modifier.databinding.FloatingWindowBinding import com.example.modifier.enums.RcsConfigureState import com.example.modifier.enums.RcsConnectionStatus import com.example.modifier.extension.kill import com.example.modifier.http.api.DeviceApi import com.example.modifier.http.api.RcsNumberApi import com.example.modifier.http.api.SysConfigApi import com.example.modifier.http.ktorClient import com.example.modifier.http.request.RcsNumberRequest import com.example.modifier.http.response.DeviceResponse import com.example.modifier.http.response.RcsNumberResponse import com.example.modifier.http.response.SysConfigResponse import com.example.modifier.model.InstallApkAction import com.example.modifier.model.SimInfo import com.example.modifier.model.SocketCallback import com.example.modifier.model.TaskAction import com.example.modifier.model.TaskConfig import com.example.modifier.model.TaskExecutionResult import com.example.modifier.serializer.Json import com.example.modifier.utils.changeClashProfile import com.example.modifier.utils.clearConv import com.example.modifier.utils.hasRootAccess import com.example.modifier.utils.isClashInstalled import com.example.modifier.utils.isOldVersion import com.example.modifier.utils.isRebooted import com.example.modifier.utils.killPhoneProcess import com.example.modifier.utils.optimize import com.example.modifier.utils.resetAll import com.example.modifier.utils.setBatteryLevel import com.example.modifier.utils.shellRun import com.example.modifier.utils.smsIntent import com.example.modifier.utils.stopClash import com.example.modifier.utils.syncTime import com.example.modifier.utils.uniqueId import com.google.android.material.color.DynamicColors import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpResponseValidator import io.ktor.client.plugins.ServerResponseException 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.prepareGet import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.core.isEmpty import io.ktor.utils.io.core.readBytes 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.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.encodeToString import org.apache.commons.lang3.RandomStringUtils import org.json.JSONException import org.json.JSONObject import java.io.File import java.time.LocalDateTime import java.time.temporal.ChronoUnit import java.util.Optional import java.util.Timer import java.util.TimerTask import java.util.concurrent.atomic.AtomicReference import kotlin.math.max import kotlin.math.min import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @SuppressLint("SetTextI18n") class ModifierService : AccessibilityService(), Emitter.Listener { companion object { const val NAME: String = BuildConfig.APPLICATION_ID + ".service.ModifierService" @JvmStatic var instance: ModifierService? = null private set } private val handler = Handler(Looper.getMainLooper()) private val mSocketOpts = IO.Options() private lateinit var mSocket: Socket private lateinit var binding: FloatingWindowBinding private var currentTaskId = 0 private var lastSend = 0L private val backupItemDao: BackupItemDao by lazy { AppDatabase.getDatabase(this).itemDao() } private val appPreferencesRepository: AppPreferencesRepository by lazy { AppPreferencesRepository(this) } private lateinit var appPreferences: StateFlow private val appStateRepository: AppStateRepository by lazy { AppStateRepository(this) } private lateinit var appState: StateFlow private val googleMessageStateRepository: GoogleMessageStateRepository by lazy { GoogleMessageStateRepository(this) } private var requestMode = 1; private var currentActivity = "" private val screenInspector: ScreenInspector by lazy { ScreenInspector(this) } private val screenController: ScreenController by lazy { ScreenController(this, screenInspector) } fun connect() { try { if (this@ModifierService::mSocket.isInitialized) { mSocket.disconnect() } 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}") 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") CoroutineScope(Dispatchers.IO).launch { delay(500) reportDeviceStatues() } } mSocket.on(Socket.EVENT_DISCONNECT) { Log.i(com.example.modifier.TAG, "Disconnected from server") } mSocket.on(Socket.EVENT_CONNECT_ERROR) { args -> Log.i(com.example.modifier.TAG, "Connection error: " + args[0]) if (args[0] is Exception) { val e = args[0] as Exception e.printStackTrace() } } mSocket.connect() } catch (e: Exception) { e.printStackTrace() } } override fun onCreate() { super.onCreate() Log.i(com.example.modifier.TAG, "Starting ModifierService") CoroutineScope(Dispatchers.IO).launch { appState = appStateRepository.getAppState() appPreferences = appPreferencesRepository.getAppPreferences() appStateRepository.updateRuntimeFlags(preparing = true) val hasRoot = run checkRoot@{ repeat(30) { if (hasRootAccess()) { return@checkRoot true } delay(1000) } false } if (!hasRoot) { System.exit(0) } 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() 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() } } } }, 0, 30.minutes.inWholeMilliseconds) } } } override fun onAccessibilityEvent(event: AccessibilityEvent) { Log.d( com.example.modifier.TAG, "eventType: ${event.eventType}, packageName: ${event.packageName}, className: ${event.className}" ) if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { if (event.packageName != null && event.className != null) { val componentName = ComponentName( event.packageName.toString(), event.className.toString() ) try { packageManager.getActivityInfo(componentName, 0) currentActivity = componentName.flattenToShortString() Log.d(com.example.modifier.TAG, "Activity: $currentActivity") } catch (_: Exception) { } } } } override fun onInterrupt() { } override fun call(vararg args: Any) { if (args.isNotEmpty()) { Log.i(com.example.modifier.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(json.toString()) CoroutineScope(Dispatchers.IO).launch { runTask(taskAction) } } else if ("installApk" == action) { val installApkAction = Json.decodeFromString(json.toString()) CoroutineScope(Dispatchers.IO).launch { installApk(installApkAction) } } } } } private suspend fun installApk(installApkAction: InstallApkAction) { try { val file = withContext(Dispatchers.IO) { File.createTempFile("files", ".apk") } HttpClient(OkHttp) { HttpResponseValidator { validateResponse { response -> if (response.status.value !in 200..299) { throw ServerResponseException( response, "Error " + response.status.value.toString() ) } } } }.prepareGet(installApkAction.data.apkUrl) .execute { httpResponse -> val channel: ByteReadChannel = httpResponse.body() while (!channel.isClosedForRead) { val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) while (!packet.isEmpty) { val bytes = packet.readBytes() file.appendBytes(bytes) } } } Log.i(com.example.modifier.TAG, "Apk file saved to ${file.path}") shellRun("pm install -d -r ${file.path}") mSocket.emit( "callback", JSONObject( Json.encodeToString( SocketCallback( id = installApkAction.id, status = 0, ) ) ) ) } catch (e: Exception) { Log.e("Modifier", "Failed to install apk", e) mSocket.emit( "callback", JSONObject( Json.encodeToString( SocketCallback( id = installApkAction.id, status = -1, error = e.message ) ) ) ) } } private suspend fun runTask(taskAction: TaskAction) { if (appState.value.busy) { mSocket.emit( "callback", JSONObject( Json.encodeToString( SocketCallback( id = taskAction.id, status = -1, error = "another task is running" ) ) ) ) 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()) { mSocket.emit( "callback", JSONObject( Json.encodeToString( SocketCallback( id = taskAction.id, status = -1, error = "RCS not available" ) ) ) ) requestNumber() appStateRepository.updateRuntimeFlags(checkingConnection = false) return } appStateRepository.updateRuntimeFlags(checkingConnection = false) } appStateRepository.updateRuntimeFlags(running = true) val success = ArrayList() val fail = ArrayList() 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(com.example.modifier.TAG, "runTaskError: ${e.message}", e) fail.add(taskItem.id) } } shellRun(CMD_BACK) mSocket.emit( "callback", JSONObject( Json.encodeToString( SocketCallback( id = taskAction.id, status = 0, data = 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(com.example.modifier.TAG, "runTaskError: ${e.message}", e) mSocket.emit( "callback", JSONObject( Json.encodeToString( SocketCallback( id = taskAction.id, status = -1, error = e.message ) ) ) ) appStateRepository.updateRuntimeFlags(running = false) } } private suspend fun send( to: String, body: String, taskConfig: TaskConfig ): Boolean { Log.i(com.example.modifier.TAG, "Sending SMS to $to: $body") startActivity(smsIntent(to, body)) try { Log.i( com.example.modifier.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(com.example.modifier.TAG, "Send button not found") } else { Log.i(com.example.modifier.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") delay(taskConfig.rcsInterval - dt) } traverseResult.sendBtn!!.performAction(AccessibilityNodeInfo.ACTION_CLICK) lastSend = System.currentTimeMillis() success = true } } else { Log.i(com.example.modifier.TAG, "RCS not detected") } appStateRepository.incrementExecutedNum(success) Log.i( com.example.modifier.TAG, "executedNum: ${appState.value.executedNum}, successNum: ${appState.value.successNum}" ) delay(1000) return success } catch (e: Exception) { e.printStackTrace() } return false } @SuppressLint("ClickableViewAccessibility") override fun onServiceConnected() { super.onServiceConnected() instance = this val displayMetrics = DisplayMetrics() val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager windowManager.defaultDisplay.getMetrics(displayMetrics) val height = displayMetrics.heightPixels val width = displayMetrics.widthPixels val mLayout = FrameLayout(this) val layoutParams = WindowManager.LayoutParams() layoutParams.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY layoutParams.format = PixelFormat.TRANSLUCENT layoutParams.flags = layoutParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE layoutParams.flags = layoutParams.flags or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT layoutParams.x = 0 layoutParams.y = 800 layoutParams.gravity = Gravity.START or Gravity.TOP val newContext = DynamicColors.wrapContextIfAvailable(applicationContext, R.style.AppTheme) val inflater = LayoutInflater.from(newContext) binding = FloatingWindowBinding.inflate(inflater, mLayout, true) binding.tvVersion.text = "v${BuildConfig.VERSION_CODE}" windowManager.addView(mLayout, layoutParams) var maxX = 0 var maxY = 0 binding.root.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY) binding.root.post { maxX = width - binding.root.measuredWidth maxY = height - binding.root.measuredHeight Log.i(com.example.modifier.TAG, "measured: $maxX, $maxY") layoutParams.x = maxX windowManager.updateViewLayout(mLayout, layoutParams) } val downX = AtomicReference(0f) val downY = AtomicReference(0f) val downParamX = AtomicReference(0) val downParamY = AtomicReference(0) val touchListener = OnTouchListener { v, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { downX.set(event.rawX) downY.set(event.rawY) downParamX.set(layoutParams.x) downParamY.set(layoutParams.y) } MotionEvent.ACTION_MOVE -> { layoutParams.x = min( max((downParamX.get() + (event.rawX - downX.get())).toDouble(), 0.0), maxX.toDouble() ) .toInt() layoutParams.y = min( max((downParamY.get() + (event.rawY - downY.get())).toDouble(), 0.0), maxY.toDouble() ) .toInt() windowManager.updateViewLayout(mLayout, layoutParams) } MotionEvent.ACTION_UP -> { return@OnTouchListener event.eventTime - event.downTime >= 200 } } false } 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() } } } CoroutineScope(Dispatchers.Main).launch { appPreferences.collect { binding.swSend.text = it.name } } binding.swSend.setOnTouchListener(touchListener) binding.swSend.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> CoroutineScope(Dispatchers.IO).launch { appStateRepository.updateSend(isChecked) } } googleMessageStateRepository.logs.observeForever { binding.tvLog.text = it binding.scroll.fullScroll(View.FOCUS_DOWN) } binding.btnReq.setOnClickListener { CoroutineScope(Dispatchers.IO).launch { requestNumber(reset = false, noBackup = true) } } binding.btnReset.setOnClickListener { binding.btnReset.isEnabled = false CoroutineScope(Dispatchers.IO).launch { reset() withContext(Dispatchers.Main) { binding.btnReset.isEnabled = true } } } binding.btnMore.setOnClickListener { showMenu(newContext, binding.btnMore, R.menu.more) } } private fun showMenu(context: Context, v: View, @MenuRes menuRes: Int) { val popup = PopupMenu(context, v) popup.menuInflater.inflate(menuRes, popup.menu) popup.setOnMenuItemClickListener { item -> binding.btnMore.isEnabled = false CoroutineScope(Dispatchers.IO).launch { when (item.itemId) { R.id.inspect -> { delay(1500) screenInspector.traverseNode(TraverseResult()) } R.id.check_availability -> { checkRcsAvailability() } R.id.toggle_on -> { screenController.toggleRcsSwitch(true) } R.id.toggle_off -> { screenController.toggleRcsSwitch(false) } R.id.clear_conv -> { clearConv() } R.id.store_numbers -> { storeNumbers() } R.id.change_profile -> { } R.id.restart_modifier -> { restartModifier() } R.id.reset_counter -> { CoroutineScope(Dispatchers.IO).launch { appStateRepository.resetExecutedNum() appStateRepository.resetSuccessNum() appStateRepository.resetRequestedNum() } } } withContext(Dispatchers.Main) { binding.btnMore.isEnabled = true } } true } popup.setOnDismissListener { // Respond to popup being dismissed. } // Show the popup menu. popup.show() } private fun reportDeviceStatues() { if (this::mSocket.isInitialized) { val data = JSONObject() try { data.put("action", "updateDevice") val dataObj = JSONObject() dataObj.put("canSend", appState.value.send) dataObj.put("busy", appState.value.busy) dataObj.put("currentCountry", Global.simInfo.country) data.put("data", dataObj) mSocket.emit("message", data) } catch (e: JSONException) { e.printStackTrace() } } } private suspend fun reset() { if (isOldVersion(this)) { withTimeout(1.hours) { while (true) { delay(100) withContext(Dispatchers.Main) { binding.tvLog.text = "Waiting for RCS switch on..." } googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED) Global.saveMock() 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( com.example.modifier.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...") continue } if (!screenController.toggleRcsSwitch(true)) { Log.e(com.example.modifier.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(com.example.modifier.TAG, "waitForRcsState: $resetSuccess") appStateRepository.resetRequestedNum() if (resetSuccess) { delay(3000) break } } } } else { withTimeout(1.hours) { while (true) { delay(100) withContext(Dispatchers.Main) { binding.tvLog.text = "Waiting for RCS switch on..." } googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED) Global.saveMock() 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( com.example.modifier.TAG, "RCS not entered default on state, retrying..." ) continue } } val switchOn = screenController.toggleRcsSwitch(true) if (!switchOn) { Log.e(com.example.modifier.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(com.example.modifier.TAG, "waitForRcsState: $resetSuccess") appStateRepository.resetRequestedNum() if (resetSuccess) { delay(3000) break } } } } } private suspend fun requestNumber( reset: Boolean = false, noBackup: Boolean = false, fresh: Boolean = false ) { val color = ContextCompat.getColorStateList(binding.root.context, R.color.btn_color) binding.btnReq.backgroundTintList = color if (appPreferences.value.preventRequest) { return } if (appState.value.requesting) { return } appStateRepository.updateRuntimeFlags(requesting = true) if (Global.simInfo.available == true) { backup( backupItemDao = backupItemDao, 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() if (isClashInstalled(applicationContext)) { val prefs = 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 = backupItemDao.findBackupForRestore( Global.simInfo.number, System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000 ) if (backup != null) { if (Global.restore(backup)) { requestSuccess = true break } else { backup(backupItemDao, "auto", 0) continue } } } if (needRest && !appPreferences.value.preventReset) { reset() retry = 0 needRest = false } googleMessageStateRepository.updateRcsState(RcsConfigureState.NOT_CONFIGURED) withContext(Dispatchers.Main) { binding.tvLog.text = "Requesting number..." } 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() Log.i(com.example.modifier.TAG, "requestNumber response: $rcsNumber") withContext(Dispatchers.Main) { binding.tvLog.text = "Requesting success, waiting for logs..." } Global.save( SimInfo( rcsNumber.number, rcsNumber.mcc, rcsNumber.mnc, Global.genICCID(rcsNumber.mnc, rcsNumber.areaCode), rcsNumber.mcc + rcsNumber.mnc + RandomStringUtils.randomNumeric( 15 - rcsNumber.mcc.length - rcsNumber.mnc.length ), Utils.generateIMEI(), rcsNumber.country, rcsNumber.areaCode, false, rcsNumber.carrierId, rcsNumber.carrierName, ) ) shellRun(CMD_MESSAGING_APP) withContext(Dispatchers.Main) { binding.tvLog.text = "Waiting for logs..." } if (rcsNumber.expiryTime.isBefore(LocalDateTime.now())) { Log.e(com.example.modifier.TAG, "RCS number expired, retrying...") continue } var sendOtpTimeout = ChronoUnit.SECONDS.between( LocalDateTime.now(), rcsNumber.expiryTime ).seconds if (sendOtpTimeout < 60.seconds) { Log.e(com.example.modifier.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( com.example.modifier.TAG, "REPLAY_REQUEST detected, may reset after 3 retry ($retry)" ) retry++ } Log.e( com.example.modifier.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(com.example.modifier.TAG, "Send OtpState Error: ${e.message}", e) } } if (rcsNumber.expiryTime.isBefore(LocalDateTime.now())) { Log.e(com.example.modifier.TAG, "RCS number expired, retrying...") continue } withTimeoutOrNull(60.seconds) { while (true) { try { rcsNumber = ktorClient(appPreferences.value.server).get(RcsNumberApi.Id(id = rcsNumber.id)) .body() Log.i(com.example.modifier.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, "wait for otp Error: ${exception.stackTrace}" ) } delay(2.seconds) } } if (rcsNumber.status != RcsNumberResponse.STATUS_SUCCESS) { Log.e(com.example.modifier.TAG, "OTP not received, retrying...") continue } val match = Regex("Your Messenger verification code is G-(\\d{6})") .find(rcsNumber.message!!) if (match != null) { val otp = match.groupValues[1] Log.i(com.example.modifier.TAG, "OTP: $otp") val sender = "3538" val msg = "Your Messenger verification code is G-$otp" val configured = run configuring@{ repeat(2) { Global.sendSmsIntent(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( com.example.modifier.TAG, "verifyOtp fail, retrying..." ) } } } false } if (!configured) { Log.e(com.example.modifier.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( com.example.modifier.TAG, "Send ConfiguredState Error: ${e.message}", e ) } } requestSuccess = true break } } } catch (e: Exception) { Log.e(com.example.modifier.TAG, "requestNumberError: ${e.message}", e) } } } if (requestSuccess) { Global.simInfo.available = true Global.save() appStateRepository.resetSuccessNum() appStateRepository.resetExecutedNum() Log.i(com.example.modifier.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") 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(requesting = false) } private suspend fun checkRcsConnectivity(): Boolean = run checkRcsConnection@{ repeat(3) { Log.i(com.example.modifier.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(com.example.modifier.TAG, "RCS is connected") shellRun(CMD_BACK) return@checkRcsConnection true } else { Log.i(com.example.modifier.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() withTimeoutOrNull(60.seconds) { try { config = ktorClient(appPreferences.value.server).get( SysConfigApi.Id( SysConfigApi(), "check_availability_numbers" ) ) .body() Log.i(com.example.modifier.TAG, "sysConfig response: $config") checkRcsAvailabilityNumbers.addAll( config.value.split(",").map { it.trim() }) } catch (exception: Exception) { Log.e( com.example.modifier.TAG, "sysConfig Error: ${exception.message}", exception ) } } if (checkRcsAvailabilityNumbers.isEmpty()) { Log.e(com.example.modifier.TAG, "checkRcsAvailabilityNumbers is empty") return true } checkRcsAvailabilityNumbers.forEach { 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( com.example.modifier.TAG, "checkRcsAvailability: RCS not detected" ) } delay(200) } } if (s == true) { Log.i(com.example.modifier.TAG, "checkRcsAvailability: $it success") delay(1000) return@checkAvailability true } } false } appStateRepository.updateRuntimeFlags(checkingConnection = false) return availability } private suspend fun storeNumbers() { repeat(100) { requestNumber(reset = true, fresh = it > 0) delay(5000) } } }