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