x1ongzhu 1 gadu atpakaļ
vecāks
revīzija
c4568cdc21

+ 10 - 5
app/build.gradle

@@ -1,5 +1,6 @@
 plugins {
 plugins {
     alias(libs.plugins.androidApplication)
     alias(libs.plugins.androidApplication)
+    alias(libs.plugins.jetbrainsKotlinAndroid)
 }
 }
 
 
 android {
 android {
@@ -8,6 +9,7 @@ android {
     useLibrary 'org.apache.http.legacy'
     useLibrary 'org.apache.http.legacy'
     buildFeatures {
     buildFeatures {
         buildConfig = true
         buildConfig = true
+        viewBinding true
     }
     }
 
 
     defaultConfig {
     defaultConfig {
@@ -40,8 +42,8 @@ android {
         }
         }
     }
     }
     compileOptions {
     compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
     }
     }
     viewBinding {
     viewBinding {
         enabled = true
         enabled = true
@@ -56,6 +58,9 @@ dependencies {
     implementation libs.constraintlayout
     implementation libs.constraintlayout
     implementation libs.navigation.fragment
     implementation libs.navigation.fragment
     implementation libs.navigation.ui
     implementation libs.navigation.ui
+    implementation libs.annotation
+    implementation libs.lifecycle.livedata.ktx
+    implementation libs.lifecycle.viewmodel.ktx
     testImplementation libs.junit
     testImplementation libs.junit
     androidTestImplementation libs.ext.junit
     androidTestImplementation libs.ext.junit
     androidTestImplementation libs.espresso.core
     androidTestImplementation libs.espresso.core
@@ -66,10 +71,10 @@ dependencies {
     implementation(libs.socket.io.client) {
     implementation(libs.socket.io.client) {
         exclude group: 'org.json', module: 'json'
         exclude group: 'org.json', module: 'json'
     }
     }
-    implementation(libs.socket.io.client) {
-        exclude group: 'org.json', module: 'json'
-    }
     implementation 'com.android.volley:volley:1.2.1'
     implementation 'com.android.volley:volley:1.2.1'
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
     implementation "androidx.work:work-runtime:2.9.0"
     implementation "androidx.work:work-runtime:2.9.0"
+    implementation 'com.google.android.gms:play-services-code-scanner:16.1.0'
+    implementation libs.coroutines.core
+    implementation libs.coroutines.android
 }
 }

+ 21 - 6
app/src/main/AndroidManifest.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
+    xmlns:tools="http://schemas.android.com/tools" >
 
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
@@ -18,17 +18,32 @@
         android:supportsRtl="true"
         android:supportsRtl="true"
         android:theme="@style/AppTheme"
         android:theme="@style/AppTheme"
         android:usesCleartextTraffic="true"
         android:usesCleartextTraffic="true"
-        tools:targetApi="31">
+        tools:targetApi="31" >
+        <activity
+            android:name=".ui.LoginActivity"
+            android:exported="true" >
+
+        </activity>
+        <activity
+            android:name=".LoginActivity1"
+            android:exported="true"
+            android:launchMode="singleInstance" >
+        </activity>
+
+        <meta-data
+            android:name="com.google.mlkit.vision.DEPENDENCIES"
+            android:value="barcode_ui" />
+
         <service
         <service
             android:name=".ModifierService"
             android:name=".ModifierService"
             android:enabled="true"
             android:enabled="true"
             android:exported="true"
             android:exported="true"
             android:label="@string/accessibility_service_label"
             android:label="@string/accessibility_service_label"
-            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
-
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" >
             <meta-data
             <meta-data
                 android:name="android.accessibilityservice"
                 android:name="android.accessibilityservice"
                 android:resource="@xml/accessibility_service_config" />
                 android:resource="@xml/accessibility_service_config" />
+
             <intent-filter>
             <intent-filter>
                 <action android:name="android.accessibilityservice.AccessibilityService" />
                 <action android:name="android.accessibilityservice.AccessibilityService" />
             </intent-filter>
             </intent-filter>
@@ -38,12 +53,12 @@
             android:name=".MainActivity"
             android:name=".MainActivity"
             android:exported="true"
             android:exported="true"
             android:launchMode="singleInstance"
             android:launchMode="singleInstance"
-            android:windowSoftInputMode="adjustResize">
+            android:windowSoftInputMode="adjustResize" >
+
             <intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <action android:name="android.intent.action.MAIN" />
 
 
                 <category android:name="android.intent.category.LAUNCHER" />
                 <category android:name="android.intent.category.LAUNCHER" />
-                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             </intent-filter>
         </activity>
         </activity>
     </application>
     </application>

+ 1 - 1
app/src/main/java/com/example/modifier/BackupAdapter.java

@@ -47,7 +47,7 @@ public class BackupAdapter extends RecyclerView.Adapter<BackupAdapter.BackupView
         holder.binding.tvNumber.setText(backup.getConfig().getNumber());
         holder.binding.tvNumber.setText(backup.getConfig().getNumber());
         holder.binding.tvTime.setText(SimpleDateFormat.getDateTimeInstance().format(backup.getDate()));
         holder.binding.tvTime.setText(SimpleDateFormat.getDateTimeInstance().format(backup.getDate()));
         holder.binding.tvInfo.setText(String.format("MCC: %s, MNC: %s, Country: %s",
         holder.binding.tvInfo.setText(String.format("MCC: %s, MNC: %s, Country: %s",
-                backup.getConfig().getMcc(), backup.getConfig().getMnc(), backup.getConfig().getCountry()));
+                backup.getConfig().getMcc(), backup.getConfig().getMcc(), backup.getConfig().getMcc()));
         holder.binding.btnRestore.setOnClickListener(v -> {
         holder.binding.btnRestore.setOnClickListener(v -> {
             new MaterialAlertDialogBuilder(context)
             new MaterialAlertDialogBuilder(context)
                     .setTitle("Restore backup")
                     .setTitle("Restore backup")

+ 11 - 77
app/src/main/java/com/example/modifier/Config.kt

@@ -1,77 +1,11 @@
-package com.example.modifier;
-
-public class Config {
-    private String number;
-    private String mcc;
-    private String mnc;
-    private String iccid;
-    private String imsi;
-    private String imei;
-    private String country;
-
-    public Config(String number, String mcc, String mnc, String iccid, String imsi, String imei, String country) {
-        this.number = number;
-        this.mcc = mcc;
-        this.mnc = mnc;
-        this.iccid = iccid;
-        this.imsi = imsi;
-        this.imei = imei;
-        this.country = country;
-    }
-
-    public String getNumber() {
-        return number;
-    }
-
-    public void setNumber(String number) {
-        this.number = number;
-    }
-
-    public String getMcc() {
-        return mcc;
-    }
-
-    public void setMcc(String mcc) {
-        this.mcc = mcc;
-    }
-
-    public String getMnc() {
-        return mnc;
-    }
-
-    public void setMnc(String mnc) {
-        this.mnc = mnc;
-    }
-
-    public String getIccid() {
-        return iccid;
-    }
-
-    public void setIccid(String iccid) {
-        this.iccid = iccid;
-    }
-
-    public String getImsi() {
-        return imsi;
-    }
-
-    public void setImsi(String imsi) {
-        this.imsi = imsi;
-    }
-
-    public String getImei() {
-        return imei;
-    }
-
-    public void setImei(String imei) {
-        this.imei = imei;
-    }
-
-    public String getCountry() {
-        return country;
-    }
-
-    public void setCountry(String country) {
-        this.country = country;
-    }
-}
+package com.example.modifier
+
+class Config(
+    var number: String,
+    var mcc: String,
+    var mnc: String,
+    var iccid: String,
+    var imsi: String,
+    var imei: String,
+    var country: String
+)

+ 136 - 147
app/src/main/java/com/example/modifier/Global.kt

@@ -1,195 +1,184 @@
-package com.example.modifier;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.content.ContextCompat;
-
-import com.android.volley.RequestQueue;
-import com.android.volley.toolbox.JsonObjectRequest;
-import com.android.volley.toolbox.RequestFuture;
-import com.android.volley.toolbox.Volley;
-import com.google.gson.Gson;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.json.JSONObject;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-public class Global {
-
-    public static String serverUrl = "";
-    public static long rcsTimeout = 2000;
-    public static Config config = new Config("", "", "", "", "", "", "");
-    ExecutorService executor = Executors.newFixedThreadPool(8);
-
-
-    public static void load() {
-        Context context = Utils.getContext();
-        serverUrl = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE).getString("server", "http://192.168.6.215:3000");
-        RequestQueue queue = Volley.newRequestQueue(context);
-        RequestFuture<JSONObject> future = RequestFuture.newFuture();
-        JsonObjectRequest request = new JsonObjectRequest(serverUrl + "/api/sys-config/rcs_wait", future, future);
-        queue.add(request);
-        try {
-            JSONObject jsonObject = future.get(60, TimeUnit.SECONDS);
-            rcsTimeout = Long.parseLong(jsonObject.optString("value"));
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-
+package com.example.modifier
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import androidx.core.content.ContextCompat
+import com.google.gson.Gson
+import org.apache.commons.io.FileUtils
+import org.apache.commons.lang3.StringUtils
+import java.io.File
+import java.io.FileWriter
+
+object Global {
+    @JvmField
+    var serverUrl: String? = ""
+
+    @JvmField
+    var name: String? = ""
+
+    @JvmField
+    var config: Config = Config("", "", "", "", "", "", "")
+
+    @JvmStatic
+    fun load() {
+        val context = Utils.getContext()
+        val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
+        serverUrl = prefs.getString("server", "http://192.168.6.215:3000")
+        name = prefs.getString("name", Build.DEVICE)
         try {
         try {
-            File file = new File(ContextCompat.getDataDir(context), "config.json");
+            val file = File(ContextCompat.getDataDir(context), "config.json")
             if (file.exists()) {
             if (file.exists()) {
-                Gson gson = new Gson();
-                String json = FileUtils.readFileToString(file, "UTF-8");
-                config = gson.fromJson(json, Config.class);
+                val gson = Gson()
+                val json = FileUtils.readFileToString(file, "UTF-8")
+                config = gson.fromJson(json, Config::class.java)
             }
             }
-        } catch (Exception e) {
-            e.printStackTrace();
+        } catch (e: Exception) {
+            e.printStackTrace()
         }
         }
     }
     }
 
 
-    public static Set<String> getServers() {
-        Context context = Utils.getContext();
-        Set<String> defServers = new HashSet<>();
-        defServers.add("http://192.168.6.215:3000");
-        defServers.add("http://192.168.50.135:3000");
-        SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
-        return new HashSet<>(prefs.getStringSet("servers", defServers));
-    }
+    @JvmStatic
+    val servers: MutableSet<String>
+        get() {
+            val context = Utils.getContext()
+            val defServers: MutableSet<String> = HashSet()
+            defServers.add("http://192.168.6.215:3000")
+            defServers.add("http://192.168.50.135:3000")
+            defServers.add("https://rcs.izouma.com")
+            val prefs =
+                context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
+            return HashSet(prefs.getStringSet("servers", defServers))
+        }
 
 
-    public static void saveServer(String server) {
-        serverUrl = server;
-        Context context = Utils.getContext();
-        SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
-        Set<String> servers = getServers();
-        servers.add(server);
+    @JvmStatic
+    fun saveServer(server: String, name: String?) {
+        serverUrl = server
+        Global.name = name
+        val context = Utils.getContext()
+        val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
+        val servers = servers
+        servers.add(server)
         prefs.edit().putStringSet("servers", servers)
         prefs.edit().putStringSet("servers", servers)
-                .putString("server", server)
-                .apply();
+            .putString("server", server)
+            .putString("name", name)
+            .apply()
     }
     }
 
 
-    public static void save(Config config) {
-        Context context = Utils.getContext();
-        Global.config.setMcc(config.getMcc());
-        Global.config.setMnc(config.getMnc());
-        Global.config.setNumber(config.getNumber());
-        Global.config.setCountry(config.getCountry());
-        Global.config.setIccid(config.getIccid());
-        Global.config.setImei(config.getImei());
-        Global.config.setImsi(config.getImsi());
+    @JvmStatic
+    fun save(config: Config) {
+        val context = Utils.getContext()
+        Global.config.mcc = config.mcc
+        Global.config.mnc = config.mnc
+        Global.config.number = config.number
+        Global.config.country = config.country
+        Global.config.iccid = config.iccid
+        Global.config.imei = config.imei
+        Global.config.imsi = config.imsi
 
 
         try {
         try {
-            File file = new File(ContextCompat.getDataDir(context), "config.json");
-            Gson gson = new Gson();
-            String json = gson.toJson(config);
+            val file = File(ContextCompat.getDataDir(context), "config.json")
+            val gson = Gson()
+            val json = gson.toJson(config)
 
 
             try {
             try {
-                FileWriter writer = new FileWriter(file);
-                writer.write(json);
-                writer.close();
-            } catch (Exception e) {
-                e.printStackTrace();
+                val writer = FileWriter(file)
+                writer.write(json)
+                writer.close()
+            } catch (e: Exception) {
+                e.printStackTrace()
             }
             }
 
 
             Utils.runAsRoot(
             Utils.runAsRoot(
-                    "cp " + file.getPath() + " /data/data/com.android.phone/rcsConfig.json",
-                    "echo 'copied to phone'",
-                    "chmod 777 /data/data/com.android.phone/rcsConfig.json");
-        } catch (Exception e) {
-            e.printStackTrace();
+                "cp " + file.path + " /data/data/com.android.phone/rcsConfig.json",
+                "echo 'copied to phone'",
+                "chmod 777 /data/data/com.android.phone/rcsConfig.json"
+            )
+        } catch (e: Exception) {
+            e.printStackTrace()
         }
         }
     }
     }
 
 
-    public static void clear(boolean gsf, boolean gms, boolean sms) {
+    @JvmStatic
+    fun clear(gsf: Boolean, gms: Boolean, sms: Boolean) {
         try {
         try {
-
-            List<String> cmds = new ArrayList<>();
+            val cmds: MutableList<String> = ArrayList()
             if (gsf) {
             if (gsf) {
-                cmds.add("pm clear com.google.android.gsf");
-                cmds.add("echo 'cleared gsf'");
+                cmds.add("pm clear com.google.android.gsf")
+                cmds.add("echo 'cleared gsf'")
             }
             }
             if (gms) {
             if (gms) {
-                cmds.add("pm clear com.google.android.gms");
-                cmds.add("echo 'cleared gms'");
+                cmds.add("pm clear com.google.android.gms")
+                cmds.add("echo 'cleared gms'")
             }
             }
             if (sms) {
             if (sms) {
-                cmds.add("pm clear com.google.android.apps.messaging");
-                cmds.add("echo 'cleared sms'");
+                cmds.add("pm clear com.google.android.apps.messaging")
+                cmds.add("echo 'cleared sms'")
             }
             }
-            Utils.runAsRoot(cmds.toArray(new String[0]));
-        } catch (Exception e) {
-            e.printStackTrace();
+            Utils.runAsRoot(*cmds.toTypedArray<String>())
+        } catch (e: Exception) {
+            e.printStackTrace()
         }
         }
     }
     }
 
 
-    public static void stop(boolean gsf, boolean gms, boolean sms) {
+    @JvmStatic
+    fun stop(gsf: Boolean, gms: Boolean, sms: Boolean) {
         try {
         try {
-            List<String> cmds = new ArrayList<>();
+            val cmds: MutableList<String> = ArrayList()
             if (gsf) {
             if (gsf) {
-                cmds.add("am force-stop com.google.android.gsf");
-                cmds.add("echo 'stopped gsf'");
+                cmds.add("am force-stop com.google.android.gsf")
+                cmds.add("echo 'stopped gsf'")
             }
             }
             if (gms) {
             if (gms) {
-                cmds.add("am force-stop com.google.android.gms");
-                cmds.add("echo 'stopped gms'");
+                cmds.add("am force-stop com.google.android.gms")
+                cmds.add("echo 'stopped gms'")
             }
             }
             if (sms) {
             if (sms) {
-                cmds.add("am force-stop com.google.android.apps.messaging");
-                cmds.add("echo 'stopped sms'");
+                cmds.add("am force-stop com.google.android.apps.messaging")
+                cmds.add("echo 'stopped sms'")
             }
             }
-            Utils.runAsRoot(cmds.toArray(new String[0]));
-        } catch (Exception e) {
-            e.printStackTrace();
+            Utils.runAsRoot(*cmds.toTypedArray<String>())
+        } catch (e: Exception) {
+            e.printStackTrace()
         }
         }
     }
     }
 
 
-    public static void clearConv() {
-        Context context = Utils.getContext();
+    @JvmStatic
+    fun clearConv() {
+        val context = Utils.getContext()
         try {
         try {
-            File dataDir = ContextCompat.getDataDir(context);
-            Utils.copyAssetFolder(context.getAssets(), "bin", new File(dataDir, "bin").getPath());
-//                        Utils.runAsRoot("su -c \"\"");
-            Log.i("Modifier", "arch: " + StringUtils.join(Build.SUPPORTED_ABIS, ", "));
-
-            String arch = null;
-            for (String supportedAbi : Build.SUPPORTED_ABIS) {
-                if ("x86".equals(supportedAbi)) {
-                    arch = "x86";
-                } else if ("x86_64".equals(supportedAbi)) {
-                    arch = "x64";
-                } else if ("arm64-v8a".equals(supportedAbi)) {
-                    arch = "arm64";
-                } else if ("armeabi-v7a".equals(supportedAbi)) {
-                    arch = "arm";
+            val dataDir = ContextCompat.getDataDir(context)
+            Utils.copyAssetFolder(context.assets, "bin", File(dataDir, "bin").path)
+            //                        Utils.runAsRoot("su -c \"\"");
+            Log.i("Modifier", "arch: " + StringUtils.join(Build.SUPPORTED_ABIS, ", "))
+
+            var arch: String? = null
+            for (supportedAbi in Build.SUPPORTED_ABIS) {
+                if ("x86" == supportedAbi) {
+                    arch = "x86"
+                } else if ("x86_64" == supportedAbi) {
+                    arch = "x64"
+                } else if ("arm64-v8a" == supportedAbi) {
+                    arch = "arm64"
+                } else if ("armeabi-v7a" == supportedAbi) {
+                    arch = "arm"
                 }
                 }
                 if (StringUtils.isNoneBlank(arch)) {
                 if (StringUtils.isNoneBlank(arch)) {
-                    String binPath = new File(dataDir, "bin/sqlite3." + arch).getPath();
-                    Log.i("Modifier", "sqlite3 binPath: " + binPath);
-                    Utils.runAsRoot("chmod +x " + binPath,
-                            binPath + " /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM conversations;\"",
-                            binPath + " /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM messages;\"",
-                            "echo ok");
-                    Global.stop(false, false, true);
-                    Utils.runAsRoot("am start com.google.android.apps.messaging");
-                    break;
+                    val binPath = File(dataDir, "bin/sqlite3.$arch").path
+                    Log.i("Modifier", "sqlite3 binPath: $binPath")
+                    Utils.runAsRoot(
+                        "chmod +x $binPath",
+                        "$binPath /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM conversations;\"",
+                        "$binPath /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM messages;\"",
+                        "echo ok"
+                    )
+                    stop(false, false, true)
+                    Utils.runAsRoot("am start com.google.android.apps.messaging")
+                    break
                 }
                 }
             }
             }
-        } catch (Exception e) {
-            e.printStackTrace();
+        } catch (e: Exception) {
+            e.printStackTrace()
         }
         }
     }
     }
 }
 }

+ 51 - 0
app/src/main/java/com/example/modifier/LoginActivity1.java

@@ -0,0 +1,51 @@
+package com.example.modifier;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.activity.EdgeToEdge;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.modifier.databinding.ActivityLogin1Binding;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Optional;
+
+public class LoginActivity1 extends AppCompatActivity {
+
+    private ActivityLogin1Binding binding;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        EdgeToEdge.enable(this);
+        binding = ActivityLogin1Binding.inflate(getLayoutInflater());
+        setContentView(binding.getRoot());
+        binding.etServer.setSimpleItems(Global.getServers().toArray(new String[0]));
+        binding.btnLogin.setOnClickListener(v -> {
+            String server = Optional.ofNullable(binding.etServer.getText()).map(Object::toString).orElse("");
+            String name = Optional.ofNullable(binding.etName.getText()).map(Object::toString).orElse("");
+            String username = Optional.ofNullable(binding.etUsername.getText()).map(Object::toString).orElse("");
+            String password = Optional.ofNullable(binding.etPassword.getText()).map(Object::toString).orElse("");
+            if (StringUtils.isBlank(server)) {
+                binding.tlServer.setError("Server is required");
+            }
+            if (StringUtils.isBlank(name)) {
+                binding.tlName.setError("Name is required");
+            }
+            if (StringUtils.isBlank(username)) {
+                binding.tlUsername.setError("Username is required");
+            }
+            if (StringUtils.isBlank(password)) {
+                binding.tlPassword.setError("Password is required");
+            }
+            if (StringUtils.isNotBlank(server) && StringUtils.isNotBlank(name) && StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
+                Global.serverUrl = server;
+                Global.name = name;
+                Global.load();
+                binding.progressBar.setVisibility(View.VISIBLE);
+            }
+        });
+    }
+}

+ 50 - 85
app/src/main/java/com/example/modifier/MainActivity.kt

@@ -1,95 +1,60 @@
-package com.example.modifier;
-
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
-import android.text.InputFilter;
-import android.util.Log;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.ArrayAdapter;
-import android.widget.Toast;
-
-import androidx.activity.EdgeToEdge;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.navigation.NavController;
-import androidx.navigation.Navigation;
-import androidx.navigation.fragment.NavHostFragment;
-import androidx.navigation.ui.NavigationUI;
-
-import com.example.modifier.databinding.ActivityMainBinding;
-import com.google.android.material.navigation.NavigationBarView;
-import com.google.gson.Gson;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.RandomStringUtils;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.BufferedReader;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-public class MainActivity extends AppCompatActivity {
-    ActivityMainBinding mBinding;
-    ExecutorService executor = Executors.newFixedThreadPool(32);
-    Handler handler = new Handler(Looper.getMainLooper());
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        EdgeToEdge.enable(this);
-        mBinding = ActivityMainBinding.inflate(getLayoutInflater());
-        setContentView(mBinding.getRoot());
-
-        handler.postDelayed(() -> {
-            executor.execute(() -> {
+package com.example.modifier
+
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.NavigationUI.setupWithNavController
+import com.example.modifier.databinding.ActivityMainBinding
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class MainActivity : AppCompatActivity() {
+    var mBinding: ActivityMainBinding? = null
+    var executor: ExecutorService = Executors.newFixedThreadPool(32)
+    var handler: Handler = Handler(Looper.getMainLooper())
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        this.enableEdgeToEdge()
+        mBinding = ActivityMainBinding.inflate(layoutInflater)
+        setContentView(mBinding!!.root)
+
+        handler.postDelayed({
+            executor.execute {
                 if (Utils.hasRootAccess()) {
                 if (Utils.hasRootAccess()) {
                     if (!Utils.isAccessibilityEnabled()) {
                     if (!Utils.isAccessibilityEnabled()) {
                         if (!Utils.enableAccessibility()) {
                         if (!Utils.enableAccessibility()) {
-                            Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
-                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                            startActivity(intent);
-                            handler.postDelayed(this::finish, 1000);
+                            val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                            startActivity(intent)
+                            handler.postDelayed({ this.finish() }, 1000)
                         }
                         }
                     }
                     }
                 } else {
                 } else {
-                    handler.post(() -> {
-                        new AlertDialog.Builder(this)
-                                .setTitle("No Root Access")
-                                .setMessage("Root access is required to run this app")
-                                .setCancelable(false)
-                                .setPositiveButton("Exit", (dialog, which) -> {
-                                    finish();
-                                })
-                                .show();
-                    });
+                    handler.post {
+                        AlertDialog.Builder(this)
+                            .setTitle("No Root Access")
+                            .setMessage("Root access is required to run this app")
+                            .setCancelable(false)
+                            .setPositiveButton("Exit") { dialog: DialogInterface?, which: Int ->
+                                finish()
+                            }
+                            .show()
+                    }
                 }
                 }
-            });
-        }, 1000);
+            }
+        }, 1000)
 
 
-        NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
-        NavController controller = navHostFragment.getNavController();
-        NavigationUI.setupWithNavController(mBinding.nav, controller);
+        val navHostFragment =
+            supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
+        val controller = navHostFragment!!.navController
+        setupWithNavController(mBinding!!.nav, controller)
     }
     }
 }
 }

+ 299 - 283
app/src/main/java/com/example/modifier/ModifierService.kt

@@ -1,356 +1,372 @@
-package com.example.modifier;
-
-import android.accessibilityservice.AccessibilityServiceInfo;
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.PixelFormat;
-import android.net.Uri;
-import android.os.Build;
-import android.os.PowerManager;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-
-import com.example.modifier.databinding.FloatingWindowBinding;
-import com.google.android.material.color.DynamicColors;
-
-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.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-import io.socket.client.IO;
-import io.socket.client.Socket;
-import io.socket.emitter.Emitter;
-
-public class ModifierService extends android.accessibilityservice.AccessibilityService implements Emitter.Listener {
-
-    public interface JobListener {
-        void onSuccess();
-
-        void onFail();
-    }
-
-    private static final String TAG = "ModifierService";
-
-    public static final String NAME = BuildConfig.APPLICATION_ID + ".ModifierService";
-
-    private static ModifierService instance;
-
-    private ScheduledExecutorService mExecutor = new ScheduledThreadPoolExecutor(8);
-
-    private Socket mSocket;
-    private IO.Options mSocketOpts = new IO.Options();
-    private boolean canSend = false;
-    private FloatingWindowBinding binding;
-
-    public static ModifierService getInstance() {
-        return instance;
-    }
-
-    public void connect() {
-        mExecutor.submit(() -> {
-            Global.load();
-            if (mSocket != null) {
-                mSocket.disconnect();
-            }
+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.GlobalScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+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
+
+    fun connect() {
+        mExecutor.submit {
             try {
             try {
-                canSend = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE).getBoolean("canSend", false);
-                mSocketOpts.query = "model=" + Build.MANUFACTURER + " " + Build.MODEL + "&name=" + Build.DEVICE + "&id=" + Utils.getUniqueID() + "&canSend=" + canSend;
-                mSocket = IO.socket(Global.serverUrl, mSocketOpts);
-                mSocket.on("message", this);
-                mSocket.on(Socket.EVENT_CONNECT, args -> {
-                    Log.i(TAG, "Connected to server");
-                });
-                mSocket.on(Socket.EVENT_DISCONNECT, args -> {
-                    Log.i(TAG, "Disconnected from server");
-                });
-                mSocket.on(Socket.EVENT_CONNECT_ERROR, args -> {
-                    Log.i(TAG, "Connection error: " + args[0]);
-                    if (args[0] instanceof Exception) {
-                        Exception e = (Exception) args[0];
-                        e.printStackTrace();
+                load()
+                if (this::binding.isInitialized) {
+                    binding.tvDeviceName.text = Global.name
+                }
+                if (this::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)
+                mSocket.on(Socket.EVENT_CONNECT, Emitter.Listener { args: Array<Any?>? ->
+                    Log.i(TAG, "Connected to server")
+                })
+                mSocket.on(Socket.EVENT_DISCONNECT, Emitter.Listener { args: Array<Any?>? ->
+                    Log.i(TAG, "Disconnected from server")
+                })
+                mSocket.on(Socket.EVENT_CONNECT_ERROR, Emitter.Listener { args: Array<Any> ->
+                    Log.i(TAG, "Connection error: " + args[0])
+                    if (args[0] is Exception) {
+                        val e = args[0] as Exception
+                        e.printStackTrace()
                     }
                     }
-                });
+                })
 
 
-                mSocket.connect();
-            } catch (Exception e) {
-                e.printStackTrace();
+                mSocket.connect()
+            } catch (e: Exception) {
+                e.printStackTrace()
             }
             }
-        });
+        }
     }
     }
 
 
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        Log.i(TAG, "Starting ModifierService");
-        connect();
+    override fun onCreate() {
+        super.onCreate()
+        Log.i(TAG, "Starting ModifierService")
+        connect()
     }
     }
 
 
-    @Override
-    public void onAccessibilityEvent(AccessibilityEvent event) {
+    override fun onAccessibilityEvent(event: AccessibilityEvent) {
 //        traverseNode(getRootInActiveWindow(), new TraverseResult());
 //        traverseNode(getRootInActiveWindow(), new TraverseResult());
     }
     }
 
 
-    @Override
-    public void onInterrupt() {
-
+    override fun onInterrupt() {
     }
     }
 
 
-    @Override
-    public void call(Object... args) {
-        if (args.length > 0) {
-            Log.i(TAG, "Received message: " + args[0]);
-            if (args[0] instanceof JSONObject) {
-                JSONObject json = (JSONObject) args[0];
-                String action = json.optString("action");
-                if ("send".equals(action)) {
-                    JSONObject data = json.optJSONObject("data");
+    override fun call(vararg args: Any) {
+        if (args.size > 0) {
+            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) {
                     if (data != null) {
-                        String to = data.optString("to");
-                        String body = data.optString("body");
-                        send(to, body);
+                        val to = data.optString("to")
+                        val body = data.optString("body")
+                        send(to, body, 2000)
                     }
                     }
-                } else if ("task".equals(action)) {
-                    JSONArray data = json.optJSONArray("data");
-                    String id = json.optString("id");
+                } else if ("task" == action) {
+                    val data = json.optJSONObject("data")
+                    val id = json.optString("id")
                     if (data != null && StringUtils.isNoneBlank(id)) {
                     if (data != null && StringUtils.isNoneBlank(id)) {
-                        runTask(id, data);
+                        runTask(id, data)
                     }
                     }
-                } else if ("changeNumber".equals(action)) {
-
+                } else if ("changeNumber" == action) {
                 }
                 }
             }
             }
         }
         }
     }
     }
 
 
