Skip to content

Commit 661c178

Browse files
Autofill Service provider - showing suggestions on other apps - internal version (#5561)
Task/Issue URL: https://app.asana.com/0/1149059203486286/1209279712661815/f ### Description Scope: Create autofill Service (only internal) Parse structure Apply heuristics Extract packageId or Domain Show suggestions autofill on suggestion clicked Show "Search in DDG" suggestion (only UI) ### Steps to test this PR The easiest way to test the logic here is to do it using #5508 And use https://app.asana.com/0/1149059203486286/1209279712661812/f for test scenarios ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)| --------- Co-authored-by: Karl Dimla <[email protected]>
1 parent d8bbe92 commit 661c178

38 files changed

+3479
-3
lines changed

autofill/autofill-impl/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
implementation Google.android.material
5555
implementation AndroidX.constraintLayout
5656
implementation JakeWharton.timber
57+
implementation "androidx.autofill:autofill:_"
5758

5859
implementation KotlinX.coroutines.core
5960
implementation AndroidX.fragment.ktx

autofill/autofill-impl/lint-baseline.xml

+66
Original file line numberDiff line numberDiff line change
@@ -2025,6 +2025,17 @@
20252025
column="25"/>
20262026
</issue>
20272027

2028+
<issue
2029+
id="Overdraw"
2030+
message="Possible overdraw: Root element paints background `@color/gray0` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
2031+
errorLine1=" android:background=&quot;@color/gray0&quot;>"
2032+
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
2033+
<location
2034+
file="src/main/res/layout/autofill_remote_view.xml"
2035+
line="11"
2036+
column="5"/>
2037+
</issue>
2038+
20282039
<issue
20292040
id="Overdraw"
20302041
message="Possible overdraw: Root element paints background `?attr/colorPrimaryDark` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
@@ -2436,4 +2447,59 @@
24362447
column="10"/>
24372448
</issue>
24382449

2450+
<issue
2451+
id="DeprecatedWidgetInXml"
2452+
message="Always favor the use of the Design System Component"
2453+
errorLine1=" &lt;TextView"
2454+
errorLine2=" ~~~~~~~~">
2455+
<location
2456+
file="src/main/res/layout/autofill_remote_view.xml"
2457+
line="29"
2458+
column="10"/>
2459+
</issue>
2460+
2461+
<issue
2462+
id="DeprecatedWidgetInXml"
2463+
message="Always favor the use of the Design System Component"
2464+
errorLine1=" &lt;TextView"
2465+
errorLine2=" ~~~~~~~~">
2466+
<location
2467+
file="src/main/res/layout/autofill_remote_view.xml"
2468+
line="36"
2469+
column="10"/>
2470+
</issue>
2471+
2472+
<issue
2473+
id="InvalidColorAttribute"
2474+
message="@colors are not allowed, used ?attr/daxColor instead"
2475+
errorLine1=" android:background=&quot;@color/gray0&quot;>"
2476+
errorLine2=" ~~~~~~~~~~~~~~~~~~">
2477+
<location
2478+
file="src/main/res/layout/autofill_remote_view.xml"
2479+
line="11"
2480+
column="5"/>
2481+
</issue>
2482+
2483+
<issue
2484+
id="InvalidColorAttribute"
2485+
message="@colors are not allowed, used ?attr/daxColor instead"
2486+
errorLine1=" android:textColor=&quot;@color/gray100&quot;/>"
2487+
errorLine2=" ~~~~~~~~~~~~~~~~~">
2488+
<location
2489+
file="src/main/res/layout/autofill_remote_view.xml"
2490+
line="34"
2491+
column="13"/>
2492+
</issue>
2493+
2494+
<issue
2495+
id="InvalidColorAttribute"
2496+
message="@colors are not allowed, used ?attr/daxColor instead"
2497+
errorLine1=" android:textColor=&quot;@color/gray100&quot;/>"
2498+
errorLine2=" ~~~~~~~~~~~~~~~~~">
2499+
<location
2500+
file="src/main/res/layout/autofill_remote_view.xml"
2501+
line="41"
2502+
column="13"/>
2503+
</issue>
2504+
24392505
</issues>

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt

+20
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import com.duckduckgo.autofill.store.feature.email.incontext.ALL_MIGRATIONS as E
4646
import com.duckduckgo.autofill.store.feature.email.incontext.EmailProtectionInContextDatabase
4747
import com.duckduckgo.autofill.store.feature.email.incontext.EmailProtectionInContextFeatureRepository
4848
import com.duckduckgo.autofill.store.feature.email.incontext.RealEmailProtectionInContextFeatureRepository
49+
import com.duckduckgo.autofill.store.targets.DomainTargetAppDao
50+
import com.duckduckgo.autofill.store.targets.DomainTargetAppsDatabase
4951
import com.duckduckgo.browser.api.UserBrowserProperties
5052
import com.duckduckgo.common.utils.DispatcherProvider
5153
import com.duckduckgo.di.scopes.ActivityScope
@@ -149,6 +151,24 @@ class AutofillModule {
149151
.addMigrations(*AutofillEngagementDatabase.ALL_MIGRATIONS)
150152
.build()
151153
}
154+
155+
@Provides
156+
@SingleInstanceIn(AppScope::class)
157+
fun providesDomainTargetAppsDatabase(
158+
context: Context,
159+
): DomainTargetAppsDatabase {
160+
return Room.databaseBuilder(context, DomainTargetAppsDatabase::class.java, "autofill_domain_target_apps.db")
161+
.addMigrations(*DomainTargetAppsDatabase.ALL_MIGRATIONS)
162+
.build()
163+
}
164+
165+
@Provides
166+
@SingleInstanceIn(AppScope::class)
167+
fun providesDomainTargetAppsDao(
168+
database: DomainTargetAppsDatabase,
169+
): DomainTargetAppDao {
170+
return database.domainTargetAppDao()
171+
}
152172
}
153173

