-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: Autofill Service #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MaterialButton>(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) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, AssistStructure.ViewNode>() | ||
| 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<String, AssistStructure.ViewNode>() | ||
| 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( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the existing database. because app is in production and we can;t effort to lose users data. refer:- Controller.kt interact with this class instead of master db directly |
||
| Passcode(name = username, value = password) | ||
| ) | ||
| } | ||
| callback.onSuccess() | ||
| } else { | ||
| callback.onFailure("Could not save credentials.") | ||
| } | ||
| } | ||
|
|
||
| private fun parseStructure( | ||
| node: AssistStructure.ViewNode, | ||
| viewNodes: MutableMap<String, AssistStructure.ViewNode> | ||
| ) { | ||
| node.autofillHints?.forEach { hint -> | ||
| if (!viewNodes.containsKey(hint)) { | ||
| viewNodes[hint] = node | ||
| } | ||
| } | ||
|
|
||
| for (i in 0 until node.childCount) { | ||
| parseStructure(node.getChildAt(i), viewNodes) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<List<Passcode>> | ||
|
|
||
| @Query("SELECT * FROM passcodes WHERE id = :id") | ||
| fun getPasscode(id: Int): Flow<Passcode> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="match_parent" | ||
| android:gravity="center" | ||
| android:orientation="vertical" | ||
| android:padding="16dp"> | ||
|
|
||
| <TextView | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:text="Enable Passcodes Autofill" | ||
| android:textAppearance="@style/TextAppearance.Material3.TitleLarge" /> | ||
|
|
||
| <TextView | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:layout_marginTop="8dp" | ||
| android:text="To use Passcodes as your password manager, you need to enable it as an autofill service in your device settings." | ||
| android:textAlignment="center" /> | ||
|
|
||
| <com.google.android.material.button.MaterialButton | ||
| android:id="@+id/enable_autofill_button" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:layout_marginTop="24dp" | ||
| android:text="Open Settings" /> | ||
|
|
||
| </LinearLayout> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content" | ||
| android:orientation="vertical" | ||
| android:padding="8dp"> | ||
|
|
||
| <TextView | ||
| android:id="@+id/autofill_username" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:textStyle="bold" /> | ||
|
|
||
| </LinearLayout> |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I don;t know why we require this file... I have seen the same over on keypass codebase.. What i mean is, if it just a empty file then, why we need this and what it is use for please explain.. if it use for configuration then how? (I am new to android so don;t mind... I think it is require by android.. but i don't know it purpose..) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <autofill-service | ||
| xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:settingsActivity="com.jeeldobariya.passcodes.autofill.AutofillSettingsActivity" /> | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don;t understand what this is for... and where this activity will be call..
I mean, currently it not called from anywhere... It was probably intend to be call from setting activity.. I guess.