package com.example.modifier.service import android.accessibilityservice.AccessibilityService import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat import android.os.Build 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.GONE import android.view.View.OnTouchListener import android.view.View.VISIBLE import android.view.WindowManager import android.view.accessibility.AccessibilityEvent 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.R import com.example.modifier.baseTag import com.example.modifier.TraverseResult import com.example.modifier.data.AppDatabase import com.example.modifier.data.AppPreferences import com.example.modifier.data.AppState import com.example.modifier.databinding.FloatingWindowBinding import com.example.modifier.enums.RequestNumberState import com.example.modifier.model.SpoofedSimInfo 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.clearConv import com.example.modifier.utils.hasRootAccess import com.example.modifier.utils.isRebooted import com.example.modifier.utils.killPhoneProcess import com.example.modifier.utils.optimize import com.example.modifier.utils.restartSelf import com.example.modifier.utils.setBatteryLevel import com.example.modifier.utils.syncTime import com.google.android.material.color.DynamicColors 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 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.minutes import kotlin.time.Duration.Companion.seconds @SuppressLint("SetTextI18n") class ModifierService : AccessibilityService() { private val tag = "$baseTag/AccessibilityService" companion object { const val NAME: String = BuildConfig.APPLICATION_ID + ".service.ModifierService" @JvmStatic var instance: ModifierService? = null private set } private lateinit var binding: FloatingWindowBinding private val backupItemDao by lazy { AppDatabase.getDatabase(this).itemDao() } private val 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 by lazy { GoogleMessageStateRepository(this) } private val screenInspector by lazy { ScreenInspector(this) } private val screenController by lazy { ScreenController(this, screenInspector) } private val spoofedSimInfoRepository by lazy { SpoofedSimInfoRepository(this) } private lateinit var spoofedSimInfo: StateFlow private val backupRepository by lazy { BackupRepository( this, backupItemDao, spoofedSimInfoRepository ) } private lateinit var socketClient: SocketClient private lateinit var taskRunner: TaskRunner override fun onAccessibilityEvent(event: AccessibilityEvent) {} override fun onInterrupt() { Log.e(tag, "onInterrupt") } @SuppressLint("ClickableViewAccessibility") override fun onServiceConnected() { super.onServiceConnected() Log.i(tag, "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(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.IO).launch { appPreferences = appPreferencesRepository.stateFlow() appState = appStateRepository.stateFlow() spoofedSimInfo = spoofedSimInfoRepository.stateFlow() taskRunner = TaskRunner( this@ModifierService, screenInspector, screenController, appStateRepository, appPreferencesRepository, spoofedSimInfoRepository, googleMessageStateRepository, backupRepository, ) 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.tvStatus.text = "" } RequestNumberState.RESET -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "Reset GMS" } RequestNumberState.BACKUP -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "backing up" } RequestNumberState.REQUEST -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "requesting number" } RequestNumberState.OTP_1 -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "waiting for OTP sent" } RequestNumberState.OTP_2 -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "waiting for OTP received" } RequestNumberState.CONFIG -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "waiting for configuration" } } } withContext(Dispatchers.IO) { reportDeviceStatues() } } } launch { appPreferences.collect { withContext(Dispatchers.Main) { binding.swSend.text = it.name } if (this@ModifierService::socketClient.isInitialized) { socketClient.disconnect() } socketClient = SocketClient( appPreferences.value.id, appPreferences.value.server, appPreferences.value.name, taskRunner ) } } launch { spoofedSimInfo.collect { withContext(Dispatchers.Main) { binding.tvCountry.text = it.country } } } appStateRepository.updateRuntimeFlags(preparing = true) val hasRoot = run checkRoot@{ repeat(30) { if (hasRootAccess()) { return@checkRoot true } delay(1000) } false } if (!hasRoot) { System.exit(0) } CoroutineScope(kotlin.coroutines.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) 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) } } 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 { taskRunner.requestNumber(reset = false, noBackup = true) } } binding.btnReset.setOnClickListener { binding.btnReset.isEnabled = false CoroutineScope(Dispatchers.IO).launch { taskRunner.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 -> { taskRunner.checkRcsAvailability() } R.id.toggle_on -> { screenController.toggleRcsSwitch(true) } R.id.toggle_off -> { screenController.toggleRcsSwitch(false) } R.id.clear_conv -> { clearConv() appStateRepository.resetExecutedNum() } R.id.store_numbers -> { storeNumbers() } R.id.change_profile -> { } R.id.restart_modifier -> { restartSelf() } 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::socketClient.isInitialized) { socketClient.reportDeviceStatues( appState.value.send, appState.value.busy, spoofedSimInfo.value.country ) } } private suspend fun storeNumbers() { // repeat(100) { // requestNumber(reset = true, fresh = it > 0) // delay(5000) // } } }