Skip to content
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

Autofill Service provider - showing suggestions on other apps - internal version #5561

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autofill/autofill-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dependencies {
implementation Google.android.material
implementation AndroidX.constraintLayout
implementation JakeWharton.timber
implementation("androidx.autofill:autofill:1.1.0")
cmonfortep marked this conversation as resolved.
Show resolved Hide resolved

implementation KotlinX.coroutines.core
implementation AndroidX.fragment.ktx
Expand Down
66 changes: 66 additions & 0 deletions autofill/autofill-impl/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2025,6 +2025,17 @@
column="25"/>
</issue>

<issue
id="Overdraw"
message="Possible overdraw: Root element paints background `@color/gray0` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
errorLine1=" android:background=&quot;@color/gray0&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/autofill_remote_view.xml"
line="11"
column="5"/>
</issue>

<issue
id="Overdraw"
message="Possible overdraw: Root element paints background `?attr/colorPrimaryDark` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
Expand Down Expand Up @@ -2436,4 +2447,59 @@
column="10"/>
</issue>

<issue
id="DeprecatedWidgetInXml"
message="Always favor the use of the Design System Component"
errorLine1=" &lt;TextView"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/autofill_remote_view.xml"
line="29"
column="10"/>
</issue>

<issue
id="DeprecatedWidgetInXml"
message="Always favor the use of the Design System Component"
errorLine1=" &lt;TextView"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/autofill_remote_view.xml"
line="36"
column="10"/>
</issue>

<issue
id="InvalidColorAttribute"
message="@colors are not allowed, used ?attr/daxColor instead"
errorLine1=" android:background=&quot;@color/gray0&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/autofill_remote_view.xml"
line="11"
column="5"/>
</issue>

<issue
id="InvalidColorAttribute"
message="@colors are not allowed, used ?attr/daxColor instead"
errorLine1=" android:textColor=&quot;@color/gray100&quot;/>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/autofill_remote_view.xml"
line="34"
column="13"/>
</issue>

<issue
id="InvalidColorAttribute"
message="@colors are not allowed, used ?attr/daxColor instead"
errorLine1=" android:textColor=&quot;@color/gray100&quot;/>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/autofill_remote_view.xml"
line="41"
column="13"/>
</issue>

</issues>
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.autofill.impl.service

import android.annotation.SuppressLint
import android.app.assist.AssistStructure
import android.app.assist.AssistStructure.ViewNode
import android.view.autofill.AutofillId
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.autofill.impl.service.AutofillFieldType.UNKNOWN
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import timber.log.Timber

interface AutofillParser {
// Parses structure, detects autofill fields, and returns a list of root nodes.
// Each root node contains a list of parsed autofill fields.
// We intend that each root node has packageId and/or website, based on child values, but it's not guaranteed.
fun parseStructure(structure: AssistStructure): MutableList<AutofillRootNode>
}

// Parsed root node of the autofill structure
data class AutofillRootNode(
val packageId: String?,
val website: String?,
val parsedAutofillFields: List<ParsedAutofillField>, // Parsed fields in the structure
)

// Parsed autofill field
data class ParsedAutofillField(
val autofillId: AutofillId,
val packageId: String?,
val website: String?,
val value: String,
val type: AutofillFieldType = AutofillFieldType.UNKNOWN,
val originalNode: ViewNode,
)

enum class AutofillFieldType {
USERNAME,
PASSWORD,
UNKNOWN,
}

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealAutofillParser @Inject constructor(
private val appBuildConfig: AppBuildConfig,
private val viewNodeClassifier: ViewNodeClassifier,
) : AutofillParser {

override fun parseStructure(structure: AssistStructure): MutableList<AutofillRootNode> {
val autofillRootNodes = mutableListOf<AutofillRootNode>()
val windowNodeCount = structure.windowNodeCount
Timber.i("DDGAutofillService windowNodeCount: $windowNodeCount")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: will comment on the logging just the once, but this comment applies to all logs added in this PR

  • are all the logs added going to be too noisy to keep around as is, especially at the info level?
  • maybe worth doing a pass seeing if they should be kept and/or downgraded to verbose

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the proposal of keeping the debug ones as verbose.

I'm gonna do some clean up, but some of them are really helpful when understanding heuristics results.

for (i in 0 until windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i)
windowNode.rootViewNode?.let { viewNode ->
autofillRootNodes.add(
traverseViewNode(viewNode).convertIntoAutofillNode(),
)
}
}
Timber.i("DDGAutofillService convertedNodes: $autofillRootNodes")
autofillRootNodes.forEach { node ->
Timber.i("DDGAutofillService Detected Fields: ${node.parsedAutofillFields.filter { it.type != UNKNOWN }}")
}
return autofillRootNodes
}

private fun traverseViewNode(
viewNode: ViewNode,
): MutableList<ParsedAutofillField> {
val autofillId = viewNode.autofillId ?: return mutableListOf()
Timber.i("DDGAutofillService Parsing NODE: $autofillId")
val traversalDataList = mutableListOf<ParsedAutofillField>()
val packageId = viewNode.validPackageId()
val website = viewNode.website()
val autofillType = viewNodeClassifier.classify(viewNode)
val value = kotlin.runCatching { viewNode.autofillValue?.textValue?.toString() ?: "" }.getOrDefault("")
val parsedAutofillField = ParsedAutofillField(
autofillId = autofillId,
packageId = packageId,
website = website,
value = value,
type = autofillType,
originalNode = viewNode,
)
Timber.i("DDGAutofillService Parsed as: $parsedAutofillField")
traversalDataList.add(parsedAutofillField)

for (i in 0 until viewNode.childCount) {
val childNode = viewNode.getChildAt(i)
traversalDataList.addAll(traverseViewNode(childNode))
}

return traversalDataList
}

private fun List<ParsedAutofillField>.convertIntoAutofillNode(): AutofillRootNode {
return AutofillRootNode(
packageId = this.firstOrNull { it.packageId != null }?.packageId,
website = this.firstOrNull { it.website != null }?.website,
parsedAutofillFields = this,
)
}

private fun ViewNode.validPackageId(): String? {
return this.idPackage
.takeUnless { it.isNullOrBlank() }
?.takeUnless { it in INVALID_PACKAGE_ID }
}

@SuppressLint("NewApi")
private fun ViewNode.website(): String? {
return this.webDomain?.takeUnless { it.isBlank() }
?.let { nonEmptyDomain ->
val scheme = if (appBuildConfig.sdkInt >= 28) {
this.webScheme.takeUnless { it.isNullOrBlank() } ?: "http"
} else {
"http"
}
"$scheme://$nonEmptyDomain"
}
}

companion object {
private val INVALID_PACKAGE_ID = listOf("android")
}
}
Loading
Loading