x1ongzhu 1 gadu atpakaļ
vecāks
revīzija
2bd7eb0678

+ 5 - 0
app/src/main/java/com/example/modifier/Global.kt

@@ -253,6 +253,11 @@ object Global {
     @JvmStatic
     fun clearConv() {
         val context = Utils.getContext()
+        if (context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+                .getBoolean("do_not_clean", false)
+        ) {
+            return
+        }
         try {
             val dataDir = ContextCompat.getDataDir(context)
             Utils.copyAssetFolder(context.assets, "bin", File(dataDir, "bin").path)

+ 8 - 40
app/src/main/java/com/example/modifier/TraverseResult.kt

@@ -1,48 +1,16 @@
-package com.example.modifier;
+package com.example.modifier
 
+import android.view.accessibility.AccessibilityNodeInfo
+import com.example.modifier.enums.RcsConnectionStatus
 
-import android.view.accessibility.AccessibilityNodeInfo;
 
-public class TraverseResult {
+class TraverseResult {
 
-    private boolean rcsCapable;
+    var isRcsCapable: Boolean = false
 
-    private AccessibilityNodeInfo sendBtn;
+    var sendBtn: AccessibilityNodeInfo? = null
 
-    private AccessibilityNodeInfo rcsSwitch;
+    var rcsSwitch: AccessibilityNodeInfo? = null
 
-    private boolean rcsConnected;
-
-
-    public boolean isRcsCapable() {
-        return rcsCapable;
-    }
-
-    public void setRcsCapable(boolean rcsCapable) {
-        this.rcsCapable = rcsCapable;
-    }
-
-    public AccessibilityNodeInfo getSendBtn() {
-        return sendBtn;
-    }
-
-    public void setSendBtn(AccessibilityNodeInfo sendBtn) {
-        this.sendBtn = sendBtn;
-    }
-
-    public AccessibilityNodeInfo getRcsSwitch() {
-        return rcsSwitch;
-    }
-
-    public void setRcsSwitch(AccessibilityNodeInfo rcsSwitch) {
-        this.rcsSwitch = rcsSwitch;
-    }
-
-    public boolean isRcsConnected() {
-        return rcsConnected;
-    }
-
-    public void setRcsConnected(boolean rcsConnected) {
-        this.rcsConnected = rcsConnected;
-    }
+    var rcsConnectionStatus = RcsConnectionStatus.UNKNOWN
 }

+ 7 - 0
app/src/main/java/com/example/modifier/enums/RcsConnectionStatus.kt

@@ -0,0 +1,7 @@
+package com.example.modifier.enums
+
+enum  class RcsConnectionStatus {
+    CONNECTED,
+    DISCONNECTED,
+    UNKNOWN
+}

+ 7 - 0
app/src/main/java/com/example/modifier/model/BaseAction.kt

@@ -0,0 +1,7 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+open class BaseAction(open val id: String,open val action: String) {
+}

+ 7 - 0
app/src/main/java/com/example/modifier/model/SocketCallback.kt

@@ -0,0 +1,7 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class SocketCallback<T>(val id: String, val status: Int, val data: T? = null, val error: String? = null) {
+}

+ 8 - 0
app/src/main/java/com/example/modifier/model/TaskAction.kt

@@ -0,0 +1,8 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class TaskAction(val id: String, val action: String, val data: TaskData) {
+
+}

+ 13 - 0
app/src/main/java/com/example/modifier/model/TaskConfig.kt

@@ -0,0 +1,13 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class TaskConfig(
+    val rcsWait: Long,
+    val rcsInterval: Long,
+    val cleanCount: Int,
+    val requestNumberInterval: Long,
+    val checkConnection: Boolean
+) {
+}

+ 7 - 0
app/src/main/java/com/example/modifier/model/TaskData.kt

@@ -0,0 +1,7 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class TaskData(val config: TaskConfig, val taskId: Int, val tasks: Array<TaskItem>) {
+}

+ 12 - 0
app/src/main/java/com/example/modifier/model/TaskExecutionResult.kt

@@ -0,0 +1,12 @@
+package com.example.modifier.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TaskExecutionResult(
+    val success: List<Int>,
+    val fail: List<Int>,
+    val retry: List<Int>? = null
+) {
+
+}

+ 20 - 0
app/src/main/java/com/example/modifier/model/TaskItem.kt

@@ -0,0 +1,20 @@
+package com.example.modifier.model
+
+import com.example.modifier.serializer.LocalDateTimeSerializer
+import kotlinx.serialization.Serializable
+import java.time.LocalDateTime
+import java.util.Date
+
+@Serializable
+class TaskItem(
+    val id: Int,
+    @Serializable(with = LocalDateTimeSerializer::class)
+    val createdAt: LocalDateTime,
+    val taskId: Int,
+    val number: String,
+    val message: String,
+    val status: String,
+    @Serializable(with = LocalDateTimeSerializer::class)
+    val sendAt: LocalDateTime
+) {
+}

+ 197 - 165
app/src/main/java/com/example/modifier/service/ModifierService.kt

@@ -34,8 +34,6 @@ import com.example.modifier.BuildConfig
 import com.example.modifier.CMD_BACK
 import com.example.modifier.CMD_CONVERSATION_LIST_ACTIVITY
 import com.example.modifier.CMD_HOME
-import com.example.modifier.CMD_MESSAGING_APP
-import com.example.modifier.CMD_MSG_SETTINGS_ACTIVITY
 import com.example.modifier.CMD_RCS_SETTINGS_ACTIVITY
 import com.example.modifier.Global
 import com.example.modifier.Global.load
@@ -45,12 +43,16 @@ import com.example.modifier.TraverseResult
 import com.example.modifier.Utils
 import com.example.modifier.databinding.FloatingWindowBinding
 import com.example.modifier.enums.RcsConfigureState
+import com.example.modifier.enums.RcsConnectionStatus
 import com.example.modifier.http.KtorClient
 import com.example.modifier.http.api.RcsNumberApi
 import com.example.modifier.http.api.SysConfigApi
 import com.example.modifier.http.request.RcsNumberRequest
 import com.example.modifier.http.response.RcsNumberResponse
 import com.example.modifier.http.response.SysConfigResponse
+import com.example.modifier.model.SocketCallback
+import com.example.modifier.model.TaskAction
+import com.example.modifier.model.TaskExecutionResult
 import com.example.modifier.model.TelephonyConfig
 import com.google.android.material.color.DynamicColors
 import io.ktor.client.call.body
@@ -71,16 +73,13 @@ import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
 import org.apache.commons.collections4.queue.CircularFifoQueue
 import org.apache.commons.lang3.RandomStringUtils
-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.coroutines.resume
 import kotlin.math.max
@@ -105,7 +104,6 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
     }
 
     private val handler = Handler(Looper.getMainLooper())
-    private val mExecutor: ScheduledExecutorService = ScheduledThreadPoolExecutor(8)
 
     private val mSocketOpts = IO.Options()
     private lateinit var mSocket: Socket
@@ -127,9 +125,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
     private var cleanCount = 0
     private var lastSend = 0L
     private var rcsInterval = 0L
-    private var requestNumberInterval = 0
+    private var requestNumberInterval = 0L
     private val running = MutableLiveData(false)
     private val requesting = MutableLiveData(false)
+    private val preparing = MutableLiveData(false)
+    private val checkingConnection = MutableLiveData(false)
     private var currentTaskId = 0
 
     private var busy = MediatorLiveData<Boolean>().apply {
@@ -139,7 +139,14 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         addSource(requesting) {
             value = it || running.value!!
         }
-        value = requesting.value!! || running.value!!
+        addSource(preparing) {
+            value = it || running.value!!
+        }
+        addSource(checkingConnection) {
+            value = it || running.value!!
+        }
+        value =
+            requesting.value!! || running.value!! || preparing.value!! || checkingConnection.value!!
     }
 
     private val rcsConfigureState = MutableLiveData(RcsConfigureState.CONFIGURED)
@@ -286,69 +293,80 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                     if (data != null) {
                         val to = data.optString("to")
                         val body = data.optString("body")
-                        send(to, body, 2000)
+                        CoroutineScope(Dispatchers.IO).launch {
+                            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)
+                    val taskAction = Json.decodeFromString<TaskAction>(json.toString())
+
+                    CoroutineScope(Dispatchers.IO).launch {
+                        runTask(taskAction)
                     }
-                } else if ("changeNumber" == action) {
                 }
             }
         }
     }
 
