Added auth activities for inline fill service.
This CL adds functionality to test both response and dataset
authentication using the inline fill service. Setting either
mAuthenticateResponses or mAuthenticateDatasets will test either
functionality.
Bug: 148815880
Test: manual verification
Change-Id: Ifb734c4fd8d5ef38dd4c6c9d4b493c80abbde305
diff --git a/samples/InlineFillService/Android.bp b/samples/InlineFillService/Android.bp
index 00fa237..f8d65d0 100644
--- a/samples/InlineFillService/Android.bp
+++ b/samples/InlineFillService/Android.bp
@@ -21,5 +21,6 @@
sdk_version: "system_current",
static_libs: [
"androidx.annotation_annotation",
+ "androidx.autofill_autofill"
],
}
\ No newline at end of file
diff --git a/samples/InlineFillService/AndroidManifest.xml b/samples/InlineFillService/AndroidManifest.xml
index 29e60da..56ebf1f 100644
--- a/samples/InlineFillService/AndroidManifest.xml
+++ b/samples/InlineFillService/AndroidManifest.xml
@@ -13,6 +13,11 @@
android:resource="@xml/autofill_service_config">
</meta-data>
</service>
+
+ <activity
+ android:name=".AuthActivity"
+ android:taskAffinity=".AuthActivity"
+ android:label="Autofill Authentication" />
</application>
</manifest>
diff --git a/samples/InlineFillService/res/layout/auth_activity.xml b/samples/InlineFillService/res/layout/auth_activity.xml
new file mode 100644
index 0000000..98f6ff8
--- /dev/null
+++ b/samples/InlineFillService/res/layout/auth_activity.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAutofill="noExcludeDescendants"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Authenticate?" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/no"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="No" />
+ <Button
+ android:id="@+id/yes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Yes" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java b/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java
new file mode 100644
index 0000000..9af2494
--- /dev/null
+++ b/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 foo.bar.inline;
+
+import static android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.util.ArrayMap;
+import android.view.autofill.AutofillId;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Activity used for autofill authentication, it simply sets the dataste upon tapping OK.
+ */
+// TODO(b/114236837): should display a small dialog, not take the full screen
+public class AuthActivity extends Activity {
+
+ private static final String EXTRA_DATASET = "dataset";
+ private static final String EXTRA_HINTS = "hints";
+ private static final String EXTRA_IDS = "ids";
+ private static final String EXTRA_AUTH_DATASETS = "auth_datasets";
+ private static final String EXTRA_INLINE_REQUEST = "inline_request";
+
+ private static int sPendingIntentId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.auth_activity);
+ findViewById(R.id.yes).setOnClickListener((view) -> onYes());
+ findViewById(R.id.no).setOnClickListener((view) -> onNo());
+ }
+
+ private void onYes() {
+ Intent myIntent = getIntent();
+ Intent replyIntent = new Intent();
+ Dataset dataset = myIntent.getParcelableExtra(EXTRA_DATASET);
+ if (dataset != null) {
+ replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, dataset);
+ } else {
+ String[] hints = myIntent.getStringArrayExtra(EXTRA_HINTS);
+ Parcelable[] ids = myIntent.getParcelableArrayExtra(EXTRA_IDS);
+ boolean authenticateDatasets = myIntent.getBooleanExtra(EXTRA_AUTH_DATASETS, false);
+ final InlineSuggestionsRequest inlineRequest =
+ myIntent.getParcelableExtra(EXTRA_INLINE_REQUEST);
+ int size = hints.length;
+ ArrayMap<String, AutofillId> fields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ fields.put(hints[i], (AutofillId) ids[i]);
+ }
+ FillResponse response =
+ InlineFillService.createResponse(this, fields, 1, authenticateDatasets,
+ inlineRequest);
+ replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, response);
+ }
+ setResult(RESULT_OK, replyIntent);
+ finish();
+ }
+
+ private void onNo() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ public static IntentSender newIntentSenderForDataset(@NonNull Context context,
+ @NonNull Dataset dataset) {
+ return newIntentSender(context, dataset, null, null, false, null);
+ }
+
+ public static IntentSender newIntentSenderForResponse(@NonNull Context context,
+ @NonNull String[] hints, @NonNull AutofillId[] ids, boolean authenticateDatasets,
+ @NonNull InlineSuggestionsRequest inlineRequest) {
+ return newIntentSender(context, null, hints, ids, authenticateDatasets, inlineRequest);
+ }
+
+ private static IntentSender newIntentSender(@NonNull Context context,
+ @Nullable Dataset dataset, @Nullable String[] hints, @Nullable AutofillId[] ids,
+ boolean authenticateDatasets, @Nullable InlineSuggestionsRequest inlineRequest) {
+ Intent intent = new Intent(context, AuthActivity.class);
+ if (dataset != null) {
+ intent.putExtra(EXTRA_DATASET, dataset);
+ } else {
+ intent.putExtra(EXTRA_HINTS, hints);
+ intent.putExtra(EXTRA_IDS, ids);
+ intent.putExtra(EXTRA_AUTH_DATASETS, authenticateDatasets);
+ intent.putExtra(EXTRA_INLINE_REQUEST, inlineRequest);
+ }
+
+ return PendingIntent.getActivity(context, ++sPendingIntentId, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
+ }
+}
diff --git a/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java b/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java
index 8737fb1..3d925da 100644
--- a/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java
+++ b/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java
@@ -20,6 +20,7 @@
import android.app.slice.Slice;
import android.app.slice.SliceSpec;
import android.content.Context;
+import android.content.IntentSender;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.CancellationSignal;
@@ -45,6 +46,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.autofill.InlinePresentationBuilder;
import java.util.Collection;
import java.util.Collections;
@@ -65,6 +67,9 @@
*/
static final int NUMBER_DATASETS = 6;
+ private final boolean mAuthenticateResponses = false;
+ private final boolean mAuthenticateDatasets = false;
+
@Override
public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
FillCallback callback) {
@@ -88,21 +93,86 @@
: Math.min(inlineRequest.getMaxSuggestionCount(), NUMBER_DATASETS);
// Create the base response
- FillResponse response = createResponse(this, fields, maxSuggestionsCount,
- request.getInlineSuggestionsRequest());
+ final FillResponse response;
+ if (mAuthenticateResponses) {
+ int size = fields.size();
+ String[] hints = new String[size];
+ AutofillId[] ids = new AutofillId[size];
+ for (int i = 0; i < size; i++) {
+ hints[i] = fields.keyAt(i);
+ ids[i] = fields.valueAt(i);
+ }
+
+ IntentSender authentication = AuthActivity.newIntentSenderForResponse(this, hints,
+ ids, mAuthenticateDatasets, inlineRequest);
+ RemoteViews presentation = newDatasetPresentation(getPackageName(),
+ "Tap to auth response");
+
+ final InlinePresentation inlinePresentation;
+ if (inlineRequest != null) {
+ final Slice authSlice = new InlinePresentationBuilder("Tap to auth respones")
+ .build();
+ final List<InlinePresentationSpec> specs = inlineRequest.getPresentationSpecs();
+ final int specsSize = specs.size();
+ final InlinePresentationSpec currentSpec = specsSize > 0 ? specs.get(0) : null;
+ inlinePresentation = new InlinePresentation(authSlice, currentSpec,
+ /* pined= */ false);
+ } else {
+ inlinePresentation = null;
+ }
+
+ response = new FillResponse.Builder()
+ .setAuthentication(ids, authentication, presentation, inlinePresentation)
+ .build();
+ } else {
+ response = createResponse(this, fields, maxSuggestionsCount, mAuthenticateDatasets,
+ request.getInlineSuggestionsRequest());
+ }
+
callback.onSuccess(response);
}
static FillResponse createResponse(@NonNull Context context,
@NonNull ArrayMap<String, AutofillId> fields, int numDatasets,
- @Nullable InlineSuggestionsRequest inlineRequest) {
+ boolean authenticateDatasets, @Nullable InlineSuggestionsRequest inlineRequest) {
String packageName = context.getPackageName();
FillResponse.Builder response = new FillResponse.Builder();
// 1.Add the dynamic datasets
for (int i = 1; i <= numDatasets; i++) {
Dataset unlockedDataset = newUnlockedDataset(context, fields, packageName, i,
inlineRequest);
- response.addDataset(unlockedDataset);
+ if (authenticateDatasets) {
+ Dataset.Builder lockedDataset = new Dataset.Builder();
+ for (Entry<String, AutofillId> field : fields.entrySet()) {
+ String hint = field.getKey();
+ AutofillId id = field.getValue();
+ String value = i + "-" + hint;
+ IntentSender authentication =
+ AuthActivity.newIntentSenderForDataset(context, unlockedDataset);
+ RemoteViews presentation = newDatasetPresentation(packageName,
+ "Tap to auth " + value);
+
+ final InlinePresentation inlinePresentation;
+ if (inlineRequest != null) {
+ final Slice authSlice = new InlinePresentationBuilder("Tap to auth " + value).build();
+ final List<InlinePresentationSpec> specs
+ = inlineRequest.getPresentationSpecs();
+ final int specsSize = specs.size();
+ final InlinePresentationSpec currentSpec =
+ specsSize > 0 ? specs.get(0) : null;
+ inlinePresentation = new InlinePresentation(authSlice, currentSpec,
+ /* pined= */ false);
+ lockedDataset.setValue(id, null, presentation, inlinePresentation)
+ .setAuthentication(authentication);
+ } else {
+ lockedDataset.setValue(id, null, presentation)
+ .setAuthentication(authentication);
+ }
+ }
+ response.addDataset(lockedDataset.build());
+ } else {
+ response.addDataset(unlockedDataset);
+ }
}
if(inlineRequest != null) {
@@ -128,10 +198,8 @@
static InlinePresentation newInlineAction(@NonNull Context context,
@NonNull Size size, int drawable) {
- final Slice suggestionSlice = new Slice.Builder(Uri.parse("inline.slice"),
- new SliceSpec("InlinePresentation", 1))
- .addIcon(Icon.createWithResource(context, drawable), null,
- Collections.singletonList(""))
+ final Slice suggestionSlice = new InlinePresentationBuilder()
+ .setStartIcon(Icon.createWithResource(context, drawable))
.build();
final InlinePresentationSpec currentSpec = new InlinePresentationSpec.Builder(size,
size).build();
@@ -158,10 +226,7 @@
if (inlineRequest != null) {
Log.d(TAG, "Found InlineSuggestionsRequest in FillRequest: " + inlineRequest);
- final Slice suggestionSlice = new Slice.Builder(Uri.parse("inline.slice"),
- new SliceSpec("InlinePresentation", 1))
- .addText(value, null, Collections.singletonList("inline_title"))
- .build();
+ final Slice suggestionSlice = new InlinePresentationBuilder(value).build();
final List<InlinePresentationSpec> specs = inlineRequest.getPresentationSpecs();
final int specsSize = specs.size();