x1ongzhu пре 1 година
родитељ
комит
cf07e8c933
22 измењених фајлова са 783 додато и 121 уклоњено
  1. 30 2
      app/build.gradle
  2. 100 0
      app/schemas/com.example.modifier.data.AppDatabase/1.json
  3. 3 1
      app/src/main/java/com/example/modifier/Global.kt
  4. 6 0
      app/src/main/java/com/example/modifier/MyApplication.kt
  5. 7 6
      app/src/main/java/com/example/modifier/adapter/BackupAdapter.kt
  6. 38 0
      app/src/main/java/com/example/modifier/data/AppContainer.kt
  7. 34 0
      app/src/main/java/com/example/modifier/data/AppDatabase.kt
  8. 57 0
      app/src/main/java/com/example/modifier/data/BackupItem.kt
  9. 27 0
      app/src/main/java/com/example/modifier/data/BackupItemDao.kt
  10. 30 0
      app/src/main/java/com/example/modifier/data/BackupItemsRepository.kt
  11. 16 0
      app/src/main/java/com/example/modifier/data/Converters.kt
  12. 15 0
      app/src/main/java/com/example/modifier/data/OfflineItemsRepository.kt
  13. 10 0
      app/src/main/java/com/example/modifier/service/ModifierService.kt
  14. 50 0
      app/src/main/java/com/example/modifier/ui/AppViewModelProvider.kt
  15. 27 106
      app/src/main/java/com/example/modifier/ui/backup/BackupFragment.kt
  16. 65 0
      app/src/main/java/com/example/modifier/ui/backup/BackupViewModel.kt
  17. 78 0
      app/src/main/java/com/example/modifier/ui/theme/Color.kt
  18. 27 0
      app/src/main/java/com/example/modifier/ui/theme/Shape.kt
  19. 131 0
      app/src/main/java/com/example/modifier/ui/theme/Theme.kt
  20. 0 1
      app/src/main/res/layout/floating_window.xml
  21. 32 4
      gradle/libs.versions.toml
  22. 0 1
      settings.gradle

+ 30 - 2
app/build.gradle