-    private void runTask(String id, JSONArray data) {
-        mExecutor.submit(() -> {
-            Global.load();
-            JSONArray success = new JSONArray();
-            JSONArray fail = new JSONArray();
-            for (int i = 0; i < data.length(); i++) {
-                JSONObject task = data.optJSONObject(i);
-                String to = task.optString("number");
-                String body = task.optString("message");
-                int taskId = task.optInt("id");
+    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 {
+            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 {
                 try {
-                    if (send(to, body)) {
-                        success.put(taskId);
+                    if (send(to, body, rcsWait)) {
+                        success.put(taskId)
                     } else {
                     } else {
-                        fail.put(taskId);
+                        fail.put(taskId)
                     }
                     }
-                } catch (Exception e) {
-                    e.printStackTrace();
-                    fail.put(taskId);
+                } catch (e: Exception) {
+                    e.printStackTrace()
+                    fail.put(taskId)
                 }
                 }
             }
             }
-            JSONObject res = new JSONObject();
+            val res = JSONObject()
             try {
             try {
-                res.put("id", id);
-                res.put("status", 0);
-                res.put("data", new JSONObject()
+                res.put("id", id)
+                res.put("status", 0)
+                res.put(
+                    "data", JSONObject()
                         .put("success", success)
                         .put("success", success)
-                        .put("fail", fail));
-            } catch (JSONException e) {
+                        .put("fail", fail)
+                )
+            } catch (e: JSONException) {
             }
             }
-            mSocket.emit("callback", res);
-            updateDevice("busy", false);
-        });
+            mSocket!!.emit("callback", res)
+            updateDevice("busy", false)
+        }
     }
     }
 
 
