Skip to content

Commit 964220e

Browse files
pocmomergify[bot]
authored andcommitted
Issue mozilla-mobile#9857: Add first test cases for FillRequestHandler.
1 parent 3c0b962 commit 964220e

22 files changed

+946
-167
lines changed

components/feature/autofill/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ dependencies {
4444
implementation Dependencies.kotlin_coroutines
4545

4646
testImplementation project(':support-test')
47+
testImplementation project(':lib-fetch-okhttp')
4748

4849
testImplementation Dependencies.androidx_test_core
4950
testImplementation Dependencies.androidx_test_junit
5051
testImplementation Dependencies.testing_mockito
5152
testImplementation Dependencies.testing_robolectric
5253
testImplementation Dependencies.testing_coroutines
54+
testImplementation Dependencies.testing_mockwebserver
5355
}
5456

5557
apply from: '../../../publish.gradle'

components/feature/autofill/src/main/java/mozilla/components/feature/autofill/AbstractAutofillService.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers
1616
import kotlinx.coroutines.GlobalScope
1717
import kotlinx.coroutines.launch
1818
import mozilla.components.feature.autofill.handler.FillRequestHandler
19+
import mozilla.components.feature.autofill.structure.toRawStructure
1920

2021
/**
2122
* Service responsible for implementing Android's Autofill framework.
@@ -35,7 +36,14 @@ abstract class AbstractAutofillService : AutofillService() {
3536
// seems to get destroyed before we invoke a method on the callback. So we need a scope that
3637
// lives longer than the service.
3738
GlobalScope.launch(Dispatchers.IO) {
38-
val response = fillHandler.handle(request)
39+
// You may be wondering why we translate the AssistStructure into a RawStructure and then
40+
// create a FillResponseBuilder that outputs the FillResponse. This is purely for testing.
41+
// Neither AssistStructure nor FillResponse can be created by us and they do not let us
42+
// inspect their data. So we create these intermediate objects that we can create and
43+
// inspect in unit tests.
44+
val structure = request.fillContexts.last().structure.toRawStructure()
45+
val responseBuilder = fillHandler.handle(structure)
46+
val response = responseBuilder?.build(this@AbstractAutofillService, configuration)
3947
callback.onSuccess(response)
4048
}
4149
}

components/feature/autofill/src/main/java/mozilla/components/feature/autofill/handler/FillRequestHandler.kt

Lines changed: 15 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,19 @@
55
package mozilla.components.feature.autofill.handler
66

77
import android.annotation.SuppressLint
8-
import android.app.PendingIntent
98
import android.app.assist.AssistStructure
109
import android.content.Context
11-
import android.content.Intent
12-
import android.content.IntentSender
1310
import android.os.Build
14-
import android.service.autofill.Dataset
1511
import android.service.autofill.FillRequest
1612
import android.service.autofill.FillResponse
17-
import android.view.autofill.AutofillValue
18-
import android.widget.RemoteViews
1913
import androidx.annotation.RequiresApi
20-
import mozilla.components.concept.storage.Login
2114
import mozilla.components.feature.autofill.AutofillConfiguration
22-
import mozilla.components.feature.autofill.R
23-
import mozilla.components.feature.autofill.structure.ParsedStructure
15+
import mozilla.components.feature.autofill.response.dataset.DatasetBuilder
16+
import mozilla.components.feature.autofill.response.dataset.LoginDatasetBuilder
17+
import mozilla.components.feature.autofill.response.fill.AuthFillResponseBuilder
18+
import mozilla.components.feature.autofill.response.fill.FillResponseBuilder
19+
import mozilla.components.feature.autofill.response.fill.LoginFillResponseBuilder
20+
import mozilla.components.feature.autofill.structure.RawStructure
2421
import mozilla.components.feature.autofill.structure.getLookupDomain
2522
import mozilla.components.feature.autofill.structure.parseStructure
2623

@@ -34,24 +31,16 @@ internal class FillRequestHandler(
3431
private val context: Context,
3532
private val configuration: AutofillConfiguration
3633
) {
37-
/**
38-
* Handles the given [FillRequest] and returns a matching [FillResponse] or `null` if this
39-
* request could not be handled.
40-
*/
41-
suspend fun handle(request: FillRequest, forceUnlock: Boolean = false): FillResponse? {
42-
return handle(request.fillContexts.last().structure, forceUnlock)
43-
}
44-
4534
/**
4635
* Handles a fill request for the given [AssistStructure] and returns a matching [FillResponse]
4736
* or `null` if the request could not be handled or the passed in [AssistStructure] is `null`.
4837
*/
4938
@SuppressLint("InlinedApi")
5039
@Suppress("ReturnCount")
5140
suspend fun handle(
52-
structure: AssistStructure?,
41+
structure: RawStructure?,
5342
forceUnlock: Boolean = false
54-
): FillResponse? {
43+
): FillResponseBuilder? {
5544
if (structure == null) {
5645
return null
5746
}
@@ -70,25 +59,19 @@ internal class FillRequestHandler(
7059
}
7160

7261
return if (!configuration.lock.keepUnlocked() && !forceUnlock) {
73-
createAuthResponse(context, configuration, parsedStructure)
62+
AuthFillResponseBuilder(parsedStructure)
7463
} else {
75-
createLoginsResponse(
76-
context,
77-
configuration,
78-
parsedStructure,
79-
logins,
80-
needsConfirmation
81-
)
64+
LoginFillResponseBuilder(parsedStructure, logins, needsConfirmation)
8265
}
8366
}
8467

