diff --git a/apps/mobile-app/android/app/src/main/AndroidManifest.xml b/apps/mobile-app/android/app/src/main/AndroidManifest.xml
index 199c166b4..87a7f3440 100644
--- a/apps/mobile-app/android/app/src/main/AndroidManifest.xml
+++ b/apps/mobile-app/android/app/src/main/AndroidManifest.xml
@@ -17,7 +17,7 @@
-
+
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/AutofillService.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/AutofillService.kt
similarity index 77%
rename from apps/mobile-app/android/app/src/main/java/net/aliasvault/app/AutofillService.kt
rename to apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/AutofillService.kt
index 566a951be..5051b7cf8 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/AutofillService.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/AutofillService.kt
@@ -5,25 +5,9 @@
* to forms. It identifies username and password fields in apps and websites,
* then offers stored credentials from AliasVault.
*
- * IMPORTANT IMPLEMENTATION NOTES:
- * 1. Since autofill services don't have direct access to activities, we need a way to
- * authenticate the user. The current implementation:
- * - Shows a Toast indicating authentication is needed
- * - In a real implementation, would launch an activity for authentication
- *
- * 2. To complete this implementation, you need to:
- * - Register this service in AndroidManifest.xml with proper metadata
- * - Add a way to communicate between the launched activity and this service
- * - Implement credential storage/retrieval with proper authentication
- *
- * 3. For full production implementation, consider:
- * - Adding a specific autofill activity for authentication
- * - Implementing dataset presentation customization
- * - Adding support for save functionality
- * - Implementing field detection heuristics for apps without autofill hints
*/
-package net.aliasvault.app
-import android.app.assist.AssistStructure
+package net.aliasvault.app.autofill
+
import android.content.Intent
import android.os.CancellationSignal
import android.service.autofill.AutofillService
@@ -34,18 +18,15 @@ import android.service.autofill.FillResponse
import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import android.util.Log
-import android.view.View
-import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import net.aliasvault.app.vaultstore.VaultStore
import net.aliasvault.app.vaultstore.VaultStore.CredentialOperationCallback
import net.aliasvault.app.vaultstore.models.Credential
-import net.aliasvault.app.autofill.CredentialMatcher
-import androidx.core.net.toUri
import android.app.PendingIntent
-import net.aliasvault.app.autofill.FieldFinder
-import net.aliasvault.app.autofill.ImageUtils
+import net.aliasvault.app.MainActivity
+import net.aliasvault.app.R
+import net.aliasvault.app.autofill.utils.*
import net.aliasvault.app.autofill.models.FieldType
class AutofillService : AutofillService() {
@@ -104,8 +85,8 @@ class AutofillService : AutofillService() {
private fun launchActivityForAutofill(fieldFinder: FieldFinder, callback: FillCallback) {
Log.d(TAG, "Launching activity for autofill authentication")
- // Get the app/website information from the structure
- val appInfo = getAppInfo(fieldFinder.structure)
+ // Get the app/website information from assist structure.
+ val appInfo = fieldFinder.getAppInfo()
Log.d(TAG, "Autofill request from: $appInfo")
// Ignore requests from our own unlock page as this would cause a loop
@@ -183,96 +164,6 @@ class AutofillService : AutofillService() {
callback.onSuccess(responseBuilder.build())
}
- private fun getAppInfo(structure: AssistStructure?): String? {
- if (structure == null) {
- return null
- }
-
- // First check if this is web content
- val nodeCount = structure.windowNodeCount
- for (i in 0 until nodeCount) {
- val windowNode = structure.getWindowNodeAt(i)
- val rootNode = windowNode.rootViewNode
-
- // Check for web-specific information
- val webInfo = findWebInfoInNode(rootNode)
- if (webInfo != null) {
- Log.d(TAG, "Found web info: $webInfo")
- return webInfo
- }
- }
-
- // If no web info found, fall back to package name
- val packageName = structure.activityComponent?.packageName
- if (packageName != null) {
- Log.d(TAG, "Using package name: $packageName")
- return packageName
- }
-
- return null
- }
-
- private fun findWebInfoInNode(node: AssistStructure.ViewNode): String? {
- // Check for web domain
- val webDomain = node.webDomain
- val webScheme = node.webScheme
- if (webDomain != null && webScheme != null) {
- return "$webScheme://$webDomain"
- }
-
- // Check for web URL
- val webUrl = node.webDomain
- if (webUrl != null) {
- try {
- val uri = webUrl.toUri()
- val host = uri.host
- if (host != null) {
- return host
- }
- } catch (e: Exception) {
- Log.e(TAG, "Error parsing web URL: $webUrl", e)
- }
- }
-
- // Check HTML info for domain or URL
- val htmlInfo = node.htmlInfo
- if (htmlInfo != null) {
- val attributes = htmlInfo.attributes
- if (attributes != null) {
- for (i in 0 until attributes.size) {
- val name = attributes.get(i)?.first
- val value = attributes.get(i)?.second
- if (name == "domain" || name == "host" || name == "url") {
- return value
- }
- }
- }
- }
-
- // Check for web-specific hints
- val hints = node.autofillHints
- if (hints != null) {
- for (hint in hints) {
- if (hint.contains("web", ignoreCase = true) ||
- hint.contains("url", ignoreCase = true) ||
- hint.contains("domain", ignoreCase = true)) {
- return hint
- }
- }
- }
-
- // Recursively check child nodes
- val childCount = node.childCount
- for (i in 0 until childCount) {
- val webInfo = findWebInfoInNode(node.getChildAt(i))
- if (webInfo != null) {
- return webInfo
- }
- }
-
- return null
- }
-
// Helper method to create a dataset from a credential
private fun createCredentialDataset(
fieldFinder: FieldFinder,
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/CredentialMatcher.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/CredentialMatcher.kt
similarity index 98%
rename from apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/CredentialMatcher.kt
rename to apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/CredentialMatcher.kt
index ae33e8661..a31e55052 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/CredentialMatcher.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/CredentialMatcher.kt
@@ -1,4 +1,4 @@
-package net.aliasvault.app.autofill
+package net.aliasvault.app.autofill.utils
import net.aliasvault.app.vaultstore.models.Credential
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/FieldFinder.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/FieldFinder.kt
similarity index 74%
rename from apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/FieldFinder.kt
rename to apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/FieldFinder.kt
index 5eca50416..41f5a9a22 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/FieldFinder.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/FieldFinder.kt
@@ -1,14 +1,18 @@
-package net.aliasvault.app.autofill
+package net.aliasvault.app.autofill.utils
import android.app.assist.AssistStructure
import android.view.View
import android.view.autofill.AutofillId
+import android.util.Log
+import androidx.core.net.toUri
import net.aliasvault.app.autofill.models.FieldType
/**
* Helper class to find fields in the assist structure.
*/
class FieldFinder(var structure: AssistStructure) {
+ private val TAG = "AliasVaultAutofill"
+
// Store pairs of (AutofillId, net.aliasvault.app.autofill.models.FieldType)
val autofillableFields = mutableListOf>()
var foundPasswordField = false
@@ -24,6 +28,33 @@ class FieldFinder(var structure: AssistStructure) {
}
}
+ /**
+ * Get the current app or website information from the assist structure to know
+ * what credential suggestions to show.
+ */
+ fun getAppInfo(): String? {
+ // First check if this is web content
+ val nodeCount = structure.windowNodeCount
+ for (i in 0 until nodeCount) {
+ val windowNode = structure.getWindowNodeAt(i)
+ val rootNode = windowNode.rootViewNode
+
+ // Check for web-specific information
+ val webInfo = findWebInfoInNode(rootNode)
+ if (webInfo != null) {
+ return webInfo
+ }
+ }
+
+ // If no web info found, fall back to package name
+ val packageName = structure.activityComponent?.packageName
+ if (packageName != null) {
+ return packageName
+ }
+
+ return null
+ }
+
/**
* Determines if a field is most likely an email field, username field, password field, or unknown.
*/
@@ -49,6 +80,70 @@ class FieldFinder(var structure: AssistStructure) {
return FieldType.UNKNOWN
}
+ /**
+ * Attempt to find the web domain or URL in the assist structure.
+ */
+ private fun findWebInfoInNode(node: AssistStructure.ViewNode): String? {
+ // Check for web domain
+ val webDomain = node.webDomain
+ val webScheme = node.webScheme
+ if (webDomain != null && webScheme != null) {
+ return "$webScheme://$webDomain"
+ }
+
+ // Check for web URL
+ val webUrl = node.webDomain
+ if (webUrl != null) {
+ try {
+ val uri = webUrl.toUri()
+ val host = uri.host
+ if (host != null) {
+ return host
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error parsing web URL: $webUrl", e)
+ }
+ }
+
+ // Check HTML info for domain or URL
+ val htmlInfo = node.htmlInfo
+ if (htmlInfo != null) {
+ val attributes = htmlInfo.attributes
+ if (attributes != null) {
+ for (i in 0 until attributes.size) {
+ val name = attributes.get(i)?.first
+ val value = attributes.get(i)?.second
+ if (name == "domain" || name == "host" || name == "url") {
+ return value
+ }
+ }
+ }
+ }
+
+ // Check for web-specific hints
+ val hints = node.autofillHints
+ if (hints != null) {
+ for (hint in hints) {
+ if (hint.contains("web", ignoreCase = true) ||
+ hint.contains("url", ignoreCase = true) ||
+ hint.contains("domain", ignoreCase = true)) {
+ return hint
+ }
+ }
+ }
+
+ // Recursively check child nodes
+ val childCount = node.childCount
+ for (i in 0 until childCount) {
+ val webInfo = findWebInfoInNode(node.getChildAt(i))
+ if (webInfo != null) {
+ return webInfo
+ }
+ }
+
+ return null
+ }
+
private fun parseNode(node: AssistStructure.ViewNode) {
val viewId = node.autofillId
diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/ImageUtils.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/ImageUtils.kt
similarity index 98%
rename from apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/ImageUtils.kt
rename to apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/ImageUtils.kt
index 0e80e1e86..e4c071ee1 100644
--- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/ImageUtils.kt
+++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/autofill/utils/ImageUtils.kt
@@ -1,4 +1,4 @@
-package net.aliasvault.app.autofill
+package net.aliasvault.app.autofill.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory