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.utils.ADB import com.example.modifier.BuildConfig import com.example.modifier.R import com.example.modifier.TraverseResult import com.example.modifier.baseTag import com.example.modifier.data.AppDatabase import com.example.modifier.databinding.FloatingWindowBinding import com.example.modifier.enums.ReqState import com.example.modifier.repo.AppPrefsRepo import com.example.modifier.repo.AppStateRepo import com.example.modifier.repo.BackupRepository import com.example.modifier.repo.GmsgStateRepo import com.example.modifier.repo.SpoofedInfoRepo import com.example.modifier.utils.ROOT_ACCESS import com.example.modifier.utils.checkPif import com.example.modifier.utils.clearConv import com.example.modifier.utils.findFirstByDesc import com.example.modifier.utils.hasRootAccess 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.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.debounce 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() { @SuppressLint("StaticFieldLeak") companion object { private const val TAG = "$baseTag/Service" const val NAME: String = BuildConfig.APPLICATION_ID + ".service.ModifierService" var instance: ModifierService? = null } private lateinit var binding: FloatingWindowBinding private val backupItemDao by lazy { AppDatabase.getDatabase(this).itemDao() } private val appPrefsRepo = AppPrefsRepo.instance private val appStateRepo = AppStateRepo.instance private val gmsgStateRepo = GmsgStateRepo.instance private val screenInspector by lazy { ScreenInspector(this) } private val screenController by lazy { ScreenController(this, screenInspector) } private val spoofedInfoRepo = SpoofedInfoRepo.instance private val backupRepository by lazy { BackupRepository(this, backupItemDao) } private lateinit var socketClient: SocketClient lateinit var taskRunner: TaskRunner private lateinit var uiContext: Context override fun onAccessibilityEvent(event: AccessibilityEvent) { // Log.w(TAG, "AccessibilityEvent: type=${event.eventType} text=${event.text}") if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { if (event.text.any { it.contains("允许 USB 调试吗?") || it.contains("Allow USB debugging?") }) { CoroutineScope(Dispatchers.IO).launch { screenController.allowDebugging() } } } } override fun onInterrupt() { Log.e(TAG, "onInterrupt") } @OptIn(FlowPreview::class) @SuppressLint("ClickableViewAccessibility") override fun onServiceConnected() { super.onServiceConnected() CoroutineScope(Dispatchers.IO).launch { taskRunner = TaskRunner( this@ModifierService, screenInspector, screenController, appStateRepo, appPrefsRepo, spoofedInfoRepo, gmsgStateRepo, backupRepository, ) launch { appStateRepo.appState.debounce(200).collect { if (this@ModifierService::socketClient.isInitialized) { socketClient.reportDeviceStatues() } } } launch { appStateRepo.appState.collect { withContext(Dispatchers.Main) { binding.swSend.isChecked = it.send binding.btnReq.isEnabled = it.reqState == ReqState.NONE 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.reqState) { ReqState.NONE -> { binding.tvStatus.visibility = GONE binding.tvStatus.text = "" } ReqState.CLEAN -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "cleaning messages" } ReqState.RESET -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "resetting GMS" } ReqState.BACKUP -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "backing up" } ReqState.RESTORE -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "restoring" } ReqState.REQUEST -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "requesting number" } ReqState.OTP_1 -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "waiting for OTP sent" } ReqState.OTP_2 -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "waiting for OTP received" } ReqState.CONFIG -> { binding.tvStatus.visibility = VISIBLE binding.tvStatus.text = "waiting for configuration" } } } } } launch { appPrefsRepo.appPrefs.collect { withContext(Dispatchers.Main) { binding.swSend.text = it.name } if (this@ModifierService::socketClient.isInitialized) { socketClient.disconnect() } socketClient = SocketClient(taskRunner) } } launch { spoofedInfoRepo.spoofedInfo.collect { withContext(Dispatchers.Main) { binding.tvCountry.text = it.country } } } launch { gmsgStateRepo.logs.debounce(200).collect { withContext(Dispatchers.Main) { binding.tvLog.text = it delay(100) binding.scroll.fullScroll(View.FOCUS_DOWN) } } } appStateRepo.updateRuntimeFlags(preparing = true) if (hasRootAccess()) { ROOT_ACCESS = true } else { if (!ADB.connect()) { binding.tvLog.text = "ADB connection failed" return@launch } } launch { gmsgStateRepo.startLogging() } delay(15.seconds) appPrefsRepo.updateId() optimize() syncTime() if (ROOT_ACCESS) { checkPif() if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) { killPhoneProcess(force = false) delay(15.seconds) } } appStateRepo.updateRuntimeFlags(preparing = false) val timer = Timer() timer.schedule(object : TimerTask() { override fun run() { CoroutineScope(Dispatchers.IO).launch { try { syncTime() if (Build.MODEL.startsWith("SM-F707") || Build.MODEL.startsWith("SM-F711")) { setBatteryLevel(100) } } catch (e: Exception) { e.printStackTrace() } } } }, 0, 5.minutes.inWholeMilliseconds) } 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 uiContext = DynamicColors.wrapContextIfAvailable(applicationContext, R.style.AppTheme) val inflater = LayoutInflater.from(uiContext) 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 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 } binding.swSend.setOnTouchListener(touchListener) binding.swSend.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> CoroutineScope(Dispatchers.IO).launch { appStateRepo.updateSend(isChecked) } } binding.btnReq.setOnClickListener { CoroutineScope(Dispatchers.IO).launch { taskRunner.requestNumberOnTask(reset = true, noBackup = true) } } binding.btnReset.setOnClickListener { if (appStateRepo.appState.value.reqState != ReqState.NONE) { return@setOnClickListener } binding.btnReset.isEnabled = false CoroutineScope(Dispatchers.IO).launch { taskRunner.reset() withContext(Dispatchers.Main) { binding.btnReset.isEnabled = true } } } binding.btnMore.setOnClickListener { showMenu(uiContext, 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.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 if (BuildConfig.DEBUG) { popup.menu.findItem(R.id.inspect).isVisible = true popup.menu.findItem(R.id.toggle_on).isVisible = true popup.menu.findItem(R.id.toggle_off).isVisible = true } popup.setOnMenuItemClickListener { item -> CoroutineScope(Dispatchers.IO).launch { when (item.itemId) { R.id.inspect -> { delay(1500) screenInspector.traverseNode(TraverseResult(), true) rootInActiveWindow.findFirstByDesc("wireless debugging") } R.id.check_availability -> { appStateRepo.updateRuntimeFlags(checkingConnection = true) taskRunner.checkRcsA10y() appStateRepo.updateRuntimeFlags(checkingConnection = false) } R.id.toggle_on -> { screenController.toggleRcsSwitch(true) } R.id.toggle_off -> { screenController.toggleRcsSwitch(false) } R.id.clear_conv -> { clearConv() appStateRepo.resetExecutedNum() } R.id.store_numbers -> { taskRunner.storeNumbers() } R.id.cancel_store_numbers -> { taskRunner.cancelStoreNumber() } R.id.change_profile -> { } R.id.restart_modifier -> { restartSelf() } R.id.reset_counter -> { CoroutineScope(Dispatchers.IO).launch { appStateRepo.resetExecutedNum() appStateRepo.resetSuccessNum() appStateRepo.resetRequestedNum() } } } } true } popup.setOnDismissListener { // Respond to popup being dismissed. } // Show the popup menu. popup.show() } }