-    private boolean send(String to, String body) {
-        Log.i(TAG, "Sending SMS to " + to + ": " + body);
-        Intent intent = new 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);
+    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 {
         try {
-            Log.i(TAG, "Command executed successfully, waiting for app to open...");
-            ScheduledFuture<Boolean> f = mExecutor.schedule(() -> {
-                long ts = System.currentTimeMillis();
-                while (System.currentTimeMillis() - ts < Global.rcsTimeout) {
-                    AccessibilityNodeInfo root = getRootInActiveWindow();
-                    String packageName = root.getPackageName().toString();
-                    TraverseResult result = new TraverseResult();
-                    traverseNode(root, result);
-                    if (result.isRcsCapable()) {
-                        if (result.getSendBtn() == null) {
-                            Log.i(TAG, "Send button not found");
+            Log.i(TAG, "Command executed successfully, waiting for app to open...")
+            val f = mExecutor.schedule<Boolean>(
+                {
+                    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)
+                                return@schedule true
+                            }
                         } else {
                         } else {
-                            Log.i(TAG, "Clicking send button");
-                            result.getSendBtn().performAction(AccessibilityNodeInfo.ACTION_CLICK);
-                            return true;
+                            Log.i(TAG, "RCS not detected")
+                        }
+                        try {
+                            Thread.sleep(500)
+                        } catch (e: InterruptedException) {
+                            e.printStackTrace()
                         }
                         }
-                    } else {
-                        Log.i(TAG, "RCS not detected");
-                    }
-                    try {
-                        Thread.sleep(500);
-                    } catch (InterruptedException e) {
-                        e.printStackTrace();
                     }
                     }
-                }
-                return false;
-            }, 1000, TimeUnit.MILLISECONDS);
-            synchronized (f) {
-                Log.i(TAG, "Waiting for task to complete...");
-                return f.get();
+                    false
+                }, 1000, TimeUnit.MILLISECONDS
+            )
+            synchronized(f) {
+                Log.i(TAG, "Waiting for task to complete...")
+                return f.get()
             }
             }
