diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 37c287eebf18e..26ac6adce95d9 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -7261,7 +7261,7 @@ public class Activity extends ContextThemeWrapper
}
} else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
- getAutofillManager().onAuthenticationResult(resultData);
+ getAutofillManager().onAuthenticationResult(requestCode, resultData);
} else {
Fragment frag = mFragments.findFragmentByWho(who);
if (frag != null) {
@@ -7406,10 +7406,11 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- final public void autofillCallbackAuthenticate(IntentSender intent, Intent fillInIntent) {
+ final public void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
+ Intent fillInIntent) {
try {
startIntentSenderForResultInner(intent, AUTO_FILL_AUTH_WHO_PREFIX,
- 0, fillInIntent, 0, 0, null);
+ authenticationId, fillInIntent, 0, 0, null);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, "authenticate() failed for intent:" + intent, e);
}
diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index 69f3f6723244f..af2eb34f87513 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -162,14 +162,18 @@ public final class Dataset implements Parcelable {
*
*
When a user triggers autofill, the system launches the provided intent
* whose extras will have the {@link
- * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content}. Once
- * you complete your authentication flow you should set the activity result to {@link
- * android.app.Activity#RESULT_OK} and provide the fully populated {@link Dataset
- * dataset} by setting it to the {@link
- * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. For example,
- * if you provided credit card information without the CVV for the data set in the
- * {@link FillResponse response} then the returned data set should contain the
- * CVV entry.
+ * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content},
+ * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client
+ * state}. Once you complete your authentication flow you should set the activity
+ * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
+ * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by
+ * setting it to the {@link
+ * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you
+ * provide a dataset in the result, it will replace the authenticated dataset and
+ * will be immediately filled in. If you provide a response, it will replace the
+ * current response and the UI will be refreshed. For example, if you provided
+ * credit card information without the CVV for the data set in the {@link FillResponse
+ * response} then the returned data set should contain the CVV entry.
*
* >Note: Do not make the provided pending intent
* immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
@@ -210,12 +214,15 @@ public final class Dataset implements Parcelable {
*
* @param id id returned by {@link
* android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
- * @param value value to be auto filled.
+ * @param value value to be auto filled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs an authentication and you have no access to the value.
+ * Filtering matches any user typed string to {@code null} values.
* @return This builder.
* @throws IllegalStateException if the builder was constructed without a presentation
* ({@link RemoteViews}).
*/
- public @NonNull Builder setValue(@NonNull AutofillId id, @NonNull AutofillValue value) {
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) {
throwIfDestroyed();
if (mPresentation == null) {
throw new IllegalStateException("Dataset presentation not set on constructor");
@@ -229,11 +236,14 @@ public final class Dataset implements Parcelable {
*
* @param id id returned by {@link
* android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
- * @param value value to be auto filled.
+ * @param value value to be auto filled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs an authentication and you have no access to the value.
+ * Filtering matches any user typed string to {@code null} values.
* @param presentation The presentation used to visualize this field.
* @return This builder.
*/
- public @NonNull Builder setValue(@NonNull AutofillId id, @NonNull AutofillValue value,
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
@NonNull RemoteViews presentation) {
throwIfDestroyed();
Preconditions.checkNotNull(presentation, "presentation cannot be null");
@@ -244,7 +254,6 @@ public final class Dataset implements Parcelable {
private void setValueAndPresentation(AutofillId id, AutofillValue value,
RemoteViews presentation) {
Preconditions.checkNotNull(id, "id cannot be null");
- Preconditions.checkNotNull(value, "value cannot be null");
if (mFieldIds != null) {
final int existingIdx = mFieldIds.indexOf(id);
if (existingIdx >= 0) {
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 1914db9eb4387..bc96e439811f6 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -236,7 +236,8 @@ public final class FillResponse implements Parcelable {
*
*
When a user triggers autofill, the system launches the provided intent
* whose extras will have the {@link AutofillManager#EXTRA_ASSIST_STRUCTURE screen
- * content}. Once you complete your authentication flow you should set the activity
+ * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE
+ * client state}. Once you complete your authentication flow you should set the activity
* result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
* {@link FillResponse response} by setting it to the {@link
* AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra.
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index c7151db3cb5c2..75a9965e32a83 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -117,6 +117,48 @@ public final class AutofillManager {
/** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2;
/** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4;
+ /** Which bits in an authentication id are used for the dataset id */
+ private static final int AUTHENTICATION_ID_DATASET_ID_MASK = 0xFFFF;
+ /** How many bits in an authentication id are used for the dataset id */
+ private static final int AUTHENTICATION_ID_DATASET_ID_SHIFT = 16;
+ /** @hide The index for an undefined data set */
+ public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF;
+
+ /**
+ * Makes an authentication id from a request id and a dataset id.
+ *
+ * @param requestId The request id.
+ * @param datasetId The dataset id.
+ * @return The authentication id.
+ * @hide
+ */
+ public static int makeAuthenticationId(int requestId, int datasetId) {
+ return (requestId << AUTHENTICATION_ID_DATASET_ID_SHIFT)
+ | (datasetId & AUTHENTICATION_ID_DATASET_ID_MASK);
+ }
+
+ /**
+ * Gets the request id from an authentication id.
+ *
+ * @param authRequestId The authentication id.
+ * @return The request id.
+ * @hide
+ */
+ public static int getRequestIdFromAuthenticationId(int authRequestId) {
+ return (authRequestId >> AUTHENTICATION_ID_DATASET_ID_SHIFT);
+ }
+
+ /**
+ * Gets the dataset id from an authentication id.
+ *
+ * @param authRequestId The authentication id.
+ * @return The dataset id.
+ * @hide
+ */
+ public static int getDatasetIdFromAuthenticationId(int authRequestId) {
+ return (authRequestId & AUTHENTICATION_ID_DATASET_ID_MASK);
+ }
+
private final MetricsLogger mMetricsLogger = new MetricsLogger();
/**
@@ -156,10 +198,12 @@ public final class AutofillManager {
/**
* Asks the client to start an authentication flow.
*
+ * @param authenticationId A unique id of the authentication operation.
* @param intent The authentication intent.
* @param fillInIntent The authentication fill-in intent.
*/
- void autofillCallbackAuthenticate(IntentSender intent, Intent fillInIntent);
+ void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
+ Intent fillInIntent);
/**
* Tells the client this manager has state to be reset.
@@ -675,7 +719,7 @@ public final class AutofillManager {
}
/** @hide */
- public void onAuthenticationResult(Intent data) {
+ public void onAuthenticationResult(int authenticationId, Intent data) {
if (!hasAutofillFeature()) {
return;
}
@@ -694,7 +738,8 @@ public final class AutofillManager {
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
try {
- mService.setAuthenticationResult(responseData, mSessionId, mContext.getUserId());
+ mService.setAuthenticationResult(responseData, mSessionId, authenticationId,
+ mContext.getUserId());
} catch (RemoteException e) {
Log.e(TAG, "Error delivering authentication result", e);
}
@@ -870,12 +915,13 @@ public final class AutofillManager {
}
}
- private void authenticate(int sessionId, IntentSender intent, Intent fillInIntent) {
+ private void authenticate(int sessionId, int authenticationId, IntentSender intent,
+ Intent fillInIntent) {
synchronized (mLock) {
if (sessionId == mSessionId) {
AutofillClient client = getClientLocked();
if (client != null) {
- client.autofillCallbackAuthenticate(intent, fillInIntent);
+ client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
}
}
}
@@ -1393,11 +1439,12 @@ public final class AutofillManager {
}
@Override
- public void authenticate(int sessionId, IntentSender intent, Intent fillInIntent) {
+ public void authenticate(int sessionId, int authenticationId, IntentSender intent,
+ Intent fillInIntent) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(
- () -> afm.authenticate(sessionId, intent, fillInIntent));
+ () -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent));
}
}
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index 4193a3cbf51c4..a12e9560115a9 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -41,7 +41,7 @@ interface IAutoFillManager {
in AutofillValue value, int action, int flags, int userId);
void finishSession(int sessionId, int userId);
void cancelSession(int sessionId, int userId);
- void setAuthenticationResult(in Bundle data, int sessionId, int userId);
+ void setAuthenticationResult(in Bundle data, int sessionId, int authenticationId, int userId);
void setHasCallback(int sessionId, int userId, boolean hasIt);
void disableOwnedAutofillServices(int userId);
boolean isServiceSupported(int userId);
diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
index 825d311a5b3bd..1d66f7f7f4633 100644
--- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl
+++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
@@ -45,7 +45,8 @@ oneway interface IAutoFillManagerClient {
/**
* Authenticates a fill response or a data set.
*/
- void authenticate(int sessionId, in IntentSender intent, in Intent fillInIntent);
+ void authenticate(int sessionId, int authenticationId, in IntentSender intent,
+ in Intent fillInIntent);
/**
* Sets the views to track. If saveOnAllViewsInvisible is set and all these view are invisible
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 27f98b3c2c45c..72011e87c61e1 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4629,6 +4629,15 @@
Contents can\u2019t be autofilled
+
+ No autofill suggestions
+
+
+
+ - One autofill suggestion
+ - %1$s autofill suggestions
+
+
Save to <b>%1$s</b>?
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index cff6eb198473e..2aab59035b2c6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2891,6 +2891,8 @@
+
+
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index e3398c96d881f..4dd0b35b6d6b7 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -454,10 +454,12 @@ public final class AutofillManagerService extends SystemService {
}
@Override
- public void setAuthenticationResult(Bundle data, int sessionId, int userId) {
+ public void setAuthenticationResult(Bundle data, int sessionId, int authenticationId,
+ int userId) {
synchronized (mLock) {
final AutofillManagerServiceImpl service = getServiceForUserLocked(userId);
- service.setAuthenticationResultLocked(data, sessionId, getCallingUid());
+ service.setAuthenticationResultLocked(data, sessionId, authenticationId,
+ getCallingUid());
}
}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index e315f9d15a745..faa61825dc71d 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -241,13 +241,13 @@ final class AutofillManagerServiceImpl {
return isEnabled();
}
- void setAuthenticationResultLocked(Bundle data, int sessionId, int uid) {
+ void setAuthenticationResultLocked(Bundle data, int sessionId, int authenticationId, int uid) {
if (!isEnabled()) {
return;
}
final Session session = mSessions.get(sessionId);
if (session != null && uid == session.uid) {
- session.setAuthenticationResultLocked(data);
+ session.setAuthenticationResultLocked(data, authenticationId);
}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3f78fb8caea7d..70771e866684a 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -29,7 +29,6 @@ import static com.android.server.autofill.Helper.findViewNodeById;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sVerbose;
import static com.android.server.autofill.ViewState.STATE_AUTOFILLED;
-import static com.android.server.autofill.ViewState.STATE_FILLABLE;
import static com.android.server.autofill.ViewState.STATE_RESTARTED_SESSION;
import android.annotation.NonNull;
@@ -58,7 +57,6 @@ import android.service.autofill.FillResponse;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
import android.util.ArrayMap;
-import android.util.DebugUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.view.autofill.AutofillId;
@@ -79,6 +77,7 @@ import com.android.server.autofill.ui.AutoFillUI;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
@@ -139,18 +138,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
@GuardedBy("mLock")
private SparseArray mResponses;
- /**
- * Response that requires a service authentitcation request.
- */
- @GuardedBy("mLock")
- private FillResponse mResponseWaitingAuth;
-
- /**
- * Dataset that when tapped launched a service authentication request.
- */
- @GuardedBy("mLock")
- private Dataset mDatasetWaitingAuth;
-
/**
* Contexts read from the app; they will be updated (sanitized, change values for save) before
* sent to {@link AutofillService}. Ordered by the time they we read.
@@ -414,10 +401,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
notifyUnavailableToClient();
}
synchronized (mLock) {
- if (response.getAuthentication() != null) {
- // TODO(b/37424539): proper implementation
- mResponseWaitingAuth = response;
- }
processResponseLocked(response);
}
@@ -525,7 +508,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// FillServiceCallbacks
@Override
- public void authenticate(int requestId, IntentSender intent, Bundle extras) {
+ public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras) {
final Intent fillInIntent;
synchronized (mLock) {
synchronized (mLock) {
@@ -541,7 +524,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mService.setAuthenticationSelected();
- mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent));
+ final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex);
+ mHandlerCaller.getHandler().post(() -> startAuthentication(authenticationId,
+ intent, fillInIntent));
}
// FillServiceCallbacks
@@ -552,7 +537,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// AutoFillUiCallback
@Override
- public void fill(int requestId, Dataset dataset) {
+ public void fill(int requestId, int datasetIndex, Dataset dataset) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#fill() rejected - session: "
@@ -560,7 +545,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
}
- mHandlerCaller.getHandler().post(() -> autoFill(requestId, dataset));
+ mHandlerCaller.getHandler().post(() -> autoFill(requestId, datasetIndex, dataset));
}
// AutoFillUiCallback
@@ -656,54 +641,42 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
});
}
- void setAuthenticationResultLocked(Bundle data) {
+ void setAuthenticationResultLocked(Bundle data, int authenticationId) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#setAuthenticationResultLocked() rejected - session: "
+ id + " destroyed");
return;
}
- if ((mResponseWaitingAuth == null && mDatasetWaitingAuth == null) || data == null) {
+
+ final int requestId = AutofillManager.getRequestIdFromAuthenticationId(authenticationId);
+ final FillResponse authenticatedResponse = mResponses.get(requestId);
+ if (authenticatedResponse == null || data == null) {
removeSelf();
- } else {
- final Parcelable result = data.getParcelable(
- AutofillManager.EXTRA_AUTHENTICATION_RESULT);
- if (sVerbose) Slog.d(TAG, "setAuthenticationResultLocked() for " + result);
+ return;
+ }
- if (result instanceof FillResponse) {
- FillResponse response = (FillResponse) result;
+ final int datasetIdx = AutofillManager.getDatasetIdFromAuthenticationId(
+ authenticationId);
+ // Authenticated a dataset - reset view state regardless if we got a response or a dataset
+ if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
+ final Dataset dataset = authenticatedResponse.getDatasets().get(datasetIdx);
+ if (dataset == null) {
+ removeSelf();
+ return;
+ }
+ resetViewStatesLocked(dataset, ViewState.STATE_WAITING_DATASET_AUTH);
+ }
- mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName);
- final int requestIndex = mResponses.indexOfValue(mResponseWaitingAuth);
- mResponseWaitingAuth = null;
- if (requestIndex >= 0) {
- response.setRequestId(mResponses.keyAt(requestIndex));
- if (response.getDatasets() == null || response.getDatasets().isEmpty()) {
- // TODO(b/37424539): there is a race condition that causes the authentication
- // dialog to be shown again after the service authreplied with a no-datasets
- // response. We're fixing it by hiding the UI when that happens, but that
- // sounds like a hack - hopefully the real problem will go away when we
- // refactor auth to support partitions; if it doesn't, we need to
- // investigate it further (it can be reproduced by running
- // LoginActivityTest.testFillResponseAuthServiceHasNoData())
- mUi.hideAll(this);
- }
- processResponseLocked(response);
- } else {
- Slog.e(TAG, "Error cannot find id for auth response");
- }
- } else if (result instanceof Dataset) {
+ final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT);
+ if (result instanceof FillResponse) {
+ final FillResponse response = (FillResponse) result;
+ mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName);
+ replaceResponseLocked(authenticatedResponse, response);
+ } else if (result instanceof Dataset) {
+ if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
final Dataset dataset = (Dataset) result;
- for (int i = 0; i < mResponses.size(); i++) {
- final FillResponse response = mResponses.valueAt(i);
- final int index = response.getDatasets().indexOf(mDatasetWaitingAuth);
- if (index >= 0) {
- response.getDatasets().set(index, dataset);
- mDatasetWaitingAuth = null;
- autoFill(mResponses.keyAt(i), dataset);
- resetViewStatesLocked(dataset, ViewState.STATE_WAITING_DATASET_AUTH);
- return;
- }
- }
+ authenticatedResponse.getDatasets().set(datasetIdx, dataset);
+ autoFill(requestId, datasetIdx, dataset);
}
}
}
@@ -939,6 +912,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
+ private static final int PARTITION_MAX_COUNT = 64;
/**
* Determines if a new partition should be started for an id.
*
@@ -952,6 +926,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
final int numResponses = mResponses.size();
+ if (numResponses >= PARTITION_MAX_COUNT) {
+ Slog.e(TAG, "Cannot create more than 64 partitions. Not creating a new partition.");
+ return false;
+ }
+
for (int responseNum = 0; responseNum < numResponses; responseNum++) {
final FillResponse response = mResponses.valueAt(responseNum);
@@ -1056,7 +1035,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// Remove the UI if the ViewState has changed.
if (mCurrentViewId != viewState.id) {
- mUi.hideFillUi(this);
+ hideFillUiIfOwnedByMe();
mCurrentViewId = viewState.id;
}
@@ -1066,7 +1045,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
case ACTION_VIEW_EXITED:
if (mCurrentViewId == viewState.id) {
if (sVerbose) Slog.d(TAG, "Exiting view " + id);
- mUi.hideFillUi(this);
+ hideFillUiIfOwnedByMe();
mCurrentViewId = null;
}
break;
@@ -1162,27 +1141,43 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
- private void processResponseLocked(@NonNull FillResponse response) {
- final int requestId = response.getRequestId();
+ private void replaceResponseLocked(@NonNull FillResponse oldResponse,
+ @NonNull FillResponse newResponse) {
+ // Disassociate view states with the old response
+ setViewStatesLocked(oldResponse, ViewState.STATE_INITIAL, true);
+ // Move over the id
+ newResponse.setRequestId(oldResponse.getRequestId());
+ // Replace the old response
+ mResponses.put(newResponse.getRequestId(), newResponse);
+ // Now process the new response
+ processResponseLocked(newResponse);
+ }
+
+ private void processResponseLocked(@NonNull FillResponse newResponse) {
+ // Make sure we are hiding the UI which will be shown
+ // only if handling the current response requires it.
+ hideAllUiIfOwnedByMe();
+
+ final int requestId = newResponse.getRequestId();
if (sVerbose) {
Slog.v(TAG, "processResponseLocked(): mCurrentViewId=" + mCurrentViewId
- + ", reqId=" + requestId + ", resp=" + response);
+ + ", reqId=" + requestId + ", resp=" + newResponse);
}
if (mResponses == null) {
mResponses = new SparseArray<>(4);
}
- mResponses.put(requestId, response);
- mClientState = response.getClientState();
+ mResponses.put(requestId, newResponse);
+ mClientState = newResponse.getClientState();
- setViewStatesLocked(response, ViewState.STATE_FILLABLE);
+ setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false);
updateTrackedIdsLocked();
if (mCurrentViewId == null) {
return;
}
- final ArrayList datasets = response.getDatasets();
+ final ArrayList datasets = newResponse.getDatasets();
if (datasets != null && datasets.size() == 1) {
// Check if it its a single response for a manual request, in which case it should
@@ -1190,7 +1185,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final FillContext context = getFillContextByRequestIdLocked(requestId);
if (context != null && (context.getStructure().getFlags() & FLAG_MANUAL_REQUEST) != 0) {
Slog.d(TAG, "autofilling manual request directly");
- autoFill(requestId, datasets.get(0));
+ autoFill(requestId, 0, datasets.get(0));
return;
}
}
@@ -1202,7 +1197,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
/**
* Sets the state of all views in the given response.
*/
- private void setViewStatesLocked(FillResponse response, int state) {
+ private void setViewStatesLocked(FillResponse response, int state, boolean clearResponse) {
final ArrayList datasets = response.getDatasets();
if (datasets != null) {
for (int i = 0; i < datasets.size(); i++) {
@@ -1211,7 +1206,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
Slog.w(TAG, "Ignoring null dataset on " + datasets);
continue;
}
- setViewStatesLocked(response, dataset, state);
+ setViewStatesLocked(response, dataset, state, clearResponse);
+ }
+ } else if (response.getAuthentication() != null) {
+ for (AutofillId autofillId : response.getAuthenticationIds()) {
+ final ViewState viewState = createOrUpdateViewStateLocked(autofillId, state, null);
+ if (!clearResponse) {
+ viewState.setResponse(response);
+ } else {
+ viewState.setResponse(null);
+ }
}
}
final SaveInfo saveInfo = response.getSaveInfo();
@@ -1234,14 +1238,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
createOrUpdateViewStateLocked(id, state, null);
}
}
-
}
/**
* Sets the state of all views in the given dataset and response.
*/
private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset,
- int state) {
+ int state, boolean clearResponse) {
final ArrayList ids = dataset.getFieldIds();
final ArrayList values = dataset.getFieldValues();
for (int j = 0; j < ids.size(); j++) {
@@ -1250,11 +1253,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final ViewState viewState = createOrUpdateViewStateLocked(id, state, value);
if (response != null) {
viewState.setResponse(response);
+ } else if (clearResponse) {
+ viewState.setResponse(null);
}
}
}
- private ViewState createOrUpdateViewStateLocked(AutofillId id, int state, AutofillValue value) {
+ private ViewState createOrUpdateViewStateLocked(@NonNull AutofillId id, int state,
+ @Nullable AutofillValue value) {
ViewState viewState = mViewStates.get(id);
if (viewState != null) {
viewState.setState(state);
@@ -1285,7 +1291,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
- void autoFill(int requestId, Dataset dataset) {
+ void autoFill(int requestId, int datasetIndex, Dataset dataset) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#autoFill() rejected - session: "
@@ -1303,11 +1309,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// ...or handle authentication.
// TODO(b/37424539): proper implementation
mService.setDatasetAuthenticationSelected(dataset.getId());
- mDatasetWaitingAuth = dataset;
- setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH);
+ setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH, false);
final Intent fillInIntent = createAuthFillInIntent(
- getFillContextByRequestIdLocked(requestId).getStructure(), null);
- startAuthentication(dataset.getAuthentication(), fillInIntent);
+ getFillContextByRequestIdLocked(requestId).getStructure(), mClientState);
+
+ final int authenticationId = AutofillManager.makeAuthenticationId(requestId,
+ datasetIndex);
+ startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent);
}
}
@@ -1317,25 +1325,18 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
- FillResponse getResponseWaitingAuth() {
- synchronized (mLock) {
- return mResponseWaitingAuth;
- }
- }
-
private Intent createAuthFillInIntent(AssistStructure structure, Bundle extras) {
final Intent fillInIntent = new Intent();
fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure);
- if (extras != null) {
- fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras);
- }
+ fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras);
return fillInIntent;
}
- private void startAuthentication(IntentSender intent, Intent fillInIntent) {
+ private void startAuthentication(int authenticationId, IntentSender intent,
+ Intent fillInIntent) {
try {
synchronized (mLock) {
- mClient.authenticate(id, intent, fillInIntent);
+ mClient.authenticate(id, authenticationId, intent, fillInIntent);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
@@ -1347,8 +1348,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
pw.print(prefix); pw.print("uid: "); pw.println(uid);
pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
pw.print(prefix); pw.print("mResponses: "); pw.println(mResponses);
- pw.print(prefix); pw.print("mResponseWaitingAuth: "); pw.println(mResponseWaitingAuth);
- pw.print(prefix); pw.print("mDatasetWaitingAuth: "); pw.println(mDatasetWaitingAuth);
pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId);
pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size());
pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
@@ -1392,8 +1391,21 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
try {
if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
- mClient.autofill(id, dataset.getFieldIds(), dataset.getFieldValues());
- setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED);
+ // Skip null values as a null values means no change
+ final int entryCount = dataset.getFieldIds().size();
+ final List ids = new ArrayList<>(entryCount);
+ final List values = new ArrayList<>(entryCount);
+ for (int i = 0; i < entryCount; i++) {
+ if (dataset.getFieldValues().get(i) == null) {
+ continue;
+ }
+ ids.add(dataset.getFieldIds().get(i));
+ values.add(dataset.getFieldValues().get(i));
+ }
+ if (!ids.isEmpty()) {
+ mClient.autofill(id, ids, values);
+ }
+ setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED, false);
} catch (RemoteException e) {
Slog.w(TAG, "Error autofilling activity: " + e);
}
@@ -1412,12 +1424,20 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
mRemoteFillService.destroy();
- mUi.hideAll(this);
+ hideAllUiIfOwnedByMe();
mUi.clearCallback(this);
mDestroyed = true;
mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName);
}
+ private void hideAllUiIfOwnedByMe() {
+ mUi.hideAll(this);
+ }
+
+ private void hideFillUiIfOwnedByMe() {
+ mUi.hideFillUi(this);
+ }
+
private void removeSelf() {
synchronized (mLock) {
removeSelfLocked();
diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java
index 561c603ea3b00..f87fa1970c4a8 100644
--- a/services/autofill/java/com/android/server/autofill/ViewState.java
+++ b/services/autofill/java/com/android/server/autofill/ViewState.java
@@ -112,7 +112,7 @@ final class ViewState {
return mAutofilledValue;
}
- void setAutofilledValue(AutofillValue value) {
+ void setAutofilledValue(@Nullable AutofillValue value) {
mAutofilledValue = value;
}
@@ -180,15 +180,9 @@ final class ViewState {
}
// First try the current response associated with this View.
if (mResponse != null) {
- if (mResponse.getDatasets() != null) {
+ if (mResponse.getDatasets() != null || mResponse.getAuthentication() != null) {
mListener.onFillReady(mResponse, this.id, mCurrentValue);
}
- return;
- }
- // Then checks if the session has a response waiting authentication; if so, uses it instead.
- final FillResponse responseWaitingAuth = mSession.getResponseWaitingAuth();
- if (responseWaitingAuth != null) {
- mListener.onFillReady(responseWaitingAuth, this.id, mCurrentValue);
}
}
diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
index 9eaabfe23b1bc..4f69f644f5e8e 100644
--- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
@@ -30,6 +30,7 @@ import android.service.autofill.SaveInfo;
import android.text.TextUtils;
import android.util.Slog;
import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
import android.view.autofill.IAutofillWindowPresenter;
import android.widget.Toast;
@@ -60,8 +61,9 @@ public final class AutoFillUI {
private final MetricsLogger mMetricsLogger = new MetricsLogger();
public interface AutoFillUiCallback {
- void authenticate(int requestId, @NonNull IntentSender intent, @Nullable Bundle extras);
- void fill(int requestId, @NonNull Dataset dataset);
+ void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent,
+ @Nullable Bundle extras);
+ void fill(int requestId, int datasetIndex, @NonNull Dataset dataset);
void save();
void cancelSave();
void requestShowFillUi(AutofillId id, int width, int height,
@@ -176,6 +178,7 @@ public final class AutoFillUI {
hideFillUiUiThread(callback);
if (mCallback != null) {
mCallback.authenticate(response.getRequestId(),
+ AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED,
response.getAuthentication(), response.getClientState());
}
}
@@ -185,7 +188,8 @@ public final class AutoFillUI {
log.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
hideFillUiUiThread(callback);
if (mCallback != null) {
- mCallback.fill(response.getRequestId(), dataset);
+ final int datasetIndex = response.getDatasets().indexOf(dataset);
+ mCallback.fill(response.getRequestId(), datasetIndex, dataset);
}
}
diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
index 31b4b55818acc..d315b3d7b70fb 100644
--- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
@@ -28,6 +28,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.service.autofill.Dataset;
import android.service.autofill.FillResponse;
+import android.text.TextUtils;
import android.util.Slog;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -36,10 +37,13 @@ import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAutofillWindowPresenter;
-import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
import android.widget.ListView;
import android.widget.RemoteViews;
@@ -49,6 +53,8 @@ import libcore.util.Objects;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
final class FillUi {
private static final String TAG = "FillUi";
@@ -81,10 +87,12 @@ final class FillUi {
private final @NonNull ListView mListView;
- private final @Nullable ArrayAdapter mAdapter;
+ private final @Nullable ItemsAdapter mAdapter;
private @Nullable String mFilterText;
+ private @Nullable AnnounceFilterResult mAnnounceFilterResult;
+
private int mContentWidth;
private int mContentHeight;
@@ -157,7 +165,9 @@ final class FillUi {
}
final AutofillValue value = dataset.getFieldValues().get(index);
String valueText = null;
- if (value.isText()) {
+ // If the dataset needs auth - don't add its text to allow guessing
+ // its content based on how filtering behaves.
+ if (value != null && value.isText() && dataset.getAuthentication() == null) {
valueText = value.getTextValue().toString().toLowerCase();
}
@@ -165,12 +175,7 @@ final class FillUi {
}
}
- mAdapter = new ArrayAdapter(context, 0, items) {
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- return getItem(position).getView();
- }
- };
+ mAdapter = new ItemsAdapter(items);
mListView = decor.findViewById(R.id.autofill_dataset_list);
mListView.setAdapter(mAdapter);
@@ -270,8 +275,7 @@ final class FillUi {
MeasureSpec.AT_MOST);
final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y,
MeasureSpec.AT_MOST);
-
- final int itemCount = Math.min(mAdapter.getCount(), VISIBLE_OPTIONS_MAX_COUNT);
+ final int itemCount = mAdapter.getCount();
for (int i = 0; i < itemCount; i++) {
View view = mAdapter.getItem(i).getView();
view.measure(widthMeasureSpec, heightMeasureSpec);
@@ -281,11 +285,14 @@ final class FillUi {
mContentWidth = newContentWidth;
changed = true;
}
- final int clampedMeasuredHeight = Math.min(view.getMeasuredHeight(), maxSize.y);
- final int newContentHeight = mContentHeight + clampedMeasuredHeight;
- if (newContentHeight != mContentHeight) {
- mContentHeight = newContentHeight;
- changed = true;
+ // Update the width to fit only the first items up to max count
+ if (i < VISIBLE_OPTIONS_MAX_COUNT) {
+ final int clampedMeasuredHeight = Math.min(view.getMeasuredHeight(), maxSize.y);
+ final int newContentHeight = mContentHeight + clampedMeasuredHeight;
+ if (newContentHeight != mContentHeight) {
+ mContentHeight = newContentHeight;
+ changed = true;
+ }
}
}
return changed;
@@ -327,6 +334,10 @@ final class FillUi {
return mDataset;
}
+ public String getValue() {
+ return mValue;
+ }
+
@Override
public String toString() {
// Used for filtering in the adapter
@@ -435,4 +446,116 @@ final class FillUi {
}
}
}
+
+ private void announceSearchResultIfNeeded() {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ if (mAnnounceFilterResult == null) {
+ mAnnounceFilterResult = new AnnounceFilterResult();
+ }
+ mAnnounceFilterResult.post();
+ }
+ }
+
+ private final class ItemsAdapter extends BaseAdapter implements Filterable {
+ private @NonNull final List mAllItems;
+
+ private @NonNull final List mFilteredItems = new ArrayList<>();
+
+ ItemsAdapter(@NonNull List items) {
+ mAllItems = Collections.unmodifiableList(new ArrayList<>(items));
+ mFilteredItems.addAll(items);
+ }
+
+ @Override
+ public Filter getFilter() {
+ return new Filter() {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ // No locking needed as mAllItems is final an immutable
+ final FilterResults results = new FilterResults();
+ if (TextUtils.isEmpty(constraint)) {
+ results.values = mAllItems;
+ results.count = mAllItems.size();
+ return results;
+ }
+ final List filteredItems = new ArrayList<>();
+ final String constraintLowerCase = constraint.toString().toLowerCase();
+ final int itemCount = mAllItems.size();
+ for (int i = 0; i < itemCount; i++) {
+ final ViewItem item = mAllItems.get(i);
+ final String value = item.getValue();
+ // No value, i.e. null, matches any filter
+ if (value == null
+ || value.toLowerCase().contains(constraintLowerCase)) {
+ filteredItems.add(item);
+ }
+ }
+ results.values = filteredItems;
+ results.count = filteredItems.size();
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ final boolean resultCountChanged;
+ final int oldItemCount = mFilteredItems.size();
+ mFilteredItems.clear();
+ @SuppressWarnings("unchecked")
+ final List items = (List) results.values;
+ mFilteredItems.addAll(items);
+ resultCountChanged = (oldItemCount != mFilteredItems.size());
+ if (resultCountChanged) {
+ announceSearchResultIfNeeded();
+ }
+ notifyDataSetChanged();
+ }
+ };
+ }
+
+ @Override
+ public int getCount() {
+ return mFilteredItems.size();
+ }
+
+ @Override
+ public ViewItem getItem(int position) {
+ return mFilteredItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return getItem(position).getView();
+ }
+ }
+
+ private final class AnnounceFilterResult implements Runnable {
+ private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
+
+ public void post() {
+ remove();
+ mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
+ }
+
+ public void remove() {
+ mListView.removeCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ final int count = mListView.getAdapter().getCount();
+ final String text;
+ if (count <= 0) {
+ text = mContext.getString(R.string.autofill_picker_no_suggestions);
+ } else {
+ text = mContext.getResources().getQuantityString(
+ R.plurals.autofill_picker_some_suggestions, count, count);
+ }
+ mListView.announceForAccessibility(text);
+ }
+ }
}