package com.example.modifier.service 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.InstallApkAction import com.example.modifier.model.SocketCallback 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.serializer.Json 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.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 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.json.JSONObject import java.io.File import java.time.LocalDateTime import java.time.temporal.ChronoUnit import kotlin.time.Duration.Companion.days 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 private lateinit var appPreferences: StateFlow private lateinit var spoofedSimInfo: StateFlow init { CoroutineScope(Dispatchers.IO).launch { appState = appStateRepository.stateFlow() appPreferences = appPreferencesRepository.stateFlow() spoofedSimInfo = spoofedSimInfoRepository.stateFlow() } } 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 } 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, checkingConnection = false) 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(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) } } 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 } } } } } 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(2.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(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.days.inWholeMilliseconds ) 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() 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() 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( requesting = false, requestNumberState = RequestNumberState.IDLE, suspended = !requestSuccess ) } 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 { val availability = run checkAvailability@{ val rcsConnected = checkRcsConnectivity() if (!rcsConnected) { return@checkAvailability false } val checkRcsAvailabilityNumbers = mutableListOf() try { val config = ktorClient(appPreferences.value.server).get( SysConfigApi.Id( SysConfigApi(), "check_availability_numbers" ) ) .body() 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@checkAvailability 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 } return availability } suspend fun installApk( installApkAction: InstallApkAction, onSuccess: () -> Unit, onError: (Exception) -> Unit ) { 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(TAG, "Apk file saved to ${file.path}") shellRun("pm install -d -r ${file.path}") onSuccess() } catch (e: Exception) { Log.e("Modifier", "Failed to install apk", e) onError(e) } } }