-        } catch (Exception e) {
-            e.printStackTrace();
+        } catch (e: Exception) {
+            e.printStackTrace()
         }
         }
-        return false;
+        return false
     }
     }
 
 
-    private void traverseNode(AccessibilityNodeInfo node, @NonNull TraverseResult result) {
+    private fun traverseNode(node: AccessibilityNodeInfo?, result: TraverseResult) {
         if (node == null) {
         if (node == null) {
-            return;
+            return
         }
         }
 
 
-        String className = node.getClassName().toString();
-        String name = node.getViewIdResourceName();
-        String text = Optional.ofNullable(node.getText()).map(CharSequence::toString).orElse(null);
-        String id = node.getViewIdResourceName();
+        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);
+        Log.d(TAG, "Node: class=$className, text=$text, name=$name, id=$id")
 
 
-        if ("Compose:Draft:Send".equals(name)) {
-            result.setSendBtn(node);
+        if ("Compose:Draft:Send" == name) {
+            result.sendBtn = node
         }
         }
 
 
-        if ("com.google.android.apps.messaging:id/send_message_button_icon".equals(id)) {
-            result.setSendBtn(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"))) {
         if (text != null && (text.contains("RCS 聊天") || text.contains("RCS chat"))) {
-            result.setRcsCapable(true);
+            result.isRcsCapable = true
         }
         }
 
 
-        if (node.getChildCount() != 0) {
-            for (int i = 0; i < node.getChildCount(); i++) {
-                traverseNode(node.getChild(i), result);
+        if (node.childCount != 0) {
+            for (i in 0 until node.childCount) {
+                traverseNode(node.getChild(i), result)
             }
             }
         }
         }
     }
     }
 
 
     @SuppressLint("ClickableViewAccessibility")
     @SuppressLint("ClickableViewAccessibility")
