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