Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 67decfa7 authored by Dake Gu's avatar Dake Gu
Browse files

autofill: support dpad/keyboard

To make autofill works on non-touch device such as TV, allow
fill ui window to gain window focus. Fill ui window does not
need IME. When IME and fill ui window are both shown, fill ui
window will intercept keyevent before IME.

Since autofill window will steal window focus from app window,
we no longer uses View.onWindowFocused() for enter/exit event.
Switched to use Activity onResume/onPause. When view
notifyViewEntered or notifyViewExited called when Activity is paused,
it will be ignored. Before Activity goes to pause state,
notifyViewExited() is fired on focus view, after Activity leaves
pause state, notifyViewEntered() is fired on focus view.

In CTS testDatasetAuthTwoFieldsUserCancelsFirstAttempt,  the
authentication activity finishes itself in onCreate() which will not
produce onPause/onResume in app activity, but it will produce window
focus loss/gain event. Since we switch from window focus to activity
onResume/onPause, we will be missing a show fill ui when return from
the never shown authentication activity. To solve this problem,
we added special code when receive ActivityResult from authentication
activity where we check if the authenticate activity never causes
onStop event, where we should issue an extra ACTION_VIEW_ENTERED
event to show fill ui.

Test: passed all existing autofilltest CTS on sailfish
      atest CtsAutoFillServiceTestCases
Bug: 70181616

Change-Id: Iafe4dca3be8f049fa6dfd34bac13ccb030c583b6
parent 66af0e82
Loading
Loading
Loading
Loading
+52 −7
Original line number Diff line number Diff line
@@ -857,6 +857,7 @@ public class Activity extends ContextThemeWrapper
    private boolean mHasCurrentPermissionsRequest;

    private boolean mAutoFillResetNeeded;
    private boolean mAutoFillIgnoreFirstResumePause;

    /** The last autofill id that was returned from {@link #getNextAutofillId()} */
    private int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
@@ -1253,10 +1254,7 @@ public class Activity extends ContextThemeWrapper
        getApplication().dispatchActivityStarted(this);

        if (mAutoFillResetNeeded) {
            AutofillManager afm = getAutofillManager();
            if (afm != null) {
                afm.onVisibleForAutofill();
            }
            getAutofillManager().onVisibleForAutofill();
        }
    }

@@ -1320,6 +1318,20 @@ public class Activity extends ContextThemeWrapper
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this);
        getApplication().dispatchActivityResumed(this);
        mActivityTransitionState.onResume(this, isTopOfTask());
        if (mAutoFillResetNeeded) {
            if (!mAutoFillIgnoreFirstResumePause) {
                View focus = getCurrentFocus();
                if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
                    // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest#
                    // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial
                    // window visibility after recreation is INVISIBLE in onResume() and next frame
                    // ViewRootImpl.performTraversals() changes window visibility to VISIBLE.
                    // So we cannot call View.notifyEnterOrExited() which will do nothing
                    // when View.isVisibleToUser() is false.
                    getAutofillManager().notifyViewEntered(focus);
                }
            }
        }
        mCalled = true;
    }

@@ -1681,6 +1693,19 @@ public class Activity extends ContextThemeWrapper
    protected void onPause() {
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onPause " + this);
        getApplication().dispatchActivityPaused(this);
        if (mAutoFillResetNeeded) {
            if (!mAutoFillIgnoreFirstResumePause) {
                if (DEBUG_LIFECYCLE) Slog.v(TAG, "autofill notifyViewExited " + this);
                View focus = getCurrentFocus();
                if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
                    getAutofillManager().notifyViewExited(focus);
                }
            } else {
                // reset after first pause()
                if (DEBUG_LIFECYCLE) Slog.v(TAG, "autofill got first pause " + this);
                mAutoFillIgnoreFirstResumePause = false;
            }
        }
        mCalled = true;
    }

