package com.example.modifier import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.annotation.SuppressLint import android.content.Intent import android.graphics.PixelFormat import android.net.Uri 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.WindowManager import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.widget.CompoundButton import android.widget.FrameLayout import com.example.modifier.Global.load import com.example.modifier.databinding.FloatingWindowBinding import com.google.android.material.color.DynamicColors 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.GlobalScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.apache.commons.lang3.StringUtils import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.util.Optional import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import kotlin.math.max import kotlin.math.min class ModifierService : AccessibilityService(), Emitter.Listener { private val mExecutor: ScheduledExecutorService = ScheduledThreadPoolExecutor(8) private lateinit var mSocket: Socket private val mSocketOpts = IO.Options() private var canSend = false private lateinit var binding: FloatingWindowBinding private var counter = 0 private var _busy = false private var busy: Boolean get() { return _busy } set(value) { _busy = value updateDevice("busy", value) } fun connect() { CoroutineScope(Dispatchers.IO).launch { try { load() if (this@ModifierService::binding.isInitialized) { withContext(Dispatchers.Main) { binding.tvDeviceName.text = Global.name } } if (this@ModifierService::mSocket.isInitialized) { mSocket.disconnect() } canSend = getSharedPreferences( BuildConfig.APPLICATION_ID, MODE_PRIVATE ).getBoolean("canSend", false) mSocketOpts.query = "model=" + Build.MANUFACTURER + " " + Build.MODEL + "&name=" + Global.name + "&id=" + Utils.getUniqueID() + "&canSend=" + canSend mSocket = IO.socket(Global.serverUrl, mSocketOpts) mSocket.on("message", this@ModifierService) mSocket.on(Socket.EVENT_CONNECT) { Log.i(TAG, "Connected to server") } mSocket.on(Socket.EVENT_DISCONNECT) { Log.i(TAG, "Disconnected from server") } mSocket.on(Socket.EVENT_CONNECT_ERROR) { args -> Log.i(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(TAG, "Starting ModifierService") connect() } override fun onAccessibilityEvent(event: AccessibilityEvent) { // traverseNode(getRootInActiveWindow(), new TraverseResult()); } override fun onInterrupt() { } 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") if ("send" == action) { val data = json.optJSONObject("data") if (data != null) { val to = data.optString("to") val body = data.optString("body") send(to, body, 2000) } } else if ("task" == action) { val data = json.optJSONObject("data") val id = json.optString("id") if (data != null && StringUtils.isNoneBlank(id)) { runTask(id, data) } } else if ("changeNumber" == action) { } } } } private fun runTask(id: String, data: JSONObject) { val config = data.optJSONObject("config") val rcsWait = config.optLong("rcsWait", 2000) val tasks = data.optJSONArray("tasks") mExecutor.submit { busy = true val success = JSONArray() val fail = JSONArray() for (i in 0 until tasks.length()) { val task = tasks.optJSONObject(i) val to = task.optString("number") val body = task.optString("message") val taskId = task.optInt("id") try { if (send(to, body, rcsWait)) { success.put(taskId) } else { fail.put(taskId) } } catch (e: Exception) { e.printStackTrace() fail.put(taskId) } } val res = JSONObject() try { res.put("id", id) res.put("status", 0) res.put( "data", JSONObject() .put("success", success) .put("fail", fail) ) } catch (e: JSONException) { } mSocket.emit("callback", res) busy = false } } private fun send(to: String, body: String, rcsWait: Long): Boolean { Log.i(TAG, "Sending SMS to $to: $body") val intent = Intent(Intent.ACTION_SENDTO) intent.setData(Uri.parse("sms:$to")) intent.putExtra("sms_body", body) intent.putExtra("exit_on_sent", true) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) try { Log.i(TAG, "Command executed successfully, waiting for app to open...") val f = mExecutor.schedule( { val ts = System.currentTimeMillis() while (System.currentTimeMillis() - ts < rcsWait) { val root = rootInActiveWindow val packageName = root.packageName.toString() val result = TraverseResult() traverseNode(root, result) if (result.isRcsCapable) { if (result.sendBtn == null) { Log.i(TAG, "Send button not found") } else { Log.i(TAG, "Clicking send button") result.sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK) counter++ if (counter == 20) { Thread.sleep(2000) Global.clearConv(); Thread.sleep(2000) } return@schedule true } } else { Log.i(TAG, "RCS not detected") } try { Thread.sleep(500) } catch (e: InterruptedException) { e.printStackTrace() } } false }, 1000, TimeUnit.MILLISECONDS ) synchronized(f) { Log.i(TAG, "Waiting for task to complete...") return f.get() } } catch (e: Exception) { e.printStackTrace() } return false } private fun traverseNode(node: AccessibilityNodeInfo?, result: TraverseResult) { if (node == null) { return } val className = node.className.toString() val name = node.viewIdResourceName val text = Optional.ofNullable(node.text).map { obj: CharSequence -> obj.toString() } .orElse(null) val id = node.viewIdResourceName Log.d(TAG, "Node: class=$className, text=$text, name=$name, id=$id") if ("Compose:Draft:Send" == name) { result.sendBtn = node } if ("com.google.android.apps.messaging:id/send_message_button_icon" == id) { result.sendBtn = node } if (text != null && (text.contains("RCS 聊天") || text.contains("RCS chat"))) { result.isRcsCapable = true } if (node.childCount != 0) { for (i in 0 until node.childCount) { traverseNode(node.getChild(i), result) } } } @SuppressLint("ClickableViewAccessibility") override fun onServiceConnected() { super.onServiceConnected() instance = this val info = AccessibilityServiceInfo() info.flags = AccessibilityServiceInfo.DEFAULT or AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN info.notificationTimeout = 100 this.serviceInfo = info val displayMetrics = DisplayMetrics() val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager windowManager.defaultDisplay.getMetrics(displayMetrics) val height = displayMetrics.heightPixels val width = displayMetrics.widthPixels val maxX = width - Utils.dp2px(this, 108) val maxY = height - Utils.dp2px(this, 108) 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.tvDeviceName.text = Global.name windowManager.addView(mLayout, layoutParams) val downX = AtomicReference(0f) val downY = AtomicReference(0f) val downParamX = AtomicReference(0) val downParamY = AtomicReference(0) binding.floatingWindow.setOnTouchListener { v: View?, event: MotionEvent -> when (event.action) { MotionEvent.ACTION_DOWN -> { downX.set(event.rawX) downY.set(event.rawY) downParamX.set(layoutParams.x) downParamY.set(layoutParams.y) return@setOnTouchListener true } 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) return@setOnTouchListener true } } false } binding.swConnect.isChecked = true binding.swConnect.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> if (isChecked) { connect() } else { if (this::mSocket.isInitialized) { mSocket.disconnect() } } } canSend = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE).getBoolean( "canSend", false ) binding.swSend.isChecked = canSend binding.swSend.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE).edit() .putBoolean("canSend", isChecked).apply() canSend = isChecked updateDevice("canSend", canSend) } // PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); // PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "RCS:MyWakelockTag"); // wakeLock.acquire(); } private fun updateDevice(key: String, value: Any) { if (this::mSocket.isInitialized) { val data = JSONObject() try { data.put("action", "updateDevice") val dataObj = JSONObject() dataObj.put(key, value) data.put("data", dataObj) mSocket.emit("message", data) } catch (e: JSONException) { e.printStackTrace() } } } companion object { private const val TAG = "ModifierService" const val NAME: String = BuildConfig.APPLICATION_ID + ".ModifierService" @JvmStatic var instance: ModifierService? = null private set } }