-    private fun runTask(id: String, data: JSONObject) {
-        val config = data.optJSONObject("config")
-        config!!
-        val rcsWait = config.optLong("rcsWait", 2000)
-        cleanCount = config.optInt("cleanCount", 20)
-        rcsInterval = config.optLong("rcsInterval", 0)
-        requestNumberInterval = config.optInt("requestNumberInterval", 0)
-        val tasks = data.optJSONArray("tasks")!!
-        currentTaskId = data.optInt("taskId", 0)
-        mExecutor.submit {
-            running.postValue(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)
+    private suspend fun runTask(taskAction: TaskAction) {
+        val rcsWait = taskAction.data.config.rcsWait
+        cleanCount = taskAction.data.config.cleanCount
+        rcsInterval = taskAction.data.config.rcsInterval
+        requestNumberInterval = taskAction.data.config.requestNumberInterval
+        currentTaskId = taskAction.data.taskId
+
+        if (taskAction.data.config.checkConnection && !checkRcsAvailability()) {
+            mSocket.emit(
+                "callback",
+                JSONObject(
+                    Json.encodeToString(
+                        SocketCallback<String>(
+                            id = taskAction.id,
+                            status = -1,
+                            error = "RCS not available"
+                        )
+                    )
+                )
+            )
+            requestNumber()
+            return
+        }
+
+        running.postValue(true)
+        val success = ArrayList<Int>()
+        val fail = ArrayList<Int>()
+        for (i in 0 until taskAction.data.tasks.size) {
+            val taskItem = taskAction.data.tasks[i]
+            try {
+                if (send(taskItem.number, taskItem.message, rcsWait)) {
+                    success.add(taskItem.id)
+                } else {
+                    fail.add(taskItem.id)
                 }
+            } catch (e: Exception) {
+                Log.e(TAG, "runTaskError: ${e.message}", e)
+                fail.add(taskItem.id)
             }
-            val res = JSONObject()
-            try {
-                res.put("id", id)
-                res.put("status", 0)
-                res.put(
-                    "data", JSONObject()
-                        .put("success", success)
-                        .put("fail", fail)
+        }
+
+        mSocket.emit(
+            "callback",
+            JSONObject(
+                Json.encodeToString(
+                    SocketCallback(
+                        id = taskAction.id,
+                        status = 0,
+                        data = TaskExecutionResult(success, fail)
+                    )
                 )
-            } catch (e: JSONException) {
-            }
-            mSocket.emit("callback", res)
-            if (requestNumberInterval in 1..sendCount) {
-                runBlocking {
-                    requestNumber()
-                }
+            )
+        )
+        if (requestNumberInterval in 1..sendCount) {
+            runBlocking {
+                requestNumber()
             }
-            running.postValue(false)
         }
+        running.postValue(false)
     }
 
     private fun smsIntent(to: String, body: String): Intent {
@@ -357,66 +375,57 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         intent.putExtra("sms_body", body)
         intent.putExtra("exit_on_sent", true)
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        intent.setPackage("com.google.android.apps.messaging")
         return intent
     }
 
-    private fun send(to: String, body: String, rcsWait: Long): Boolean {
+    private suspend fun send(to: String, body: String, rcsWait: Long): Boolean {
         Log.i(TAG, "Sending SMS to $to: $body")
         startActivity(smsIntent(to, body))
         try {
             Log.i(TAG, "Command executed successfully, waiting for app to open...")
-            val f = mExecutor.schedule<Boolean>(
-                {
-                    var success = false
-                    val ts = System.currentTimeMillis()
-                    while (System.currentTimeMillis() - ts < rcsWait) {
-                        val root = rootInActiveWindow
-                        val traverseResult = TraverseResult()
-                        traverseNode(root, traverseResult)
-                        if (traverseResult.isRcsCapable) {
-                            if (traverseResult.sendBtn == null) {
-                                Log.i(TAG, "Send button not found")
-                            } else {
-                                Log.i(TAG, "Clicking send button")
+            delay(1000)
+            var success = false
+            withTimeoutOrNull(rcsWait) {
+                while (true) {
+                    val root = rootInActiveWindow
+                    val traverseResult = TraverseResult()
+                    traverseNode(root, traverseResult)
+                    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 (rcsInterval > 0 && dt < rcsInterval) {
-                                    Log.i(TAG, "Waiting for RCS interval")
-                                    Thread.sleep(rcsInterval - dt)
-                                }
-                                traverseResult.sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
-                                lastSend = System.currentTimeMillis()
-                                success = true
-                                sendCount++
-                                break
+                            val dt = System.currentTimeMillis() - lastSend
+                            if (rcsInterval > 0 && dt < rcsInterval) {
+                                Log.i(TAG, "Waiting for RCS interval")
+                                delay(rcsInterval - dt)
                             }
-                        } else {
-                            Log.i(TAG, "RCS not detected")
-                        }
-                        try {
-                            Thread.sleep(500)
-                        } catch (e: InterruptedException) {
-                            e.printStackTrace()
+                            traverseResult.sendBtn!!.performAction(AccessibilityNodeInfo.ACTION_CLICK)
+                            lastSend = System.currentTimeMillis()
+                            success = true
+                            sendCount++
+                            break
                         }
+                    } else {
+                        Log.i(TAG, "RCS not detected")
                     }
-                    counter++
-                    Log.i(
-                        TAG,
-                        "sendCount: $sendCount, Counter: $counter, cleanCount: $cleanCount, requestNumberInterval: $requestNumberInterval"
-                    )
-                    if (cleanCount in 1..counter) {
-                        counter = 0
-                        Thread.sleep(2000)
-                        Global.clearConv();
-                        Thread.sleep(2000)
-                    }
-                    success
-                }, 1000, TimeUnit.MILLISECONDS
+                    delay(200)
+                }
+            }
+            counter++
+            Log.i(
+                TAG,
+                "sendCount: $sendCount, Counter: $counter, cleanCount: $cleanCount, requestNumberInterval: $requestNumberInterval"
             )
-            synchronized(f) {
-                Log.i(TAG, "Waiting for task to complete...")
-                return f.get()
+            if (cleanCount in 1..counter) {
+                counter = 0
+                delay(2000)
+                Global.clearConv();
+                delay(2000)
             }
+            return success
         } catch (e: Exception) {
             e.printStackTrace()
         }
@@ -467,7 +476,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
 
         if ("com.google.android.apps.messaging:id/rcs_sim_status_status_text" == id) {
             if (text.lowercase().contains("connected") || text.lowercase().contains("已连接")) {
-                result.isRcsConnected = true
+                result.rcsConnectionStatus = RcsConnectionStatus.CONNECTED
             }
         }
 
@@ -585,6 +594,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
             }
         }
         binding.btnInspect.setOnClickListener {
+            CoroutineScope(Dispatchers.IO).launch {
+                traverseNode(rootInActiveWindow, TraverseResult())
+            }
+        }
+        binding.btnCheck.setOnClickListener {
             CoroutineScope(Dispatchers.IO).launch {
                 checkRcsAvailability()
             }
@@ -643,6 +657,11 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
     }
 
     private suspend fun requestNumber() {
+        if (getSharedPreferences("settings", Context.MODE_PRIVATE)
+                .getBoolean("do_not_request", false)
+        ) {
+            return
+        }
         requestNumberCount++
         requesting.postValue(true)
         val result = withTimeoutOrNull(1.hours) {
@@ -730,7 +749,7 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
                                 continue
                             }
                             val rect = Rect()
-                            res.rcsSwitch.getBoundsInScreen(rect)
+                            res.rcsSwitch!!.getBoundsInScreen(rect)
                             Utils.runAsRoot(
                                 "input tap ${rect.centerX()} ${rect.centerY()}",
                                 "sleep 1",
@@ -843,72 +862,85 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
     }
 
     suspend fun checkRcsAvailability(): Boolean {
-        var rcsConnected = false
-        repeat(3) {
-            Utils.runAsRoot(
-                CMD_CONVERSATION_LIST_ACTIVITY,
-                "sleep 2",
-                CMD_RCS_SETTINGS_ACTIVITY,
-                "sleep 1",
-            )
-            val res = TraverseResult()
-            traverseNode(rootInActiveWindow, res)
-            if (res.isRcsConnected) {
-                rcsConnected = true
-                return@repeat
-            }
-            Utils.runAsRoot(CMD_HOME, "sleep 1")
-        }
-        if (!rcsConnected) {
-            return false
-        }
-
-        var config: SysConfigResponse
-        val checkRcsAvailabilityNumbers = mutableListOf<String>()
-        withTimeoutOrNull(60.seconds) {
-            while (true) {
-                try {
-                    config = KtorClient.get(
-                        SysConfigApi.Id(
-                            SysConfigApi(),
-                            "check_availability_numbers"
-                        )
+        checkingConnection.postValue(true)
+        val availability = run checkAvailability@{
+            val rcsConnected = run checkRcsConnection@{
+                repeat(3) {
+                    Log.i(TAG, "Checking RCS status...")
+                    Utils.runAsRoot(
+                        CMD_CONVERSATION_LIST_ACTIVITY,
+                        "sleep 1",
+                        CMD_RCS_SETTINGS_ACTIVITY,
+                        "sleep 1",
                     )
-                        .body<SysConfigResponse>()
-                    Log.i(TAG, "sysConfig response: $config")
-                    checkRcsAvailabilityNumbers.addAll(config.value.split(",").map { it.trim() })
-                    break
-                } catch (exception: Exception) {
-                    Log.e(TAG, "sysConfig Error: ${exception.message}", exception)
+                    val res = TraverseResult()
+                    traverseNode(rootInActiveWindow, res)
+                    if (res.rcsConnectionStatus == RcsConnectionStatus.CONNECTED) {
+                        Log.i(TAG, "RCS is connected")
+                        Utils.runAsRoot(CMD_BACK)
+                        return@checkRcsConnection true
+                    } else {
+                        Log.i(TAG, "RCS not connected, retrying...")
+                    }
+                    Utils.runAsRoot(CMD_BACK, "sleep ${it * 2}")
                 }
-                delay(1.seconds)
+                false
+            }
+            if (!rcsConnected) {
+                return@checkAvailability false
             }
-        }
 
-        checkRcsAvailabilityNumbers.forEach {
-            startActivity(smsIntent(it, ""))
-            val s = withTimeoutOrNull(3.seconds) {
+            var config: SysConfigResponse
+            val checkRcsAvailabilityNumbers = mutableListOf<String>()
+            withTimeoutOrNull(60.seconds) {
                 while (true) {
-                    val root = rootInActiveWindow
-                    val traverseResult = TraverseResult()
-                    traverseNode(root, traverseResult)
-                    if (traverseResult.isRcsCapable) {
-                        return@withTimeoutOrNull true
-                    } else {
-                        Log.i(TAG, "RCS not detected")
-                    }
                     try {
-                        delay(200)
-                    } catch (e: InterruptedException) {
-                        e.printStackTrace()
+                        config = KtorClient.get(
+                            SysConfigApi.Id(
+                                SysConfigApi(),
+                                "check_availability_numbers"
+                            )
+                        )
+                            .body<SysConfigResponse>()
+                        Log.i(TAG, "sysConfig response: $config")
+                        checkRcsAvailabilityNumbers.addAll(
+                            config.value.split(",").map { it.trim() })
+                        break
+                    } catch (exception: Exception) {
+                        Log.e(TAG, "sysConfig Error: ${exception.message}", exception)
                     }
+                    delay(1.seconds)
                 }
             }
-            if (s == true) {
-                Log.i(TAG, "checkRcsAvailability: $it success")
-                return true
+
+            checkRcsAvailabilityNumbers.forEach {
+                startActivity(smsIntent(it, ""))
+                val s = withTimeoutOrNull(3.seconds) {
+                    while (true) {
+                        val root = rootInActiveWindow
+                        val traverseResult = TraverseResult()
+                        traverseNode(root, traverseResult)
+                        if (traverseResult.isRcsCapable) {
+                            return@withTimeoutOrNull true
+                        } else {
+                            Log.i(TAG, "RCS not detected")
+                        }
+                        try {
+                            delay(200)
+                        } catch (e: InterruptedException) {
+                            e.printStackTrace()
+                        }
+                    }
+                }
+                if (s == true) {
+                    Log.i(TAG, "checkRcsAvailability: $it success")
+                    delay(1000)
+                    return@checkAvailability true
+                }
             }
+            false
         }
-        return false
+        checkingConnection.postValue(false)
+        return availability
     }
 }

+ 14 - 0
app/src/main/java/com/example/modifier/ui/settings/SettingsFragment.kt

@@ -1,6 +1,7 @@
 package com.example.modifier.ui.settings
 
 import android.annotation.SuppressLint
+import android.content.Context
 import android.content.DialogInterface
 import android.content.Intent
 import android.os.Bundle
@@ -12,6 +13,9 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.Toast
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStore
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.lifecycleScope
 import com.example.modifier.Global
@@ -193,6 +197,16 @@ class SettingsFragment : Fragment() {
             }
         }
 
+        val prefs = requireContext().getSharedPreferences("settings", Context.MODE_PRIVATE)
+        binding.switchClean.isChecked = prefs.getBoolean("do_not_clean", false)
+        binding.switchClean.setOnCheckedChangeListener { buttonView, isChecked ->
+            prefs.edit().putBoolean("do_not_clean", isChecked).apply()
+        }
+        binding.switchRequest.isChecked = prefs.getBoolean("do_not_request", false)
+        binding.switchRequest.setOnCheckedChangeListener { buttonView, isChecked ->
+            prefs.edit().putBoolean("do_not_request", isChecked).apply()
+        }
+
         executor.execute {
             load()
             handler.post {

+ 10 - 0
app/src/main/res/drawable/ic_ecg_heart.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480ZM480,840Q462,840 445.5,833.5Q429,827 416,814L148,545Q113,510 96.5,465Q80,420 80,371Q80,268 147,194Q214,120 314,120Q362,120 404.5,139Q447,158 480,192L480,192L480,192Q512,158 554.5,139Q597,120 645,120Q745,120 812.5,194Q880,268 880,370Q880,419 863,464Q846,509 812,544L543,814Q530,827 514,833.5Q498,840 480,840ZM520,320Q530,320 539,325Q548,330 553,338L621,440L787,440Q794,423 797.5,405.5Q801,388 801,370Q799,301 755,251.5Q711,202 645,202Q614,202 585.5,214Q557,226 536,249L509,278Q504,284 496,287.5Q488,291 480,291Q472,291 464,287.5Q456,284 450,278L423,249Q402,226 374,213Q346,200 314,200Q248,200 204,250.5Q160,301 160,370Q160,388 163,405.5Q166,423 173,440L360,440Q370,440 379,445Q388,450 393,458L428,510L482,348Q486,336 496.5,328Q507,320 520,320ZM532,450L478,612Q474,624 463,632Q452,640 439,640Q429,640 420,635Q411,630 406,622L338,520L236,520L473,757Q475,759 476.5,759.5Q478,760 480,760Q482,760 483.5,759.5Q485,759 487,757L723,520L600,520Q590,520 581,515Q572,510 566,502L532,450Z"/>
+</vector>

+ 11 - 0
app/src/main/res/drawable/ic_manage_search.xml

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal"
+    android:autoMirrored="true">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M80,760L80,680L480,680L480,760L80,760ZM80,560L80,480L280,480L280,560L80,560ZM80,360L80,280L280,280L280,360L80,360ZM824,760L670,606Q646,623 617.5,631.5Q589,640 560,640Q477,640 418.5,581.5Q360,523 360,440Q360,357 418.5,298.5Q477,240 560,240Q643,240 701.5,298.5Q760,357 760,440Q760,469 751.5,497.5Q743,526 726,550L880,704L824,760ZM560,560Q610,560 645,525Q680,490 680,440Q680,390 645,355Q610,320 560,320Q510,320 475,355Q440,390 440,440Q440,490 475,525Q510,560 560,560Z"/>
+</vector>

+ 10 - 0
app/src/main/res/drawable/ic_search.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M784,840L532,588Q502,612 463,626Q424,640 380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L840,784L784,840ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z"/>
+</vector>

+ 14 - 3
app/src/main/res/layout/floating_window.xml

@@ -80,12 +80,23 @@
 
                     <com.google.android.material.button.MaterialButton
                         android:id="@+id/btn_inspect"
+                        style="?attr/materialIconButtonOutlinedStyle"
                         android:layout_width="50dp"
-                        android:layout_height="30dp"
+                        android:layout_height="34dp"
+                        android:layout_marginLeft="8dp"
+                        android:padding="0dp"
+                        app:icon="@drawable/ic_manage_search"
+                        app:iconGravity="textStart"
+                        app:iconPadding="0dp" />
+
+                    <com.google.android.material.button.MaterialButton
+                        android:id="@+id/btn_check"
+                        style="?attr/materialIconButtonFilledTonalStyle"
+                        android:layout_width="50dp"
+                        android:layout_height="34dp"
                         android:layout_marginLeft="8dp"
                         android:padding="0dp"
-                        android:visibility="gone"
-                        app:icon="@drawable/ic_signal_cellular"
+                        app:icon="@drawable/ic_ecg_heart"
                         app:iconGravity="textStart"
                         app:iconPadding="0dp" />
                 </LinearLayout>

+ 31 - 0
app/src/main/res/layout/fragment_settings.xml

@@ -259,6 +259,37 @@
 
                     </LinearLayout>
                 </com.google.android.material.card.MaterialCardView>
+
+                <com.google.android.material.card.MaterialCardView
+                    style="@style/Widget.Material3.CardView.Filled"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="16dp"
+                    app:cardBackgroundColor="?attr/colorSurfaceContainer">
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical"
+                        android:padding="16dp">
+
+                        <com.google.android.material.materialswitch.MaterialSwitch
+                            android:id="@+id/switch_clean"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:text="DO NOT CLEAN" />
+
+                        <com.google.android.material.materialswitch.MaterialSwitch
+                            android:id="@+id/switch_request"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:text="DO NOT REQUEST" />
+                    </LinearLayout>
+
+                </com.google.android.material.card.MaterialCardView>
+
+
             </LinearLayout>
         </ScrollView>
     </androidx.constraintlayout.widget.ConstraintLayout>