-    @Override
-    protected void onServiceConnected() {
-        super.onServiceConnected();
-
-        instance = this;
-
-        AccessibilityServiceInfo info = new AccessibilityServiceInfo();
-        info.flags = AccessibilityServiceInfo.DEFAULT | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
-        info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
-        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
-        info.notificationTimeout = 100;
-        this.setServiceInfo(info);
-
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
-        windowManager.getDefaultDisplay().getMetrics(displayMetrics);
-        int height = displayMetrics.heightPixels;
-        int width = displayMetrics.widthPixels;
-        int maxX = width - Utils.dp2px(this, 108);
-        int maxY = height - Utils.dp2px(this, 108);
-
-
-        FrameLayout mLayout = new FrameLayout(this);
-        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
-        layoutParams.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
-        layoutParams.format = PixelFormat.TRANSLUCENT;
-        layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-        layoutParams.flags |= 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 | Gravity.TOP;
-
-        Context newContext = DynamicColors.wrapContextIfAvailable(getApplicationContext(), R.style.AppTheme);
-        LayoutInflater inflater = LayoutInflater.from(newContext);
-        binding = FloatingWindowBinding.inflate(inflater, mLayout, true);
-        windowManager.addView(mLayout, layoutParams);
-
-        AtomicReference<Float> downX = new AtomicReference<>(0f);
-        AtomicReference<Float> downY = new AtomicReference<>(0f);
-        AtomicReference<Integer> downParamX = new AtomicReference<>(0);
-        AtomicReference<Integer> downParamY = new AtomicReference<>(0);
-
-        binding.floatingWindow.setOnTouchListener((v, event) -> {
-            switch (event.getAction()) {
-                case MotionEvent.ACTION_DOWN:
-                    downX.set(event.getRawX());
-                    downY.set(event.getRawY());
-                    downParamX.set(layoutParams.x);
-                    downParamY.set(layoutParams.y);
-                    return true;
-
-                case MotionEvent.ACTION_MOVE:
-                    layoutParams.x = (int) Math.min(Math.max(downParamX.get() + (event.getRawX() - downX.get()), 0), maxX);
-                    layoutParams.y = (int) Math.min(Math.max(downParamY.get() + (event.getRawY() - downY.get()), 0), maxY);
-                    windowManager.updateViewLayout(mLayout, layoutParams);
-                    return true;
+    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
+                }
             }
             }
-            return false;
-        });
+            false
+        }
 
 
 
 
-        binding.swConnect.setChecked(true);
-        binding.swConnect.setOnCheckedChangeListener((buttonView, isChecked) -> {
+        binding!!.swConnect.isChecked = true
+        binding!!.swConnect.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
             if (isChecked) {
             if (isChecked) {
-                connect();
+                connect()
             } else {
             } else {
-                mSocket.disconnect();
+                if (mSocket != null) {
+                    mSocket!!.disconnect()
+                }
             }
             }
-        });
-
-        canSend = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE).getBoolean("canSend", false);
-        binding.swSend.setChecked(canSend);
-        binding.swSend.setOnCheckedChangeListener((buttonView, isChecked) -> {
-            getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE).edit().putBoolean("canSend", isChecked).apply();
-            canSend = isChecked;
+        }
 
 
-            updateDevice("canSend", canSend);
-        });
+        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 pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 //        PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "RCS:MyWakelockTag");
 //        PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "RCS:MyWakelockTag");
 //        wakeLock.acquire();
 //        wakeLock.acquire();
     }
     }
 
 
-    private void updateDevice(String key, Object value) {
-        JSONObject data = new JSONObject();
+    private fun updateDevice(key: String, value: Any) {
+        val data = JSONObject()
         try {
         try {
-            data.put("action", "updateDevice");
-            JSONObject dataObj = new JSONObject();
-            dataObj.put(key, value);
-            data.put("data", dataObj);
-            mSocket.emit("message", data);
-        } catch (JSONException e) {
-            e.printStackTrace();
+            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
+    }
 }
 }

+ 3 - 4
app/src/main/java/com/example/modifier/Utils.java

@@ -9,9 +9,8 @@ import android.provider.Settings;
 import android.text.TextUtils;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Log;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager;
-import android.widget.Button;
 
 
-import androidx.appcompat.widget.AppCompatButton;
+import androidx.annotation.DrawableRes;
 
 
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.progressindicator.CircularProgressIndicatorSpec;
 import com.google.android.material.progressindicator.CircularProgressIndicatorSpec;
@@ -22,7 +21,6 @@ import org.apache.commons.lang3.StringUtils;
 
 
 import java.io.DataOutputStream;
 import java.io.DataOutputStream;
 import java.io.File;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStream;
@@ -202,11 +200,12 @@ public class Utils {
         return imei;
         return imei;
     }
     }
 
 
