diff options
Diffstat (limited to 'android/view/autofill/AutofillManager.java')
-rw-r--r-- | android/view/autofill/AutofillManager.java | 802 |
1 files changed, 670 insertions, 132 deletions
diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java index 4b24a71c..88300dbd 100644 --- a/android/view/autofill/AutofillManager.java +++ b/android/view/autofill/AutofillManager.java @@ -20,14 +20,18 @@ import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.view.autofill.Helper.sDebug; import static android.view.autofill.Helper.sVerbose; +import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresFeature; import android.annotation.SystemService; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.metrics.LogMaker; import android.os.Bundle; @@ -41,13 +45,24 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; +import android.view.Choreographer; +import android.view.KeyEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -58,7 +73,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -// TODO: use java.lang.ref.Cleaner once Android supports Java 9 +//TODO: use java.lang.ref.Cleaner once Android supports Java 9 import sun.misc.Cleaner; /** @@ -122,6 +137,7 @@ import sun.misc.Cleaner; * <p>It is safe to call into its methods from any thread. */ @SystemService(Context.AUTOFILL_MANAGER_SERVICE) +@RequiresFeature(PackageManager.FEATURE_AUTOFILL) public final class AutofillManager { private static final String TAG = "AutofillManager"; @@ -178,7 +194,6 @@ public final class AutofillManager { private static final String STATE_TAG = "android:state"; private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData"; - /** @hide */ public static final int ACTION_START_SESSION = 1; /** @hide */ public static final int ACTION_VIEW_ENTERED = 2; /** @hide */ public static final int ACTION_VIEW_EXITED = 3; @@ -259,6 +274,16 @@ public final class AutofillManager { public static final int STATE_DISABLED_BY_SERVICE = 4; /** + * Same as {@link #STATE_UNKNOWN}, but used on + * {@link AutofillManagerClient#setSessionFinished(int)} when the session was finished because + * the URL bar changed on client mode + * + * @hide + */ + public static final int STATE_UNKNOWN_COMPAT_MODE = 5; + + + /** * Timeout in ms for calls to the field classification service. * @hide */ @@ -343,6 +368,16 @@ public final class AutofillManager { @GuardedBy("mLock") @Nullable private ArraySet<AutofillId> mFillableIds; + /** id of last requested autofill ui */ + @Nullable private AutofillId mIdShownFillUi; + + /** + * Views that were already "entered" - if they're entered again when the session is not active, + * they're ignored + * */ + @GuardedBy("mLock") + @Nullable private ArraySet<AutofillId> mEnteredIds; + /** If set, session is commited when the field is clicked. */ @GuardedBy("mLock") @Nullable private AutofillId mSaveTriggerId; @@ -355,6 +390,10 @@ public final class AutofillManager { @GuardedBy("mLock") private boolean mSaveOnFinish; + /** If compatibility mode is enabled - this is a bridge to interact with a11y */ + @GuardedBy("mLock") + private CompatibilityBridge mCompatibilityBridge; + /** @hide */ public interface AutofillClient { /** @@ -364,13 +403,13 @@ public final class AutofillManager { * @param intent The authentication intent. * @param fillInIntent The authentication fill-in intent. */ - void autofillCallbackAuthenticate(int authenticationId, IntentSender intent, + void autofillClientAuthenticate(int authenticationId, IntentSender intent, Intent fillInIntent); /** * Tells the client this manager has state to be reset. */ - void autofillCallbackResetableStateAvailable(); + void autofillClientResetableStateAvailable(); /** * Request showing the autofill UI. @@ -382,29 +421,43 @@ public final class AutofillManager { * @param presenter The presenter that controls the fill UI window. * @return Whether the UI was shown. */ - boolean autofillCallbackRequestShowFillUi(@NonNull View anchor, int width, int height, + boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, int height, @Nullable Rect virtualBounds, IAutofillWindowPresenter presenter); /** + * Dispatch unhandled keyevent from Autofill window + * @param anchor The real view the UI needs to anchor to. + * @param keyEvent Unhandled KeyEvent from autofill window. + */ + void autofillClientDispatchUnhandledKey(@NonNull View anchor, @NonNull KeyEvent keyEvent); + + /** * Request hiding the autofill UI. * * @return Whether the UI was hidden. */ - boolean autofillCallbackRequestHideFillUi(); + boolean autofillClientRequestHideFillUi(); + + /** + * Gets whether the fill UI is currenlty being shown. + * + * @return Whether the fill UI is currently being shown + */ + boolean autofillClientIsFillUiShowing(); /** * Checks if views are currently attached and visible. * * @return And array with {@code true} iff the view is attached or visible */ - @NonNull boolean[] getViewVisibility(@NonNull int[] viewId); + @NonNull boolean[] autofillClientGetViewVisibility(@NonNull AutofillId[] autofillIds); /** * Checks is the client is currently visible as understood by autofill. * * @return {@code true} if the client is currently visible */ - boolean isVisibleForAutofill(); + boolean autofillClientIsVisibleForAutofill(); /** * Client might disable enter/exit event e.g. when activity is paused. @@ -414,30 +467,61 @@ public final class AutofillManager { /** * Finds views by traversing the hierarchies of the client. * - * @param viewIds The autofill ids of the views to find + * @param autofillIds The autofill ids of the views to find * * @return And array containing the views (empty if no views found). */ - @NonNull View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds); + @NonNull View[] autofillClientFindViewsByAutofillIdTraversal( + @NonNull AutofillId[] autofillIds); /** * Finds a view by traversing the hierarchies of the client. * - * @param viewId The autofill id of the views to find + * @param autofillId The autofill id of the views to find + * + * @return The view, or {@code null} if not found + */ + @Nullable View autofillClientFindViewByAutofillIdTraversal(@NonNull AutofillId autofillId); + + /** + * Finds a view by a11y id in a given client window. + * + * @param viewId The accessibility id of the views to find + * @param windowId The accessibility window id where to search * * @return The view, or {@code null} if not found */ - @Nullable View findViewByAutofillIdTraversal(int viewId); + @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId); /** * Runs the specified action on the UI thread. */ - void runOnUiThread(Runnable action); + void autofillClientRunOnUiThread(Runnable action); /** * Gets the complete component name of this client. */ - ComponentName getComponentName(); + ComponentName autofillClientGetComponentName(); + + /** + * Gets the activity token + */ + @Nullable IBinder autofillClientGetActivityToken(); + + /** + * @return Whether compatibility mode is enabled. + */ + boolean autofillClientIsCompatibilityModeEnabled(); + + /** + * Gets the next unique autofill ID. + * + * <p>Typically used to manage views whose content is recycled - see + * {@link View#setAutofillId(AutofillId)} for more info. + * + * @return An ID that is unique in the activity. + */ + @Nullable AutofillId autofillClientGetNextAutofillId(); } /** @@ -449,6 +533,19 @@ public final class AutofillManager { } /** + * @hide + */ + public void enableCompatibilityMode() { + synchronized (mLock) { + // The accessibility manager is a singleton so we may need to plug + // different bridge based on which activity is currently focused + // in the current process. Since compat would be rarely used, just + // create and register a new instance every time. + mCompatibilityBridge = new CompatibilityBridge(); + } + } + + /** * Restore state after activity lifecycle * * @param savedInstanceState The state to be restored @@ -477,7 +574,8 @@ public final class AutofillManager { if (client != null) { try { final boolean sessionWasRestored = mService.restoreSession(mSessionId, - mContext.getActivityToken(), mServiceClient.asBinder()); + client.autofillClientGetActivityToken(), + mServiceClient.asBinder()); if (!sessionWasRestored) { Log.w(TAG, "Session " + mSessionId + " could not be restored"); @@ -488,7 +586,7 @@ public final class AutofillManager { Log.d(TAG, "session " + mSessionId + " was restored"); } - client.autofillCallbackResetableStateAvailable(); + client.autofillClientResetableStateAvailable(); } } catch (RemoteException e) { Log.e(TAG, "Could not figure out if there was an autofill session", e); @@ -501,22 +599,29 @@ public final class AutofillManager { /** * Called once the client becomes visible. * - * @see AutofillClient#isVisibleForAutofill() + * @see AutofillClient#autofillClientIsVisibleForAutofill() * * {@hide} */ public void onVisibleForAutofill() { - synchronized (mLock) { - if (mEnabled && isActiveLocked() && mTrackedViews != null) { - mTrackedViews.onVisibleForAutofillLocked(); + // This gets called when the client just got visible at which point the visibility + // of the tracked views may not have been computed (due to a pending layout, etc). + // While generally we have no way to know when the UI has settled. We will evaluate + // the tracked views state at the end of next frame to guarantee that everything + // that may need to be laid out is laid out. + Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> { + synchronized (mLock) { + if (mEnabled && isActiveLocked() && mTrackedViews != null) { + mTrackedViews.onVisibleForAutofillChangedLocked(); + } } - } + }, null); } /** * Called once the client becomes invisible. * - * @see AutofillClient#isVisibleForAutofill() + * @see AutofillClient#autofillClientIsVisibleForAutofill() * * {@hide} */ @@ -551,6 +656,14 @@ public final class AutofillManager { } /** + * @hide + */ + @GuardedBy("mLock") + public boolean isCompatibilityModeEnabledLocked() { + return mCompatibilityBridge != null; + } + + /** * Checks whether autofill is enabled for the current user. * * <p>Typically used to determine whether the option to explicitly request autofill should @@ -636,24 +749,37 @@ public final class AutofillManager { notifyViewEntered(view, 0); } - private boolean shouldIgnoreViewEnteredLocked(@NonNull View view, int flags) { + @GuardedBy("mLock") + private boolean shouldIgnoreViewEnteredLocked(@NonNull AutofillId id, int flags) { if (isDisabledByServiceLocked()) { if (sVerbose) { - Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view - + ") on state " + getStateAsStringLocked()); + Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id + + ") on state " + getStateAsStringLocked() + " because disabled by svc"); } return true; } - if (sVerbose && isFinishedLocked()) { - Log.v(TAG, "not ignoring notifyViewEntered(flags=" + flags + ", view=" + view - + ") on state " + getStateAsStringLocked()); + if (isFinishedLocked()) { + // Session already finished: ignore if automatic request and view already entered + if ((flags & FLAG_MANUAL_REQUEST) == 0 && mEnteredIds != null + && mEnteredIds.contains(id)) { + if (sVerbose) { + Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + id + + ") on state " + getStateAsStringLocked() + + " because view was already entered: " + mEnteredIds); + } + return true; + } + } + if (sVerbose) { + Log.v(TAG, "not ignoring notifyViewEntered(flags=" + flags + ", view=" + id + + ", state " + getStateAsStringLocked() + ", enteredIds=" + mEnteredIds); } return false; } private boolean isClientVisibleForAutofillLocked() { final AutofillClient client = getClient(); - return client != null && client.isVisibleForAutofill(); + return client != null && client.autofillClientIsVisibleForAutofill(); } private boolean isClientDisablingEnterExitEvent() { @@ -676,8 +802,10 @@ public final class AutofillManager { } /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */ + @GuardedBy("mLock") private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) { - if (shouldIgnoreViewEnteredLocked(view, flags)) return null; + final AutofillId id = view.getAutofillId(); + if (shouldIgnoreViewEnteredLocked(id, flags)) return null; AutofillCallback callback = null; @@ -690,7 +818,6 @@ public final class AutofillManager { } else { // don't notify entered when Activity is already in background if (!isClientDisablingEnterExitEvent()) { - final AutofillId id = getAutofillId(view); final AutofillValue value = view.getAutofillValue(); if (!isActiveLocked()) { @@ -700,6 +827,7 @@ public final class AutofillManager { // Update focus on existing session. updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags); } + addEnteredIdLocked(id); } } return callback; @@ -719,13 +847,14 @@ public final class AutofillManager { } } + @GuardedBy("mLock") void notifyViewExitedLocked(@NonNull View view) { ensureServiceClientAddedIfNeededLocked(); if (mEnabled && isActiveLocked()) { // dont notify exited when Activity is already in background if (!isClientDisablingEnterExitEvent()) { - final AutofillId id = getAutofillId(view); + final AutofillId id = view.getAutofillId(); // Update focus on existing session. updateSessionLocked(id, null, null, ACTION_VIEW_EXITED, 0); @@ -768,6 +897,7 @@ public final class AutofillManager { if (mEnabled && isActiveLocked()) { final AutofillId id = virtual ? getAutofillId(view, virtualId) : view.getAutofillId(); + if (sVerbose) Log.v(TAG, "visibility changed for " + id + ": " + isVisible); if (!isVisible && mFillableIds != null) { if (mFillableIds.contains(id)) { if (sDebug) Log.d(TAG, "Hidding UI when view " + id + " became invisible"); @@ -776,6 +906,8 @@ public final class AutofillManager { } if (mTrackedViews != null) { mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible); + } else if (sVerbose) { + Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views"); } } } @@ -820,10 +952,12 @@ public final class AutofillManager { } /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */ + @GuardedBy("mLock") private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds, int flags) { + final AutofillId id = getAutofillId(view, virtualId); AutofillCallback callback = null; - if (shouldIgnoreViewEnteredLocked(view, flags)) return callback; + if (shouldIgnoreViewEnteredLocked(id, flags)) return callback; ensureServiceClientAddedIfNeededLocked(); @@ -834,8 +968,6 @@ public final class AutofillManager { } else { // don't notify entered when Activity is already in background if (!isClientDisablingEnterExitEvent()) { - final AutofillId id = getAutofillId(view, virtualId); - if (!isActiveLocked()) { // Starts new session. startSessionLocked(id, bounds, null, flags); @@ -843,11 +975,20 @@ public final class AutofillManager { // Update focus on existing session. updateSessionLocked(id, bounds, null, ACTION_VIEW_ENTERED, flags); } + addEnteredIdLocked(id); } } return callback; } + @GuardedBy("mLock") + private void addEnteredIdLocked(@NonNull AutofillId id) { + if (mEnteredIds == null) { + mEnteredIds = new ArraySet<>(1); + } + mEnteredIds.add(id); + } + /** * Called when a virtual view that supports autofill is exited. * @@ -855,6 +996,7 @@ public final class AutofillManager { * @param virtualId id identifying the virtual child inside the parent view. */ public void notifyViewExited(@NonNull View view, int virtualId) { + if (sVerbose) Log.v(TAG, "notifyViewExited(" + view.getAutofillId() + ", " + virtualId); if (!hasAutofillFeature()) { return; } @@ -863,6 +1005,7 @@ public final class AutofillManager { } } + @GuardedBy("mLock") private void notifyViewExitedLocked(@NonNull View view, int virtualId) { ensureServiceClientAddedIfNeededLocked(); @@ -896,7 +1039,7 @@ public final class AutofillManager { if (mLastAutofilledData == null) { view.setAutofilled(false); } else { - id = getAutofillId(view); + id = view.getAutofillId(); if (mLastAutofilledData.containsKey(id)) { value = view.getAutofillValue(); valueWasRead = true; @@ -913,15 +1056,15 @@ public final class AutofillManager { } if (!mEnabled || !isActiveLocked()) { - if (sVerbose && mEnabled) { - Log.v(TAG, "notifyValueChanged(" + view + "): ignoring on state " - + getStateAsStringLocked()); + if (sVerbose) { + Log.v(TAG, "notifyValueChanged(" + view.getAutofillId() + + "): ignoring on state " + getStateAsStringLocked()); } return; } if (id == null) { - id = getAutofillId(view); + id = view.getAutofillId(); } if (!valueWasRead) { @@ -945,6 +1088,10 @@ public final class AutofillManager { } synchronized (mLock) { if (!mEnabled || !isActiveLocked()) { + if (sVerbose) { + Log.v(TAG, "notifyValueChanged(" + view.getAutofillId() + ":" + virtualId + + "): ignoring on state " + getStateAsStringLocked()); + } return; } @@ -953,18 +1100,35 @@ public final class AutofillManager { } } + /** + * Called to indicate a {@link View} is clicked. + * + * @param view view that has been clicked. + */ + public void notifyViewClicked(@NonNull View view) { + notifyViewClicked(view.getAutofillId()); + } /** - * Called when a {@link View} is clicked. Currently only used by views that should trigger save. + * Called to indicate a virtual view has been clicked. * - * @hide + * @param view the virtual view parent. + * @param virtualId id identifying the virtual child inside the parent view. */ - public void notifyViewClicked(View view) { - final AutofillId id = view.getAutofillId(); + public void notifyViewClicked(@NonNull View view, int virtualId) { + notifyViewClicked(getAutofillId(view, virtualId)); + } + private void notifyViewClicked(AutofillId id) { + if (!hasAutofillFeature()) { + return; + } if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId); synchronized (mLock) { + if (!mEnabled || !isActiveLocked()) { + return; + } if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) { if (sDebug) Log.d(TAG, "triggering commit by click of " + id); commitLocked(); @@ -979,16 +1143,16 @@ public final class AutofillManager { * * @hide */ - public void onActivityFinished() { + public void onActivityFinishing() { if (!hasAutofillFeature()) { return; } synchronized (mLock) { if (mSaveOnFinish) { - if (sDebug) Log.d(TAG, "Committing session on finish() as requested by service"); + if (sDebug) Log.d(TAG, "onActivityFinishing(): calling commitLocked()"); commitLocked(); } else { - if (sDebug) Log.d(TAG, "Cancelling session on finish() as requested by service"); + if (sDebug) Log.d(TAG, "onActivityFinishing(): calling cancelLocked()"); cancelLocked(); } } @@ -1009,11 +1173,13 @@ public final class AutofillManager { if (!hasAutofillFeature()) { return; } + if (sVerbose) Log.v(TAG, "commit() called by app"); synchronized (mLock) { commitLocked(); } } + @GuardedBy("mLock") private void commitLocked() { if (!mEnabled && !isActiveLocked()) { return; @@ -1042,6 +1208,7 @@ public final class AutofillManager { } } + @GuardedBy("mLock") private void cancelLocked() { if (!mEnabled && !isActiveLocked()) { return; @@ -1099,6 +1266,30 @@ public final class AutofillManager { } /** + * Gets the id of the {@link UserData} used for + * <a href="AutofillService.html#FieldClassification">field classification</a>. + * + * <p>This method is useful when the service must check the status of the {@link UserData} in + * the device without fetching the whole object. + * + * <p><b>Note:</b> This method should only be called by an app providing an autofill service, + * and it's ignored if the caller currently doesn't have an enabled autofill service for + * the user. + * + * @return id of the {@link UserData} previously set by {@link #setUserData(UserData)} + * or {@code null} if it was reset or if the caller currently does not have an enabled autofill + * service for the user. + */ + @Nullable public String getUserDataId() { + try { + return mService.getUserDataId(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return null; + } + } + + /** * Gets the user data used for * <a href="AutofillService.html#FieldClassification">field classification</a>. * @@ -1119,7 +1310,7 @@ public final class AutofillManager { } /** - * Sets the user data used for + * Sets the {@link UserData} used for * <a href="AutofillService.html#FieldClassification">field classification</a> * * <p><b>Note:</b> This method should only be called by an app providing an autofill service, @@ -1226,6 +1417,15 @@ public final class AutofillManager { return client; } + /** + * Check if autofill ui is showing, must be called on UI thread. + * @hide + */ + public boolean isAutofillUiShowing() { + final AutofillClient client = mContext.getAutofillClient(); + return client != null && client.autofillClientIsFillUiShowing(); + } + /** @hide */ public void onAuthenticationResult(int authenticationId, Intent data, View focusView) { if (!hasAutofillFeature()) { @@ -1273,19 +1473,41 @@ public final class AutofillManager { } } - private static AutofillId getAutofillId(View view) { - return new AutofillId(view.getAutofillViewId()); + /** + * Gets the next unique autofill ID for the activity context. + * + * <p>Typically used to manage views whose content is recycled - see + * {@link View#setAutofillId(AutofillId)} for more info. + * + * @return An ID that is unique in the activity, or {@code null} if autofill is not supported in + * the {@link Context} associated with this {@link AutofillManager}. + */ + @Nullable + public AutofillId getNextAutofillId() { + final AutofillClient client = getClient(); + if (client == null) return null; + + final AutofillId id = client.autofillClientGetNextAutofillId(); + + if (id == null && sDebug) { + Log.d(TAG, "getNextAutofillId(): client " + client + " returned null"); + } + + return id; } private static AutofillId getAutofillId(View parent, int virtualId) { return new AutofillId(parent.getAutofillViewId(), virtualId); } + @GuardedBy("mLock") private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds, @NonNull AutofillValue value, int flags) { if (sVerbose) { Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value - + ", flags=" + flags + ", state=" + getStateAsStringLocked()); + + ", flags=" + flags + ", state=" + getStateAsStringLocked() + + ", compatMode=" + isCompatibilityModeEnabledLocked() + + ", enteredIds=" + mEnteredIds); } if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { if (sVerbose) { @@ -1296,20 +1518,22 @@ public final class AutofillManager { } try { final AutofillClient client = getClient(); - if (client == null) return; // NOTE: getClient() already logd it.. + if (client == null) return; // NOTE: getClient() already logged it.. - mSessionId = mService.startSession(mContext.getActivityToken(), + mSessionId = mService.startSession(client.autofillClientGetActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), - mCallback != null, flags, client.getComponentName()); + mCallback != null, flags, client.autofillClientGetComponentName(), + isCompatibilityModeEnabledLocked()); if (mSessionId != NO_SESSION) { mState = STATE_ACTIVE; } - client.autofillCallbackResetableStateAvailable(); + client.autofillClientResetableStateAvailable(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + @GuardedBy("mLock") private void finishSessionLocked() { if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + getStateAsStringLocked()); @@ -1321,9 +1545,10 @@ public final class AutofillManager { throw e.rethrowFromSystemServer(); } - resetSessionLocked(); + resetSessionLocked(/* resetEnteredIds= */ true); } + @GuardedBy("mLock") private void cancelSessionLocked() { if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + getStateAsStringLocked()); @@ -1335,20 +1560,26 @@ public final class AutofillManager { throw e.rethrowFromSystemServer(); } - resetSessionLocked(); + resetSessionLocked(/* resetEnteredIds= */ true); } - private void resetSessionLocked() { + @GuardedBy("mLock") + private void resetSessionLocked(boolean resetEnteredIds) { mSessionId = NO_SESSION; mState = STATE_UNKNOWN; mTrackedViews = null; mFillableIds = null; mSaveTriggerId = null; + mIdShownFillUi = null; + if (resetEnteredIds) { + mEnteredIds = null; + } } + @GuardedBy("mLock") private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action, int flags) { - if (sVerbose && action != ACTION_VIEW_EXITED) { + if (sVerbose) { Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value + ", action=" + action + ", flags=" + flags); } @@ -1359,14 +1590,16 @@ public final class AutofillManager { final AutofillClient client = getClient(); if (client == null) return; // NOTE: getClient() already logd it.. - final int newId = mService.updateOrRestartSession(mContext.getActivityToken(), + final int newId = mService.updateOrRestartSession( + client.autofillClientGetActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), - mCallback != null, flags, client.getComponentName(), mSessionId, action); + mCallback != null, flags, client.autofillClientGetComponentName(), + mSessionId, action, isCompatibilityModeEnabledLocked()); if (newId != mSessionId) { if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId); mSessionId = newId; mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE; - client.autofillCallbackResetableStateAvailable(); + client.autofillClientResetableStateAvailable(); } } else { mService.updateSession(mSessionId, id, bounds, value, action, flags, @@ -1378,6 +1611,7 @@ public final class AutofillManager { } } + @GuardedBy("mLock") private void ensureServiceClientAddedIfNeededLocked() { if (getClient() == null) { return; @@ -1465,9 +1699,10 @@ public final class AutofillManager { AutofillClient client = getClient(); if (client != null) { - if (client.autofillCallbackRequestShowFillUi(anchor, width, height, - anchorBounds, presenter) && mCallback != null) { + if (client.autofillClientRequestShowFillUi(anchor, width, height, + anchorBounds, presenter)) { callback = mCallback; + mIdShownFillUi = id; } } } @@ -1492,7 +1727,25 @@ public final class AutofillManager { // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill() // before onAuthenticationResult() mOnInvisibleCalled = false; - client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent); + client.autofillClientAuthenticate(authenticationId, intent, fillInIntent); + } + } + } + } + + private void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent keyEvent) { + final View anchor = findView(id); + if (anchor == null) { + return; + } + + AutofillCallback callback = null; + synchronized (mLock) { + if (mSessionId == sessionId) { + AutofillClient client = getClient(); + + if (client != null) { + client.autofillClientDispatchUnhandledKey(anchor, keyEvent); } } } @@ -1515,7 +1768,7 @@ public final class AutofillManager { mEnabled = (flags & SET_STATE_FLAG_ENABLED) != 0; if (!mEnabled || (flags & SET_STATE_FLAG_RESET_SESSION) != 0) { // Reset the session state - resetSessionLocked(); + resetSessionLocked(/* resetEnteredIds= */ true); } if ((flags & SET_STATE_FLAG_RESET_CLIENT) != 0) { // Reset connection to system @@ -1543,7 +1796,7 @@ public final class AutofillManager { if (mLastAutofilledData == null) { mLastAutofilledData = new ParcelableMap(1); } - mLastAutofilledData.put(getAutofillId(view), targetValue); + mLastAutofilledData.put(view.getAutofillId(), targetValue); } view.setAutofilled(true); } @@ -1563,7 +1816,10 @@ public final class AutofillManager { final int itemCount = ids.size(); int numApplied = 0; ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null; - final View[] views = client.findViewsByAutofillIdTraversal(getViewIds(ids)); + final View[] views = client.autofillClientFindViewsByAutofillIdTraversal( + Helper.toArray(ids)); + + ArrayList<AutofillId> failedIds = null; for (int i = 0; i < itemCount; i++) { final AutofillId id = ids.get(i); @@ -1571,7 +1827,14 @@ public final class AutofillManager { final int viewId = id.getViewId(); final View view = views[i]; if (view == null) { - Log.w(TAG, "autofill(): no View with id " + viewId); + // Most likely view has been removed after the initial request was sent to the + // the service; this is fine, but we need to update the view status in the + // server side so it can be triggered again. + Log.d(TAG, "autofill(): no View with id " + id); + if (failedIds == null) { + failedIds = new ArrayList<>(); + } + failedIds.add(id); continue; } if (id.isVirtual()) { @@ -1605,12 +1868,28 @@ public final class AutofillManager { } } + if (failedIds != null) { + if (sVerbose) { + Log.v(TAG, "autofill(): total failed views: " + failedIds); + } + try { + mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId()); + } catch (RemoteException e) { + // In theory, we could ignore this error since it's not a big deal, but + // in reality, we rather crash the app anyways, as the failure could be + // a consequence of something going wrong on the server side... + e.rethrowFromSystemServer(); + } + } + if (virtualValues != null) { for (int i = 0; i < virtualValues.size(); i++) { final View parent = virtualValues.keyAt(i); final SparseArray<AutofillValue> childrenValues = virtualValues.valueAt(i); parent.autofill(childrenValues); numApplied += childrenValues.size(); + // TODO: we should provide a callback so the parent can call failures; something + // like notifyAutofillFailed(View view, int[] childrenIds); } } @@ -1703,22 +1982,44 @@ public final class AutofillManager { * Marks the state of the session as finished. * * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null} - * FillResponse), {@link #STATE_UNKNOWN} (because the session was removed), or - * {@link #STATE_DISABLED_BY_SERVICE} (because the autofill service disabled further autofill - * requests for the activity). + * FillResponse), {@link #STATE_UNKNOWN} (because the session was removed), + * {@link #STATE_UNKNOWN_COMPAT_MODE} (beucase the session was finished when the URL bar + * changed on compat mode), or {@link #STATE_DISABLED_BY_SERVICE} (because the autofill service + * disabled further autofill requests for the activity). */ private void setSessionFinished(int newState) { synchronized (mLock) { - if (sVerbose) Log.v(TAG, "setSessionFinished(): from " + mState + " to " + newState); - resetSessionLocked(); - mState = newState; + if (sVerbose) { + Log.v(TAG, "setSessionFinished(): from " + getStateAsStringLocked() + " to " + + getStateAsString(newState)); + } + if (newState == STATE_UNKNOWN_COMPAT_MODE) { + resetSessionLocked(/* resetEnteredIds= */ true); + mState = STATE_UNKNOWN; + } else { + resetSessionLocked(/* resetEnteredIds= */ false); + mState = newState; + } } } - private void requestHideFillUi(AutofillId id) { - final View anchor = findView(id); + /** @hide */ + public void requestHideFillUi() { + requestHideFillUi(mIdShownFillUi, true); + } + + private void requestHideFillUi(AutofillId id, boolean force) { + final View anchor = id == null ? null : findView(id); if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor); if (anchor == null) { + if (force) { + // When user taps outside autofill window, force to close fill ui even id does + // not match. + AutofillClient client = getClient(); + if (client != null) { + client.autofillClientRequestHideFillUi(); + } + } return; } requestHideFillUi(id, anchor); @@ -1734,7 +2035,8 @@ public final class AutofillManager { // service being uninstalled and the UI being dismissed. AutofillClient client = getClient(); if (client != null) { - if (client.autofillCallbackRequestHideFillUi() && mCallback != null) { + if (client.autofillClientRequestHideFillUi()) { + mIdShownFillUi = null; callback = mCallback; } } @@ -1783,35 +2085,6 @@ public final class AutofillManager { } /** - * Get an array of viewIds from a List of {@link AutofillId}. - * - * @param autofillIds The autofill ids to convert - * - * @return The array of viewIds. - */ - // TODO: move to Helper as static method - @NonNull private int[] getViewIds(@NonNull AutofillId[] autofillIds) { - final int numIds = autofillIds.length; - final int[] viewIds = new int[numIds]; - for (int i = 0; i < numIds; i++) { - viewIds[i] = autofillIds[i].getViewId(); - } - - return viewIds; - } - - // TODO: move to Helper as static method - @NonNull private int[] getViewIds(@NonNull List<AutofillId> autofillIds) { - final int numIds = autofillIds.size(); - final int[] viewIds = new int[numIds]; - for (int i = 0; i < numIds; i++) { - viewIds[i] = autofillIds.get(i).getViewId(); - } - - return viewIds; - } - - /** * Find a single view by its id. * * @param autofillId The autofill id of the view @@ -1820,12 +2093,10 @@ public final class AutofillManager { */ private View findView(@NonNull AutofillId autofillId) { final AutofillClient client = getClient(); - - if (client == null) { - return null; + if (client != null) { + return client.autofillClientFindViewByAutofillIdTraversal(autofillId); } - - return client.findViewByAutofillIdTraversal(autofillId.getViewId()); + return null; } /** @hide */ @@ -1869,37 +2140,51 @@ public final class AutofillManager { pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds); } pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); + pw.print(pfx); pw.print("entered ids: "); pw.println(mEnteredIds); pw.print(pfx); pw.print("save trigger id: "); pw.println(mSaveTriggerId); pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish); + pw.print(pfx); pw.print("compat mode enabled: "); pw.println( + isCompatibilityModeEnabledLocked()); pw.print(pfx); pw.print("debug: "); pw.print(sDebug); pw.print(" verbose: "); pw.println(sVerbose); } + @GuardedBy("mLock") private String getStateAsStringLocked() { - switch (mState) { + return getStateAsString(mState); + } + + @NonNull + private static String getStateAsString(int state) { + switch (state) { case STATE_UNKNOWN: - return "STATE_UNKNOWN"; + return "UNKNOWN"; case STATE_ACTIVE: - return "STATE_ACTIVE"; + return "ACTIVE"; case STATE_FINISHED: - return "STATE_FINISHED"; + return "FINISHED"; case STATE_SHOWING_SAVE_UI: - return "STATE_SHOWING_SAVE_UI"; + return "SHOWING_SAVE_UI"; case STATE_DISABLED_BY_SERVICE: - return "STATE_DISABLED_BY_SERVICE"; + return "DISABLED_BY_SERVICE"; + case STATE_UNKNOWN_COMPAT_MODE: + return "UNKNOWN_COMPAT_MODE"; default: - return "INVALID:" + mState; + return "INVALID:" + state; } } + @GuardedBy("mLock") private boolean isActiveLocked() { return mState == STATE_ACTIVE; } + @GuardedBy("mLock") private boolean isDisabledByServiceLocked() { return mState == STATE_DISABLED_BY_SERVICE; } + @GuardedBy("mLock") private boolean isFinishedLocked() { return mState == STATE_FINISHED; } @@ -1910,7 +2195,242 @@ public final class AutofillManager { if (sVerbose) Log.v(TAG, "ignoring post() because client is null"); return; } - client.runOnUiThread(runnable); + client.autofillClientRunOnUiThread(runnable); + } + + /** + * Implementation of the accessibility based compatibility. + */ + private final class CompatibilityBridge implements AccessibilityManager.AccessibilityPolicy { + @GuardedBy("mLock") + private final Rect mFocusedBounds = new Rect(); + @GuardedBy("mLock") + private final Rect mTempBounds = new Rect(); + + @GuardedBy("mLock") + private int mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + @GuardedBy("mLock") + private long mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID; + + // Need to report a fake service in case a11y clients check the service list + @NonNull + @GuardedBy("mLock") + AccessibilityServiceInfo mCompatServiceInfo; + + CompatibilityBridge() { + final AccessibilityManager am = AccessibilityManager.getInstance(mContext); + am.setAccessibilityPolicy(this); + } + + private AccessibilityServiceInfo getCompatServiceInfo() { + synchronized (mLock) { + if (mCompatServiceInfo != null) { + return mCompatServiceInfo; + } + final Intent intent = new Intent(); + intent.setComponent(new ComponentName("android", + "com.android.server.autofill.AutofillCompatAccessibilityService")); + final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService( + intent, PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA); + try { + mCompatServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext); + } catch (XmlPullParserException | IOException e) { + Log.e(TAG, "Cannot find compat autofill service:" + intent); + throw new IllegalStateException("Cannot find compat autofill service"); + } + return mCompatServiceInfo; + } + } + + @Override + public boolean isEnabled(boolean accessibilityEnabled) { + return true; + } + + @Override + public int getRelevantEventTypes(int relevantEventTypes) { + return relevantEventTypes | AccessibilityEvent.TYPE_VIEW_FOCUSED + | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + | AccessibilityEvent.TYPE_VIEW_CLICKED + | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; + } + + @Override + public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList( + List<AccessibilityServiceInfo> installedServices) { + if (installedServices == null) { + installedServices = new ArrayList<>(); + } + installedServices.add(getCompatServiceInfo()); + return installedServices; + } + + @Override + public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( + int feedbackTypeFlags, List<AccessibilityServiceInfo> enabledService) { + if (enabledService == null) { + enabledService = new ArrayList<>(); + } + enabledService.add(getCompatServiceInfo()); + return enabledService; + } + + @Override + public AccessibilityEvent onAccessibilityEvent(AccessibilityEvent event, + boolean accessibilityEnabled, int relevantEventTypes) { + switch (event.getEventType()) { + case AccessibilityEvent.TYPE_VIEW_FOCUSED: { + synchronized (mLock) { + if (mFocusedWindowId == event.getWindowId() + && mFocusedNodeId == event.getSourceNodeId()) { + return event; + } + if (mFocusedWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID + && mFocusedNodeId != AccessibilityNodeInfo.UNDEFINED_NODE_ID) { + notifyViewExited(mFocusedWindowId, mFocusedNodeId); + mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID; + mFocusedBounds.set(0, 0, 0, 0); + } + final int windowId = event.getWindowId(); + final long nodeId = event.getSourceNodeId(); + if (notifyViewEntered(windowId, nodeId, mFocusedBounds)) { + mFocusedWindowId = windowId; + mFocusedNodeId = nodeId; + } + } + } break; + + case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: { + synchronized (mLock) { + if (mFocusedWindowId == event.getWindowId() + && mFocusedNodeId == event.getSourceNodeId()) { + notifyValueChanged(event.getWindowId(), event.getSourceNodeId()); + } + } + } break; + + case AccessibilityEvent.TYPE_VIEW_CLICKED: { + synchronized (mLock) { + notifyViewClicked(event.getWindowId(), event.getSourceNodeId()); + } + } break; + + case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { + final AutofillClient client = getClient(); + if (client != null) { + synchronized (mLock) { + if (client.autofillClientIsFillUiShowing()) { + notifyViewEntered(mFocusedWindowId, mFocusedNodeId, mFocusedBounds); + } + updateTrackedViewsLocked(); + } + } + } break; + } + + return accessibilityEnabled ? event : null; + } + + private boolean notifyViewEntered(int windowId, long nodeId, Rect focusedBounds) { + final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); + if (!isVirtualNode(virtualId)) { + return false; + } + final View view = findViewByAccessibilityId(windowId, nodeId); + if (view == null) { + return false; + } + final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId); + if (node == null) { + return false; + } + if (!node.isEditable()) { + return false; + } + final Rect newBounds = mTempBounds; + node.getBoundsInScreen(newBounds); + if (newBounds.equals(focusedBounds)) { + return false; + } + focusedBounds.set(newBounds); + AutofillManager.this.notifyViewEntered(view, virtualId, newBounds); + return true; + } + + private void notifyViewExited(int windowId, long nodeId) { + final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); + if (!isVirtualNode(virtualId)) { + return; + } + final View view = findViewByAccessibilityId(windowId, nodeId); + if (view == null) { + return; + } + AutofillManager.this.notifyViewExited(view, virtualId); + } + + private void notifyValueChanged(int windowId, long nodeId) { + final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); + if (!isVirtualNode(virtualId)) { + return; + } + final View view = findViewByAccessibilityId(windowId, nodeId); + if (view == null) { + return; + } + final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId); + if (node == null) { + return; + } + AutofillManager.this.notifyValueChanged(view, virtualId, + AutofillValue.forText(node.getText())); + } + + private void notifyViewClicked(int windowId, long nodeId) { + final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId); + if (!isVirtualNode(virtualId)) { + return; + } + final View view = findViewByAccessibilityId(windowId, nodeId); + if (view == null) { + return; + } + final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId); + if (node == null) { + return; + } + AutofillManager.this.notifyViewClicked(view, virtualId); + } + + @GuardedBy("mLock") + private void updateTrackedViewsLocked() { + if (mTrackedViews != null) { + mTrackedViews.onVisibleForAutofillChangedLocked(); + } + } + + private View findViewByAccessibilityId(int windowId, long nodeId) { + final AutofillClient client = getClient(); + if (client == null) { + return null; + } + final int viewId = AccessibilityNodeInfo.getAccessibilityViewId(nodeId); + return client.autofillClientFindViewByAccessibilityIdTraversal(viewId, windowId); + } + + private AccessibilityNodeInfo findVirtualNodeByAccessibilityId(View view, int virtualId) { + final AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); + if (provider == null) { + return null; + } + return provider.createAccessibilityNodeInfo(virtualId); + } + + private boolean isVirtualNode(int nodeId) { + return nodeId != AccessibilityNodeProvider.HOST_VIEW_ID + && nodeId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID; + } } /** @@ -1989,11 +2509,12 @@ public final class AutofillManager { */ TrackedViews(@Nullable AutofillId[] trackedIds) { final AutofillClient client = getClient(); - if (trackedIds != null && client != null) { + if (!ArrayUtils.isEmpty(trackedIds) && client != null) { final boolean[] isVisible; - if (client.isVisibleForAutofill()) { - isVisible = client.getViewVisibility(getViewIds(trackedIds)); + if (client.autofillClientIsVisibleForAutofill()) { + if (sVerbose) Log.v(TAG, "client is visible, check tracked ids"); + isVisible = client.autofillClientGetViewVisibility(trackedIds); } else { // All false isVisible = new boolean[trackedIds.length]; @@ -2012,7 +2533,7 @@ public final class AutofillManager { } if (sVerbose) { - Log.v(TAG, "TrackedViews(trackedIds=" + trackedIds + "): " + Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(trackedIds) + "): " + " mVisibleTrackedIds=" + mVisibleTrackedIds + " mInvisibleTrackedIds=" + mInvisibleTrackedIds); } @@ -2028,9 +2549,10 @@ public final class AutofillManager { * @param id the id of the view/virtual view whose visibility changed. * @param isVisible visible if the view is visible in the view hierarchy. */ + @GuardedBy("mLock") void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) { if (sDebug) { - Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible=" + Log.d(TAG, "notifyViewVisibilityChangedLocked(): id=" + id + " isVisible=" + isVisible); } @@ -2059,20 +2581,25 @@ public final class AutofillManager { /** * Called once the client becomes visible. * - * @see AutofillClient#isVisibleForAutofill() + * @see AutofillClient#autofillClientIsVisibleForAutofill() */ - void onVisibleForAutofillLocked() { + @GuardedBy("mLock") + void onVisibleForAutofillChangedLocked() { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. AutofillClient client = getClient(); ArraySet<AutofillId> updatedVisibleTrackedIds = null; ArraySet<AutofillId> updatedInvisibleTrackedIds = null; if (client != null) { + if (sVerbose) { + Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds + + " vis=" + mVisibleTrackedIds); + } if (mInvisibleTrackedIds != null) { final ArrayList<AutofillId> orderedInvisibleIds = new ArrayList<>(mInvisibleTrackedIds); - final boolean[] isVisible = client.getViewVisibility( - getViewIds(orderedInvisibleIds)); + final boolean[] isVisible = client.autofillClientGetViewVisibility( + Helper.toArray(orderedInvisibleIds)); final int numInvisibleTrackedIds = orderedInvisibleIds.size(); for (int i = 0; i < numInvisibleTrackedIds; i++) { @@ -2092,8 +2619,8 @@ public final class AutofillManager { if (mVisibleTrackedIds != null) { final ArrayList<AutofillId> orderedVisibleIds = new ArrayList<>(mVisibleTrackedIds); - final boolean[] isVisible = client.getViewVisibility( - getViewIds(orderedVisibleIds)); + final boolean[] isVisible = client.autofillClientGetViewVisibility( + Helper.toArray(orderedVisibleIds)); final int numVisibleTrackedIds = orderedVisibleIds.size(); for (int i = 0; i < numVisibleTrackedIds; i++) { @@ -2116,6 +2643,9 @@ public final class AutofillManager { } if (mVisibleTrackedIds == null) { + if (sVerbose) { + Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids"); + } finishSessionLocked(); } } @@ -2232,7 +2762,7 @@ public final class AutofillManager { public void requestHideFillUi(int sessionId, AutofillId id) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> afm.requestHideFillUi(id)); + afm.post(() -> afm.requestHideFillUi(id, false)); } } @@ -2245,6 +2775,14 @@ public final class AutofillManager { } @Override + public void dispatchUnhandledKey(int sessionId, AutofillId id, KeyEvent fullScreen) { + final AutofillManager afm = mAfm.get(); + if (afm != null) { + afm.post(() -> afm.dispatchUnhandledKey(sessionId, id, fullScreen)); + } + } + + @Override public void startIntentSender(IntentSender intentSender, Intent intent) { final AutofillManager afm = mAfm.get(); if (afm != null) { |