8568
/**
86-
* Handles a fill request for the given [AssistStructure] and returns only a [Dataset] for the
87-
* given [loginId] - or `null` if the request could not be handled or the passed in
88-
* [AssistStructure] is `null`
69+
* Handles a fill request for the given [RawStructure] and returns only a [DatasetBuilder] for
70+
* the given [loginId] - or `null` if the request could not be handled or the passed in
71+
* [RawStructure] is `null`
8972
*/
9073
@Suppress("ReturnCount")
91-
suspend fun handleConfirmation(structure: AssistStructure?, loginId: String): Dataset? {
74+
suspend fun handleConfirmation(structure: RawStructure?, loginId: String): DatasetBuilder? {
9275
if (structure == null) {
9376
return null
9477
}
@@ -103,133 +86,6 @@ internal class FillRequestHandler(
10386

10487
val login = logins.firstOrNull { login -> login.guid == loginId } ?: return null
10588

106-
return createDataSetResponse(
107-
context,
108-
configuration,
109-
login,
110-
parsedStructure,
111-
needsConfirmation = false
112-
)
113-
}
114-
}
115-
116-
@RequiresApi(Build.VERSION_CODES.O)
117-
private fun createAuthResponse(
118-
context: Context,
119-
configuration: AutofillConfiguration,
120-
parsedStructure: ParsedStructure
121-
): FillResponse {
122-
val builder = FillResponse.Builder()
123-
124-
val autofillIds = listOfNotNull(parsedStructure.usernameId, parsedStructure.passwordId)
125-
126-
val authPresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1).apply {
127-
setTextViewText(
128-
android.R.id.text1,
129-
context.getString(R.string.mozac_feature_autofill_popup_unlock_application, configuration.applicationName)
130-
)
131-
}
132-
133-
val authIntent = Intent(context, configuration.unlockActivity)
134-
135-
val intentSender: IntentSender = PendingIntent.getActivity(
136-
context,
137-
configuration.activityRequestCode,
138-
authIntent,
139-
PendingIntent.FLAG_CANCEL_CURRENT
140-
).intentSender
141-
142-
builder.setAuthentication(autofillIds.toTypedArray(), intentSender, authPresentation)
143-
144-
return builder.build()
145-
}
146-
147-
@RequiresApi(Build.VERSION_CODES.O)
148-
private fun createLoginsResponse(
149-
context: Context,
150-
configuration: AutofillConfiguration,
151-
parsedStructure: ParsedStructure,
152-
logins: List<Login>,
153-
needsConfirmation: Boolean
154-
): FillResponse {
155-
val builder = FillResponse.Builder()
156-
157-
logins.forEachIndexed { index, login ->
158-
val dataset = createDataSetResponse(
159-
context,
160-
configuration,
161-
login,
162-
parsedStructure,
163-
needsConfirmation,
164-
requestOffset = index
165-
)
166-
builder.addDataset(dataset)
167-
}
168-
169-
return builder.build()
170-
}
171-
172-
@RequiresApi(Build.VERSION_CODES.O)
173-
@Suppress("LongParameterList")
174-
private fun createDataSetResponse(
175-
context: Context,
176-
configuration: AutofillConfiguration,
177-
login: Login,
178-
parsedStructure: ParsedStructure,
179-
needsConfirmation: Boolean,
180-
requestOffset: Int = 0
181-
): Dataset {
182-
val dataset = Dataset.Builder()
183-
184-
val usernamePresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1)
185-
usernamePresentation.setTextViewText(android.R.id.text1, login.usernamePresentationOrFallback(context))
186-
val passwordPresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1)
187-
passwordPresentation.setTextViewText(android.R.id.text1, login.passwordPresentation(context))
188-
189-
parsedStructure.usernameId?.let { id ->
190-
dataset.setValue(
191-
id,
192-
if (needsConfirmation) null else AutofillValue.forText(login.username),
193-
usernamePresentation
194-
)
195-
}
196-
197-
parsedStructure.passwordId?.let { id ->
198-
dataset.setValue(
199-
id,
200-
if (needsConfirmation) null else AutofillValue.forText(login.password),
201-
passwordPresentation
202-
)
89+
return LoginDatasetBuilder(parsedStructure, login, needsConfirmation = false)
20390
}
204-
205-
if (needsConfirmation) {
206-
val confirmIntent = Intent(context, configuration.confirmActivity)
207-
confirmIntent.putExtra(EXTRA_LOGIN_ID, login.guid)
208-
209-
val intentSender: IntentSender = PendingIntent.getActivity(
210-
context,
211-
configuration.activityRequestCode + requestOffset,
212-
confirmIntent,
213-
PendingIntent.FLAG_CANCEL_CURRENT
214-
).intentSender
215-
216-
dataset.setAuthentication(intentSender)
217-
}
218-
219-
return dataset.build()
220-
}
221-
222-
private fun Login.usernamePresentationOrFallback(context: Context): String {
223-
return if (username.isNotEmpty()) {
224-
username
225-
} else {
226-
context.getString(R.string.mozac_feature_autofill_popup_no_username)
227-
}
228-
}
229-
230-
private fun Login.passwordPresentation(context: Context): String {
231-
return context.getString(
232-
R.string.mozac_feature_autofill_popup_password,
233-
usernamePresentationOrFallback(context)
234-
)
23591
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.feature.autofill.response.dataset
6+
7+
import android.content.Context
8+
import android.service.autofill.Dataset
9+
import mozilla.components.feature.autofill.AutofillConfiguration
10+
11+
internal interface DatasetBuilder {
12+
fun build(
13+
context: Context,
14+
configuration: AutofillConfiguration
15+
): Dataset
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.feature.autofill.response.dataset
6+
7+
import android.app.PendingIntent
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.content.IntentSender
11+
import android.os.Build
12+
import android.service.autofill.Dataset
13+
import android.view.autofill.AutofillValue
14+
import android.widget.RemoteViews
15+
import androidx.annotation.RequiresApi
16+
import mozilla.components.concept.storage.Login
17+
import mozilla.components.feature.autofill.AutofillConfiguration
18+
import mozilla.components.feature.autofill.handler.EXTRA_LOGIN_ID
19+
import mozilla.components.feature.autofill.structure.ParsedStructure
20+
21+
@RequiresApi(Build.VERSION_CODES.O)
22+
internal data class LoginDatasetBuilder(
23+
val parsedStructure: ParsedStructure,
24+
val login: Login,
25+
val needsConfirmation: Boolean,
26+
val requestOffset: Int = 0
27+
) : DatasetBuilder {
28+
override fun build(
29+
context: Context,
30+
configuration: AutofillConfiguration
31+
): Dataset {
32+
val dataset = Dataset.Builder()
33+
34+
val usernamePresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1)
35+
usernamePresentation.setTextViewText(android.R.id.text1, login.usernamePresentationOrFallback(context))
36+
val passwordPresentation = RemoteViews(context.packageName, android.R.layout.simple_list_item_1)
37+
passwordPresentation.setTextViewText(android.R.id.text1, login.passwordPresentation(context))
38+
39+
parsedStructure.usernameId?.let { id ->
40+
dataset.setValue(
41+
id,
42+
if (needsConfirmation) null else AutofillValue.forText(login.username),
43+
usernamePresentation
44+
)
45+
}
46+
47+
parsedStructure.passwordId?.let { id ->
48+
dataset.setValue(
49+
id,
50+
if (needsConfirmation) null else AutofillValue.forText(login.password),
51+
passwordPresentation
52+
)
53+
}
54+
55+
if (needsConfirmation) {
56+
val confirmIntent = Intent(context, configuration.confirmActivity)
57+
confirmIntent.putExtra(EXTRA_LOGIN_ID, login.guid)
58+
59+
val intentSender: IntentSender = PendingIntent.getActivity(
60+
context,
61+
configuration.activityRequestCode + requestOffset,
62+
confirmIntent,
63+
PendingIntent.FLAG_CANCEL_CURRENT
64+
).intentSender
65+
66+
dataset.setAuthentication(intentSender)
67+
}
68+
69+
return dataset.build()
70+
}
71+
}
72+
73+
private fun Login.usernamePresentationOrFallback(context: Context): String {
74+
return if (username.isNotEmpty()) {
75+
username
76+
} else {
77+
context.getString(mozilla.components.feature.autofill.R.string.mozac_feature_autofill_popup_no_username)
78+
}
79+
}
80+
81+
private fun Login.passwordPresentation(context: Context): String {
82+
return context.getString(
83+
mozilla.components.feature.autofill.R.string.mozac_feature_autofill_popup_password,
84+
usernamePresentationOrFallback(context)
85+
)
86+
}

0 commit comments

Comments
 (0)