This commit is contained in:
Leendert de Borst
2025-05-27 17:16:12 +02:00
parent e430ae9f4f
commit 9d0a003b2d
5 changed files with 106 additions and 120 deletions

View File

@@ -17,7 +17,7 @@
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<service android:name=".AutofillService" android:permission="android.permission.BIND_AUTOFILL_SERVICE" android:exported="true">
<service android:name=".autofill.AutofillService" android:permission="android.permission.BIND_AUTOFILL_SERVICE" android:exported="true">
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
package net.aliasvault.app.autofill
package net.aliasvault.app.autofill.utils
import net.aliasvault.app.vaultstore.models.Credential

View File

@@ -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<Pair<AutofillId, FieldType>>()
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

View File

@@ -1,4 +1,4 @@
package net.aliasvault.app.autofill
package net.aliasvault.app.autofill.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory