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); + } + } }