diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 701d5de7..5e0d7c3a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,7 +11,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.LibrePass">
@@ -21,5 +21,15 @@
+
+
+
+
+
+
diff --git a/app/src/main/java/dev/medzik/librepass/android/activity/AutofillLauncherActivity.kt b/app/src/main/java/dev/medzik/librepass/android/activity/AutofillLauncherActivity.kt
new file mode 100644
index 00000000..ad3f98bb
--- /dev/null
+++ b/app/src/main/java/dev/medzik/librepass/android/activity/AutofillLauncherActivity.kt
@@ -0,0 +1,34 @@
+package dev.medzik.librepass.android.activity
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+class AutofillLauncherActivity : AppCompatActivity() {
+ companion object {
+ private val TAG = AutofillLauncherActivity::class.java.name
+
+ fun getPendingIntent(context: Context): PendingIntent? {
+ try {
+ return PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, AutofillLauncherActivity::class.java),
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
+ } else {
+ PendingIntent.FLAG_CANCEL_CURRENT
+ }
+ )
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Unable to create pending intent for selection", e)
+ return null
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt b/app/src/main/java/dev/medzik/librepass/android/activity/MainActivity.kt
similarity index 96%
rename from app/src/main/java/dev/medzik/librepass/android/MainActivity.kt
rename to app/src/main/java/dev/medzik/librepass/android/activity/MainActivity.kt
index 17ed814c..13257980 100644
--- a/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt
+++ b/app/src/main/java/dev/medzik/librepass/android/activity/MainActivity.kt
@@ -1,4 +1,4 @@
-package dev.medzik.librepass.android
+package dev.medzik.librepass.android.activity
import android.os.Bundle
import android.util.Log
@@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController
import dagger.hilt.android.AndroidEntryPoint
import dev.medzik.android.components.navigate
+import dev.medzik.librepass.android.Migrations
import dev.medzik.librepass.android.data.Repository
import dev.medzik.librepass.android.ui.LibrePassNavigation
import dev.medzik.librepass.android.ui.Screen
diff --git a/app/src/main/java/dev/medzik/librepass/android/autofill/AutofillHandler.kt b/app/src/main/java/dev/medzik/librepass/android/autofill/AutofillHandler.kt
new file mode 100644
index 00000000..9eb4e9df
--- /dev/null
+++ b/app/src/main/java/dev/medzik/librepass/android/autofill/AutofillHandler.kt
@@ -0,0 +1,110 @@
+package dev.medzik.librepass.android.autofill
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.os.CancellationSignal
+import android.service.autofill.FillCallback
+import android.service.autofill.FillRequest
+import android.util.Log
+import androidx.annotation.RequiresApi
+import dev.medzik.librepass.android.autofill.Utils.getWindowNodes
+import dev.medzik.librepass.android.utils.Vault
+
+@RequiresApi(Build.VERSION_CODES.O)
+object AutofillHandler {
+ private val TAG = AutofillHandler::class.java.name
+
+ fun handleAutofill(
+ vault: Vault,
+ context: Context,
+ request: FillRequest,
+ cancellationSignal: CancellationSignal,
+ callback: FillCallback
+ ) {
+ cancellationSignal.setOnCancelListener {
+ Log.w(TAG, "Cancelling autofill")
+ }
+
+ val windowNode = getWindowNodes(request.fillContexts).lastOrNull()
+ if (windowNode?.rootViewNode == null) {
+ Log.i(TAG, "No window node found")
+ return callback.onSuccess(null)
+ }
+
+ val structure = request.fillContexts.last().structure
+ StructureParser(structure).parse()?.let {
+ // TODO: maybe add blocklist support
+
+ launchSelection(it, vault, context, callback)
+ }
+
+// val exceptionHandler =
+// CoroutineExceptionHandler { _, exception ->
+// Log.e(TAG, exception.toString())
+// callback.onSuccess(null)
+// }
+// val job =
+// CoroutineScope(Dispatchers.IO)
+// .launch(exceptionHandler) {
+// // TODO: searchAndFill
+// }
+//
+// cancellationSignal.setOnCancelListener {
+// job.cancel()
+// }
+ }
+
+ @SuppressLint("RemoteViewLayout")
+ private fun launchSelection(
+ parsedStructure: StructureParser.AutofillResult,
+ vault: Vault,
+ context: Context,
+ callback: FillCallback
+ ) {
+// val responseBuilder = FillResponse.Builder()
+
+// AutofillLauncherActivity.getPendingIntent(context)?.intentSender?.let { intentSender ->
+// val remoteViewUnlock = RemoteViews(
+// context.packageName,
+// R.layout.autofill_unlock
+// )
+//
+// remoteViewUnlock.setTextViewText(R.id.text1, "Test")
+//
+// responseBuilder.setAuthentication(
+// parsedStructure.getAutofillIDs(),
+// intentSender,
+// remoteViewUnlock
+// )
+// }
+//
+// callback.onSuccess(responseBuilder.build())
+
+// val usernamePresentation = RemoteViews(packageName, R.layout.autofill_unlock)
+// usernamePresentation.setTextViewText(R.id.text1, "my_username")
+//
+// val fillResponse = FillResponse.Builder()
+// .addDataset(
+// Dataset.Builder()
+// .setValue(
+// parsedStructure.usernameId!!,
+// AutofillValue.forText("test"),
+// usernamePresentation
+// )
+// .build())
+// .build()
+//
+// callback.onSuccess(fillResponse)
+
+// if (!vault.openedDatabase) {
+//
+// }
+//
+// parserResult.getAutofillIDs().let { autofillId ->
+// if (autofillId.isNotEmpty()) {
+//
+// }
+// }
+ }
+}
diff --git a/app/src/main/java/dev/medzik/librepass/android/autofill/AutofillService.kt b/app/src/main/java/dev/medzik/librepass/android/autofill/AutofillService.kt
new file mode 100644
index 00000000..b4d47aa3
--- /dev/null
+++ b/app/src/main/java/dev/medzik/librepass/android/autofill/AutofillService.kt
@@ -0,0 +1,40 @@
+package dev.medzik.librepass.android.autofill
+
+import android.os.Build
+import android.os.CancellationSignal
+import android.service.autofill.AutofillService
+import android.service.autofill.FillCallback
+import android.service.autofill.FillRequest
+import android.service.autofill.SaveCallback
+import android.service.autofill.SaveRequest
+import androidx.annotation.RequiresApi
+import dev.medzik.librepass.android.utils.Vault
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.O)
+class LibrePassAutofillService : AutofillService() {
+ // TODO: does not init, why?
+ @Inject
+ lateinit var vault: Vault
+
+ override fun onFillRequest(
+ request: FillRequest,
+ cancellationSignal: CancellationSignal,
+ callback: FillCallback
+ ) {
+ AutofillHandler.handleAutofill(
+ vault,
+ context = this,
+ request,
+ cancellationSignal,
+ callback
+ )
+ }
+
+ override fun onSaveRequest(
+ request: SaveRequest,
+ callback: SaveCallback
+ ) {
+ // TODO: implement
+ }
+}
diff --git a/app/src/main/java/dev/medzik/librepass/android/autofill/StructureParser.kt b/app/src/main/java/dev/medzik/librepass/android/autofill/StructureParser.kt
new file mode 100644
index 00000000..3aa353a1
--- /dev/null
+++ b/app/src/main/java/dev/medzik/librepass/android/autofill/StructureParser.kt
@@ -0,0 +1,206 @@
+package dev.medzik.librepass.android.autofill
+
+import android.app.assist.AssistStructure
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.view.autofill.AutofillId
+import android.view.autofill.AutofillValue
+import androidx.annotation.RequiresApi
+import java.util.Locale
+
+@RequiresApi(Build.VERSION_CODES.O)
+class StructureParser(private val structure: AssistStructure) {
+ companion object {
+ private val TAG = StructureParser::class.java.name
+ }
+
+ private var result = AutofillResult()
+
+ fun parse(): AutofillResult? {
+ result = AutofillResult()
+
+ mainLoop@ for (i in 0 until structure.windowNodeCount) {
+ val windowNode = structure.getWindowNodeAt(i)
+ val applicationId = windowNode.title.toString().split("/")[0]
+ Log.d(TAG, "Autofill applicationId: $applicationId")
+
+ if (parseViewNode(windowNode.rootViewNode))
+ break@mainLoop
+ }
+
+ return if (result.usernameId != null)
+ result
+ else
+ null
+ }
+
+ private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
+ var webDomain: String? = null
+// var webScheme: String? = null
+
+ var returnValue = false
+
+ node.webDomain?.let {
+ webDomain = it
+ Log.d(TAG, "Autofill domain: $webDomain")
+ }
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+// node.webDomain?.let {
+// webScheme = it
+// Log.d(TAG, "Autofill scheme: $webScheme")
+// }
+// }
+
+ if (node.visibility == View.VISIBLE) {
+ if (node.autofillId != null) {
+ val hints = node.autofillId
+ if (hints != null) {
+ if (parseNodeByAutofillHint(node))
+ returnValue = true
+ } else if (parseNodeByHtml(node)) {
+ returnValue = true
+ }
+
+ // TODO: else if (parseNodeByAndroidInput)
+ }
+
+ if (webDomain?.isNotEmpty() == true && returnValue)
+ return true
+
+ // process each node
+ for (i in 0 until node.childCount) {
+ if (parseViewNode(node.getChildAt(i)))
+ returnValue = true
+ if (webDomain?.isNotEmpty() == true && returnValue)
+ return true
+ }
+ }
+
+ return returnValue
+ }
+
+ private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
+ node.autofillHints?.forEach {
+ when {
+ it.contains(View.AUTOFILL_HINT_USERNAME, true) ||
+ it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true) ||
+ it.contains("email", true) ||
+ it.contains("e-mail", true) ||
+ it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
+ result.usernameId = node.autofillId
+ result.usernameValue = node.autofillValue
+ }
+
+ it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
+ result.passwordId = node.autofillId
+ result.passwordValue = node.autofillValue
+
+ return true
+ }
+
+ // ignore autocomplete="off"
+ // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
+ it.equals("off", true) -> {
+ return parseNodeByHtml(node)
+ }
+
+ else -> Log.d(TAG, "Unsupported autofill hint: $it")
+ }
+ }
+
+ return false
+ }
+
+ private fun parseNodeByHtml(node: AssistStructure.ViewNode): Boolean {
+ when (node.htmlInfo?.tag?.lowercase(Locale.ENGLISH)) {
+ "input" -> {
+ node.htmlInfo?.attributes?.forEach { attributePair ->
+ when (attributePair.first.lowercase(Locale.ENGLISH)) {
+ "tel", "email" -> {
+ result.usernameId = node.autofillId
+ result.usernameValue = node.autofillValue
+ }
+
+ // sometimes the email address field is not marked correctly,
+ // guess it is before password field
+ "text" -> {
+ if (result.usernameId == null && result.passwordId == null) {
+ result.usernameId = node.autofillId
+ result.usernameValue = node.autofillValue
+ }
+ }
+
+ "password" -> {
+ result.passwordId = node.autofillId
+ result.passwordValue = node.autofillValue
+
+ return true
+ }
+ }
+ }
+ }
+ }
+
+ return false
+ }
+
+// private fun parseByAndroidInput(node: AssistStructure.ViewNode): Boolean {
+// var usernameIdCandidate: AutofillId? = null
+// var usernameValueCandidate: AutofillValue? = null
+//
+// when (node.inputType and InputType.TYPE_MASK_CLASS) {
+// InputType.TYPE_CLASS_TEXT -> {
+// when {
+// androidInputIsVariationType(
+// node.inputType,
+// InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
+// InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
+// ) -> {
+// usernameIdCandidate = node.autofillId
+// usernameValueCandidate = node.autofillValue
+// }
+//
+// androidInputIsVariationType(
+// node.inputType,
+// InputType.TYPE_TEXT_VARIATION_NORMAL,
+// InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
+// InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+// ) -> {
+// usernameIdCandidate = node.autofillId
+// usernameValueCandidate = node.autofillValue
+// }
+// }
+// }
+// }
+// }
+//
+// private fun androidInputIsVariationType(
+// inputType: Int,
+// vararg type: Int
+// ): Boolean {
+// type.forEach {
+// if (inputType and InputType.TYPE_MASK_VARIATION == it) {
+// return true
+// }
+// }
+//
+// return false
+// }
+
+ data class AutofillResult(
+ var usernameId: AutofillId? = null,
+ var usernameValue: AutofillValue? = null,
+ var passwordId: AutofillId? = null,
+ var passwordValue: AutofillValue? = null
+ ) {
+ fun getAutofillIDs(): Array {
+ val all = ArrayList()
+
+ usernameId?.let { all.add(it) }
+ passwordId?.let { all.add(it) }
+
+ return all.toTypedArray()
+ }
+ }
+}
diff --git a/app/src/main/java/dev/medzik/librepass/android/autofill/Utils.kt b/app/src/main/java/dev/medzik/librepass/android/autofill/Utils.kt
new file mode 100644
index 00000000..e9ed1b2f
--- /dev/null
+++ b/app/src/main/java/dev/medzik/librepass/android/autofill/Utils.kt
@@ -0,0 +1,24 @@
+package dev.medzik.librepass.android.autofill
+
+import android.app.assist.AssistStructure
+import android.os.Build
+import android.service.autofill.FillContext
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.O)
+object Utils {
+ fun getWindowNodes(fillContexts: List): List {
+ val fillContext =
+ fillContexts
+ .lastOrNull { !it.structure.activityComponent.className.contains("PopupWindow") }
+ ?: return emptyList()
+
+ val structure = fillContext.structure
+
+ return if (structure.windowNodeCount > 0) {
+ (0 until structure.windowNodeCount).map { structure.getWindowNodeAt(it) }
+ } else {
+ emptyList()
+ }
+ }
+}
diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt b/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt
index b40bc6bd..4df6f0a0 100644
--- a/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt
+++ b/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt
@@ -38,8 +38,8 @@ import dev.medzik.android.components.NavScreen
import dev.medzik.android.components.navigate
import dev.medzik.android.components.rememberDialogState
import dev.medzik.android.components.rememberMutableBoolean
-import dev.medzik.librepass.android.MainActivity
import dev.medzik.librepass.android.R
+import dev.medzik.librepass.android.activity.MainActivity
import dev.medzik.librepass.android.ui.components.CipherTypeDialog
import dev.medzik.librepass.android.ui.components.TopBar
import dev.medzik.librepass.android.ui.components.TopBarBackIcon
diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt
index 471ea518..39fdf3d0 100644
--- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt
+++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt
@@ -25,8 +25,8 @@ import dev.medzik.android.components.PropertyPreference
import dev.medzik.android.components.SwitcherPreference
import dev.medzik.android.components.rememberDialogState
import dev.medzik.android.crypto.KeyStore
-import dev.medzik.librepass.android.MainActivity
import dev.medzik.librepass.android.R
+import dev.medzik.librepass.android.activity.MainActivity
import dev.medzik.librepass.android.ui.LibrePassViewModel
import dev.medzik.librepass.android.utils.KeyAlias
import dev.medzik.librepass.android.utils.SecretStore.readKey
diff --git a/app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt b/app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt
index c5ee15cc..7fcbb10a 100644
--- a/app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt
+++ b/app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt
@@ -136,7 +136,7 @@ class Vault(
aesKey = byteArrayOf()
runOnIOThread {
- context.dataStore.deleteEncrypted(AES_KEY_STORE_KEY)
+ context.dataStore.deleteEncrypted(SecretStore.AES_KEY_STORE_KEY)
}
}
}