Merge "Allow null fill values to support authentication case" into oc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
90bcc1c8bc
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <b><xliff:g id="label" example="MyPass">%1$s</xliff:g></b>?</string>
|
||||
|
||||
@@ -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 " />
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user