154174
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.autofill.impl.service
18+
19+
import android.annotation.SuppressLint
20+
import android.app.assist.AssistStructure
21+
import android.app.assist.AssistStructure.ViewNode
22+
import android.view.autofill.AutofillId
23+
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
24+
import com.duckduckgo.autofill.impl.service.AutofillFieldType.UNKNOWN
25+
import com.duckduckgo.di.scopes.AppScope
26+
import com.squareup.anvil.annotations.ContributesBinding
27+
import dagger.SingleInstanceIn
28+
import javax.inject.Inject
29+
import timber.log.Timber
30+
31+
interface AutofillParser {
32+
// Parses structure, detects autofill fields, and returns a list of root nodes.
33+
// Each root node contains a list of parsed autofill fields.
34+
// We intend that each root node has packageId and/or website, based on child values, but it's not guaranteed.
35+
fun parseStructure(structure: AssistStructure): MutableList<AutofillRootNode>
36+
}
37+
38+
// Parsed root node of the autofill structure
39+
data class AutofillRootNode(
40+
val packageId: String?,
41+
val website: String?,
42+
val parsedAutofillFields: List<ParsedAutofillField>, // Parsed fields in the structure
43+
)
44+
45+
// Parsed autofill field
46+
data class ParsedAutofillField(
47+
val autofillId: AutofillId,
48+
val packageId: String?,
49+
val website: String?,
50+
val value: String,
51+
val type: AutofillFieldType = AutofillFieldType.UNKNOWN,
52+
val originalNode: ViewNode,
53+
)
54+
55+
enum class AutofillFieldType {
56+
USERNAME,
57+
PASSWORD,
58+
UNKNOWN,
59+
}
60+
61+
@SingleInstanceIn(AppScope::class)
62+
@ContributesBinding(AppScope::class)
63+
class RealAutofillParser @Inject constructor(
64+
private val appBuildConfig: AppBuildConfig,
65+
private val viewNodeClassifier: ViewNodeClassifier,
66+
) : AutofillParser {
67+
68+
override fun parseStructure(structure: AssistStructure): MutableList<AutofillRootNode> {
69+
val autofillRootNodes = mutableListOf<AutofillRootNode>()
70+
val windowNodeCount = structure.windowNodeCount
71+
for (i in 0 until windowNodeCount) {
72+
val windowNode = structure.getWindowNodeAt(i)
73+
windowNode.rootViewNode?.let { viewNode ->
74+
autofillRootNodes.add(
75+
traverseViewNode(viewNode).convertIntoAutofillNode(),
76+
)
77+
}
78+
}
79+
return autofillRootNodes
80+
}
81+
82+
private fun traverseViewNode(
83+
viewNode: ViewNode,
84+
): MutableList<ParsedAutofillField> {
85+
val autofillId = viewNode.autofillId ?: return mutableListOf()
86+
Timber.v("DDGAutofillService Parsing NODE: $autofillId")
87+
val traversalDataList = mutableListOf<ParsedAutofillField>()
88+
val packageId = viewNode.validPackageId()
89+
val website = viewNode.website()
90+
val autofillType = viewNodeClassifier.classify(viewNode)
91+
val value = kotlin.runCatching { viewNode.autofillValue?.textValue?.toString() ?: "" }.getOrDefault("")
92+
val parsedAutofillField = ParsedAutofillField(
93+
autofillId = autofillId,
94+
packageId = packageId,
95+
website = website,
96+
value = value,
97+
type = autofillType,
98+
originalNode = viewNode,
99+
)
100+
Timber.v("DDGAutofillService Parsed as: $parsedAutofillField")
101+
traversalDataList.add(parsedAutofillField)
102+
103+
for (i in 0 until viewNode.childCount) {
104+
val childNode = viewNode.getChildAt(i)
105+
traversalDataList.addAll(traverseViewNode(childNode))
106+
}
107+
108+
return traversalDataList
109+
}
110+
111+
private fun List<ParsedAutofillField>.convertIntoAutofillNode(): AutofillRootNode {
112+
return AutofillRootNode(
113+
packageId = this.firstOrNull { it.packageId != null }?.packageId,
114+
website = this.firstOrNull { it.website != null }?.website,
115+
parsedAutofillFields = this,
116+
)
117+
}
118+
119+
private fun ViewNode.validPackageId(): String? {
120+
return this.idPackage
121+
.takeUnless { it.isNullOrBlank() }
122+
?.takeUnless { it in INVALID_PACKAGE_ID }
123+
}
124+
125+
@SuppressLint("NewApi")
126+
private fun ViewNode.website(): String? {
127+
return this.webDomain?.takeUnless { it.isBlank() }
128+
?.let { nonEmptyDomain ->
129+
val scheme = if (appBuildConfig.sdkInt >= 28) {
130+
this.webScheme.takeUnless { it.isNullOrBlank() } ?: "http"
131+
} else {
132+
"http"
133+
}
134+
"$scheme://$nonEmptyDomain"
135+
}
136+
}
137+
138+
companion object {
139+
private val INVALID_PACKAGE_ID = listOf("android")
140+
}
141+
}

0 commit comments

Comments
 (0)