diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eb7cd94a..f9a6e71d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,7 +158,10 @@ dependencies { implementation(libs.coroutines.android) implementation(libs.lifecycle.runtime) - // implementation(libs.lifecycle.viewmodel) + implementation(libs.lifecycle.viewmodel) + + implementation(libs.koin) + implementation(libs.koin.viewmodel) // test testImplementation(libs.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21615d35..2c896861 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + + + + + + + + diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/PasscodesApplication.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/PasscodesApplication.kt new file mode 100644 index 00000000..190dd4f5 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/PasscodesApplication.kt @@ -0,0 +1,16 @@ +package com.jeeldobariya.passcodes + +import android.app.Application +import com.jeeldobariya.passcodes.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class PasscodesApplication : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@PasscodesApplication) + modules(appModule) + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/AutofillSettingsActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/AutofillSettingsActivity.kt new file mode 100644 index 00000000..3319e917 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/AutofillSettingsActivity.kt @@ -0,0 +1,23 @@ +package com.jeeldobariya.passcodes.autofill + +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.button.MaterialButton +import com.jeeldobariya.passcodes.R + +class AutofillSettingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_autofill_settings) + + val enableAutofillButton = findViewById(R.id.enable_autofill_button) + enableAutofillButton.setOnClickListener { + val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE) + intent.data = android.net.Uri.parse("package:$packageName") + startActivity(intent) + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt new file mode 100644 index 00000000..85af800b --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt @@ -0,0 +1,112 @@ +package com.jeeldobariya.passcodes.autofill + +import android.app.assist.AssistStructure +import android.os.CancellationSignal +import android.service.autofill.AutofillService +import android.service.autofill.FillCallback +import android.service.autofill.FillRequest +import android.service.autofill.FillResponse +import android.service.autofill.SaveCallback +import android.service.autofill.SaveRequest +import android.view.autofill.AutofillValue +import android.widget.RemoteViews +import com.jeeldobariya.passcodes.R +import com.jeeldobariya.passcodes.data.Passcode +import com.jeeldobariya.passcodes.data.PasscodeDatabase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class PasswordAutofillService : AutofillService() { + + private val serviceScope = CoroutineScope(Dispatchers.IO) + + override fun onFillRequest( + request: FillRequest, + cancellationSignal: CancellationSignal, + callback: FillCallback + ) { + val context = request.fillContexts + val structure = context.last().structure + + val viewNodes = mutableMapOf() + parseStructure(structure.getWindowNodeAt(0).rootViewNode, viewNodes) + + val usernameNode = viewNodes["username"] ?: viewNodes["emailAddress"] + val passwordNode = viewNodes["password"] + + if (usernameNode?.autofillId == null || passwordNode?.autofillId == null) { + callback.onSuccess(null) + return + } + + val usernameId = usernameNode.autofillId!! + val passwordId = passwordNode.autofillId!! + + cancellationSignal.setOnCancelListener { + // Handle cancellation + } + + serviceScope.launch { + val passcodes = + PasscodeDatabase.getDatabase(applicationContext).passcodeDao().getAllPasscodes() + .first() + val responseBuilder = FillResponse.Builder() + + for (passcode in passcodes) { + val presentation = RemoteViews(packageName, R.layout.autofill_list_item).apply { + setTextViewText(R.id.autofill_username, passcode.name) + } + + val dataset = android.service.autofill.Dataset.Builder(presentation) + .setValue(usernameId, AutofillValue.forText(passcode.name)) + .setValue(passwordId, AutofillValue.forText(passcode.value)) + .build() + responseBuilder.addDataset(dataset) + } + + callback.onSuccess(responseBuilder.build()) + } + } + + override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { + val context = request.fillContexts + val structure = context.last().structure + + val viewNodes = mutableMapOf() + parseStructure(structure.getWindowNodeAt(0).rootViewNode, viewNodes) + + val usernameNode = viewNodes["username"] ?: viewNodes["emailAddress"] + val passwordNode = viewNodes["password"] + + val username = usernameNode?.text?.toString() + val password = passwordNode?.text?.toString() + + if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) { + serviceScope.launch { + PasscodeDatabase.getDatabase(applicationContext).passcodeDao().insert( + Passcode(name = username, value = password) + ) + } + callback.onSuccess() + } else { + callback.onFailure("Could not save credentials.") + } + } + + private fun parseStructure( + node: AssistStructure.ViewNode, + viewNodes: MutableMap + ) { + node.autofillHints?.forEach { hint -> + if (!viewNodes.containsKey(hint)) { + viewNodes[hint] = node + } + } + + for (i in 0 until node.childCount) { + parseStructure(node.getChildAt(i), viewNodes) + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt new file mode 100644 index 00000000..e175e96f --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt @@ -0,0 +1,12 @@ +package com.jeeldobariya.passcodes.data + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "passcodes") +data class Passcode( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val name: String, + val value: String +) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt new file mode 100644 index 00000000..957d5a37 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt @@ -0,0 +1,28 @@ +package com.jeeldobariya.passcodes.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow + +@Dao +interface PasscodeDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(passcode: Passcode) + + @Update + suspend fun update(passcode: Passcode) + + @Delete + suspend fun delete(passcode: Passcode) + + @Query("SELECT * FROM passcodes ORDER BY name ASC") + fun getAllPasscodes(): Flow> + + @Query("SELECT * FROM passcodes WHERE id = :id") + fun getPasscode(id: Int): Flow +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt new file mode 100644 index 00000000..1fe03c95 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt @@ -0,0 +1,29 @@ +package com.jeeldobariya.passcodes.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Passcode::class], version = 1, exportSchema = false) +abstract class PasscodeDatabase : RoomDatabase() { + + abstract fun passcodeDao(): PasscodeDao + + companion object { + @Volatile + private var INSTANCE: PasscodeDatabase? = null + + fun getDatabase(context: Context): PasscodeDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + PasscodeDatabase::class.java, + "passcode_database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/database/MasterDatabase.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/database/MasterDatabase.kt index 9adf6926..32515b70 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/database/MasterDatabase.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/database/MasterDatabase.kt @@ -1,9 +1,9 @@ -package com.jeeldobariya.passcodes.database; +package com.jeeldobariya.passcodes.database -import android.content.Context; -import androidx.room.Database; -import androidx.room.Room; -import androidx.room.RoomDatabase; +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase @Database( entities = [Password::class], @@ -19,13 +19,13 @@ abstract class MasterDatabase : RoomDatabase() { fun getDatabase(context: Context): MasterDatabase { return INSTANCE ?: synchronized(this) { - + val instance = Room.databaseBuilder( context.applicationContext, MasterDatabase::class.java, "master" ) - .build() + .build() INSTANCE = instance instance } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/database/PasswordsDao.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/database/PasswordsDao.kt index 1c7ba6c9..55b246c7 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/database/PasswordsDao.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/database/PasswordsDao.kt @@ -1,10 +1,10 @@ package com.jeeldobariya.passcodes.database import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Update -import androidx.room.Delete import kotlinx.coroutines.flow.Flow @Dao diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/di/appModule.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/di/appModule.kt new file mode 100644 index 00000000..04dc71b1 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/di/appModule.kt @@ -0,0 +1,34 @@ +package com.jeeldobariya.passcodes.di + +import com.jeeldobariya.passcodes.ui.LoadPasswordViewModel +import com.jeeldobariya.passcodes.ui.SavePasswordViewModel +import com.jeeldobariya.passcodes.ui.UpdatePasswordViewModel +import com.jeeldobariya.passcodes.ui.ViewPasswordViewModel +import com.jeeldobariya.passcodes.utils.Controller +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val appModule = module { + + single { + Controller(androidContext()) + } + + viewModel { + UpdatePasswordViewModel(get()) + } + + viewModel { + SavePasswordViewModel(get()) + } + + viewModel { + LoadPasswordViewModel(get()) + } + + viewModel { + ViewPasswordViewModel(get()) + } + +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt index 1bd2445b..b42f0a89 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/flags/FeatureFlagManager.kt @@ -5,14 +5,18 @@ import androidx.core.content.edit import com.jeeldobariya.passcodes.utils.Constant class FeatureFlagManager private constructor(context: Context) { - private val prefs = context.getSharedPreferences(Constant.FEATURE_FLAGS_PREFS_NAME, Context.MODE_PRIVATE) + private val prefs = + context.getSharedPreferences(Constant.FEATURE_FLAGS_PREFS_NAME, Context.MODE_PRIVATE) var latestFeaturesEnabled: Boolean get() = prefs.getBoolean(Constant.LATEST_FEATURES_KEY, false) - set(value) { prefs.edit { putBoolean(Constant.LATEST_FEATURES_KEY, value) } } + set(value) { + prefs.edit { putBoolean(Constant.LATEST_FEATURES_KEY, value) } + } companion object { - @Volatile private var INSTANCE: FeatureFlagManager? = null + @Volatile + private var INSTANCE: FeatureFlagManager? = null fun get(context: Context): FeatureFlagManager { return INSTANCE ?: synchronized(this) { diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutUsActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutUsActivity.kt index b8824ab8..8fb28137 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutUsActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutUsActivity.kt @@ -1,55 +1,55 @@ package com.jeeldobariya.passcodes.ui -import com.jeeldobariya.passcodes.databinding.ActivityAboutUsBinding -import com.jeeldobariya.passcodes.utils.Constant import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri +import com.jeeldobariya.passcodes.databinding.ActivityAboutUsBinding import com.jeeldobariya.passcodes.utils.CommonUtils +import com.jeeldobariya.passcodes.utils.Constant class AboutUsActivity : AppCompatActivity() { - private lateinit var binding: ActivityAboutUsBinding - - override fun onCreate(savedInstanceState: Bundle?) { - CommonUtils.updateCurrTheme(this) - super.onCreate(savedInstanceState) - binding = ActivityAboutUsBinding.inflate(layoutInflater) - setContentView(binding.root) + private lateinit var binding: ActivityAboutUsBinding - binding.toolbar.setNavigationOnClickListener { - onBackPressedDispatcher.onBackPressed() - } + override fun onCreate(savedInstanceState: Bundle?) { + CommonUtils.updateCurrTheme(this) + super.onCreate(savedInstanceState) + binding = ActivityAboutUsBinding.inflate(layoutInflater) + setContentView(binding.root) - // Set up button click listeners - setupButtonListeners() - } + binding.toolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } - private fun openBrowser(url: String) { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } - - private fun setupButtonListeners() { - binding.cardSecurityGuidelines.setOnClickListener { - openBrowser(Constant.SECURITY_GUIDE_URL) + // Set up button click listeners + setupButtonListeners() } - binding.cardReleaseNotes.setOnClickListener { - openBrowser(Constant.RELEASE_NOTE_URL) + private fun openBrowser(url: String) { + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) } - binding.cardLicense.setOnClickListener { - startActivity(Intent(this, LicenseActivity::class.java)) - } + private fun setupButtonListeners() { + binding.cardSecurityGuidelines.setOnClickListener { + openBrowser(Constant.SECURITY_GUIDE_URL) + } - binding.cardReportBug.setOnClickListener { - openBrowser(Constant.REPORT_BUG_URL) - } + binding.cardReleaseNotes.setOnClickListener { + openBrowser(Constant.RELEASE_NOTE_URL) + } + + binding.cardLicense.setOnClickListener { + startActivity(Intent(this, LicenseActivity::class.java)) + } + + binding.cardReportBug.setOnClickListener { + openBrowser(Constant.REPORT_BUG_URL) + } - binding.cardTelegramCommunity.setOnClickListener { - openBrowser(Constant.TELEGRAM_COMMUNITY_URL) + binding.cardTelegramCommunity.setOnClickListener { + openBrowser(Constant.TELEGRAM_COMMUNITY_URL) + } } - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordActivity.kt index e53687ce..39491487 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordActivity.kt @@ -1,29 +1,24 @@ package com.jeeldobariya.passcodes.ui -import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.database.Password import com.jeeldobariya.passcodes.databinding.ActivityLoadPasswordBinding import com.jeeldobariya.passcodes.ui.adapter.PasswordAdapter import com.jeeldobariya.passcodes.utils.CommonUtils -import com.jeeldobariya.passcodes.utils.Controller -import com.jeeldobariya.passcodes.utils.DatabaseOperationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.flow.catch +import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import org.koin.androidx.viewmodel.ext.android.viewModel class LoadPasswordActivity : AppCompatActivity() { + private val viewModel: LoadPasswordViewModel by viewModel() + private lateinit var binding: ActivityLoadPasswordBinding private lateinit var passwordAdapter: PasswordAdapter - private lateinit var controller: Controller override fun onCreate(savedInstanceState: Bundle?) { CommonUtils.updateCurrTheme(this) @@ -31,50 +26,37 @@ class LoadPasswordActivity : AppCompatActivity() { binding = ActivityLoadPasswordBinding.inflate(layoutInflater) setContentView(binding.root) - controller = Controller(this) // Initialize the controller here + collectLatestLifecycleFlow(viewModel.passwordsListState) { passwordList -> + if (!this@LoadPasswordActivity::passwordAdapter.isInitialized) { + passwordAdapter = + PasswordAdapter(this@LoadPasswordActivity, passwordList) + binding.passwordList.adapter = passwordAdapter + } else { + passwordAdapter.updateData(passwordList) + } + } + + collectLatestLifecycleFlow(viewModel.isErrorState) { error -> + if (error) { + Toast.makeText( + this@LoadPasswordActivity, + "${getString(R.string.something_went_wrong_msg)}", + Toast.LENGTH_LONG + ).show() + } + } // Add event onclick listener addOnClickListenerOnButton() // Make window fullscreen WindowCompat.setDecorFitsSystemWindows(window, false) - - // Start collecting the password list Flow when the activity is created - // This collection will automatically update the UI when database changes occur. - collectPasswordList() } - private fun collectPasswordList() { - lifecycleScope.launch { - controller.getAllPasswords() - .catch { e -> - withContext(Dispatchers.Main) { - Toast.makeText( - this@LoadPasswordActivity, - "${getString(R.string.something_went_wrong_msg)}: ${e.message}", - Toast.LENGTH_LONG - ).show() - e.printStackTrace() - // Ensure adapter is initialized with empty list on error - if (!this@LoadPasswordActivity::passwordAdapter.isInitialized) { - passwordAdapter = PasswordAdapter(this@LoadPasswordActivity, emptyList()) - binding.passwordList.adapter = passwordAdapter - } - } - } - .collect { passwordList -> - // This block will be executed every time the list of passwords changes in the database - // and emitted by the Flow. - withContext(Dispatchers.Main) { // Ensure UI updates are on the main thread - if (!this@LoadPasswordActivity::passwordAdapter.isInitialized) { - passwordAdapter = PasswordAdapter(this@LoadPasswordActivity, passwordList) - binding.passwordList.adapter = passwordAdapter - } else { - passwordAdapter.updateData(passwordList) - } - } - } - } + override fun onResume() { + super.onResume() + + viewModel.loadInitialData() } // Added all the onclick event listeners @@ -89,16 +71,4 @@ class LoadPasswordActivity : AppCompatActivity() { startActivity(intent) } } - - // onResume is no longer needed to explicitly call fillPasswordList() - // because Flow collection started in onCreate will handle updates. - // However, if your activity might be killed and recreated, the onCreate will - // re-initiate the collection. For simple cases, this is fine. - // If you need to stop collection when the activity goes to background, - // and restart on foreground, you might manage the coroutine job more explicitly. - // But lifecycleScope handles stopping on destroy for you. - // In this specific setup, `onResume`'s `super.onResume()` is enough. - // No need for a custom `onResume` override anymore if it only contained `fillPasswordList()` - // and `collectPasswordList()` is in `onCreate`. - // Removed the `onResume` override as it's now redundant with Flow collection in onCreate. } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordViewModel.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordViewModel.kt new file mode 100644 index 00000000..e682e89b --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/LoadPasswordViewModel.kt @@ -0,0 +1,35 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.jeeldobariya.passcodes.database.Password +import com.jeeldobariya.passcodes.utils.Controller +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class LoadPasswordViewModel( + var controller: Controller +) : ViewModel() { + + private val _passwordsListState = MutableStateFlow(emptyList()) + val passwordsListState = _passwordsListState.asStateFlow() + + private val _isErrorState = MutableStateFlow(false) + val isErrorState = _isErrorState.asStateFlow() + + fun loadInitialData() { + viewModelScope.launch { + _passwordsListState.update { + controller.getAllPasswords().catch { + _isErrorState.update { + true + } + }.first() + } + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt index 8d97690a..1789a3f8 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt @@ -1,20 +1,16 @@ package com.jeeldobariya.passcodes.ui -import android.content.Context -import android.os.Bundle import android.content.Intent +import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import android.view.LayoutInflater import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.BuildConfig import com.jeeldobariya.passcodes.databinding.ActivityMainBinding import com.jeeldobariya.passcodes.utils.CommonUtils import com.jeeldobariya.passcodes.utils.UpdateChecker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch // import com.jeeldobariya.passcodes.utils.Permissions diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/PasswordManagerActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/PasswordManagerActivity.kt index 3796e70f..e28777d2 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/PasswordManagerActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/PasswordManagerActivity.kt @@ -1,9 +1,8 @@ package com.jeeldobariya.passcodes.ui import android.content.Intent -import android.view.View.GONE import android.os.Bundle -import android.util.Log +import android.view.View.GONE import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -49,9 +48,10 @@ class PasswordManagerActivity : AppCompatActivity() { if (result.resultCode == RESULT_OK) { val uri = result.data?.data if (uri != null) { - val CSVData: String? = contentResolver.openInputStream(uri)?.bufferedReader()?.use { - it.readText() - } + val CSVData: String? = + contentResolver.openInputStream(uri)?.bufferedReader()?.use { + it.readText() + } lifecycleScope.launch(Dispatchers.IO) { if (CSVData != null) { @@ -64,7 +64,7 @@ class PasswordManagerActivity : AppCompatActivity() { getString(R.string.import_success, result[0]), Toast.LENGTH_LONG ).show() - + if (result[1] != 0) { Toast.makeText( this@PasswordManagerActivity, @@ -97,7 +97,8 @@ class PasswordManagerActivity : AppCompatActivity() { contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write(tmpExportCSVData!!.toByteArray()) } - Toast.makeText(this, getString(R.string.export_success), Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.export_success), Toast.LENGTH_SHORT) + .show() } } } @@ -122,13 +123,21 @@ class PasswordManagerActivity : AppCompatActivity() { } binding.importPasswordBtn.setOnClickListener { - Toast.makeText(this@PasswordManagerActivity, getString(R.string.preview_feature), Toast.LENGTH_LONG).show() + Toast.makeText( + this@PasswordManagerActivity, + getString(R.string.preview_feature), + Toast.LENGTH_LONG + ).show() importCsvFilePicker() } binding.exportPasswordBtn.setOnClickListener { - Toast.makeText(this@PasswordManagerActivity, getString(R.string.preview_feature), Toast.LENGTH_LONG).show() + Toast.makeText( + this@PasswordManagerActivity, + getString(R.string.preview_feature), + Toast.LENGTH_LONG + ).show() lifecycleScope.launch(Dispatchers.IO) { val csvDataExportBlob = controller.exportDataToCsvString() diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordActivity.kt index 2f2c846b..7b9c85fb 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordActivity.kt @@ -1,23 +1,17 @@ package com.jeeldobariya.passcodes.ui import android.os.Bundle -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivitySavePasswordBinding import com.jeeldobariya.passcodes.utils.CommonUtils -import com.jeeldobariya.passcodes.utils.Controller -import com.jeeldobariya.passcodes.utils.DatabaseOperationException -import com.jeeldobariya.passcodes.utils.InvalidInputException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import org.koin.androidx.viewmodel.ext.android.viewModel class SavePasswordActivity : AppCompatActivity() { - private lateinit var controller: Controller + private val viewModel: SavePasswordViewModel by viewModel() + private lateinit var binding: ActivitySavePasswordBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -26,75 +20,48 @@ class SavePasswordActivity : AppCompatActivity() { binding = ActivitySavePasswordBinding.inflate(layoutInflater) setContentView(binding.root) - controller = Controller(this) // Initialize controller - - // Add event onclick listener - addOnClickListenerOnButton() - - // Make window fullscreen - WindowCompat.setDecorFitsSystemWindows(window, false) - } - - // Added all the onclick event listeners - private fun addOnClickListenerOnButton() { - binding.inputDomain.setOnFocusChangeListener { v, hasFocus -> + binding.inputDomain.setOnFocusChangeListener { v, hasFocus -> if (hasFocus) { - binding.inputDomain.setHint(getString(R.string.placeholder_domain_field)); + binding.inputDomain.setHint(getString(R.string.placeholder_domain_field)) } else { - binding.inputDomain.setHint(""); + binding.inputDomain.setHint("") } - }; + } - binding.inputUsername.setOnFocusChangeListener { v, hasFocus -> + binding.inputUsername.setOnFocusChangeListener { v, hasFocus -> if (hasFocus) { - binding.inputUsername.setHint(getString(R.string.placeholder_username_field)); + binding.inputUsername.setHint(getString(R.string.placeholder_username_field)) } else { - binding.inputUsername.setHint(""); + binding.inputUsername.setHint("") } - }; + } - binding.inputPassword.setOnFocusChangeListener { v, hasFocus -> + binding.inputPassword.setOnFocusChangeListener { v, hasFocus -> if (hasFocus) { - binding.inputPassword.setHint(getString(R.string.placeholder_password_field)); + binding.inputPassword.setHint(getString(R.string.placeholder_password_field)) } else { - binding.inputPassword.setHint(""); + binding.inputPassword.setHint("") } - }; + } - binding.savePasswordBtn.setOnClickListener { - val domain = binding.inputDomain.text.toString() - val username = binding.inputUsername.text.toString() - val password = binding.inputPassword.text.toString() - val notes = binding.inputNotes.text.toString() + // Add event onclick listener + addOnClickListenerOnButton() - performSavePasswordAction(domain, username, password, notes) - } + // Make window fullscreen + WindowCompat.setDecorFitsSystemWindows(window, false) } - fun performSavePasswordAction(domain: String, username: String, password: String, notes: String) { - // Launch a coroutine to call the suspend function - lifecycleScope.launch { - try { - val rowId = controller.savePasswordEntity(domain, username, password, notes) - // Switch back to Main dispatcher for UI updates - withContext(Dispatchers.Main) { - Toast.makeText(this@SavePasswordActivity, "${getString(R.string.success_clause)} $rowId", Toast.LENGTH_SHORT).show() - finish() - } - } catch (e: InvalidInputException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@SavePasswordActivity, getString(R.string.warn_fill_form), Toast.LENGTH_SHORT).show() - } - } catch (e: DatabaseOperationException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@SavePasswordActivity, "${getString(R.string.fail_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Toast.makeText(this@SavePasswordActivity, "${getString(R.string.fail_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - } + // Added all the onclick event listeners + private fun addOnClickListenerOnButton() { + binding.savePasswordBtn.setOnClickListener { + viewModel.onChangeDomainText(binding.inputDomain.text.toString()) + viewModel.onChangeUsernameText(binding.inputUsername.text.toString()) + viewModel.onChangePasswordText(binding.inputPassword.text.toString()) + viewModel.onChangeNotesText(binding.inputNotes.text.toString()) + + viewModel.onSavePasswordButtonClick() + if (!viewModel.isErrorState.value) { + finish() } } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordViewModel.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordViewModel.kt new file mode 100644 index 00000000..793d648a --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SavePasswordViewModel.kt @@ -0,0 +1,69 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.jeeldobariya.passcodes.utils.Controller +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class SavePasswordViewModel( + val controller: Controller +) : ViewModel() { + private val _domainState = MutableStateFlow("") + // val domainState = _domainState.asStateFlow() + + private val _usernameState = MutableStateFlow("") + // val usernameState = _usernameState.asStateFlow() + + private val _passwordState = MutableStateFlow("") + // val passwordState = _passwordState.asStateFlow() + + private val _notesState = MutableStateFlow("") + // val notesState = _notesState.asStateFlow() + + private val _isErrorState = MutableStateFlow(false) + val isErrorState = _isErrorState.asStateFlow() + + fun onChangeDomainText(text: String) { + _domainState.update { + text + } + } + + fun onChangeUsernameText(text: String) { + _usernameState.update { + text + } + } + + fun onChangePasswordText(text: String) { + _passwordState.update { + text + } + } + + fun onChangeNotesText(text: String) { + _notesState.update { + text + } + } + + fun onSavePasswordButtonClick() { + viewModelScope.launch { + try { + controller.savePasswordEntity( + _domainState.value, + _usernameState.value, + _passwordState.value, + _notesState.value + ) + } catch (e: Exception) { + _isErrorState.update { + true + } + } + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsActivity.kt index 492d7edd..ff164a16 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsActivity.kt @@ -2,20 +2,18 @@ package com.jeeldobariya.passcodes.ui import android.content.Context import android.os.Bundle -import android.widget.Toast +import android.view.View import android.widget.AdapterView +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit import androidx.core.os.LocaleListCompat -import android.view.View import androidx.core.view.WindowCompat -import android.view.LayoutInflater - +import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R import com.jeeldobariya.passcodes.databinding.ActivitySettingsBinding import com.jeeldobariya.passcodes.flags.FeatureFlagManager -import androidx.core.content.edit -import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.utils.CommonUtils import com.jeeldobariya.passcodes.utils.Constant import com.jeeldobariya.passcodes.utils.Controller @@ -66,27 +64,38 @@ class SettingsActivity : AppCompatActivity() { } binding.langSwitchDropdown.setSelection(0) - Toast.makeText(this@SettingsActivity, getString(R.string.something_went_wrong_msg), Toast.LENGTH_SHORT).show() + Toast.makeText( + this@SettingsActivity, + getString(R.string.something_went_wrong_msg), + Toast.LENGTH_SHORT + ).show() } // Added all the onclick event listeners private fun addOnClickListenerOnButton() { - binding.langSwitchDropdown.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - val languageTags = resources.getStringArray(R.array.lang_locale_tags) - val localeTag = languageTags[position] - val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(localeTag) - AppCompatDelegate.setApplicationLocales(appLocale) + binding.langSwitchDropdown.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + val languageTags = resources.getStringArray(R.array.lang_locale_tags) + val localeTag = languageTags[position] + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(localeTag) + AppCompatDelegate.setApplicationLocales(appLocale) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // Not needed in this case, as we've already set a default + } } - override fun onNothingSelected(parent: AdapterView<*>?) { - // Not needed in this case, as we've already set a default - } - } - binding.toggleThemeBtn.setOnClickListener { - val sharedPrefs = getSharedPreferences(Constant.APP_PREFS_NAME, Context.MODE_PRIVATE) - val currentThemeStyle = sharedPrefs.getInt(Constant.THEME_KEY, R.style.PasscodesTheme_Default) + val sharedPrefs = getSharedPreferences(Constant.APP_PREFS_NAME, MODE_PRIVATE) + val currentThemeStyle = + sharedPrefs.getInt(Constant.THEME_KEY, R.style.PasscodesTheme_Default) val currentIndex = THEMES.indexOf(currentThemeStyle) val nextIndex = (currentIndex + 1) % THEMES.size @@ -96,20 +105,29 @@ class SettingsActivity : AppCompatActivity() { sharedPrefs.edit { putInt(Constant.THEME_KEY, newThemeStyle) } recreate() - Toast.makeText(this@SettingsActivity, getString(R.string.restart_app_require), Toast.LENGTH_SHORT).show() + Toast.makeText( + this@SettingsActivity, + getString(R.string.restart_app_require), + Toast.LENGTH_SHORT + ).show() } binding.switchLatestFeatures.setOnCheckedChangeListener { _, isChecked -> FeatureFlagManager.get(this).latestFeaturesEnabled = isChecked - Toast.makeText(this@SettingsActivity, getString(R.string.future_feat_clause) + isChecked.toString(), Toast.LENGTH_SHORT).show() + Toast.makeText( + this@SettingsActivity, + getString(R.string.future_feat_clause) + isChecked.toString(), + Toast.LENGTH_SHORT + ).show() } binding.clearAllDataBtn.setOnClickListener { v -> - lifecycleScope.launch { + lifecycleScope.launch { controller.clearAllData() } - Toast.makeText(this@SettingsActivity, "Delete the user data!!", Toast.LENGTH_SHORT).show() + Toast.makeText(this@SettingsActivity, "Delete the user data!!", Toast.LENGTH_SHORT) + .show() } } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordActivity.kt index 8f0f7288..287139ee 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordActivity.kt @@ -1,53 +1,59 @@ package com.jeeldobariya.passcodes.ui -import android.content.Context -import android.content.Intent +import android.app.AlertDialog import android.os.Bundle import android.widget.Toast -import android.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R -import com.jeeldobariya.passcodes.database.Password import com.jeeldobariya.passcodes.databinding.ActivityUpdatePasswordBinding import com.jeeldobariya.passcodes.utils.CommonUtils -import com.jeeldobariya.passcodes.utils.Controller -import com.jeeldobariya.passcodes.utils.DatabaseOperationException -import com.jeeldobariya.passcodes.utils.InvalidInputException -import com.jeeldobariya.passcodes.utils.PasswordNotFoundException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import org.koin.androidx.viewmodel.ext.android.viewModel + /* * Activity expects id as intent parameters. */ class UpdatePasswordActivity : AppCompatActivity() { - private var passwordEntityId: Int = 0 - private lateinit var controller: Controller + private val viewModel: UpdatePasswordViewModel by viewModel() + private lateinit var binding: ActivityUpdatePasswordBinding override fun onCreate(savedInstanceState: Bundle?) { CommonUtils.updateCurrTheme(this) + super.onCreate(savedInstanceState) binding = ActivityUpdatePasswordBinding.inflate(layoutInflater) setContentView(binding.root) val intent = intent - passwordEntityId = intent.getIntExtra("id", -1) // -1 is an invalid id. + val passwordEntityId = intent.getIntExtra("id", -1) // -1 is an invalid id. if (passwordEntityId == -1) { // invalid entity - Toast.makeText(this, getString(R.string.error_invalid_password_id), Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.error_invalid_password_id), Toast.LENGTH_SHORT) + .show() finish() return // Exit onCreate if ID is invalid } - controller = Controller(this) + viewModel.loadInitialData(passwordEntityId) + + binding.tvId.text = "${getString(R.string.id_prefix)} ${viewModel.passwordEntityId}" - // Fill data into views - fillDataInViews() // Call this to populate initial data + collectLatestLifecycleFlow(viewModel.domainState) { domain -> + binding.inputDomain.setText(domain) + } + collectLatestLifecycleFlow(viewModel.usernameState) { username -> + binding.inputUsername.setText(username) + } + collectLatestLifecycleFlow(viewModel.passwordState) { password -> + binding.inputPassword.setText(password) + } + collectLatestLifecycleFlow(viewModel.notesState) { notes -> + binding.inputNotes.setText(notes) + } // Add event onclick listener addOnClickListenerOnButton() @@ -56,96 +62,30 @@ class UpdatePasswordActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) } - private fun fillDataInViews() { - lifecycleScope.launch { - try { - val passwordEntity: Password = controller.getPasswordById(passwordEntityId) - withContext(Dispatchers.Main) { - binding.tvId.text = "${getString(R.string.id_prefix)} $passwordEntityId" - binding.inputDomain.setText(passwordEntity.domain) - binding.inputUsername.setText(passwordEntity.username) - binding.inputPassword.setText(passwordEntity.password) - binding.inputNotes.setText(passwordEntity.notes) - } - } catch (e: PasswordNotFoundException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, e.message, Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() - } - } catch (e: DatabaseOperationException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, "${getString(R.string.something_went_wrong_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, "${getString(R.string.something_went_wrong_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() - } - } - } - } - // Added all the onclick event listeners private fun addOnClickListenerOnButton() { binding.updatePasswordBtn.setOnClickListener { - val newDomain = binding.inputDomain.text.toString() - val newUsername = binding.inputUsername.text.toString() - val newPassword = binding.inputPassword.text.toString() - val newNotes = binding.inputNotes.text.toString() + viewModel.onChangeDomainText(binding.inputDomain.text.toString()) + viewModel.onChangeUsernameText(binding.inputUsername.text.toString()) + viewModel.onChangePasswordText(binding.inputPassword.text.toString()) + viewModel.onChangeNotesText(binding.inputNotes.text.toString()) val confirmDialog = AlertDialog.Builder(this@UpdatePasswordActivity) .setTitle(R.string.update_password_dialog_title) .setMessage(R.string.irreversible_dialog_desc) - .setPositiveButton(R.string.confirm_dialog_button_text) { dialog, which -> - performUpdatePasswordAction(newDomain, newUsername, newPassword, newNotes); - } - .setNegativeButton(R.string.discard_dialog_button_text) { dialog, which -> - Toast.makeText(this, getString(R.string.action_discard), Toast.LENGTH_SHORT).show(); - } - .create(); - - confirmDialog.show(); - } - } - - fun performUpdatePasswordAction(newDomain: String, newUsername: String, newPassword: String, newNotes: String) { - lifecycleScope.launch { - try { - val rowsAffected = controller.updatePassword(passwordEntityId, newDomain, newUsername, newPassword, newNotes) - withContext(Dispatchers.Main) { - if (rowsAffected > 0) { - Toast.makeText(this@UpdatePasswordActivity, getString(R.string.update_success_msg), Toast.LENGTH_SHORT).show() - finish() - } else { - Toast.makeText(this@UpdatePasswordActivity, getString(R.string.something_went_wrong_msg) + ": No changes applied or password not found.", Toast.LENGTH_SHORT).show() + .setPositiveButton(R.string.confirm_dialog_button_text) { dialog, which -> + viewModel.onUpdatePasswordButtonClick() + if (!viewModel.isErrorState.value) { finish() } } - } catch (e: InvalidInputException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, getString(R.string.warn_fill_form), Toast.LENGTH_SHORT).show() - } - } catch (e: PasswordNotFoundException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, e.message, Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() + .setNegativeButton(R.string.discard_dialog_button_text) { dialog, which -> + Toast.makeText(this, getString(R.string.action_discard), Toast.LENGTH_SHORT) + .show() } - } catch (e: DatabaseOperationException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, "${getString(R.string.fail_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Toast.makeText(this@UpdatePasswordActivity, "${getString(R.string.fail_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - } - } + .create() + + confirmDialog.show() } } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordViewModel.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordViewModel.kt new file mode 100644 index 00000000..94800511 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/UpdatePasswordViewModel.kt @@ -0,0 +1,93 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.jeeldobariya.passcodes.database.Password +import com.jeeldobariya.passcodes.utils.Controller +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class UpdatePasswordViewModel( + val controller: Controller +) : ViewModel() { + + var passwordEntityId: Int = -1 + + private val _domainState = MutableStateFlow("") + val domainState = _domainState.asStateFlow() + + private val _usernameState = MutableStateFlow("") + val usernameState = _usernameState.asStateFlow() + + private val _passwordState = MutableStateFlow("") + val passwordState = _passwordState.asStateFlow() + + private val _notesState = MutableStateFlow("") + val notesState = _notesState.asStateFlow() + + private val _isErrorState = MutableStateFlow(false) + val isErrorState = _isErrorState.asStateFlow() + + fun loadInitialData(passwordId: Int) { + passwordEntityId = passwordId + + viewModelScope.launch { + try { + val password: Password = controller.getPasswordById(passwordId) + + _domainState.update { password.domain } + _usernameState.update { password.username } + _passwordState.update { password.password } + _notesState.update { password.notes } + } catch (e: Exception) { + _isErrorState.update { + true + } + } + } + } + + fun onChangeDomainText(text: String) { + _domainState.update { + text + } + } + + fun onChangeUsernameText(text: String) { + _usernameState.update { + text + } + } + + fun onChangePasswordText(text: String) { + _passwordState.update { + text + } + } + + fun onChangeNotesText(text: String) { + _notesState.update { + text + } + } + + fun onUpdatePasswordButtonClick() { + viewModelScope.launch { + try { + controller.updatePassword( + passwordEntityId, + _domainState.value, + _usernameState.value, + _passwordState.value, + _notesState.value + ) + } catch (e: Exception) { + _isErrorState.update { + true + } + } + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordActivity.kt index 536c3471..54138ccd 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordActivity.kt @@ -1,54 +1,80 @@ package com.jeeldobariya.passcodes.ui -import android.content.ClipData; +import android.app.AlertDialog +import android.content.ClipData import android.content.ClipboardManager -import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast -import android.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope import com.jeeldobariya.passcodes.R -import com.jeeldobariya.passcodes.database.Password import com.jeeldobariya.passcodes.databinding.ActivityViewPasswordBinding import com.jeeldobariya.passcodes.flags.FeatureFlagManager import com.jeeldobariya.passcodes.utils.CommonUtils -import com.jeeldobariya.passcodes.utils.Controller -import com.jeeldobariya.passcodes.utils.DatabaseOperationException -import com.jeeldobariya.passcodes.utils.DateTimeUtils -import com.jeeldobariya.passcodes.utils.PasswordNotFoundException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import com.jeeldobariya.passcodes.utils.collectLatestLifecycleFlow +import kotlinx.coroutines.runBlocking +import org.koin.androidx.viewmodel.ext.android.viewModel /* * Activity expects id as intent parameters. */ class ViewPasswordActivity : AppCompatActivity() { - private var passwordEntityId: Int = 0 + private val viewModel: ViewPasswordViewModel by viewModel() + private lateinit var binding: ActivityViewPasswordBinding - private lateinit var controller: Controller - private lateinit var passwordEntity: Password + override fun onCreate(savedInstanceState: Bundle?) { CommonUtils.updateCurrTheme(this) + super.onCreate(savedInstanceState) binding = ActivityViewPasswordBinding.inflate(layoutInflater) setContentView(binding.root) val intent = intent - passwordEntityId = intent.getIntExtra("id", -1) // -1 is an invalid id. + val passwordEntityId = intent.getIntExtra("id", -1) // -1 is an invalid id. if (passwordEntityId == -1) { // invalid entity - Toast.makeText(this, getString(R.string.error_invalid_password_id), Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.error_invalid_password_id), Toast.LENGTH_SHORT) + .show() finish() return // Exit onCreate if ID is invalid } - controller = Controller(this) + viewModel.loadInitialData(passwordEntityId) + + collectLatestLifecycleFlow(viewModel.domainState) { domain -> + binding.tvDomain.text = + "${getString(R.string.domain_prefix)} $domain" + } + collectLatestLifecycleFlow(viewModel.usernameState) { username -> + binding.tvUsername.text = + "${getString(R.string.username_prefix)} $username" + } + collectLatestLifecycleFlow(viewModel.passwordState) { password -> + binding.tvPassword.text = + "${getString(R.string.password_prefix)} $password" + } + collectLatestLifecycleFlow(viewModel.notesState) { notes -> + binding.tvNotes.text = + "${getString(R.string.notes_prefix)} $notes" + } + collectLatestLifecycleFlow(viewModel.lastUpdatedAtState) { lastUpdatedAt -> + binding.tvUpdatedAt.text = + "${getString(R.string.updatedat_prefix)} $lastUpdatedAt" + } + collectLatestLifecycleFlow(viewModel.isErrorState) { error -> + if (error) { + Toast.makeText( + this@ViewPasswordActivity, + getString(R.string.something_went_wrong_msg), + Toast.LENGTH_LONG + ).show() + finish() + } + } binding.copyPasswordBtn.isEnabled = FeatureFlagManager.get(this).latestFeaturesEnabled @@ -59,41 +85,6 @@ class ViewPasswordActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) } - private fun fillDataInTextview() { - lifecycleScope.launch { - try { - passwordEntity = controller.getPasswordById(passwordEntityId) - withContext(Dispatchers.Main) { - val lastUpdatedAt = DateTimeUtils.getRelativeDays(passwordEntity.updatedAt.orEmpty(), "yyyy-MM-dd HH:mm:ss") - - binding.tvDomain.text = "${getString(R.string.domain_prefix)} ${passwordEntity.domain}" - binding.tvUsername.text = "${getString(R.string.username_prefix)} ${passwordEntity.username}" - binding.tvPassword.text = "${getString(R.string.password_prefix)} ${passwordEntity.password}" - binding.tvNotes.text = "${getString(R.string.notes_prefix)} ${passwordEntity.notes}" - binding.tvUpdatedAt.text = "${getString(R.string.updatedat_prefix)} ${lastUpdatedAt}" - } - } catch (e: PasswordNotFoundException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@ViewPasswordActivity, e.message, Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() - } - } catch (e: DatabaseOperationException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@ViewPasswordActivity, "${getString(R.string.something_went_wrong_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Toast.makeText(this@ViewPasswordActivity, "${getString(R.string.something_went_wrong_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - finish() - } - } - } - } - // Added all the onclick event listeners private fun addOnClickListenerOnButton() { binding.copyPasswordBtn.setOnClickListener { @@ -102,29 +93,36 @@ class ViewPasswordActivity : AppCompatActivity() { val confirmDialog = AlertDialog.Builder(this@ViewPasswordActivity) .setTitle(R.string.copy_password_dialog_title) .setMessage(R.string.danger_copy_to_clipboard_desc) - .setPositiveButton(R.string.confirm_dialog_button_text) { dialog, which -> - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - val clip: ClipData = ClipData.newPlainText(passwordEntity.username, passwordEntity.password) + .setPositiveButton(R.string.confirm_dialog_button_text) { dialog, which -> + val clipboard = getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager + val clip: ClipData = + ClipData.newPlainText( + viewModel.usernameState.value, + viewModel.passwordState.value + ) // Set the ClipData to the clipboard if (clipboard != null) { clipboard.setPrimaryClip(clip) - Toast.makeText(this, getString(R.string.copy_success), Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.copy_success), Toast.LENGTH_SHORT) + .show() } else { - Toast.makeText(this, "Clipboard service not available.", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Clipboard service not available.", Toast.LENGTH_SHORT) + .show() } } - .setNegativeButton(R.string.discard_dialog_button_text) { dialog, which -> - Toast.makeText(this, getString(R.string.action_discard), Toast.LENGTH_SHORT).show(); + .setNegativeButton(R.string.discard_dialog_button_text) { dialog, which -> + Toast.makeText(this, getString(R.string.action_discard), Toast.LENGTH_SHORT) + .show() } - .create(); + .create() - confirmDialog.show(); + confirmDialog.show() } binding.updatePasswordBtn.setOnClickListener { val viewPasswordIntent = Intent(this, UpdatePasswordActivity::class.java) - viewPasswordIntent.putExtra("id", passwordEntityId) + viewPasswordIntent.putExtra("id", viewModel.passwordEntityId) startActivity(viewPasswordIntent) } @@ -132,47 +130,17 @@ class ViewPasswordActivity : AppCompatActivity() { val confirmDialog = AlertDialog.Builder(this@ViewPasswordActivity) .setTitle(R.string.delete_password_dialog_title) .setMessage(R.string.irreversible_dialog_desc) - .setPositiveButton(R.string.confirm_dialog_button_text) { dialog, which -> - performDeletePasswordAction() + .setPositiveButton(R.string.confirm_dialog_button_text) { dialog, which -> + runBlocking { viewModel.onDeletePasswordButtonClick() } + finish() } - .setNegativeButton(R.string.discard_dialog_button_text) { dialog, which -> - Toast.makeText(this, getString(R.string.action_discard), Toast.LENGTH_SHORT).show(); + .setNegativeButton(R.string.discard_dialog_button_text) { dialog, which -> + Toast.makeText(this, getString(R.string.action_discard), Toast.LENGTH_SHORT) + .show() } - .create(); - - confirmDialog.show(); - } - } + .create() - private fun performDeletePasswordAction() { - lifecycleScope.launch { - try { - val rowsDeleted = controller.deletePassword(passwordEntityId) - withContext(Dispatchers.Main) { - if (rowsDeleted > 0) { - Toast.makeText(this@ViewPasswordActivity, getString(R.string.delete_success_msg), Toast.LENGTH_SHORT).show() - finish() - } else { - Toast.makeText(this@ViewPasswordActivity, getString(R.string.something_went_wrong_msg) + ": Password not found for deletion or no rows affected.", Toast.LENGTH_SHORT).show() - finish() - } - } - } catch (e: DatabaseOperationException) { - withContext(Dispatchers.Main) { - Toast.makeText(this@ViewPasswordActivity, "${getString(R.string.something_went_wrong_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Toast.makeText(this@ViewPasswordActivity, "${getString(R.string.something_went_wrong_msg)}: ${e.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - } - } + confirmDialog.show() } } - - override fun onResume() { - super.onResume() - fillDataInTextview() - } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordViewModel.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordViewModel.kt new file mode 100644 index 00000000..425c82e0 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/ViewPasswordViewModel.kt @@ -0,0 +1,69 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.jeeldobariya.passcodes.database.Password +import com.jeeldobariya.passcodes.utils.Controller +import com.jeeldobariya.passcodes.utils.DateTimeUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ViewPasswordViewModel( + val controller: Controller +) : ViewModel() { + var passwordEntityId: Int = -1 + + private val _domainState = MutableStateFlow("") + val domainState = _domainState.asStateFlow() + + private val _usernameState = MutableStateFlow("") + val usernameState = _usernameState.asStateFlow() + + private val _passwordState = MutableStateFlow("") + val passwordState = _passwordState.asStateFlow() + + private val _notesState = MutableStateFlow("") + val notesState = _notesState.asStateFlow() + + private val _lastUpdatedAtState = MutableStateFlow("") + val lastUpdatedAtState = _lastUpdatedAtState.asStateFlow() + + private val _isErrorState = MutableStateFlow(false) + val isErrorState = _isErrorState.asStateFlow() + + fun loadInitialData(passwordId: Int) { + passwordEntityId = passwordId + + viewModelScope.launch { + try { + val password: Password = controller.getPasswordById(passwordId) + + _domainState.update { password.domain } + _usernameState.update { password.username } + _passwordState.update { password.password } + _notesState.update { password.notes } + _lastUpdatedAtState.update { + DateTimeUtils.getRelativeDays(password.updatedAt.orEmpty()) + } + } catch (_: Exception) { + _isErrorState.update { + true + } + } + } + } + + fun onDeletePasswordButtonClick() { + viewModelScope.launch { + try { + controller.deletePassword(passwordEntityId) + } catch (_: Exception) { + _isErrorState.update { + true + } + } + } + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt index c0644414..e5d7f238 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/CommonUtils.kt @@ -8,7 +8,8 @@ class CommonUtils { companion object { fun getCurrTheme(context: Context): Int { val sharedPrefs = context.getSharedPreferences(Constant.APP_PREFS_NAME, MODE_PRIVATE) - val savedThemeStyle = sharedPrefs.getInt(Constant.THEME_KEY, R.style.PasscodesTheme_Default) + val savedThemeStyle = + sharedPrefs.getInt(Constant.THEME_KEY, R.style.PasscodesTheme_Default) return savedThemeStyle } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt index 84fd0c9b..edb0ab29 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Controller.kt @@ -1,17 +1,24 @@ package com.jeeldobariya.passcodes.utils import android.content.Context -import android.widget.Toast import com.jeeldobariya.passcodes.database.MasterDatabase import com.jeeldobariya.passcodes.database.Password import com.jeeldobariya.passcodes.database.PasswordsDao import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -class InvalidInputException(message: String = "Input parameters cannot be blank.") : Exception(message) -class DatabaseOperationException(message: String = "A database operation error occurred.", cause: Throwable? = null) : Exception(message, cause) -class PasswordNotFoundException(message: String = "Password with the given ID was not found.") : Exception(message) -class InvalidImportFormat(message: String = "Given Data Is In Invalid Format") : Exception(message) +class InvalidInputException(message: String = "Input parameters cannot be blank.") : + Exception(message) + +class DatabaseOperationException( + message: String = "A database operation error occurred.", + cause: Throwable? = null +) : Exception(message, cause) + +class PasswordNotFoundException(message: String = "Password with the given ID was not found.") : + Exception(message) + +class InvalidImportFormat(message: String = "Given Data Is In Invalid Format") : Exception(message) class Controller(context: Context) { private val passwordsDao: PasswordsDao @@ -32,7 +39,12 @@ class Controller(context: Context) { * @throws InvalidInputException if parameters are blank. * @throws DatabaseOperationException if a database error occurs. */ - suspend fun savePasswordEntity(domain: String, username: String, password: String, notes: String): Long { + suspend fun savePasswordEntity( + domain: String, + username: String, + password: String, + notes: String + ): Long { if (domain.isBlank() || username.isBlank() || password.isBlank()) { throw InvalidInputException() } @@ -96,7 +108,13 @@ class Controller(context: Context) { return passwordsDao.getAllPasswords() } - suspend fun updatePassword(id: Int, domain: String, username: String, password: String, notes: String): Int { + suspend fun updatePassword( + id: Int, + domain: String, + username: String, + password: String, + notes: String + ): Int { if (domain.isBlank() || username.isBlank() || password.isBlank()) { throw InvalidInputException() } @@ -156,7 +174,7 @@ class Controller(context: Context) { if (lines.isEmpty()) { throw InvalidImportFormat("Given data seems to be Empty!!") } - + if (lines[0] != CSV_HEADER) { throw InvalidImportFormat("Given data is not in valid csv format!! correct format:- ${CSV_HEADER}") } @@ -168,12 +186,15 @@ class Controller(context: Context) { val cols = line.split(",") /* NOTE: this need to be done, because our app not allow empty domain. */ - val chosenDomain : String = if (!cols[0].isBlank()) { + val chosenDomain: String = if (!cols[0].isBlank()) { cols[0].trim() // used: name } else cols[1].trim() // used: url try { - val password: Password? = passwordsDao.getPasswordByUsernameAndDomain(username = cols[2].trim(), domain = chosenDomain) + val password: Password? = passwordsDao.getPasswordByUsernameAndDomain( + username = cols[2].trim(), + domain = chosenDomain + ) if (password != null) { updatePassword( diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Permissions.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Permissions.kt index 64bddbd2..db1e16cc 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Permissions.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/Permissions.kt @@ -17,8 +17,10 @@ class Permissions(private val activity: Activity) { * @return True if permissions are granted, false otherwise. */ fun checkPermission(): Boolean { - val resultWrite = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - val resultRead = ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) + val resultWrite = + ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + val resultRead = + ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) return resultWrite == PackageManager.PERMISSION_GRANTED && resultRead == PackageManager.PERMISSION_GRANTED } @@ -28,7 +30,10 @@ class Permissions(private val activity: Activity) { fun requestPermission() { ActivityCompat.requestPermissions( activity, - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), + arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ), PERMISSION_REQUEST_CODE ) } @@ -43,8 +48,8 @@ class Permissions(private val activity: Activity) { // We expect two permissions (WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE) // and check if both were granted. return grantResults.isNotEmpty() && - grantResults.size >= 2 && // Ensure we got results for at least two permissions - grantResults[0] == PackageManager.PERMISSION_GRANTED && - grantResults[1] == PackageManager.PERMISSION_GRANTED + grantResults.size >= 2 && // Ensure we got results for at least two permissions + grantResults[0] == PackageManager.PERMISSION_GRANTED && + grantResults[1] == PackageManager.PERMISSION_GRANTED } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt index 37090715..560becaf 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/UpdateChecker.kt @@ -2,7 +2,11 @@ package com.jeeldobariya.passcodes.utils import android.content.Context import android.widget.Toast -import okhttp3.* +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response import java.io.IOException object UpdateChecker { @@ -57,7 +61,10 @@ object UpdateChecker { } if (!userReleaseFound) { - showToast(appcontext, "⚠️ Version ($currentNormalizeVersion) not found on GitHub releases... Join telegram community (${Constant.TELEGRAM_COMMUNITY_URL})") + showToast( + appcontext, + "⚠️ Version ($currentNormalizeVersion) not found on GitHub releases... Join telegram community (${Constant.TELEGRAM_COMMUNITY_URL})" + ) } } }) diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/tempmigration.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/tempmigration.kt new file mode 100644 index 00000000..284e494b --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/utils/tempmigration.kt @@ -0,0 +1,17 @@ +package com.jeeldobariya.passcodes.utils + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +fun AppCompatActivity.collectLatestLifecycleFlow(flow: Flow, collect: (T) -> Unit) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collectLatest(collect) + } + } +} diff --git a/app/src/main/res/layout/activity_autofill_settings.xml b/app/src/main/res/layout/activity_autofill_settings.xml new file mode 100644 index 00000000..1e1fb8db --- /dev/null +++ b/app/src/main/res/layout/activity_autofill_settings.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1f595077..d4d0aac0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,7 +11,7 @@ android:layout_width="128dp" android:layout_height="128dp" android:background="@drawable/ic_passodes" - android:contentDescription="Passcodes Logo" /> + android:contentDescription="@string/passcodes_logo" /> - - - - + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afd8f58b..88a3ab1e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,4 +98,5 @@ Copying sensitive data like passwords to clipboard is not so good for security!!! Confirm Discard + Passcodes Logo diff --git a/app/src/main/res/xml/autofill_service.xml b/app/src/main/res/xml/autofill_service.xml new file mode 100644 index 00000000..eb9e48c4 --- /dev/null +++ b/app/src/main/res/xml/autofill_service.xml @@ -0,0 +1,4 @@ + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 94c75d95..211749a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,9 +12,10 @@ androidx-test-ext-junit = "1.2.1" espresso-core = "3.6.1" coroutines = "1.10.2" lifecycle = "2.9.2" +koin = "4.1.1" # Plugin versions -agp = "8.11.0" +agp = "8.13.0" kotlin-plugin = "2.1.21" ksp = "2.1.21-2.0.2" oss-license-plugin = "0.10.6" # latest safe version for oss-licenses-plugin @@ -37,7 +38,11 @@ coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-androi coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } -# lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } + +koin = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-viewmodel = { module = "io.insert-koin:koin-android", version.ref = "koin" } +# koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } json = { module = "org.json:json", version.ref = "json" } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755