@@ -1871,6 +1896,10 @@ public class Activity extends ContextThemeWrapper
        mTranslucentCallback = null;
        mCalled = true;

        if (mAutoFillResetNeeded) {
            getAutofillManager().onInvisibleForAutofill();
        }

        if (isFinishing()) {
            if (mAutoFillResetNeeded) {
                getAutofillManager().onActivityFinished();
@@ -6266,7 +6295,7 @@ public class Activity extends ContextThemeWrapper

        mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);

        final AutofillManager afm = getAutofillManager();
        final AutofillManager afm = mAutofillManager;
        if (afm != null) {
            afm.dump(prefix, writer);
        } else {
@@ -7120,13 +7149,23 @@ public class Activity extends ContextThemeWrapper
        }
    }

    final void performResume() {
    final void performResume(boolean followedByPause) {
        performRestart(true /* start */);

        mFragments.execPendingActions();

        mLastNonConfigurationInstances = null;

        if (mAutoFillResetNeeded) {
            // When Activity is destroyed in paused state, and relaunch activity, there will be
            // extra onResume and onPause event,  ignore the first onResume and onPause.
            // see ActivityThread.handleRelaunchActivity()
            mAutoFillIgnoreFirstResumePause = followedByPause;
            if (mAutoFillIgnoreFirstResumePause && DEBUG_LIFECYCLE) {
                Slog.v(TAG, "autofill will ignore first pause when relaunching " + this);
            }
        }

        mCalled = false;
        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);
@@ -7311,7 +7350,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(requestCode, resultData);
            getAutofillManager().onAuthenticationResult(requestCode, resultData, getCurrentFocus());
        } else {
            Fragment frag = mFragments.findFragmentByWho(who);
            if (frag != null) {
@@ -7585,6 +7624,12 @@ public class Activity extends ContextThemeWrapper
        return !mStopped;
    }

    /** @hide */
    @Override
    public boolean isDisablingEnterExitEventForAutofill() {
        return mAutoFillIgnoreFirstResumePause || !mResumed;
    }

    /**
     * If set to true, this indicates to the system that it should never take a
     * screenshot of the activity to be used as a representation while it is not in a started state.
+3 −3
Original line number Diff line number Diff line
@@ -3071,7 +3071,7 @@ public final class ActivityThread extends ClientTransactionHandler {
        checkAndBlockForNetworkAccess();
        deliverNewIntents(r, intents);
        if (resumed) {
            r.activity.performResume();
            r.activity.performResume(false);
            r.activity.mTemporaryPause = false;
        }

@@ -3681,7 +3681,7 @@ public final class ActivityThread extends ClientTransactionHandler {
                    deliverResults(r, r.pendingResults);
                    r.pendingResults = null;
                }
                r.activity.performResume();
                r.activity.performResume(r.startsNotResumed);

                synchronized (mResourcesManager) {
                    // If there is a pending local relaunch that was requested when the activity was
@@ -4400,7 +4400,7 @@ public final class ActivityThread extends ClientTransactionHandler {
            checkAndBlockForNetworkAccess();
            deliverResults(r, results);
            if (resumed) {
                r.activity.performResume();
                r.activity.performResume(false);
                r.activity.mTemporaryPause = false;
            }
        }
+13 −6
Original line number Diff line number Diff line
@@ -7207,20 +7207,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
    }
    private void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
        if (isAutofillable() && isAttachedToWindow()) {
    /** @hide */
    public void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
        if (canNotifyAutofillEnterExitEvent()) {
            AutofillManager afm = getAutofillManager();
            if (afm != null) {
                if (enter && hasWindowFocus() && isFocused()) {
                if (enter && isFocused()) {
                    // We have not been laid out yet, hence cannot evaluate
                    // whether this view is visible to the user, we will do
                    // the evaluation once layout is complete.
                    if (!isLaidOut()) {
                        mPrivateFlags3 |= PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
                    } else if (isVisibleToUser()) {
                        // TODO This is a potential problem that View gets focus before it's visible
                        // to User. Ideally View should handle the event when isVisibleToUser()
                        // becomes true where it should issue notifyViewEntered().
                        afm.notifyViewEntered(this);
                    }
                } else if (!hasWindowFocus() || !isFocused()) {
                } else if (!isFocused()) {
                    afm.notifyViewExited(this);
                }
            }
@@ -8283,6 +8287,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                && getAutofillViewId() > LAST_APP_AUTOFILL_ID;
    }
    /** @hide */
    public boolean canNotifyAutofillEnterExitEvent() {
        return isAutofillable() && isAttachedToWindow();
    }
    private void populateVirtualStructure(ViewStructure structure,
            AccessibilityNodeProvider provider, AccessibilityNodeInfo info) {
        structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()),
@@ -12400,8 +12409,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            imm.focusIn(this);
        }
        notifyEnterOrExitForAutoFillIfNeeded(hasWindowFocus);
        refreshDrawableState();
    }
+121 −36
Original line number Diff line number Diff line
@@ -341,6 +341,10 @@ public final class AutofillManager {
    @GuardedBy("mLock")
    @Nullable private AutofillId mSaveTriggerId;

    /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */
    @GuardedBy("mLock")
    private boolean mOnInvisibleCalled;

    /** If set, session is commited when the activity is finished; otherwise session is canceled. */
    @GuardedBy("mLock")
    private boolean mSaveOnFinish;
@@ -396,6 +400,11 @@ public final class AutofillManager {
         */
        boolean isVisibleForAutofill();

        /**
         * Client might disable enter/exit event e.g. when activity is paused.
         */
        boolean isDisablingEnterExitEventForAutofill();

        /**
         * Finds views by traversing the hierarchies of the client.
         *
@@ -498,6 +507,19 @@ public final class AutofillManager {
        }
    }

    /**
     * Called once the client becomes invisible.
     *
     * @see AutofillClient#isVisibleForAutofill()
     *
     * {@hide}
     */
    public void onInvisibleForAutofill() {
        synchronized (mLock) {
            mOnInvisibleCalled = true;
        }
    }

    /**
     * Save state before activity lifecycle
     *
@@ -623,13 +645,35 @@ public final class AutofillManager {
        return false;
    }

    private boolean isClientVisibleForAutofillLocked() {
        final AutofillClient client = getClient();
        return client != null && client.isVisibleForAutofill();
    }

    private boolean isClientDisablingEnterExitEvent() {
        final AutofillClient client = getClient();
        return client != null && client.isDisablingEnterExitEventForAutofill();
    }

    private void notifyViewEntered(@NonNull View view, int flags) {
        if (!hasAutofillFeature()) {
            return;
        }
        AutofillCallback callback = null;
        AutofillCallback callback;
        synchronized (mLock) {
            if (shouldIgnoreViewEnteredLocked(view, flags)) return;
            callback = notifyViewEnteredLocked(view, flags);
        }

        if (callback != null) {
            mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
        }
    }

    /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
    private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) {
        if (shouldIgnoreViewEnteredLocked(view, flags)) return null;

        AutofillCallback callback = null;

        ensureServiceClientAddedIfNeededLocked();

@@ -638,6 +682,8 @@ public final class AutofillManager {
                callback = mCallback;
            }
        } else {
            // don't notify entered when Activity is already in background
            if (!isClientDisablingEnterExitEvent()) {
                final AutofillId id = getAutofillId(view);
                final AutofillValue value = view.getAutofillValue();

@@ -650,10 +696,7 @@ public final class AutofillManager {
                }
            }
        }

        if (callback != null) {
            mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
        }
        return callback;
    }

    /**
@@ -666,9 +709,16 @@ public final class AutofillManager {
            return;
        }
        synchronized (mLock) {
            notifyViewExitedLocked(view);
        }
    }

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

                // Update focus on existing session.
@@ -719,7 +769,7 @@ public final class AutofillManager {
                    }
                }
                if (mTrackedViews != null) {
                    mTrackedViews.notifyViewVisibilityChanged(id, isVisible);
                    mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible);
                }
            }
        }
@@ -752,9 +802,22 @@ public final class AutofillManager {
        if (!hasAutofillFeature()) {
            return;
        }
        AutofillCallback callback = null;
        AutofillCallback callback;
        synchronized (mLock) {
            if (shouldIgnoreViewEnteredLocked(view, flags)) return;
            callback = notifyViewEnteredLocked(view, virtualId, bounds, flags);
        }

        if (callback != null) {
            callback.onAutofillEvent(view, virtualId,
                    AutofillCallback.EVENT_INPUT_UNAVAILABLE);
        }
    }

    /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
    private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds,
                                                     int flags) {
        AutofillCallback callback = null;
        if (shouldIgnoreViewEnteredLocked(view, flags)) return callback;

        ensureServiceClientAddedIfNeededLocked();

@@ -763,6 +826,8 @@ public final class AutofillManager {
                callback = mCallback;
            }
        } else {
            // don't notify entered when Activity is already in background
            if (!isClientDisablingEnterExitEvent()) {
                final AutofillId id = getAutofillId(view, virtualId);

                if (!isActiveLocked()) {
@@ -774,11 +839,7 @@ public final class AutofillManager {
                }
            }
        }

        if (callback != null) {
            callback.onAutofillEvent(view, virtualId,
                    AutofillCallback.EVENT_INPUT_UNAVAILABLE);
        }
        return callback;
    }

    /**
@@ -792,9 +853,16 @@ public final class AutofillManager {
            return;
        }
        synchronized (mLock) {
            notifyViewExitedLocked(view, virtualId);
        }
    }

    private void notifyViewExitedLocked(@NonNull View view, int virtualId) {
        ensureServiceClientAddedIfNeededLocked();

        if (mEnabled && isActiveLocked()) {
            // don't notify exited when Activity is already in background
            if (!isClientDisablingEnterExitEvent()) {
                final AutofillId id = getAutofillId(view, virtualId);

                // Update focus on existing session.
@@ -1155,7 +1223,7 @@ public final class AutofillManager {
    }

    /** @hide */
    public void onAuthenticationResult(int authenticationId, Intent data) {
    public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
        if (!hasAutofillFeature()) {
            return;
        }
@@ -1167,9 +1235,24 @@ public final class AutofillManager {
        if (sDebug) Log.d(TAG, "onAuthenticationResult(): d=" + data);

        synchronized (mLock) {
            if (!isActiveLocked() || data == null) {
            if (!isActiveLocked()) {
                return;
            }
            // If authenticate activity closes itself during onCreate(), there is no onStop/onStart
            // of app activity.  We enforce enter event to re-show fill ui in such case.
            // CTS example:
            //     LoginActivityTest#testDatasetAuthTwoFieldsUserCancelsFirstAttempt
            //     LoginActivityTest#testFillResponseAuthBothFieldsUserCancelsFirstAttempt
            if (!mOnInvisibleCalled && focusView != null
                    && focusView.canNotifyAutofillEnterExitEvent()) {
                notifyViewExitedLocked(focusView);
                notifyViewEnteredLocked(focusView, 0);
            }
            if (data == null) {
                // data is set to null when result is not RESULT_OK
                return;
            }

            final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
            final Bundle responseData = new Bundle();
            responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
@@ -1402,6 +1485,9 @@ public final class AutofillManager {
            if (sessionId == mSessionId) {
                final AutofillClient client = getClient();
                if (client != null) {
                    // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
                    // before onAuthenticationResult()
                    mOnInvisibleCalled = false;
                    client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
                }
            }
@@ -1767,6 +1853,7 @@ public final class AutofillManager {
        pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled);
        pw.print(pfx); pw.print("hasService: "); pw.println(mService != null);
        pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null);
        pw.print(pfx); pw.print("onInvisibleCalled "); pw.println(mOnInvisibleCalled);
        pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData);
        pw.print(pfx); pw.print("tracked views: ");
        if (mTrackedViews == null) {
@@ -1937,15 +2024,13 @@ 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.
         */
        void notifyViewVisibilityChanged(@NonNull AutofillId id, boolean isVisible) {
            AutofillClient client = getClient();

        void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) {
            if (sDebug) {
                Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible="
                        + isVisible);
            }

            if (client != null && client.isVisibleForAutofill()) {
            if (isClientVisibleForAutofillLocked()) {
                if (isVisible) {
                    if (isInSet(mInvisibleTrackedIds, id)) {
                        mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
+3 −1
Original line number Diff line number Diff line
@@ -78,8 +78,10 @@ public class AutofillPopupWindow extends PopupWindow {
    public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) {
        mWindowPresenter = new WindowPresenter(presenter);

        setTouchModal(false);
        setOutsideTouchable(true);
        setInputMethodMode(INPUT_METHOD_NEEDED);
        setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
        setFocusable(true);
    }

    @Override
Loading