Allow null fill values to support authentication case

If a dataset needs to be authenticated the fill service
may not have the values but needs to tell the system for
which fields to show the fill UI. We now allow passing
a null value to mean the view is a part of the dataset
semantically but its value should remain unchanged.

If a dataset has no values, i.e. the related autofill ids
are mapped to null, we cannot properly filter. In this case
we always match such items regardless what the user typed.

While at this improved accessibility support for filtering
to announce when the number of items being filtered changes.

Also while at this allowed a dataset authentication to return
a response which replaces the current response and refreshes
the UI. Matching datasets with null values to any text plus
allowing a response to be returned from a dataset auth enables
the use case where there is always "Import" item at the
end of the list which when clicked can show arbitrarily more
data entries associated to other apps.

Another change is that we now provide the client state
bundle on both request and dataset auth.

Finally, this change gets rid of dataset waiting auth and
response waiting auth concepts since the reference to the
response and the dataset is piped with the auth request.

Fixed a bug where the width of the autofill UI was not
properly measured by going over all items in the adapter.
Now we measure enough height to fit the first three and the
width id the width of the widest item in the adapter.

Test: Added LoginActivityTest#testDatasetAuthTwoFieldsReplaceResponse
      Added LoginActivityTest#testDatasetAuthTwoFieldsNoValues
      Added LiginActivityTest#filterTextNullValuesAlwaysMatched
      All autofill CTS tests pass

bug:37724701
bug:37424539

Change-Id: Ic19e5d7cbdbb7d110c9e7da0ad60b540cbf1aecf
This commit is contained in:
Svetoslav Ganov
2017-05-09 17:40:24 -07:00
parent 62643dddc7
commit a9379d0b44
14 changed files with 367 additions and 154 deletions

View File

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

View File

@@ -162,14 +162,18 @@ public final class Dataset implements Parcelable {
*
* <p>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.</p>
* 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.
*
* <p></><strong>Note:</strong> 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) {

View File

@@ -236,7 +236,8 @@ public final class FillResponse implements Parcelable {
*
* <p>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.

View File

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

View File

@@ -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);

View File

@@ -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

View File

@@ -4629,6 +4629,15 @@
<!-- Toast message shown when user manually request autofill but service could not figure out the data that would autofill the screen contents. [CHAR LIMIT=NONE] -->
<string name="autofill_error_cannot_autofill">Contents can\u2019t be autofilled</string>
<!-- Accessibility string to announce there are no autofill suggestions in the autofill picker. [CHAR LIMIT=NONE] -->
<string name="autofill_picker_no_suggestions">No autofill suggestions</string>
<!-- Accessibility string to announce there are some autofill suggestions in the autofill picker. [CHAR LIMIT=NONE] -->
<plurals name="autofill_picker_some_suggestions">
<item quantity="one">One autofill suggestion</item>
<item quantity="other"><xliff:g id="count" example="Two">%1$s</xliff:g> autofill suggestions</item>
</plurals>
<!-- Title for the autofill save dialog shown when the the contents of the activity can be saved
by an autofill service, but the service does not know what the activity represents [CHAR LIMIT=NONE] -->
<string name="autofill_save_title">Save to &lt;b><xliff:g id="label" example="MyPass">%1$s</xliff:g>&lt;/b>?</string>

View File

@@ -2891,6 +2891,8 @@
<java-symbol type="id" name="autofill_save_yes" />
<java-symbol type="id" name="autofill_save_close" />
<java-symbol type="string" name="autofill_error_cannot_autofill" />
<java-symbol type="string" name="autofill_picker_no_suggestions" />
<java-symbol type="plurals" name="autofill_picker_some_suggestions" />
<java-symbol type="string" name="autofill" />
<java-symbol type="string" name="autofill_picker_accessibility_title " />
<java-symbol type="string" name="autofill_save_accessibility_title " />

View File

@@ -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());
}
}

View File

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

View File

@@ -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<FillResponse> 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<Dataset> datasets = response.getDatasets();
final ArrayList<Dataset> 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<Dataset> 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<AutofillId> ids = dataset.getFieldIds();
final ArrayList<AutofillValue> 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<AutofillId> ids = new ArrayList<>(entryCount);
final List<AutofillValue> 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();

View File

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

View File

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

View File

@@ -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<ViewItem> 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<ViewItem>(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<ViewItem> mAllItems;
private @NonNull final List<ViewItem> mFilteredItems = new ArrayList<>();
ItemsAdapter(@NonNull List<ViewItem> 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<ViewItem> 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<ViewItem> items = (List<ViewItem>) 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);
}
}
}