@@ -12,6 +12,10 @@ android {
     useLibrary 'org.apache.http.legacy'
     buildFeatures {
         buildConfig = true
+        compose = true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion = "1.5.14"
     }
     dataBinding {
         enabled = true
@@ -25,6 +29,12 @@ android {
         archivesBaseName = "modifier-${versionCode}"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments["room.schemaLocation"] =
+                        "$projectDir/schemas"
+            }
+        }
     }
 
     signingConfigs {
@@ -74,6 +84,7 @@ dependencies {
     implementation libs.activity
     implementation libs.constraintlayout
     implementation libs.navigation.fragment
+    implementation libs.navigation.fragment.ktx
     implementation libs.navigation.ui
     implementation libs.annotation
     implementation libs.datastore.preferences
@@ -106,12 +117,29 @@ dependencies {
     implementation libs.ktor.client.resources
     implementation libs.ktor.serialization.kotlinx.json
     implementation libs.kotlinx.serialization.json
+    implementation(libs.room.runtime)
+    kapt(libs.room.compiler)
+    implementation(libs.room.ktx)
+    implementation(libs.fragment.ktx)
+    implementation libs.savedstate
+
+    def composeBom = libs.androidx.compose.bom
+    implementation composeBom
+    androidTestImplementation composeBom
+    implementation libs.material3
+    implementation libs.ui.tooling.preview
+    debugImplementation libs.ui.tooling
+    implementation libs.androidx.material.icons.core
+    implementation libs.androidx.material.icons.extended
+    implementation libs.androidx.material3.window.size
+    implementation libs.androidx.activity.compose
+    implementation libs.lifecycle.viewmodel.compose
+    implementation libs.androidx.runtime.livedata
+    implementation(libs.navigation.compose)
 
-    implementation 'com.android.volley:volley:1.2.1'
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
     implementation 'com.google.android.gms:play-services-code-scanner:16.1.0'
     implementation("com.github.leandroborgesferreira:loading-button-android:2.3.0")
-    compileOnly 'de.robv.android.xposed:api:82'
     implementation 'com.google.code.gson:gson:2.10.1'
     implementation 'org.apache.commons:commons-lang3:3.14.0'
     implementation 'commons-io:commons-io:2.16.1'

+ 100 - 0
app/schemas/com.example.modifier.data.AppDatabase/1.json

@@ -0,0 +1,100 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "c4b2fc1aa20ac555446cdde24166c0a0",
+    "entities": [
+      {
+        "tableName": "BackupItem",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `createdAt` TEXT NOT NULL, `number` TEXT NOT NULL, `country` TEXT NOT NULL, `code` TEXT NOT NULL, `mcc` TEXT NOT NULL, `mnc` TEXT NOT NULL, `imei` TEXT NOT NULL, `imsi` TEXT NOT NULL, `iccid` TEXT NOT NULL, `sendCount` INTEGER NOT NULL, `path` TEXT NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "number",
+            "columnName": "number",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "country",
+            "columnName": "country",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "code",
+            "columnName": "code",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mcc",
+            "columnName": "mcc",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mnc",
+            "columnName": "mnc",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "imei",
+            "columnName": "imei",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "imsi",
+            "columnName": "imsi",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "iccid",
+            "columnName": "iccid",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sendCount",
+            "columnName": "sendCount",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "path",
+            "columnName": "path",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c4b2fc1aa20ac555446cdde24166c0a0')"
+    ]
+  }
+}

+ 3 - 1
app/src/main/java/com/example/modifier/Global.kt

@@ -522,7 +522,7 @@ object Global {
 
     @SuppressLint("SdCardPath")
     @JvmStatic
-    suspend fun backup() {
+    suspend fun backup():String {
         val context = Utils.getContext()
         val dest = File(
             ContextCompat.getExternalFilesDirs(context, "backup")[0],
@@ -568,6 +568,8 @@ object Global {
 
         cmds.addAll(packages.reversed().map { "pm unsuspend $it" })
         shellRun(*cmds.toTypedArray())
+
+        return dest.path
     }
 
     @JvmStatic

+ 6 - 0
app/src/main/java/com/example/modifier/MyApplication.kt

@@ -1,12 +1,18 @@
 package com.example.modifier
 
 import android.app.Application
+import com.example.modifier.data.AppContainer
+import com.example.modifier.data.AppDataContainer
 import dagger.hilt.android.HiltAndroidApp
 
 @HiltAndroidApp
 class MyApplication : Application() {
+
+    lateinit var container: AppContainer
+
     override fun onCreate() {
         super.onCreate()
         //        DynamicColors.applyToActivitiesIfAvailable(this);
+        container = AppDataContainer(this)
     }
 }

+ 7 - 6
app/src/main/java/com/example/modifier/adapter/BackupAdapter.kt

@@ -15,6 +15,7 @@ import com.example.modifier.Global.suspend
 import com.example.modifier.Global.unsuspend
 import com.example.modifier.Utils
 import com.example.modifier.adapter.BackupAdapter.BackupViewHolder
+import com.example.modifier.data.BackupItem
 import com.example.modifier.databinding.ItemBackupBinding
 import com.example.modifier.model.Backup
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -25,7 +26,7 @@ import kotlinx.coroutines.withContext
 import java.io.File
 import java.text.SimpleDateFormat
 
-class BackupAdapter(private val context: Context, private val backups: MutableList<Backup>) :
+class BackupAdapter(private val context: Context, private val backups: List<BackupItem>) :
     RecyclerView.Adapter<BackupViewHolder>() {
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupViewHolder {
         val binding = ItemBackupBinding.inflate(
@@ -39,11 +40,11 @@ class BackupAdapter(private val context: Context, private val backups: MutableLi
     @SuppressLint("DefaultLocale")
     override fun onBindViewHolder(holder: BackupViewHolder, position: Int) {
         val backup = backups[position]
-        holder.binding.tvNumber.text = "+" + backup.config.areaCode + " " + backup.config.number
-        holder.binding.tvTime.text = SimpleDateFormat.getDateTimeInstance().format(backup.date)
+        holder.binding.tvNumber.text = "+" + backup.code + " " + backup.number
+        holder.binding.tvTime.text = backup.createdAt.toString()
         holder.binding.tvInfo.text = String.format(
             "MCC: %s, MNC: %s, Country: %s",
-            backup.config.mcc, backup.config.mnc, backup.config.country
+            backup.mcc, backup.mnc, backup.country
         )
         holder.binding.btnRestore.setOnClickListener { v: View? ->
             MaterialAlertDialogBuilder(
@@ -56,7 +57,7 @@ class BackupAdapter(private val context: Context, private val backups: MutableLi
                     Utils.makeLoadingButton(context, holder.binding.btnRestore)
                     CoroutineScope(Dispatchers.IO).launch {
                         try {
-                            Global.restore(backup)
+//                            Global.restore(backup)
                         } catch (e: Exception) {
                             e.printStackTrace()
                         }
@@ -81,7 +82,7 @@ class BackupAdapter(private val context: Context, private val backups: MutableLi
                     if (file.exists()) {
                         try {
                             Utils.runAsRoot("rm -rf " + backup.path)
-                            backups.remove(backup)
+//                            backups.remove(backup)
                             notifyItemRemoved(position)
                         } catch (e: Exception) {
                             e.printStackTrace()

+ 38 - 0
app/src/main/java/com/example/modifier/data/AppContainer.kt

@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.modifier.data
+
+import android.content.Context
+
+/**
+ * App container for Dependency injection.
+ */
+interface AppContainer {
+    val itemsRepository: BackupItemsRepository
+}
+
+/**
+ * [AppContainer] implementation that provides instance of [OfflineItemsRepository]
+ */
+class AppDataContainer(private val context: Context) : AppContainer {
+    /**
+     * Implementation for [BackupItemsRepository]
+     */
+    override val itemsRepository: BackupItemsRepository by lazy {
+        OfflineItemsRepository(AppDatabase.getDatabase(context).itemDao())
+    }
+}

+ 34 - 0
app/src/main/java/com/example/modifier/data/AppDatabase.kt

@@ -0,0 +1,34 @@
+package com.example.modifier.data
+
+import android.content.Context
+import androidx.room.AutoMigration
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+
+@Database(
+    entities = [BackupItem::class],
+    version = 1,
+    exportSchema = true
+)
+@TypeConverters(Converters::class)
+abstract class AppDatabase : RoomDatabase() {
+
+    abstract fun itemDao(): BackupItemDao
+
+    companion object {
+        @Volatile
+        private var Instance: AppDatabase? = null
+
+        fun getDatabase(context: Context): AppDatabase {
+            // if the Instance is not null, return it, otherwise create a new database instance.
+            return Instance ?: synchronized(this) {
+                Room.databaseBuilder(context, AppDatabase::class.java, "app")
+                    .build()
+                    .also { Instance = it }
+            }
+        }
+    }
+
+}

+ 57 - 0
app/src/main/java/com/example/modifier/data/BackupItem.kt

@@ -0,0 +1,57 @@
+package com.example.modifier.data
+
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.PrimaryKey
+import java.time.LocalDateTime
+import java.util.Date
+
+@Entity
+data class BackupItem(
+    @PrimaryKey(autoGenerate = true)
+    var id: Int? = null,
+    val createdAt: LocalDateTime,
+    val number: String,
+    val country: String,
+    val code: String,
+    val mcc: String,
+    val mnc: String,
+    val imei: String,
+    val imsi: String,
+    val iccid: String,
+    val sendCount: Int,
+    val path: String,
+    @Ignore
+    var deleting: Boolean = false,
+    @Ignore
+    var restoring: Boolean = false
+
+
+) {
+    constructor(
+        createdAt: LocalDateTime,
+        number: String,
+        country: String,
+        code: String,
+        mcc: String,
+        mnc: String,
+        imei: String,
+        imsi: String,
+        iccid: String,
+        sendCount: Int,
+        path: String
+    ) : this(
+        null,
+        createdAt,
+        number,
+        country,
+        code,
+        mcc,
+        mnc,
+        imei,
+        imsi,
+        iccid,
+        sendCount,
+        path
+    )
+}

+ 27 - 0
app/src/main/java/com/example/modifier/data/BackupItemDao.kt

@@ -0,0 +1,27 @@
+package com.example.modifier.data
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface BackupItemDao {
+
+    @Insert
+    suspend fun insert(backupItem: BackupItem): Long
+
+    @Update
+    suspend fun update(backupItem: BackupItem)
+
+    @Delete
+    suspend fun delete(backupItem: BackupItem)
+
+    @Query("SELECT * from backupitem WHERE id = :id")
+    suspend fun getItem(id: Int): BackupItem?
+
+    @Query("SELECT * FROM backupitem")
+    suspend fun getAll(): List<BackupItem>
+}

+ 30 - 0
app/src/main/java/com/example/modifier/data/BackupItemsRepository.kt

@@ -0,0 +1,30 @@
+package com.example.modifier.data
+
+import kotlinx.coroutines.flow.Flow
+
+interface BackupItemsRepository {
+    /**
+     * Retrieve all the items from the the given data source.
+     */
+    suspend fun getAll(): List<BackupItem>
+
+    /**
+     * Retrieve an item from the given data source that matches with the [id].
+     */
+    suspend fun getItem(id: Int): BackupItem?
+
+    /**
+     * Insert item in the data source
+     */
+    suspend fun insertItem(item: BackupItem): Long
+
+    /**
+     * Delete item from the data source
+     */
+    suspend fun deleteItem(item: BackupItem)
+
+    /**
+     * Update item in the data source
+     */
+    suspend fun updateItem(item: BackupItem)
+}

+ 16 - 0
app/src/main/java/com/example/modifier/data/Converters.kt

@@ -0,0 +1,16 @@
+package com.example.modifier.data
+
+import androidx.room.TypeConverter
+import java.time.LocalDateTime
+
+class Converters {
+    @TypeConverter
+    fun fromTimestamp(value: String?): LocalDateTime? {
+        return value?.let { LocalDateTime.parse(it) }
+    }
+
+    @TypeConverter
+    fun dateToTimestamp(date: LocalDateTime?): String? {
+        return date?.toString()
+    }
+}

+ 15 - 0
app/src/main/java/com/example/modifier/data/OfflineItemsRepository.kt

@@ -0,0 +1,15 @@
+package com.example.modifier.data
+
+import kotlinx.coroutines.flow.Flow
+
+class OfflineItemsRepository(private val itemDao: BackupItemDao) : BackupItemsRepository {
+    override suspend fun getAll(): List<BackupItem> = itemDao.getAll()
+
+    override suspend fun getItem(id: Int): BackupItem? = itemDao.getItem(id)
+
+    override suspend fun insertItem(item: BackupItem) = itemDao.insert(item)
+
+    override suspend fun deleteItem(item: BackupItem) = itemDao.delete(item)
+
+    override suspend fun updateItem(item: BackupItem) = itemDao.update(item)
+}

+ 10 - 0
app/src/main/java/com/example/modifier/service/ModifierService.kt

@@ -41,9 +41,13 @@ import com.example.modifier.CMD_RCS_SETTINGS_ACTIVITY
 import com.example.modifier.Global
 import com.example.modifier.Global.load
 import com.example.modifier.Global.resetAll
+import com.example.modifier.MyApplication
 import com.example.modifier.R
 import com.example.modifier.TraverseResult
 import com.example.modifier.Utils
+import com.example.modifier.data.AppDatabase
+import com.example.modifier.data.BackupItemsRepository
+import com.example.modifier.data.OfflineItemsRepository
 import com.example.modifier.databinding.FloatingWindowBinding
 import com.example.modifier.enums.RcsConfigureState
 import com.example.modifier.enums.RcsConnectionStatus
@@ -245,6 +249,10 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         }
     }
 
+    private val backupItemsRepository: BackupItemsRepository by lazy {
+        (application as MyApplication).container.itemsRepository
+    }
+
     fun connect() {
         try {
             load()
@@ -283,6 +291,8 @@ class ModifierService : AccessibilityService(), Emitter.Listener {
         }
     }
 
+
+
     override fun onCreate() {
         super.onCreate()
         Log.i(TAG, "Starting ModifierService")

+ 50 - 0
app/src/main/java/com/example/modifier/ui/AppViewModelProvider.kt

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.modifier.ui
+
+import android.app.Application
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
+import androidx.lifecycle.createSavedStateHandle
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.example.modifier.MyApplication
+import com.example.modifier.ui.backup.BackupViewModel
+
+/**
+ * Provides Factory to create instance of ViewModel for the entire Inventory app
+ */
+object AppViewModelProvider {
+    val Factory = viewModelFactory {
+        // Initializer for ItemEditViewModel
+        initializer {
+            BackupViewModel(
+                this.createSavedStateHandle(),
+                myApplication().container.itemsRepository
+            )
+        }
+        // Initializer for ItemEntryViewModel
+
+    }
+}
+
+/**
+ * Extension function to queries for [Application] object and returns an instance of
+ * [InventoryApplication].
+ */
+fun CreationExtras.myApplication(): MyApplication =
+    (this[AndroidViewModelFactory.APPLICATION_KEY] as MyApplication)

+ 27 - 106
app/src/main/java/com/example/modifier/ui/backup/BackupFragment.kt

@@ -1,57 +1,26 @@
 package com.example.modifier.ui.backup
 
+import android.annotation.SuppressLint
 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.core.content.ContextCompat
 import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
-import com.example.modifier.Global
-import com.example.modifier.Global.suspend
-import com.example.modifier.Global.unsuspend
-import com.example.modifier.R
-import com.example.modifier.Utils
+import com.example.modifier.MyApplication
 import com.example.modifier.adapter.BackupAdapter
+import com.example.modifier.data.BackupItem
+import com.example.modifier.data.BackupItemsRepository
 import com.example.modifier.databinding.FragmentBackupBinding
-import com.example.modifier.model.Backup
-import com.example.modifier.model.TelephonyConfig
-import com.google.gson.Gson
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.apache.commons.io.FileUtils
-import org.apache.commons.io.IOUtils
-import java.io.File
-import java.io.IOException
-import java.nio.file.Files
-import java.util.Date
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
 
 class BackupFragment : Fragment() {
-    lateinit var binding: FragmentBackupBinding
-    var handler: Handler = Handler(Looper.getMainLooper())
-    var executor: ExecutorService = Executors.newFixedThreadPool(8)
-
-    var adapter: BackupAdapter? = null
-    var backups: MutableList<Backup>? = null
-
-    init {
-        // Required empty public constructor
-        Log.i("BackupFragment", "BackupFragment created")
-    }
-
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        backups = ArrayList()
-        adapter = BackupAdapter(requireContext(), backups!!)
+    private lateinit var binding: FragmentBackupBinding
+    private val backupItemsRepository: BackupItemsRepository by lazy {
+        (requireActivity().application as MyApplication).container.itemsRepository
     }
 
     override fun onCreateView(
@@ -62,78 +31,30 @@ class BackupFragment : Fragment() {
             return binding.root
         }
         binding = FragmentBackupBinding.inflate(inflater, container, false)
-        binding.fabBackup.setOnClickListener { v: View? ->
-            Utils.makeLoadingButton(context, binding.fabBackup)
-            binding.fabBackup.isEnabled = false
-            CoroutineScope(Dispatchers.IO).launch {
-                createBackup()
-                withContext(Dispatchers.Main) {
-                    binding.fabBackup.setIconResource(R.drawable.ic_done)
-                    binding.fabBackup.text = "OK"
-                    delay(1500)
-                    binding.fabBackup.isEnabled = true
-                    binding.fabBackup.setIconResource(R.drawable.ic_add)
-                    binding.fabBackup.text = "Backup"
-                }
-            }
-        }
 
-        binding.rvBackup.layoutManager = LinearLayoutManager(context)
+        val list = mutableListOf<BackupItem>()
+        val adapter = BackupAdapter(requireContext(), list)
         binding.rvBackup.adapter = adapter
-        binding.refresh.setOnRefreshListener {
-            CoroutineScope(Dispatchers.IO).launch {
-                loadBackups()
+        binding.rvBackup.layoutManager = LinearLayoutManager(requireContext())
+
+        binding.lifecycleOwner = this
+        viewLifecycleOwner.lifecycleScope.launch {
+            // repeatOnLifecycle launches the block in a new coroutine every time the
+            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
+            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Trigger the flow and start listening for values.
+                // This happens when lifecycle is STARTED and stops
+                // collecting when the lifecycle is STOPPED
+                val all = backupItemsRepository.getAll()
+                list.clear()
+                list.addAll(all)
+                adapter.notifyDataSetChanged()
             }
         }
 
-        CoroutineScope(Dispatchers.IO).launch {
-            loadBackups()
-        }
         return binding.root
     }
 
-    private suspend fun createBackup() {
-        try {
-            Global.backup()
-            loadBackups()
-        } catch (e: Exception) {
-            e.printStackTrace()
-        }
-    }
 
-    suspend fun loadBackups() {
-        val backupDir = ContextCompat.getExternalFilesDirs(requireContext(), "backup")[0]
-        if (backupDir.exists()) {
-            val list: MutableList<Backup> = ArrayList()
-            for (file in backupDir.listFiles()!!) {
-                if (file.isDirectory) {
-                    val configFile = File(file, "config.json")
-                    if (!configFile.exists() || !configFile.isFile) {
-                        continue
-                    }
-                    val dataDir = File(file, "data")
-                    if (!dataDir.exists() || !dataDir.isDirectory) {
-                        continue
-                    }
-                    try {
-                        val telephonyConfig = Gson().fromJson(
-                            FileUtils.readFileToString(configFile, "UTF-8"),
-                            TelephonyConfig::class.java
-                        )
-                        list.add(Backup(telephonyConfig, Date(file.lastModified()), file.path))
-                    } catch (e: IOException) {
-                        throw RuntimeException(e)
-                    }
-                }
-            }
-            list.sortWith { o1: Backup, o2: Backup -> o2.date.compareTo(o1.date) }
-            backups!!.clear()
-            backups!!.addAll(list)
-        }
-        handler.post {
-            adapter!!.notifyDataSetChanged()
-            binding.refresh.isRefreshing = false
-        }
 
-    }
-}
+}

+ 65 - 0
app/src/main/java/com/example/modifier/ui/backup/BackupViewModel.kt

@@ -0,0 +1,65 @@
+package com.example.modifier.ui.backup
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.modifier.Global
+import com.example.modifier.data.BackupItem
+import com.example.modifier.data.BackupItemsRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.time.LocalDateTime
+
+class BackupViewModel(
+    savedStateHandle: SavedStateHandle,
+    private val itemsRepository: BackupItemsRepository
+) : ViewModel() {
+
+    val _uiState = MutableStateFlow(BackupUiState())
+    val uiState = _uiState.asStateFlow()
+
+    fun addBackup() {
+        viewModelScope.launch {
+            _uiState.update {
+                it.copy(backingUp = true)
+            }
+            val path = Global.backup()
+            val item = BackupItem(
+                createdAt = LocalDateTime.now(),
+                number = Global.telephonyConfig.number,
+                country = Global.telephonyConfig.country,
+                code = Global.telephonyConfig.areaCode,
+                mcc = Global.telephonyConfig.mcc,
+                mnc = Global.telephonyConfig.mnc,
+                imei = Global.telephonyConfig.imei,
+                imsi = Global.telephonyConfig.imsi,
+                iccid = Global.telephonyConfig.iccid,
+                sendCount = 0,
+                path = path
+            )
+            item.id = itemsRepository.insertItem(item).toInt()
+            _uiState.update {
+                it.itemList.add(item)
+                it.copy(backingUp = false)
+            }
+        }
+    }
+
+    fun loadBackups() {
+        viewModelScope.launch {
+            val all = itemsRepository.getAll()
+            _uiState.update {
+                it.itemList.apply { clear(); addAll(all) }
+                it.copy(itemList = it.itemList)
+            }
+        }
+    }
+}
+
+data class BackupUiState(
+    val itemList: MutableList<BackupItem> = mutableListOf(),
+    val backingUp: Boolean = false
+)

+ 78 - 0
app/src/main/java/com/example/modifier/ui/theme/Color.kt

@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.modifier.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val md_theme_light_primary = Color(0xFF6B3DD4)
+val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+val md_theme_light_primaryContainer = Color(0xFFE9DDFF)
+val md_theme_light_onPrimaryContainer = Color(0xFF22005D)
+val md_theme_light_secondary = Color(0xFF625B71)
+val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
+val md_theme_light_onSecondaryContainer = Color(0xFF1E192B)
+val md_theme_light_tertiary = Color(0xFF7E5260)
+val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+val md_theme_light_tertiaryContainer = Color(0xFFFFD9E3)
+val md_theme_light_onTertiaryContainer = Color(0xFF31101D)
+val md_theme_light_error = Color(0xFFBA1A1A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+val md_theme_light_onError = Color(0xFFFFFFFF)
+val md_theme_light_onErrorContainer = Color(0xFF410002)
+val md_theme_light_background = Color(0xFFF3FEFF)
+val md_theme_light_onBackground = Color(0xFF002022)
+val md_theme_light_surface = Color(0xFFF3FEFF)
+val md_theme_light_onSurface = Color(0xFF002022)
+val md_theme_light_surfaceVariant = Color(0xFFE7E0EB)
+val md_theme_light_onSurfaceVariant = Color(0xFF49454E)
+val md_theme_light_outline = Color(0xFF7A757F)
+val md_theme_light_inverseOnSurface = Color(0xFFC3FBFF)
+val md_theme_light_inverseSurface = Color(0xFF00373A)
+val md_theme_light_inversePrimary = Color(0xFFCFBCFF)
+val md_theme_light_surfaceTint = Color(0xFF6B3DD4)
+val md_theme_light_outlineVariant = Color(0xFFCAC4CF)
+val md_theme_light_scrim = Color(0xFF000000)
+
+val md_theme_dark_primary = Color(0xFFCFBCFF)
+val md_theme_dark_onPrimary = Color(0xFF3A0092)
+val md_theme_dark_primaryContainer = Color(0xFF531BBC)
+val md_theme_dark_onPrimaryContainer = Color(0xFFE9DDFF)
+val md_theme_dark_secondary = Color(0xFFCBC2DB)
+val md_theme_dark_onSecondary = Color(0xFF332D41)
+val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
+val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
+val md_theme_dark_tertiary = Color(0xFFEFB8C8)
+val md_theme_dark_onTertiary = Color(0xFF4A2532)
+val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
+val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E3)
+val md_theme_dark_error = Color(0xFFFFB4AB)
+val md_theme_dark_errorContainer = Color(0xFF93000A)
+val md_theme_dark_onError = Color(0xFF690005)
+val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+val md_theme_dark_background = Color(0xFF002022)
+val md_theme_dark_onBackground = Color(0xFF70F5FF)
+val md_theme_dark_surface = Color(0xFF002022)
+val md_theme_dark_onSurface = Color(0xFF70F5FF)
+val md_theme_dark_surfaceVariant = Color(0xFF49454E)
+val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4CF)
+val md_theme_dark_outline = Color(0xFF948F99)
+val md_theme_dark_inverseOnSurface = Color(0xFF002022)
+val md_theme_dark_inverseSurface = Color(0xFF70F5FF)
+val md_theme_dark_inversePrimary = Color(0xFF6B3DD4)
+val md_theme_dark_surfaceTint = Color(0xFFCFBCFF)
+val md_theme_dark_outlineVariant = Color(0xFF49454E)
+val md_theme_dark_scrim = Color(0xFF000000)

+ 27 - 0
app/src/main/java/com/example/modifier/ui/theme/Shape.kt

@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.modifier.ui.theme
+
+import androidx.compose.foundation.shape.CutCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+
+    extraSmall = CutCornerShape(topEnd = 8.dp, bottomStart = 8.dp),
+    small = CutCornerShape(topEnd = 8.dp, bottomStart = 8.dp),
+    medium = CutCornerShape(topEnd = 16.dp, bottomStart = 16.dp)
+)

+ 131 - 0
app/src/main/java/com/example/modifier/ui/theme/Theme.kt

@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.modifier.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val LightColorScheme = lightColorScheme(
+    primary = md_theme_light_primary,
+    onPrimary = md_theme_light_onPrimary,
+    primaryContainer = md_theme_light_primaryContainer,
+    onPrimaryContainer = md_theme_light_onPrimaryContainer,
+    secondary = md_theme_light_secondary,
+    onSecondary = md_theme_light_onSecondary,
+    secondaryContainer = md_theme_light_secondaryContainer,
+    onSecondaryContainer = md_theme_light_onSecondaryContainer,
+    tertiary = md_theme_light_tertiary,
+    onTertiary = md_theme_light_onTertiary,
+    tertiaryContainer = md_theme_light_tertiaryContainer,
+    onTertiaryContainer = md_theme_light_onTertiaryContainer,
+    error = md_theme_light_error,
+    errorContainer = md_theme_light_errorContainer,
+    onError = md_theme_light_onError,
+    onErrorContainer = md_theme_light_onErrorContainer,
+    background = md_theme_light_background,
+    onBackground = md_theme_light_onBackground,
+    surface = md_theme_light_surface,
+    onSurface = md_theme_light_onSurface,
+    surfaceVariant = md_theme_light_surfaceVariant,
+    onSurfaceVariant = md_theme_light_onSurfaceVariant,
+    outline = md_theme_light_outline,
+    inverseOnSurface = md_theme_light_inverseOnSurface,
+    inverseSurface = md_theme_light_inverseSurface,
+    inversePrimary = md_theme_light_inversePrimary,
+    surfaceTint = md_theme_light_surfaceTint,
+    outlineVariant = md_theme_light_outlineVariant,
+    scrim = md_theme_light_scrim,
+)
+
+private val DarkColorScheme = darkColorScheme(
+    primary = md_theme_dark_primary,
+    onPrimary = md_theme_dark_onPrimary,
+    primaryContainer = md_theme_dark_primaryContainer,
+    onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+    secondary = md_theme_dark_secondary,
+    onSecondary = md_theme_dark_onSecondary,
+    secondaryContainer = md_theme_dark_secondaryContainer,
+    onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+    tertiary = md_theme_dark_tertiary,
+    onTertiary = md_theme_dark_onTertiary,
+    tertiaryContainer = md_theme_dark_tertiaryContainer,
+    onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+    error = md_theme_dark_error,
+    errorContainer = md_theme_dark_errorContainer,
+    onError = md_theme_dark_onError,
+    onErrorContainer = md_theme_dark_onErrorContainer,
+    background = md_theme_dark_background,
+    onBackground = md_theme_dark_onBackground,
+    surface = md_theme_dark_surface,
+    onSurface = md_theme_dark_onSurface,
+    surfaceVariant = md_theme_dark_surfaceVariant,
+    onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+    outline = md_theme_dark_outline,
+    inverseOnSurface = md_theme_dark_inverseOnSurface,
+    inverseSurface = md_theme_dark_inverseSurface,
+    inversePrimary = md_theme_dark_inversePrimary,
+    surfaceTint = md_theme_dark_surfaceTint,
+    outlineVariant = md_theme_dark_outlineVariant,
+    scrim = md_theme_dark_scrim,
+)
+
+@Composable
+fun AppTheme(
+    darkTheme: Boolean = isSystemInDarkTheme(),
+    // Dynamic color is available on Android 12+
+    // Dynamic color in this app is turned off for learning purposes
+    dynamicColor: Boolean = false,
+    content: @Composable () -> Unit
+) {
+    val colorScheme = when {
+        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+            val context = LocalContext.current
+            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+        }
+
+        darkTheme -> DarkColorScheme
+        else -> LightColorScheme
+    }
+
+    val view = LocalView.current
+    if (!view.isInEditMode) {
+        SideEffect {
+            val window = (view.context as Activity).window
+            window.statusBarColor = colorScheme.primary.toArgb()
+            WindowCompat
+                .getInsetsController(window, view)
+                .isAppearanceLightStatusBars = darkTheme
+        }
+    }
+
+    MaterialTheme(
+        colorScheme = colorScheme,
+        shapes = Shapes,
+        content = content
+    )
+}

+ 0 - 1
app/src/main/res/layout/floating_window.xml

@@ -100,7 +100,6 @@
                         android:layout_height="34dp"
                         android:layout_marginLeft="8dp"
                         android:padding="0dp"
-                        android:visibility="gone"
                         app:icon="@drawable/ic_manage_search"
                         app:iconGravity="textStart"
                         app:iconPadding="0dp" />

+ 32 - 4
gradle/libs.versions.toml

@@ -4,7 +4,9 @@ commonsCollections4 = "4.4"
 commonsIo = "2.16.1"
 commonsLang3 = "3.14.0"
 commonsValidator = "1.8.0"
+composeBom = "2024.06.00"
 datastore = "1.1.1"
+fragmentKtx = "1.8.1"
 gson = "2.10.1"
 hilt = "2.48"
 junit = "4.13.2"
@@ -14,23 +16,39 @@ appcompat = "1.6.1"
 kotlinxCoroutines = "1.8.1"
 kotlinxSerializationJson = "1.5.1"
 ktor = "2.3.11"
+lifecycleViewmodelCompose = "2.8.3"
 material = "1.12.0"
 activity = "1.9.0"
 constraintlayout = "2.1.4"
+material3 = "1.2.1"
+materialIconsExtended = "1.6.8"
+roomRuntime = "2.6.1"
+runtimeLivedata = "1.6.8"
+savedstateVersion = "1.2.1"
 socketIoClient = "2.0.0"
-navigationFragment = "2.7.7"
-navigationUi = "2.7.7"
+navigation = "2.7.7"
 kotlin = "1.9.24"
 annotation = "1.8.0"
 lifecycle = "2.8.0"
 coreKtx = "1.13.1"
+uiToolingPreview = "1.6.8"
+uiTooling = "1.6.8"
 
 [libraries]
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
+androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
+
+androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsExtended" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
+androidx-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3" }
+
+androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "runtimeLivedata" }
 commons-collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "commonsCollections4" }
 commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" }
 commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" }
 commons-validator = { module = "commons-validator:commons-validator", version.ref = "commonsValidator" }
 datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
+fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
 gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
 hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
 hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
@@ -51,14 +69,24 @@ ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serializatio
 material = { group = "com.google.android.material", name = "material", version.ref = "material" }
 activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
 constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
+room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
+room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
+room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
+savedstate = { module = "androidx.savedstate:savedstate", version.ref = "savedstateVersion" }
 socket-io-client = { module = "io.socket:socket.io-client", version.ref = "socketIoClient" }
-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
+navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" }
+navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
+navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" }
+navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
 annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
 lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
 lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
 lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
+lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
 core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" }
+ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreview" }
 
 [plugins]
 androidApplication = { id = "com.android.application", version.ref = "agp" }

+ 0 - 1
settings.gradle

@@ -17,7 +17,6 @@ dependencyResolutionManagement {
         google()
         mavenCentral()
         maven { url 'https://jitpack.io' }
-        maven { url 'https://api.xposed.info/' }
     }
 }