From 8b0df00edcf8a05cbb2d724367594067068ad0c7 Mon Sep 17 00:00:00 2001 From: hexCode63 Date: Thu, 2 Oct 2025 17:48:32 +0100 Subject: [PATCH] Updated: - Ability to set as default password management application - Added local database storage to store, save and recall user saved password information --- app/src/main/AndroidManifest.xml | 15 +++ .../autofill/AutofillSettingsActivity.kt | 23 ++++ .../autofill/PasswordAutofillService.kt | 112 ++++++++++++++++++ .../jeeldobariya/passcodes/data/Passcode.kt | 12 ++ .../passcodes/data/PasscodeDao.kt | 28 +++++ .../passcodes/data/PasscodeDatabase.kt | 29 +++++ .../res/layout/activity_autofill_settings.xml | 29 +++++ app/src/main/res/layout/activity_main.xml | 10 +- .../main/res/layout/autofill_list_item.xml | 14 +++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/autofill_service.xml | 4 + gradle/libs.versions.toml | 2 +- 12 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/AutofillSettingsActivity.kt create mode 100644 app/src/main/kotlin/com/jeeldobariya/passcodes/autofill/PasswordAutofillService.kt create mode 100644 app/src/main/kotlin/com/jeeldobariya/passcodes/data/Passcode.kt create mode 100644 app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDao.kt create mode 100644 app/src/main/kotlin/com/jeeldobariya/passcodes/data/PasscodeDatabase.kt create mode 100644 app/src/main/res/layout/activity_autofill_settings.xml create mode 100644 app/src/main/res/layout/autofill_list_item.xml create mode 100644 app/src/main/res/xml/autofill_service.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21615d35..93fbdc42 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + + + + + + + + + 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..13472f86 --- /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.jeeldobariya.passcodes.R +import com.google.android.material.button.MaterialButton + +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..32845d8c --- /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.Dataset +import android.service.autofill.FillCallback +import android.service.autofill.FillContext +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/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..d7fcb303 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ coroutines = "1.10.2" lifecycle = "2.9.2" # 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