-    public static void addProgressIndicator(Context context, MaterialButton button) {
+    public static void makeLoadingButton(Context context, MaterialButton button) {
         CircularProgressIndicatorSpec spec = new CircularProgressIndicatorSpec(context, null, 0,
         CircularProgressIndicatorSpec spec = new CircularProgressIndicatorSpec(context, null, 0,
                 com.google.android.material.R.style.Widget_Material3_CircularProgressIndicator_ExtraSmall);
                 com.google.android.material.R.style.Widget_Material3_CircularProgressIndicator_ExtraSmall);
         IndeterminateDrawable progressIndicatorDrawable = IndeterminateDrawable.createCircularDrawable(context, spec);
         IndeterminateDrawable progressIndicatorDrawable = IndeterminateDrawable.createCircularDrawable(context, spec);
         button.setIcon(progressIndicatorDrawable);
         button.setIcon(progressIndicatorDrawable);
+        button.setEnabled(false);
     }
     }
 
 
     public static boolean copyAssetFolder(AssetManager assetManager,
     public static boolean copyAssetFolder(AssetManager assetManager,

+ 1 - 2
app/src/main/java/com/example/modifier/fragments/BackupFragment.java

@@ -1,6 +1,5 @@
 package com.example.modifier.fragments;
 package com.example.modifier.fragments;
 
 
-import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Looper;
@@ -65,7 +64,7 @@ public class BackupFragment extends Fragment {
         binding = FragmentBackupBinding.inflate(inflater, container, false);
         binding = FragmentBackupBinding.inflate(inflater, container, false);
         binding.fabBackup.setOnClickListener(v -> {
         binding.fabBackup.setOnClickListener(v -> {
 
 
-            Utils.addProgressIndicator(getContext(), binding.fabBackup);
+            Utils.makeLoadingButton(getContext(), binding.fabBackup);
             binding.fabBackup.setEnabled(false);
             binding.fabBackup.setEnabled(false);
 
 
             executor.submit(() -> {
             executor.submit(() -> {

+ 10 - 17
app/src/main/java/com/example/modifier/fragments/SettingsFragment.java

@@ -3,11 +3,6 @@ package com.example.modifier.fragments;
 import android.annotation.SuppressLint;
 import android.annotation.SuppressLint;
 import android.content.Intent;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-
 import android.os.Handler;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Looper;
 import android.util.Log;
 import android.util.Log;
@@ -16,13 +11,17 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup;
 import android.widget.Toast;
 import android.widget.Toast;
 
 
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
 import com.android.volley.Request;
 import com.android.volley.Request;
 import com.android.volley.RequestQueue;
 import com.android.volley.RequestQueue;
 import com.android.volley.toolbox.JsonObjectRequest;
 import com.android.volley.toolbox.JsonObjectRequest;
 import com.android.volley.toolbox.RequestFuture;
 import com.android.volley.toolbox.RequestFuture;
 import com.android.volley.toolbox.Volley;
 import com.android.volley.toolbox.Volley;
-import com.example.modifier.Global;
 import com.example.modifier.Config;
 import com.example.modifier.Config;
+import com.example.modifier.Global;
 import com.example.modifier.MainActivity;
 import com.example.modifier.MainActivity;
 import com.example.modifier.ModifierService;
 import com.example.modifier.ModifierService;
 import com.example.modifier.R;
 import com.example.modifier.R;
@@ -36,19 +35,12 @@ import org.apache.commons.lang3.StringUtils;
 import org.json.JSONObject;
 import org.json.JSONObject;
 
 
 import java.io.File;
 import java.io.File;
-import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.FileWriter;
-import java.io.IOException;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Objects;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 import java.util.regex.Matcher;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
@@ -103,7 +95,7 @@ public class SettingsFragment extends Fragment {
                 Toast.makeText(getContext(), "Server is required", Toast.LENGTH_SHORT).show();
                 Toast.makeText(getContext(), "Server is required", Toast.LENGTH_SHORT).show();
                 return;
                 return;
             }
             }
-            Global.saveServer(server);
+            Global.saveServer(server, binding.etName.getText().toString());
             binding.etServer.setSimpleItems(Global.getServers().toArray(new String[0]));
             binding.etServer.setSimpleItems(Global.getServers().toArray(new String[0]));
 
 
             ModifierService modifierService = ModifierService.getInstance();
             ModifierService modifierService = ModifierService.getInstance();
@@ -111,7 +103,7 @@ public class SettingsFragment extends Fragment {
                 modifierService.connect();
                 modifierService.connect();
             }
             }
 
 
-            Utils.addProgressIndicator(getContext(), binding.btnServer);
+            Utils.makeLoadingButton(getContext(), binding.btnServer);
             binding.btnServer.setEnabled(false);
             binding.btnServer.setEnabled(false);
             handler.postDelayed(() -> {
             handler.postDelayed(() -> {
                 binding.btnServer.setIconResource(R.drawable.ic_done);
                 binding.btnServer.setIconResource(R.drawable.ic_done);
@@ -126,7 +118,7 @@ public class SettingsFragment extends Fragment {
 
 
         binding.btnRequest.setOnClickListener(v -> {
         binding.btnRequest.setOnClickListener(v -> {
 
 
-            Utils.addProgressIndicator(getContext(), binding.btnRequest);
+            Utils.makeLoadingButton(getContext(), binding.btnRequest);
             binding.btnRequest.setEnabled(false);
             binding.btnRequest.setEnabled(false);
             executor.submit(() -> {
             executor.submit(() -> {
                 try {
                 try {
@@ -252,6 +244,7 @@ public class SettingsFragment extends Fragment {
             handler.post(() -> {
             handler.post(() -> {
                 binding.etServer.setText(Global.serverUrl);
                 binding.etServer.setText(Global.serverUrl);
                 binding.etServer.setSimpleItems(Global.getServers().toArray(new String[0]));
                 binding.etServer.setSimpleItems(Global.getServers().toArray(new String[0]));
+                binding.etName.setText(Global.name);
                 Config config = Global.config;
                 Config config = Global.config;
                 binding.etNumber.setText(config.getNumber());
                 binding.etNumber.setText(config.getNumber());
                 binding.etMcc.setText(config.getMcc());
                 binding.etMcc.setText(config.getMcc());
@@ -267,7 +260,7 @@ public class SettingsFragment extends Fragment {
     }
     }
 
 
     private void onSave() {
     private void onSave() {
-        Utils.addProgressIndicator(getContext(), binding.btnSave);
+        Utils.makeLoadingButton(getContext(), binding.btnSave);
         binding.btnSave.setEnabled(false);
         binding.btnSave.setEnabled(false);
         executor.execute(() -> {
         executor.execute(() -> {
             save();
             save();

+ 122 - 130
app/src/main/java/com/example/modifier/fragments/UtilsFragment.kt

@@ -1,140 +1,132 @@
-package com.example.modifier.fragments;
-
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-
-import com.example.modifier.Global;
-import com.example.modifier.R;
-import com.example.modifier.Utils;
-import com.example.modifier.databinding.FragmentUtilsBinding;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.File;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class UtilsFragment extends Fragment {
-
-    private FragmentUtilsBinding binding;
-
-    Handler handler = new Handler(Looper.getMainLooper());
-    ExecutorService executor = Executors.newFixedThreadPool(4);
-
-    public UtilsFragment() {
+package com.example.modifier.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.example.modifier.Global.clear
+import com.example.modifier.Global.clearConv
+import com.example.modifier.Global.stop
+import com.example.modifier.R
+import com.example.modifier.Utils
+import com.example.modifier.databinding.FragmentUtilsBinding
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class UtilsFragment : Fragment() {
+    private lateinit var binding: FragmentUtilsBinding
+
+    var handler: Handler = Handler(Looper.getMainLooper())
+    var executor: ExecutorService = Executors.newFixedThreadPool(4)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
     }
     }
 
 
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-    }
-
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
-                             Bundle savedInstanceState) {
-        if (binding != null) {
-            return binding.getRoot();
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        if (this::binding.isInitialized) {
+            return binding.root
         }
         }
-        binding = FragmentUtilsBinding.inflate(inflater, container, false);
-        binding.btnClear.setOnClickListener(v -> {
-            onClear();
-        });
-        binding.btnStop.setOnClickListener(v -> {
-            onStopClick();
-        });
-        binding.btnSend.setOnClickListener(v -> {
-            binding.btnSend.setEnabled(false);
-            Utils.addProgressIndicator(getContext(), binding.btnSend);
-            handler.post(() -> {
-                String otp = binding.etOtp.getText().toString();
-                Intent intent = new Intent();
-                intent.setAction("com.example.modifier.sms");
-                intent.putExtra("sender", "3538");
-                intent.putExtra("message", "Your Messenger verification code is G-" + otp);
-                getContext().sendBroadcast(intent);
-
-                binding.btnSend.setIconResource(R.drawable.ic_done);
-                binding.btnSend.setText("OK");
-                handler.postDelayed(() -> {
-                    binding.btnSend.setEnabled(true);
-                    binding.btnSend.setIcon(null);
-                    binding.btnSend.setText("Send");
-                }, 1500);
-            });
-        });
-        binding.btnClearConv.setOnClickListener(v -> {
-            binding.btnClearConv.setEnabled(false);
-            Utils.addProgressIndicator(getContext(), binding.btnClearConv);
-            handler.post(() -> {
-                executor.execute(() -> {
-                    Global.clearConv();
-                    handler.post(() -> {
-                        binding.btnClearConv.setIconResource(R.drawable.ic_done);
-                        binding.btnClearConv.setText("OK");
-                        handler.postDelayed(() -> {
-                            binding.btnClearConv.setEnabled(true);
-                            binding.btnClearConv.setIcon(null);
-                            binding.btnClearConv.setText("Clear Conversations");
-                        }, 1500);
-                    });
-                });
-            });
-
-        });
-        return binding.getRoot();
+        binding = FragmentUtilsBinding.inflate(inflater, container, false)
+        binding.btnClear.setOnClickListener { v: View? ->
+            onClear()
+        }
+        binding.btnStop.setOnClickListener { v: View? ->
+            onStopClick()
+        }
+        binding.btnSend.setOnClickListener { v: View? ->
+            Utils.makeLoadingButton(context, binding.btnSend)
+            CoroutineScope(Dispatchers.Main).launch {
+                delay(1000L)
+
+                val otp = binding.etOtp.text.toString()
+                val intent = Intent()
+                intent.setAction("com.example.modifier.sms")
+                intent.putExtra("sender", "3538")
+                intent.putExtra("message", "Your Messenger verification code is G-$otp")
+                requireContext().sendBroadcast(intent)
+
+                binding.btnSend.setIconResource(R.drawable.ic_done)
+                binding.btnSend.text = "OK"
+
+                delay(1500L)
+
+                binding.btnSend.isEnabled = true
+                binding.btnSend.icon = null
+                binding.btnSend.text = "Send"
+            }
+        }
+        binding.btnClearConv.setOnClickListener { v: View? ->
+            binding.btnClearConv.isEnabled = false
+            Utils.makeLoadingButton(context, binding.btnClearConv)
+            handler.post {
+                executor.execute {
+                    clearConv()
+                    handler.post {
+                        binding.btnClearConv.setIconResource(R.drawable.ic_done)
+                        binding.btnClearConv.text = "OK"
+                        handler.postDelayed({
+                            binding.btnClearConv.isEnabled = true
+                            binding.btnClearConv.icon = null
+                            binding.btnClearConv.text = "Clear Conversations"
+                        }, 1500)
+                    }
+                }
+            }
+        }
+        return binding.root
     }
     }
 
 
-    private void onClear() {
-        binding.btnClear.setEnabled(false);
-        Utils.addProgressIndicator(getContext(), binding.btnClear);
-        executor.execute(() -> {
-            boolean gsf = binding.cbGsf.isChecked();
-            boolean gms = binding.cbGms.isChecked();
-            boolean sms = binding.cbSms.isChecked();
-            Global.clear(gsf, gms, sms);
-            Global.stop(gsf, gms, sms);
-            handler.post(() -> {
-                binding.btnClear.setIconResource(R.drawable.ic_done);
-                binding.btnClear.setText("OK");
-                handler.postDelayed(() -> {
-                    binding.btnClear.setEnabled(true);
-                    binding.btnClear.setIcon(null);
-                    binding.btnClear.setText("Clear");
-                }, 1500);
-            });
-        });
+    private fun onClear() {
+        binding.btnClear.isEnabled = false
+        Utils.makeLoadingButton(context, binding.btnClear)
+        executor.execute {
+            val gsf = binding.cbGsf.isChecked
+            val gms = binding.cbGms.isChecked
+            val sms = binding.cbSms.isChecked
+            clear(gsf, gms, sms)
+            stop(gsf, gms, sms)
+            handler.post {
+                binding.btnClear.setIconResource(R.drawable.ic_done)
+                binding.btnClear.text = "OK"
+                handler.postDelayed({
+                    binding.btnClear.isEnabled = true
+                    binding.btnClear.icon = null
+                    binding.btnClear.text = "Clear"
+                }, 1500)
+            }
+        }
     }
     }
 
 
 
 
-    private void onStopClick() {
-        binding.btnStop.setEnabled(false);
-        Utils.addProgressIndicator(getContext(), binding.btnStop);
-        executor.execute(() -> {
-            boolean gsf = binding.cbGsf.isChecked();
-            boolean gms = binding.cbGms.isChecked();
-            boolean sms = binding.cbSms.isChecked();
-            Global.stop(gsf, gms, sms);
-            handler.post(() -> {
-                binding.btnStop.setIconResource(R.drawable.ic_done);
-                binding.btnStop.setText("OK");
-                handler.postDelayed(() -> {
-                    binding.btnStop.setEnabled(true);
-                    binding.btnStop.setIcon(null);
-                    binding.btnStop.setText("Stop");
-                }, 1500);
-            });
-        });
+    private fun onStopClick() {
+        binding.btnStop.isEnabled = false
+        Utils.makeLoadingButton(context, binding.btnStop)
+        executor.execute {
+            val gsf = binding.cbGsf.isChecked
+            val gms = binding.cbGms.isChecked
+            val sms = binding.cbSms.isChecked
+            stop(gsf, gms, sms)
+            handler.post {
+                binding.btnStop.setIconResource(R.drawable.ic_done)
+                binding.btnStop.text = "OK"
+                handler.postDelayed({
+                    binding.btnStop.isEnabled = true
+                    binding.btnStop.icon = null
+                    binding.btnStop.text = "Stop"
+                }, 1500)
+            }
+        }
     }
     }
 }
 }

+ 61 - 0
app/src/main/java/com/example/modifier/ui/LoginActivity.kt

@@ -0,0 +1,61 @@
+package com.example.modifier.ui
+
+import android.os.Bundle
+import android.view.View
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import com.example.modifier.Global
+import com.example.modifier.R
+import com.example.modifier.databinding.ActivityLoginBinding
+import org.apache.commons.lang3.StringUtils
+import java.util.Optional
+import java.util.function.Function
+
+class LoginActivity : AppCompatActivity() {
+    private lateinit var binding: ActivityLoginBinding
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enableEdgeToEdge()
+        binding = ActivityLoginBinding.inflate(layoutInflater)
+        val view = binding.root
+        setContentView(view)
+        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+            insets
+        }
+        binding.etServer.setSimpleItems(Global.servers.toTypedArray())
+
+        binding.btnLogin.setOnClickListener { v ->
+            val server = binding.etName.text?.toString()
+            val name = binding.etName.text?.toString()
+            val username = binding.etUsername.text?.toString()
+            val password = binding.etPassword.text?.toString()
+            if (StringUtils.isBlank(server)) {
+                binding.tlServer.error = "Server is required"
+            }
+            if (StringUtils.isBlank(name)) {
+                binding.tlName.error = "Name is required"
+            }
+            if (StringUtils.isBlank(username)) {
+                binding.tlUsername.error = "Username is required"
+            }
+            if (StringUtils.isBlank(password)) {
+                binding.tlPassword.error = "Password is required"
+            }
+            if (StringUtils.isNotBlank(server)
+                && StringUtils.isNotBlank(name)
+                && StringUtils.isNotBlank(username)
+                && StringUtils.isNotBlank(password)
+            ) {
+                Global.serverUrl = server
+                Global.name = name
+                Global.load()
+                binding.progressBar.visibility = View.VISIBLE
+            }
+        }
+
+    }
+}

+ 10 - 0
app/src/main/res/drawable/ic_qr_code.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="M120,440L120,120L440,120L440,440L120,440ZM200,360L360,360L360,200L200,200L200,360ZM120,840L120,520L440,520L440,840L120,840ZM200,760L360,760L360,600L200,600L200,760ZM520,440L520,120L840,120L840,440L520,440ZM600,360L760,360L760,200L600,200L600,360ZM760,840L760,760L840,760L840,840L760,840ZM520,600L520,520L600,520L600,600L520,600ZM600,680L600,600L680,600L680,680L600,680ZM520,760L520,680L600,680L600,760L520,760ZM600,840L600,760L680,760L680,840L600,840ZM680,760L680,680L760,680L760,760L680,760ZM680,600L680,520L760,520L760,600L680,600ZM760,680L760,600L840,600L840,680L760,680Z"/>
+</vector>

+ 119 - 0
app/src/main/res/layout/activity_login.xml

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".LoginActivity1">
+
+    <com.google.android.material.card.MaterialCardView
+        style="?attr/materialCardViewFilledStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        app:cardBackgroundColor="?attr/colorSurfaceContainer"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.progressindicator.LinearProgressIndicator
+                android:id="@+id/progress_bar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:indeterminate="true"
+                android:visibility="gone"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical"
+                android:padding="16dp">
+
+                <TextView
+                    android:id="@+id/tv_login"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="8dp"
+                    android:text="Login"
+                    android:textColor="?attr/colorPrimary"
+                    android:textSize="16sp"
+                    android:textStyle="bold" />
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_server"
+                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Server"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.MaterialAutoCompleteTextView
+                        android:id="@+id/et_server"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content" />
+
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_username"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Username"
+                    app:endIconDrawable="@drawable/ic_qr_code"
+                    app:endIconMode="custom"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/et_username"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lines="1" />
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_password"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Password"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/et_password"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:inputType="textPassword"
+                        android:lines="1" />
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_name"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Device Name"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/et_name"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lines="1" />
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.button.MaterialButton
+                    android:id="@+id/btn_login"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"
+                    android:text="Login" />
+            </LinearLayout>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.google.android.material.card.MaterialCardView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 119 - 0
app/src/main/res/layout/activity_login1.xml

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".LoginActivity1">
+
+    <com.google.android.material.card.MaterialCardView
+        style="?attr/materialCardViewFilledStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        app:cardBackgroundColor="?attr/colorSurfaceContainer"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.progressindicator.LinearProgressIndicator
+                android:id="@+id/progress_bar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:indeterminate="true"
+                android:visibility="gone"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical"
+                android:padding="16dp">
+
+                <TextView
+                    android:id="@+id/tv_login"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="8dp"
+                    android:text="Login"
+                    android:textColor="?attr/colorPrimary"
+                    android:textSize="16sp"
+                    android:textStyle="bold" />
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_server"
+                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Server"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.MaterialAutoCompleteTextView
+                        android:id="@+id/et_server"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content" />
+
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_username"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Username"
+                    app:endIconDrawable="@drawable/ic_qr_code"
+                    app:endIconMode="custom"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/et_username"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lines="1" />
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_password"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Password"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/et_password"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:inputType="textPassword"
+                        android:lines="1" />
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/tl_name"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:hint="Device Name"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/et_name"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lines="1" />
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.button.MaterialButton
+                    android:id="@+id/btn_login"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"
+                    android:text="Login" />
+            </LinearLayout>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.google.android.material.card.MaterialCardView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -39,10 +39,23 @@
                         android:textSize="14sp"
                         android:textSize="14sp"
                         android:textStyle="bold" />
                         android:textStyle="bold" />
 
 
+                    <com.google.android.material.textfield.TextInputLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:hint="Name">
+
+                        <com.google.android.material.textfield.TextInputEditText
+                            android:id="@+id/et_name"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:lines="1" />
+                    </com.google.android.material.textfield.TextInputLayout>
+
                     <com.google.android.material.textfield.TextInputLayout
                     <com.google.android.material.textfield.TextInputLayout
                         style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
                         style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
                         android:layout_width="match_parent"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:layout_height="wrap_content"
+                        android:layout_marginTop="16dp"
                         android:hint="Server">
                         android:hint="Server">
 
 
                         <com.google.android.material.textfield.MaterialAutoCompleteTextView
                         <com.google.android.material.textfield.MaterialAutoCompleteTextView

+ 3 - 0
app/src/main/res/values-land/dimens.xml

@@ -0,0 +1,3 @@
+<resources>
+    <dimen name="activity_horizontal_margin">48dp</dimen>
+</resources>

+ 3 - 0
app/src/main/res/values-w1240dp/dimens.xml

@@ -0,0 +1,3 @@
+<resources>
+    <dimen name="activity_horizontal_margin">200dp</dimen>
+</resources>

+ 3 - 0
app/src/main/res/values-w600dp/dimens.xml

@@ -0,0 +1,3 @@
+<resources>
+    <dimen name="activity_horizontal_margin">48dp</dimen>
+</resources>

+ 5 - 0
app/src/main/res/values/dimens.xml

@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>

+ 9 - 0
app/src/main/res/values/strings.xml

@@ -9,4 +9,13 @@
     <string name="settings">Settings</string>
     <string name="settings">Settings</string>
     <!-- TODO: Remove or change this placeholder text -->
     <!-- TODO: Remove or change this placeholder text -->
     <string name="hello_blank_fragment">Hello blank fragment</string>
     <string name="hello_blank_fragment">Hello blank fragment</string>
+    <string name="title_activity_login">LoginActivity</string>
+    <string name="prompt_email">Email</string>
+    <string name="prompt_password">Password</string>
+    <string name="action_sign_in">Sign in or register</string>
+    <string name="action_sign_in_short">Sign in</string>
+    <string name="welcome">"Welcome !"</string>
+    <string name="invalid_username">Not a valid username</string>
+    <string name="invalid_password">Password must be >5 characters</string>
+    <string name="login_failed">"Login failed"</string>
 </resources>
 </resources>

+ 1 - 0
build.gradle

@@ -1,4 +1,5 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 plugins {
 plugins {
 alias(libs.plugins.androidApplication) apply false
 alias(libs.plugins.androidApplication) apply false
+    alias(libs.plugins.jetbrainsKotlinAndroid) apply false
 }
 }

+ 11 - 0
gradle/libs.versions.toml

@@ -8,12 +8,17 @@ junit = "4.13.2"
 junitVersion = "1.1.5"
 junitVersion = "1.1.5"
 espressoCore = "3.5.1"
 espressoCore = "3.5.1"
 appcompat = "1.6.1"
 appcompat = "1.6.1"
+kotlinxCoroutines = "1.8.1"
 material = "1.11.0"
 material = "1.11.0"
 activity = "1.8.0"
 activity = "1.8.0"
 constraintlayout = "2.1.4"
 constraintlayout = "2.1.4"
 socketIoClient = "2.0.0"
 socketIoClient = "2.0.0"
 navigationFragment = "2.7.7"
 navigationFragment = "2.7.7"
 navigationUi = "2.7.7"
 navigationUi = "2.7.7"
+kotlin = "1.9.0"
+annotation = "1.7.1"
+lifecycleLivedataKtx = "2.7.0"
+lifecycleViewmodelKtx = "2.7.0"
 
 
 [libraries]
 [libraries]
 commons-collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections4" }
 commons-collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections4" }
@@ -24,13 +29,19 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
 ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
 ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
 espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
 espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
 appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
 appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
+coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
 material = { group = "com.google.android.material", name = "material", version.ref = "material" }
 material = { group = "com.google.android.material", name = "material", version.ref = "material" }
 activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
 activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
 constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
 constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
 socket-io-client = { module = "io.socket:socket.io-client", version.ref = "socketIoClient" }
 socket-io-client = { module = "io.socket:socket.io-client", version.ref = "socketIoClient" }
 navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
 navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
 navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
 navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
+annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
+lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
+lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
 
 
 [plugins]
 [plugins]
 androidApplication = { id = "com.android.application", version.ref = "agp" }
 androidApplication = { id = "com.android.application", version.ref = "agp" }
+jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }