diff --git a/clientapp/android/.idea/kotlinc.xml b/clientapp/android/.idea/kotlinc.xml index fdf8d994..6d0ee1c2 100644 --- a/clientapp/android/.idea/kotlinc.xml +++ b/clientapp/android/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/clientapp/android/.idea/other.xml b/clientapp/android/.idea/other.xml new file mode 100644 index 00000000..0d3a1fbb --- /dev/null +++ b/clientapp/android/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/clientapp/android/app/build.gradle.kts b/clientapp/android/app/build.gradle.kts index 9dc16992..e706caa1 100644 --- a/clientapp/android/app/build.gradle.kts +++ b/clientapp/android/app/build.gradle.kts @@ -1,8 +1,9 @@ -import java.util.Properties - plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.androidx.room) + alias(libs.plugins.com.google.devtools.ksp) + alias(libs.plugins.com.google.dagger.hilt.android) } android { @@ -59,6 +60,13 @@ android { excludes += "META-INF/LICENSE-notice.md" } } + room { + schemaDirectory("$projectDir/schemas") + } +} + +ksp { + arg("room.generateKotlin", "true") } dependencies { @@ -73,9 +81,14 @@ dependencies { implementation(libs.androidx.preference.ktx) implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.window) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + implementation(libs.hilt) - testImplementation(libs.junit) + ksp(libs.androidx.room.compiler) + ksp(libs.hilt.compiler) + testImplementation(libs.junit) androidTestImplementation(libs.junit) androidTestImplementation(libs.junit.jupiter) androidTestImplementation(libs.androidx.espresso.core) diff --git a/clientapp/android/app/schemas/com.royna.tgbotclient.datastore.ChatIDDatabase/1.json b/clientapp/android/app/schemas/com.royna.tgbotclient.datastore.ChatIDDatabase/1.json new file mode 100644 index 00000000..f41bbd8c --- /dev/null +++ b/clientapp/android/app/schemas/com.royna.tgbotclient.datastore.ChatIDDatabase/1.json @@ -0,0 +1,50 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "6780e511c5a2371a71169f6bd0de9e12", + "entities": [ + { + "tableName": "ChatIDEntry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chat_id` INTEGER NOT NULL, `chat_name` TEXT NOT NULL, PRIMARY KEY(`chat_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "chat_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "chat_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "chat_id" + ] + }, + "indices": [ + { + "name": "index_ChatIDEntry_chat_name", + "unique": true, + "columnNames": [ + "chat_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatIDEntry_chat_name` ON `${TABLE_NAME}` (`chat_name`)" + } + ], + "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, '6780e511c5a2371a71169f6bd0de9e12')" + ] + } +} \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ClientApplication.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ClientApplication.kt index 342b9289..d63e1578 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ClientApplication.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ClientApplication.kt @@ -2,9 +2,10 @@ package com.royna.tgbotclient import android.app.Application import com.royna.tgbotclient.ui.settings.TgClientSettings -import com.royna.tgbotclient.util.DeviceUtils import com.royna.tgbotclient.util.Logging +import dagger.hilt.android.HiltAndroidApp +@HiltAndroidApp class ClientApplication : Application() { override fun onCreate() { super.onCreate() diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/MainActivity.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/MainActivity.kt index 50332064..fd6256f0 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/MainActivity.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/MainActivity.kt @@ -21,7 +21,9 @@ import com.royna.tgbotclient.pm.IStoragePermission import com.royna.tgbotclient.pm.StoragePermissionPreR import com.royna.tgbotclient.pm.StoragePermissionR import com.royna.tgbotclient.ui.settings.SettingsActivity +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var binding: ActivityMainBinding diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDDatabase.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDDatabase.kt new file mode 100644 index 00000000..626a577d --- /dev/null +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDDatabase.kt @@ -0,0 +1,9 @@ +package com.royna.tgbotclient.datastore + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database(entities = [ChatIDEntry::class], version = 1) +abstract class ChatIDDatabase : RoomDatabase() { + abstract fun impl(): IChatIDOperations +} \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDEntry.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDEntry.kt new file mode 100644 index 00000000..d863e3a4 --- /dev/null +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDEntry.kt @@ -0,0 +1,18 @@ +package com.royna.tgbotclient.datastore + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(indices = [Index(value = ["chat_name"], unique = true)]) +data class ChatIDEntry ( + @PrimaryKey + @ColumnInfo(name="chat_id") + val id: ChatID, + + @ColumnInfo(name="chat_name") + val name: String +) + +typealias ChatID = Long \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDModule.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDModule.kt new file mode 100644 index 00000000..cd12e23f --- /dev/null +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/ChatIDModule.kt @@ -0,0 +1,24 @@ +package com.royna.tgbotclient.datastore + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class ChatIDModule { + @Provides + fun provideChatIDOperations(database: ChatIDDatabase): IChatIDOperations + = database.impl() + + @Provides + fun provideChatIDDatabase(@ApplicationContext context: Context): ChatIDDatabase = Room.databaseBuilder( + context, + ChatIDDatabase::class.java, + "chatid.db" + ).build() +} \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/IChatIDOperations.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/IChatIDOperations.kt new file mode 100644 index 00000000..3b9ad87c --- /dev/null +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/IChatIDOperations.kt @@ -0,0 +1,27 @@ +package com.royna.tgbotclient.datastore + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query + +@Dao +interface IChatIDOperations { + @Insert + fun add(vararg entries: ChatIDEntry) + + @Query("SELECT * FROM ChatIDEntry") + fun getAll(): List + + @Delete + fun remove(entry: ChatIDEntry) + + @Query("SELECT chat_id FROM ChatIDEntry WHERE chat_name = :name") + fun getChatID(name: String): ChatID + + @Query("SELECT chat_name FROM ChatIDEntry WHERE chat_id = :id") + fun getChatName(id: ChatID): String + + @Query("DELETE FROM ChatIDEntry") + fun clearAll() +} \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/IDataStore.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/IDataStore.kt deleted file mode 100644 index 205f0828..00000000 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/IDataStore.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.royna.tgbotclient.datastore - -/** - * IDataStore interface defines the contract for a key-value data store. - * - * @param K the type of keys maintained by this data store. - * @param V the type of mapped values. - */ -interface IDataStore { - - /** - * Creates the data store. - * - * @return `true` if the data store is successfully created, `false` otherwise. - */ - fun create() : Boolean - - /** - * Writes the specified value with the specified key in the data store. - * - * @param key the key with which the specified value is to be associated. - * @param value the value to be associated with the specified key. - * @return `true` if the value is successfully written, `false` otherwise. - */ - fun write(key: K, value: V) : Boolean - - /** - * Deletes the value associated with the specified key in the data store. - * - * @param key the key whose associated value is to be deleted. - * @return `true` if the value is successfully deleted, `false` otherwise. - */ - fun delete(key: K) : Boolean - - /** - * Reads all key-value pairs from the data store. - * - * @return a map containing all key-value pairs in the data store. - */ - fun readAll() : Map - - /** - * Clears all key-value pairs from the data store. - * - * @return `true` if the data store is successfully cleared, `false` otherwise. - */ - fun clearAll() : Boolean - - /** - * Reads the value associated with the specified key from the data store. - * - * @param key the key whose associated value is to be read. - * @return the value associated with the specified key, or `null` if the key does not exist. - */ - fun read(key: K) : V? { - readAll().forEach { - if (it.key == key) { - return it.value - } - } - return null - } - - /** - * Finds the key associated with the specified value in the data store. - * - * @param value the value whose associated key is to be found. - * @return the key associated with the specified value, or `null` - * if the value does not exist in the data store. - */ - fun findKey(value: V) : K? { - readAll().forEach { - if (it.value == value) { - return it.key - } - } - return null - } -} diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/SQLiteChatDatastore.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/SQLiteChatDatastore.kt deleted file mode 100644 index 229846f0..00000000 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/SQLiteChatDatastore.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.royna.tgbotclient.datastore.chat - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import com.royna.tgbotclient.datastore.IDataStore -import com.royna.tgbotclient.util.Logging - -class SQLiteChatDatastore(context: Context) : IDataStore, - SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - override fun create(): Boolean = true - override fun readAll(): Map { - Logging.verbose("Reading all") - val result = mutableMapOf() - readableDatabase.use { db -> - db.query( - TABLE_NAME, - arrayOf(COLUMN_NAME, COLUMN_ID), - null, - null, - null, - null, - null - ).use { - while (it.moveToNext()) { - result[it.getString(0)] = it.getLong(1) - } - } - Logging.verbose("Read all: result $result") - } - return result - } - - override fun delete(key: String): Boolean { - val success : Boolean - - Logging.verbose("Deleting $key") - readableDatabase.use { - success = it.delete(TABLE_NAME, "$COLUMN_NAME = ?", arrayOf(key)) == 1 - } - Logging.verbose("Deleted $key: result $success") - return success - } - - override fun write(key: String, value: ChatId): Boolean { - Logging.verbose("Writing $key:$value") - writableDatabase.use { wdb -> - val cntv = ContentValues().apply { - put(COLUMN_NAME, key) - put(COLUMN_ID, value) - } - return runCatching { - wdb.insertOrThrow(TABLE_NAME, null, cntv) > 0 - }.getOrElse { - Logging.error("Failed to write $key:$value", it) - false - } - } - } - - override fun read(key: String): ChatId? { - var chatId : ChatId? = null - - Logging.verbose("Reading $key") - readableDatabase.use { db -> - db.query( - TABLE_NAME, - arrayOf(COLUMN_ID), - "$COLUMN_NAME = ?", - arrayOf(key), - null, - null, - null - ).use { - if (it.moveToFirst()) { - chatId = it.getLong(0) - } - } - } - Logging.verbose("Read $key: result $chatId") - return chatId - } - - override fun clearAll(): Boolean { - Logging.verbose("Clearing all") - val success = writableDatabase.delete(TABLE_NAME, null, null) > 0 - Logging.verbose("Cleared all: result $success") - return success - } - - override fun findKey(value: ChatId): String? { - Logging.verbose("Finding $value") - readableDatabase.use { db -> - db.query( - TABLE_NAME, - arrayOf(COLUMN_NAME), - "$COLUMN_ID = ?", - arrayOf(value.toString()), - null, - null, - null).use { - if (it.moveToFirst()) { - return it.getString(0) - } - } - } - return null - } - - override fun onCreate(db: SQLiteDatabase?) { - db?.execSQL("CREATE TABLE $TABLE_NAME ($COLUMN_ID BIGINT UNIQUE," + - "$COLUMN_NAME TEXT UNIQUE NOT NULL)") - } - - override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { - if (oldVersion < newVersion) { - db?.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) - } - } - - companion object { - const val DATABASE_NAME = "chat_ids.db" - const val DATABASE_VERSION = 1 - const val TABLE_NAME = "chat_ids" - const val COLUMN_ID = "id" - const val COLUMN_NAME = "name" - } -} - -typealias ChatId = Long \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatDatastoreViewModel.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatDatastoreViewModel.kt deleted file mode 100644 index 00aa6cf6..00000000 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatDatastoreViewModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.royna.tgbotclient.datastore.chat.ui - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.royna.tgbotclient.datastore.chat.ChatId - -class ChatDatastoreViewModel : ViewModel() { - private var _chatId = MutableLiveData() - private var _chatName = MutableLiveData() - val chatId: LiveData = _chatId - val chatName: LiveData = _chatName - fun setChatId(chatId: ChatId) { - _chatId.value = chatId - } - fun setChatName(chatName: String) { - _chatName.value = chatName - } -} \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatTableViewItem.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatTableViewItem.kt deleted file mode 100644 index 5a9e75a2..00000000 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatTableViewItem.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.royna.tgbotclient.datastore.chat.ui - -import com.royna.tgbotclient.datastore.chat.ChatId - -data class ChatTableViewItem(var index: Int = 0, val chatName: String, val chatId: ChatId) diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/SingleViewModelBase.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/SingleViewModelBase.kt index 3f05ec2f..304bec18 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/SingleViewModelBase.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/SingleViewModelBase.kt @@ -4,10 +4,8 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.royna.tgbotclient.SocketCommandNative -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel abstract class SingleViewModelBase : ViewModel() { protected var _liveData = MutableLiveData() @@ -16,12 +14,7 @@ abstract class SingleViewModelBase : ViewModel() { _liveData.value = inval } - protected val gMainScope = CoroutineScope(Dispatchers.Main) + protected val gMainScope = viewModelScope abstract suspend fun coroutineFunction(activity: FragmentActivity) : V abstract fun execute(activity: FragmentActivity, callback: SocketCommandNative.ICommandCallback) - - override fun onCleared() { - super.onCleared() - gMainScope.cancel() - } } \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatDatastoreFragment.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatDatastoreFragment.kt similarity index 72% rename from clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatDatastoreFragment.kt rename to clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatDatastoreFragment.kt index b028aec0..6870464c 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatDatastoreFragment.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatDatastoreFragment.kt @@ -1,4 +1,4 @@ -package com.royna.tgbotclient.datastore.chat.ui +package com.royna.tgbotclient.ui.chatid import android.os.Bundle import android.view.LayoutInflater @@ -6,14 +6,20 @@ import android.view.View import android.view.ViewGroup import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar import com.royna.tgbotclient.databinding.FragmentDatastoreChatBinding -import com.royna.tgbotclient.datastore.chat.SQLiteChatDatastore +import com.royna.tgbotclient.datastore.ChatIDEntry import com.royna.tgbotclient.util.DeviceUtils import com.royna.tgbotclient.util.Logging +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +@AndroidEntryPoint class ChatDatastoreFragment : Fragment() { private var _binding: FragmentDatastoreChatBinding? = null @@ -21,24 +27,25 @@ class ChatDatastoreFragment : Fragment() { // onDestroyView. private val binding get() = _binding!! + private val vm : ChatDatastoreViewModel by viewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val vm = ViewModelProvider(this)[ChatDatastoreViewModel::class.java] _binding = FragmentDatastoreChatBinding.inflate(inflater, container, false) - kDatastore = SQLiteChatDatastore(requireContext()) binding.datastoreSaveButton.setOnClickListener { if (vm.chatId.value == null || vm.chatName.value == null) { Snackbar.make(it, "Fill all fields", Snackbar.LENGTH_SHORT).show() return@setOnClickListener } - if (kDatastore.write(vm.chatName.value!!, vm.chatId.value!!)) { + val id = vm.chatId.value!! + val name = vm.chatName.value!! + vm.add(ChatIDEntry(id, name)).invokeOnCompletion { ex -> + assert(ex == null) Snackbar.make(it, "Saved", Snackbar.LENGTH_SHORT).show() - kAdapter.addItem(ChatTableViewItem(0, vm.chatName.value!!, vm.chatId.value!!)) - } else { - Snackbar.make(it, "Failed", Snackbar.LENGTH_SHORT).show() + kAdapter.addItem(ChatTableViewItem(0, name, id)) } } binding.datastoreChatEdit.doOnTextChanged { text, _, _, _ -> @@ -52,7 +59,7 @@ class ChatDatastoreFragment : Fragment() { vm.setChatName(text.toString()) } binding.datastoreClearButton.setOnClickListener { - kDatastore.clearAll() + vm.clearAll().start() kAdapter.clearAll() } kAdapter = ChatTableViewAdapter() @@ -71,9 +78,14 @@ class ChatDatastoreFragment : Fragment() { } } kAdapter.clearAll() - kDatastore.readAll().forEach { - Logging.debug("Read: $it") - kAdapter.addItem(ChatTableViewItem(chatName = it.key, chatId = it.value)) + CoroutineScope(Dispatchers.IO).launch { + vm.getAll().await().forEach { + Logging.debug("Read: $it") + withContext(Dispatchers.Main) { + kAdapter.addItem(ChatTableViewItem(chatName = it.name, chatId = it.id)) + + } + } } } @@ -82,6 +94,5 @@ class ChatDatastoreFragment : Fragment() { _binding = null } - private lateinit var kDatastore : SQLiteChatDatastore private lateinit var kAdapter : ChatTableViewAdapter } \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatDatastoreViewModel.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatDatastoreViewModel.kt new file mode 100644 index 00000000..8357ea63 --- /dev/null +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatDatastoreViewModel.kt @@ -0,0 +1,44 @@ +package com.royna.tgbotclient.ui.chatid + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.royna.tgbotclient.datastore.ChatID +import com.royna.tgbotclient.datastore.ChatIDEntry +import com.royna.tgbotclient.datastore.IChatIDOperations +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class ChatDatastoreViewModel @Inject constructor(private val operation: IChatIDOperations) : ViewModel() { + private var _chatId = MutableLiveData() + private var _chatName = MutableLiveData() + val chatId: LiveData = _chatId + val chatName: LiveData = _chatName + fun setChatId(chatId: ChatID) { + _chatId.value = chatId + } + fun setChatName(chatName: String) { + _chatName.value = chatName + } + fun clearAll() = viewModelScope.launch { + withContext(Dispatchers.IO) { + operation.clearAll() + } + } + fun add(entry: ChatIDEntry) = viewModelScope.launch { + withContext(Dispatchers.IO) { + operation.add(entry) + } + } + fun getAll() = viewModelScope.async { + withContext(Dispatchers.IO) { + operation.getAll() + } + } +} \ No newline at end of file diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatTableViewAdapter.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatTableViewAdapter.kt similarity index 98% rename from clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatTableViewAdapter.kt rename to clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatTableViewAdapter.kt index b4d2022c..620c64da 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/datastore/chat/ui/ChatTableViewAdapter.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatTableViewAdapter.kt @@ -1,4 +1,4 @@ -package com.royna.tgbotclient.datastore.chat.ui +package com.royna.tgbotclient.ui.chatid import android.annotation.SuppressLint import android.view.LayoutInflater diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatTableViewItem.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatTableViewItem.kt new file mode 100644 index 00000000..129c4662 --- /dev/null +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/chatid/ChatTableViewItem.kt @@ -0,0 +1,5 @@ +package com.royna.tgbotclient.ui.chatid + +import com.royna.tgbotclient.datastore.ChatID + +data class ChatTableViewItem(var index: Int = 0, val chatName: String, val chatId: ChatID) diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatFragment.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatFragment.kt index 04466dbd..b5819fed 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatFragment.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatFragment.kt @@ -10,12 +10,17 @@ import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.commit -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import com.royna.tgbotclient.R import com.royna.tgbotclient.databinding.FragmentSendMessageBinding -import com.royna.tgbotclient.datastore.chat.SQLiteChatDatastore +import com.royna.tgbotclient.datastore.ChatIDEntry import com.royna.tgbotclient.ui.CurrentSettingFragment +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +@AndroidEntryPoint class TextToChatFragment : Fragment() { private var _binding: FragmentSendMessageBinding? = null @@ -23,13 +28,13 @@ class TextToChatFragment : Fragment() { // onDestroyView. private val binding get() = _binding!! + private val textToChatViewModel : TextToChatViewModel by viewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val textToChatViewModel = - ViewModelProvider(this)[TextToChatViewModel::class.java] _binding = FragmentSendMessageBinding.inflate(inflater, container, false) val root: View = binding.root @@ -46,10 +51,11 @@ class TextToChatFragment : Fragment() { } textToChatViewModel.chatId.observe(viewLifecycleOwner) { - binding.showChatIdText.text = if (mChatIdMap.containsValue(it)) { - getString(R.string.destination_chat_fmt, mChatIdMap.filterValues { - v-> v == it - }.keys.first(), it) + val chatName = mChatIdMap.find { ent -> + ent.id == it + }?.name + binding.showChatIdText.text = if (chatName != null) { + getString(R.string.destination_chat_fmt, chatName, it) } else if (it != InvalidChatId) { getString(R.string.destination_chat_id_fmt, it) } else { @@ -69,22 +75,16 @@ class TextToChatFragment : Fragment() { computeSendButtonState() }.onFailure { // This is not a number, query it on DB - mChatIdMap[editor.toString()].let { - if (it != null) { + mChatIdMap.filter { + it.name.lowercase() == editor.toString().lowercase() + }.let { map -> + if (map.isNotEmpty()) { // Found - textToChatViewModel.setChatId(it) + assert(map.size == 1) + textToChatViewModel.setChatId(map.first().id) } else { // Not found - mChatIdMap.filterKeys { key -> - key.lowercase() == editor.toString().lowercase() - }.also { map -> - assert(map.size == 1 || map.isEmpty()) - if (map.isEmpty()) { - textToChatViewModel.setChatId(InvalidChatId) - } - }.forEach { entry -> - textToChatViewModel.setChatId(entry.value) - } + textToChatViewModel.setChatId(InvalidChatId) } computeSendButtonState() } @@ -116,7 +116,9 @@ class TextToChatFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mChatIdMap = SQLiteChatDatastore(requireContext()).readAll() + CoroutineScope(Dispatchers.IO).launch { + mChatIdMap = textToChatViewModel.getAll().await() + } childFragmentManager.commit { replace(R.id.current_setting_container, CurrentSettingFragment()) } @@ -127,7 +129,7 @@ class TextToChatFragment : Fragment() { _binding = null } - private var mChatIdMap: Map = mapOf() + private var mChatIdMap: List = listOf() companion object { private const val InvalidChatId = 0L } diff --git a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatViewModel.kt b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatViewModel.kt index 0c11c508..bc3073e0 100644 --- a/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatViewModel.kt +++ b/clientapp/android/app/src/main/java/com/royna/tgbotclient/ui/commands/sendmsg/TextToChatViewModel.kt @@ -3,15 +3,21 @@ package com.royna.tgbotclient.ui.commands.sendmsg import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.royna.tgbotclient.SocketCommandNative +import com.royna.tgbotclient.datastore.IChatIDOperations +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import javax.inject.Inject -class TextToChatViewModel : ViewModel() { +@HiltViewModel +class TextToChatViewModel @Inject constructor(private val operation: IChatIDOperations) : ViewModel() { // Private MutableLiveData private val _messageText = MutableLiveData() private val _chatId = MutableLiveData() @@ -66,4 +72,10 @@ class TextToChatViewModel : ViewModel() { super.onCleared() gMainScope.cancel() } + + fun getAll() = viewModelScope.async { + withContext(Dispatchers.IO) { + operation.getAll() + } + } } \ No newline at end of file diff --git a/clientapp/android/app/src/main/res/layout/fragment_datastore_chat.xml b/clientapp/android/app/src/main/res/layout/fragment_datastore_chat.xml index bf4d887c..08d5db28 100644 --- a/clientapp/android/app/src/main/res/layout/fragment_datastore_chat.xml +++ b/clientapp/android/app/src/main/res/layout/fragment_datastore_chat.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerHorizontal="true" - tools:context=".datastore.chat.ui.ChatDatastoreFragment"> + tools:context=".ui.chatid.ChatDatastoreFragment"> \ No newline at end of file diff --git a/clientapp/android/build.gradle.kts b/clientapp/android/build.gradle.kts index f74b04bf..74fda1c8 100644 --- a/clientapp/android/build.gradle.kts +++ b/clientapp/android/build.gradle.kts @@ -2,4 +2,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.androidx.room) apply false + alias(libs.plugins.com.google.devtools.ksp) apply false + alias(libs.plugins.com.google.dagger.hilt.android) apply false } \ No newline at end of file diff --git a/clientapp/android/gradle/libs.versions.toml b/clientapp/android/gradle/libs.versions.toml index 53d0d318..e3b02d89 100644 --- a/clientapp/android/gradle/libs.versions.toml +++ b/clientapp/android/gradle/libs.versions.toml @@ -1,8 +1,9 @@ [versions] -agp = "8.4.2" -espressoCoreVersion = "3.5.1" +agp = "8.5.0" +hilt = "2.48" +espressoCoreVersion = "3.6.1" junitJupiter = "5.8.1" -kotlin = "1.9.0" +kotlin = "2.0.0" coreKtx = "1.13.1" junit = "4.13.2" appcompat = "1.7.0" @@ -13,13 +14,21 @@ lifecycleViewmodelKtx = "2.8.2" navigationFragmentKtx = "2.7.7" navigationUiKtx = "2.7.7" preferenceKtx = "1.2.1" -fragmentKtx = "1.8.0" +fragmentKtx = "1.8.1" +roomCompiler = "2.6.1" window = "1.3.0" +room = "2.6.1" +ksp = "2.0.0-1.0.22" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCoreVersion" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomCompiler" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomCompiler" } androidx-window = { module = "androidx.window:window", version.ref = "window" } +hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junitJupiter" } @@ -35,4 +44,6 @@ androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", ve [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - +androidx-room = { id = "androidx.room", version.ref="room" } +com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } \ No newline at end of file diff --git a/clientapp/android/gradle/wrapper/gradle-wrapper.properties b/clientapp/android/gradle/wrapper/gradle-wrapper.properties index c6669bce..d0999e0f 100644 --- a/clientapp/android/gradle/wrapper/gradle-wrapper.properties +++ b/clientapp/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri May 17 22:50:33 KST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists