diff options
11 files changed, 573 insertions, 81 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index f79dbf9ee4f2..4e258a3a4b47 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1863,8 +1863,18 @@ public class Activity extends ContextThemeWrapper getApplication().dispatchActivityStopped(this); mTranslucentCallback = null; mCalled = true; - if (isFinishing() && mAutoFillResetNeeded) { - getAutofillManager().commit(); + + if (isFinishing()) { + if (mAutoFillResetNeeded) { + getAutofillManager().commit(); + } else if (mIntent != null + && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { + // Activity was launched when user tapped a link in the Autofill Save UI - since + // user launched another activity, the Save UI should not be restored when this + // activity is finished. + getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_CANCEL, + mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)); + } } } @@ -5491,6 +5501,13 @@ public class Activity extends ContextThemeWrapper } else { mParent.finishFromChild(this); } + + // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must + // be restored now. + if (mIntent != null && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { + getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE, + mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)); + } } /** @@ -6225,6 +6242,11 @@ public class Activity extends ContextThemeWrapper } mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix); + + final AutofillManager afm = getAutofillManager(); + if (afm != null) { + afm.dump(prefix, writer); + } } /** diff --git a/core/java/android/service/autofill/CustomDescription.java b/core/java/android/service/autofill/CustomDescription.java index 4f06bd759e47..9a4cbc415d64 100644 --- a/core/java/android/service/autofill/CustomDescription.java +++ b/core/java/android/service/autofill/CustomDescription.java @@ -19,6 +19,8 @@ package android.service.autofill; import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; +import android.app.Activity; +import android.app.PendingIntent; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; @@ -130,6 +132,18 @@ public final class CustomDescription implements Parcelable { /** * Default constructor. * + * <p><b>Note:</b> If any child view of presentation triggers a + * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent + * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise + * it might not be triggered or the Save affordance might not be shown when its activity + * is finished: + * <ul> + * <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag. + * <li>It must be a PendingIntent for an {@link Activity}. + * <li>The activity must call {@link Activity#finish()} when done. + * <li>The activity should not launch other activities. + * </ul> + * * @param parentPresentation template presentation with (optional) children views. */ public Builder(RemoteViews parentPresentation) { diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 29e5523ceb7c..61cbce976844 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -30,12 +30,14 @@ import android.content.IntentSender; import android.graphics.Rect; import android.metrics.LogMaker; import android.os.Bundle; +import android.os.IBinder; import android.os.Parcelable; import android.os.RemoteException; import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.DebugUtils; import android.util.Log; import android.util.SparseArray; import android.view.View; @@ -44,6 +46,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; @@ -154,8 +157,15 @@ public final class AutofillManager { public static final String EXTRA_CLIENT_STATE = "android.view.autofill.extra.CLIENT_STATE"; - static final String SESSION_ID_TAG = "android:sessionId"; - static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData"; + + /** @hide */ + public static final String EXTRA_RESTORE_SESSION_TOKEN = + "android.view.autofill.extra.RESTORE_SESSION_TOKEN"; + + private static final String SESSION_ID_TAG = "android:sessionId"; + 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; @@ -175,6 +185,44 @@ public final class AutofillManager { public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF; /** + * Used on {@link #onPendingSaveUi(int, IBinder)} to cancel the pending UI. + * + * @hide + */ + public static final int PENDING_UI_OPERATION_CANCEL = 1; + + /** + * Used on {@link #onPendingSaveUi(int, IBinder)} to restore the pending UI. + * + * @hide + */ + public static final int PENDING_UI_OPERATION_RESTORE = 2; + + /** + * Initial state of the autofill context, set when there is no session (i.e., when + * {@link #mSessionId} is {@link #NO_SESSION}). + * + * @hide + */ + public static final int STATE_UNKNOWN = 1; + + /** + * State where the autofill context hasn't been {@link #commit() finished} nor + * {@link #cancel() canceled} yet. + * + * @hide + */ + public static final int STATE_ACTIVE = 2; + + /** + * State where the autofill context has been {@link #commit() finished} but the server still has + * a session because the Save UI hasn't been dismissed yet. + * + * @hide + */ + public static final int STATE_SHOWING_SAVE_UI = 4; + + /** * Makes an authentication id from a request id and a dataset id. * * @param requestId The request id. @@ -233,6 +281,9 @@ public final class AutofillManager { private int mSessionId = NO_SESSION; @GuardedBy("mLock") + private int mState = STATE_UNKNOWN; + + @GuardedBy("mLock") private boolean mEnabled; /** If a view changes to this mapping the autofill operation was successful */ @@ -344,12 +395,13 @@ public final class AutofillManager { synchronized (mLock) { mLastAutofilledData = savedInstanceState.getParcelable(LAST_AUTOFILLED_DATA_TAG); - if (mSessionId != NO_SESSION) { + if (isActiveLocked()) { Log.w(TAG, "New session was started before onCreate()"); return; } mSessionId = savedInstanceState.getInt(SESSION_ID_TAG, NO_SESSION); + mState = savedInstanceState.getInt(STATE_TAG, STATE_UNKNOWN); if (mSessionId != NO_SESSION) { ensureServiceClientAddedIfNeededLocked(); @@ -363,6 +415,7 @@ public final class AutofillManager { if (!sessionWasRestored) { Log.w(TAG, "Session " + mSessionId + " could not be restored"); mSessionId = NO_SESSION; + mState = STATE_UNKNOWN; } else { if (sDebug) { Log.d(TAG, "session " + mSessionId + " was restored"); @@ -387,7 +440,7 @@ public final class AutofillManager { */ public void onVisibleForAutofill() { synchronized (mLock) { - if (mEnabled && mSessionId != NO_SESSION && mTrackedViews != null) { + if (mEnabled && isActiveLocked() && mTrackedViews != null) { mTrackedViews.onVisibleForAutofillLocked(); } } @@ -408,7 +461,9 @@ public final class AutofillManager { if (mSessionId != NO_SESSION) { outState.putInt(SESSION_ID_TAG, mSessionId); } - + if (mState != STATE_UNKNOWN) { + outState.putInt(STATE_TAG, mState); + } if (mLastAutofilledData != null) { outState.putParcelable(LAST_AUTOFILLED_DATA_TAG, mLastAutofilledData); } @@ -514,7 +569,7 @@ public final class AutofillManager { final AutofillId id = getAutofillId(view); final AutofillValue value = view.getAutofillValue(); - if (mSessionId == NO_SESSION) { + if (!isActiveLocked()) { // Starts new session. startSessionLocked(id, null, value, flags); } else { @@ -541,7 +596,7 @@ public final class AutofillManager { synchronized (mLock) { ensureServiceClientAddedIfNeededLocked(); - if (mEnabled && mSessionId != NO_SESSION) { + if (mEnabled && isActiveLocked()) { final AutofillId id = getAutofillId(view); // Update focus on existing session. @@ -582,7 +637,7 @@ public final class AutofillManager { private void notifyViewVisibilityChangedInternal(@NonNull View view, int virtualId, boolean isVisible, boolean virtual) { synchronized (mLock) { - if (mEnabled && mSessionId != NO_SESSION) { + if (mEnabled && isActiveLocked()) { final AutofillId id = virtual ? getAutofillId(view, virtualId) : view.getAutofillId(); if (!isVisible && mFillableIds != null) { @@ -636,7 +691,7 @@ public final class AutofillManager { } else { final AutofillId id = getAutofillId(view, virtualId); - if (mSessionId == NO_SESSION) { + if (!isActiveLocked()) { // Starts new session. startSessionLocked(id, bounds, null, flags); } else { @@ -665,7 +720,7 @@ public final class AutofillManager { synchronized (mLock) { ensureServiceClientAddedIfNeededLocked(); - if (mEnabled && mSessionId != NO_SESSION) { + if (mEnabled && isActiveLocked()) { final AutofillId id = getAutofillId(view, virtualId); // Update focus on existing session. @@ -709,7 +764,7 @@ public final class AutofillManager { } } - if (!mEnabled || mSessionId == NO_SESSION) { + if (!mEnabled || !isActiveLocked()) { return; } @@ -737,7 +792,7 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled || mSessionId == NO_SESSION) { + if (!mEnabled || !isActiveLocked()) { return; } @@ -762,7 +817,7 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && mSessionId == NO_SESSION) { + if (!mEnabled && !isActiveLocked()) { return; } @@ -786,7 +841,7 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && mSessionId == NO_SESSION) { + if (!mEnabled && !isActiveLocked()) { return; } @@ -868,7 +923,7 @@ public final class AutofillManager { if (sDebug) Log.d(TAG, "onAuthenticationResult(): d=" + data); synchronized (mLock) { - if (mSessionId == NO_SESSION || data == null) { + if (!isActiveLocked() || data == null) { return; } final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT); @@ -895,13 +950,19 @@ public final class AutofillManager { @NonNull AutofillValue value, int flags) { if (sVerbose) { Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value - + ", flags=" + flags); + + ", flags=" + flags + ", state=" + mState); + } + if (mState != STATE_UNKNOWN) { + if (sDebug) Log.d(TAG, "not starting session for " + id + " on state " + mState); + return; } - try { mSessionId = mService.startSession(mContext.getActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), mCallback != null, flags, mContext.getOpPackageName()); + if (mSessionId != NO_SESSION) { + mState = STATE_ACTIVE; + } final AutofillClient client = getClientLocked(); if (client != null) { client.autofillCallbackResetableStateAvailable(); @@ -912,7 +973,9 @@ public final class AutofillManager { } private void finishSessionLocked() { - if (sVerbose) Log.v(TAG, "finishSessionLocked()"); + if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + mState); + + if (!isActiveLocked()) return; try { mService.finishSession(mSessionId, mContext.getUserId()); @@ -920,12 +983,13 @@ public final class AutofillManager { throw e.rethrowFromSystemServer(); } - mTrackedViews = null; - mSessionId = NO_SESSION; + resetSessionLocked(); } private void cancelSessionLocked() { - if (sVerbose) Log.v(TAG, "cancelSessionLocked()"); + if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + mState); + + if (!isActiveLocked()) return; try { mService.cancelSession(mSessionId, mContext.getUserId()); @@ -938,7 +1002,9 @@ public final class AutofillManager { private void resetSessionLocked() { mSessionId = NO_SESSION; + mState = STATE_UNKNOWN; mTrackedViews = null; + mFillableIds = null; } private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action, @@ -947,7 +1013,6 @@ public final class AutofillManager { Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value + ", action=" + action + ", flags=" + flags); } - boolean restartIfNecessary = (flags & FLAG_MANUAL_REQUEST) != 0; try { @@ -958,6 +1023,7 @@ public final class AutofillManager { if (newId != mSessionId) { if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId); mSessionId = newId; + mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE; final AutofillClient client = getClientLocked(); if (client != null) { client.autofillCallbackResetableStateAvailable(); @@ -1219,6 +1285,27 @@ public final class AutofillManager { } } + private void setSaveUiState(int sessionId, boolean shown) { + if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown); + synchronized (mLock) { + if (mSessionId != NO_SESSION) { + // Race condition: app triggered a new session after the previous session was + // finished but before server called setSaveUiState() - need to cancel the new + // session to avoid further inconsistent behavior. + Log.w(TAG, "setSaveUiState(" + sessionId + ", " + shown + + ") called on existing session " + mSessionId + "; cancelling it"); + cancelSessionLocked(); + } + if (shown) { + mSessionId = sessionId; + mState = STATE_SHOWING_SAVE_UI; + } else { + mSessionId = NO_SESSION; + mState = STATE_UNKNOWN; + } + } + } + private void requestHideFillUi(AutofillId id) { final View anchor = findView(id); if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor); @@ -1329,6 +1416,46 @@ public final class AutofillManager { return mService != null; } + /** @hide */ + public void onPendingSaveUi(int operation, IBinder token) { + if (sVerbose) Log.v(TAG, "onPendingSaveUi(" + operation + "): " + token); + + synchronized (mLock) { + try { + mService.onPendingSaveUi(operation, token); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + } + + /** @hide */ + public void dump(String outerPrefix, PrintWriter pw) { + pw.print(outerPrefix); pw.println("AutofillManager:"); + final String pfx = outerPrefix + " "; + pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId); + pw.print(pfx); pw.print("state: "); pw.println( + DebugUtils.flagsToString(AutofillManager.class, "STATE_", mState)); + 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("last autofilled data: "); pw.println(mLastAutofilledData); + pw.print(pfx); pw.print("tracked views: "); + if (mTrackedViews == null) { + pw.println("null"); + } else { + final String pfx2 = pfx + " "; + pw.println(); + pw.print(pfx2); pw.print("visible:"); pw.println(mTrackedViews.mVisibleTrackedIds); + pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds); + } + pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); + } + + private boolean isActiveLocked() { + return mState == STATE_ACTIVE; + } + private void post(Runnable runnable) { final AutofillClient client = getClientLocked(); if (client == null) { @@ -1668,12 +1795,12 @@ public final class AutofillManager { } @Override - public void startIntentSender(IntentSender intentSender) { + public void startIntentSender(IntentSender intentSender, Intent intent) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> { try { - afm.mContext.startIntentSender(intentSender, null, 0, 0, 0); + afm.mContext.startIntentSender(intentSender, intent, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent:" + intentSender, e); } @@ -1691,5 +1818,13 @@ public final class AutofillManager { ); } } + + @Override + public void setSaveUiState(int sessionId, boolean shown) { + final AutofillManager afm = mAfm.get(); + if (afm != null) { + afm.post(() ->afm.setSaveUiState(sessionId, shown)); + } + } } } diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl index 627afa7f8364..6bd9bec368c8 100644 --- a/core/java/android/view/autofill/IAutoFillManager.aidl +++ b/core/java/android/view/autofill/IAutoFillManager.aidl @@ -49,4 +49,5 @@ interface IAutoFillManager { void disableOwnedAutofillServices(int userId); boolean isServiceSupported(int userId); boolean isServiceEnabled(int userId, String packageName); + void onPendingSaveUi(int operation, IBinder token); } diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index d18b1816e09e..0eae85860383 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -72,7 +72,12 @@ oneway interface IAutoFillManagerClient { void notifyNoFillUi(int sessionId, in AutofillId id); /** - * Starts the provided intent sender + * Starts the provided intent sender. */ - void startIntentSender(in IntentSender intentSender); + void startIntentSender(in IntentSender intentSender, in Intent intent); + + /** + * Sets the state of the Autofill Save UI for a given session. + */ + void setSaveUiState(int sessionId, boolean shown); } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index 71f699c8da54..ddc819d39d5e 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -655,6 +655,21 @@ public final class AutofillManagerService extends SystemService { } @Override + public void onPendingSaveUi(int operation, IBinder token) { + Preconditions.checkNotNull(token, "token"); + Preconditions.checkArgument(operation == AutofillManager.PENDING_UI_OPERATION_CANCEL + || operation == AutofillManager.PENDING_UI_OPERATION_RESTORE, + "invalid operation: %d", operation); + synchronized (mLock) { + final AutofillManagerServiceImpl service = peekServiceForUserLocked( + UserHandle.getCallingUserId()); + if (service != null) { + service.onPendingSaveUi(operation, token); + } + } + } + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 751c0547afd6..20ccee286fbc 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -41,7 +41,6 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteCallbackList; import android.os.RemoteException; -import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.service.autofill.AutofillService; @@ -52,10 +51,12 @@ import android.service.autofill.FillResponse; import android.service.autofill.IAutoFillService; import android.text.TextUtils; import android.util.ArraySet; +import android.util.DebugUtils; import android.util.LocalLog; import android.util.Slog; import android.util.SparseArray; import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; @@ -233,26 +234,6 @@ final class AutofillManagerServiceImpl { } } - /** - * Used by {@link AutofillManagerServiceShellCommand} to request save for the current top app. - */ - void requestSaveForUserLocked(IBinder activityToken) { - if (!isEnabled()) { - return; - } - - final int numSessions = mSessions.size(); - for (int i = 0; i < numSessions; i++) { - final Session session = mSessions.valueAt(i); - if (session.getActivityTokenLocked().equals(activityToken)) { - session.callSaveLocked(); - return; - } - } - - Slog.w(TAG, "requestSaveForUserLocked(): no session for " + activityToken); - } - boolean addClientLocked(IAutoFillManagerClient client) { if (mClients == null) { mClients = new RemoteCallbackList<>(); @@ -290,6 +271,7 @@ final class AutofillManagerServiceImpl { if (!isEnabled()) { return 0; } + if (sVerbose) Slog.v(TAG, "startSession(): token=" + activityToken + ", flags=" + flags); // Occasionally clean up abandoned sessions pruneAbandonedSessionsLocked(); @@ -461,6 +443,25 @@ final class AutofillManagerServiceImpl { } } + void onPendingSaveUi(int operation, @NonNull IBinder token) { + if (sVerbose) Slog.v(TAG, "onPendingSaveUi(" + operation + "): " + token); + synchronized (mLock) { + final int sessionCount = mSessions.size(); + for (int i = sessionCount - 1; i >= 0; i--) { + final Session session = mSessions.valueAt(i); + if (session.isSaveUiPendingForToken(token)) { + session.onPendingSaveUi(operation, token); + return; + } + } + } + if (sDebug) { + Slog.d(TAG, "No pending Save UI for token " + token + " and operation " + + DebugUtils.flagsToString(AutofillManager.class, "PENDING_UI_OPERATION_", + operation)); + } + } + void destroyLocked() { if (sVerbose) Slog.v(TAG, "destroyLocked()"); @@ -622,8 +623,12 @@ final class AutofillManagerServiceImpl { } void destroySessionsLocked() { + if (mSessions.size() == 0) { + mUi.destroyAll(AutofillManager.NO_SESSION, null, null); + return; + } while (mSessions.size() > 0) { - mSessions.valueAt(0).removeSelfLocked(); + mSessions.valueAt(0).forceRemoveSelfLocked(); } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index f8fb13a54115..95db6039b696 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -77,6 +77,7 @@ import com.android.internal.os.HandlerCaller; import com.android.internal.os.IResultReceiver; import com.android.internal.util.ArrayUtils; import com.android.server.autofill.ui.AutoFillUI; +import com.android.server.autofill.ui.PendingUi; import java.io.PrintWriter; import java.util.ArrayList; @@ -164,10 +165,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private boolean mDestroyed; - /** Whether the session is currently saving */ + /** Whether the session is currently saving. */ @GuardedBy("mLock") private boolean mIsSaving; + /** + * Helper used to handle state of Save UI when it must be hiding to show a custom description + * link and later recovered. + */ + @GuardedBy("mLock") + private PendingUi mPendingSaveUi; /** * Receiver of assist data from the app's {@link Activity}. @@ -701,7 +708,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mHandlerCaller.getHandler().post(() -> { try { synchronized (mLock) { - mClient.startIntentSender(intentSender); + mClient.startIntentSender(intentSender, null); } } catch (RemoteException e) { Slog.e(TAG, "Error launching auth intent", e); @@ -964,8 +971,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (sDebug) Slog.d(TAG, "Good news, everyone! All checks passed, show save UI!"); mService.setSaveShown(id); + final IAutoFillManagerClient client = getClient(); + mPendingSaveUi = new PendingUi(mActivityToken); getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, - valueFinder, mPackageName, this); + valueFinder, mPackageName, this, mPendingSaveUi, id, client); + if (client != null) { + try { + client.setSaveUiState(id, true); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying client to set save UI state to shown: " + e); + } + } mIsSaving = true; return false; } @@ -1246,7 +1262,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // Remove the UI if the ViewState has changed. if (mCurrentViewId != viewState.id) { - hideFillUiIfOwnedByMe(); + mUi.hideFillUi(this); mCurrentViewId = viewState.id; } @@ -1256,7 +1272,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState case ACTION_VIEW_EXITED: if (mCurrentViewId == viewState.id) { if (sVerbose) Slog.d(TAG, "Exiting view " + id); - hideFillUiIfOwnedByMe(); + mUi.hideFillUi(this); mCurrentViewId = null; } break; @@ -1396,7 +1412,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private void processResponseLocked(@NonNull FillResponse newResponse, int flags) { // Make sure we are hiding the UI which will be shown // only if handling the current response requires it. - hideAllUiIfOwnedByMe(); + mUi.hideAll(this); final int requestId = newResponse.getRequestId(); if (sVerbose) { @@ -1583,6 +1599,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size()); pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed); pw.print(prefix); pw.print("mIsSaving: "); pw.println(mIsSaving); + pw.print(prefix); pw.print("mPendingSaveUi: "); pw.println(mPendingSaveUi); for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) { pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey()); entry.getValue().dump(prefix2, pw); @@ -1644,7 +1661,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if (!ids.isEmpty()) { if (waitingDatasetAuth) { - hideFillUiIfOwnedByMe(); + mUi.hideFillUi(this); } if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); @@ -1664,38 +1681,65 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + /** + * Cleans up this session. + * + * <p>Typically called in 2 scenarios: + * + * <ul> + * <li>When the session naturally finishes (i.e., from {@link #removeSelfLocked()}. + * <li>When the service hosting the session is finished (for example, because the user + * disabled it). + * </ul> + */ RemoteFillService destroyLocked() { if (mDestroyed) { return null; } - hideAllUiIfOwnedByMe(); + mUi.destroyAll(id, getClient(), this); mUi.clearCallback(this); mDestroyed = true; mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName); return mRemoteFillService; } - private void hideAllUiIfOwnedByMe() { - mUi.hideAll(this); - } + /** + * Cleans up this session and remove it from the service always, even if it does have a pending + * Save UI. + */ + void forceRemoveSelfLocked() { + if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi); - private void hideFillUiIfOwnedByMe() { - mUi.hideFillUi(this); + mPendingSaveUi = null; + removeSelfLocked(); + mUi.destroyAll(id, getClient(), this); } + /** + * Thread-safe version of {@link #removeSelfLocked()}. + */ private void removeSelf() { synchronized (mLock) { removeSelfLocked(); } } + /** + * Cleans up this session and remove it from the service, but but only if it does not have a + * pending Save UI. + */ void removeSelfLocked() { - if (sVerbose) Slog.v(TAG, "removeSelfLocked()"); + if (sVerbose) Slog.v(TAG, "removeSelfLocked(): " + mPendingSaveUi); if (mDestroyed) { Slog.w(TAG, "Call to Session#removeSelfLocked() rejected - session: " + id + " destroyed"); return; } + if (isSaveUiPending()) { + Slog.i(TAG, "removeSelfLocked() ignored, waiting for pending save ui"); + return; + } + final RemoteFillService remoteFillService = destroyLocked(); mService.removeSessionLocked(id); if (remoteFillService != null) { @@ -1703,6 +1747,25 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + void onPendingSaveUi(int operation, @NonNull IBinder token) { + getUiForShowing().onPendingSaveUi(operation, token); + } + + /** + * Checks whether this session is hiding the Save UI to handle a custom description link for + * a specific {@code token} created by {@link PendingUi#PendingUi(IBinder)}. + */ + boolean isSaveUiPendingForToken(@NonNull IBinder token) { + return isSaveUiPending() && token.equals(mPendingSaveUi.getToken()); + } + + /** + * Checks whether this session is hiding the Save UI to handle a custom description link. + */ + private boolean isSaveUiPending() { + return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING; + } + private int getLastResponseIndexLocked() { // The response ids are monotonically increasing so // we just find the largest id which is the last. We diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 67ee1858f583..7febf8305d57 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -25,6 +25,8 @@ import android.content.IntentSender; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.service.autofill.SaveInfo; @@ -33,6 +35,7 @@ import android.text.TextUtils; import android.util.Slog; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; +import android.view.autofill.IAutoFillManagerClient; import android.view.autofill.IAutofillWindowPresenter; import android.widget.Toast; @@ -143,7 +146,6 @@ public final class AutoFillUI { if (callback != mCallback) { return; } - hideSaveUiUiThread(callback); if (mFillUi != null) { mFillUi.setFilterText(filterText); } @@ -245,7 +247,8 @@ public final class AutoFillUI { */ public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info, @NonNull ValueFinder valueFinder, @NonNull String packageName, - @NonNull AutoFillUiCallback callback) { + @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingUi, + int sessionId, @Nullable IAutoFillManagerClient client) { if (sVerbose) Slog.v(TAG, "showSaveUi() for " + packageName + ": " + info); int numIds = 0; numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length; @@ -260,21 +263,22 @@ public final class AutoFillUI { return; } hideAllUiThread(callback); - mSaveUi = new SaveUi(mContext, providerLabel, info, valueFinder, mOverlayControl, - new SaveUi.OnSaveListener() { + mSaveUi = new SaveUi(mContext, pendingUi, providerLabel, info, valueFinder, + mOverlayControl, client, new SaveUi.OnSaveListener() { @Override public void onSave() { log.setType(MetricsProto.MetricsEvent.TYPE_ACTION); - hideSaveUiUiThread(callback); + hideSaveUiUiThread(mCallback); if (mCallback != null) { mCallback.save(); } + destroySaveUiUiThread(sessionId, client); } @Override public void onCancel(IntentSender listener) { log.setType(MetricsProto.MetricsEvent.TYPE_DISMISS); - hideSaveUiUiThread(callback); + hideSaveUiUiThread(mCallback); if (listener != null) { try { listener.sendIntent(mContext, 0, null, null, null); @@ -286,6 +290,7 @@ public final class AutoFillUI { if (mCallback != null) { mCallback.cancelSave(); } + destroySaveUiUiThread(sessionId, client); } @Override @@ -304,12 +309,33 @@ public final class AutoFillUI { } /** + * Executes an operation in the pending save UI, if any. + */ + public void onPendingSaveUi(int operation, @NonNull IBinder token) { + mHandler.post(() -> { + if (mSaveUi != null) { + mSaveUi.onPendingUi(operation, token); + } else { + Slog.w(TAG, "onPendingSaveUi(" + operation + "): no save ui"); + } + }); + } + + /** * Hides all UI affordances. */ public void hideAll(@Nullable AutoFillUiCallback callback) { mHandler.post(() -> hideAllUiThread(callback)); } + /** + * Destroy all UI affordances. + */ + public void destroyAll(int sessionId, @Nullable IAutoFillManagerClient client, + @Nullable AutoFillUiCallback callback) { + mHandler.post(() -> destroyAllUiThread(sessionId, client, callback)); + } + public void dump(PrintWriter pw) { pw.println("Autofill UI"); final String prefix = " "; @@ -343,12 +369,41 @@ public final class AutoFillUI { + ", mCallback=" + mCallback); } if (mSaveUi != null && (callback == null || callback == mCallback)) { - mSaveUi.destroy(); - mSaveUi = null; + mSaveUi.hide(); } } @android.annotation.UiThread + private void destroySaveUiUiThread(int sessionId, @Nullable IAutoFillManagerClient client) { + if (mSaveUi == null) { + // Calling destroySaveUiUiThread() twice is normal - it usually happens when the + // first call is made after the SaveUI is hidden and the second when the session is + // finished. + if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): already destroyed"); + return; + } + + if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): id=" + sessionId); + mSaveUi.destroy(); + mSaveUi = null; + if (client != null) { + try { + if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): notifying client"); + client.setSaveUiState(sessionId, false); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying client to set save UI state to hidden: " + e); + } + } + } + + @android.annotation.UiThread + private void destroyAllUiThread(int sessionId, @Nullable IAutoFillManagerClient client, + @Nullable AutoFillUiCallback callback) { + hideFillUiUiThread(callback); + destroySaveUiUiThread(sessionId, client); + } + + @android.annotation.UiThread private void hideAllUiThread(@Nullable AutoFillUiCallback callback) { hideFillUiUiThread(callback); hideSaveUiUiThread(callback); diff --git a/services/autofill/java/com/android/server/autofill/ui/PendingUi.java b/services/autofill/java/com/android/server/autofill/ui/PendingUi.java new file mode 100644 index 000000000000..87263ed61ee9 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/ui/PendingUi.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.autofill.ui; + +import android.annotation.NonNull; +import android.os.IBinder; +import android.util.DebugUtils; + +/** + * Helper class used to handle a pending Autofill affordance such as the Save UI. + * + * <p>This class is not thread safe. + */ +// NOTE: this class could be an interface implemented by Session, but that would make it harder +// to move the Autofill UI logic to a different process. +public final class PendingUi { + + public static final int STATE_CREATED = 1; + public static final int STATE_PENDING = 2; + public static final int STATE_FINISHED = 4; + + private final IBinder mToken; + private int mState; + + /** + * Default constructor. + * + * @param token token used to identify this pending UI. + */ + public PendingUi(@NonNull IBinder token) { + mToken = token; + mState = STATE_CREATED; + } + + /** + * Gets the token used to identify this pending UI. + */ + @NonNull + public IBinder getToken() { + return mToken; + } + + /** + * Sets the current lifecycle state. + */ + public void setState(int state) { + mState = state; + } + + /** + * Gets the current lifecycle state. + */ + public int getState() { + return mState; + } + + /** + * Determines whether the given token matches the token used to identify this pending UI. + */ + public boolean matches(IBinder token) { + return mToken.equals(token); + } + + @Override + public String toString() { + return "PendingUi: [token=" + mToken + ", state=" + + DebugUtils.flagsToString(PendingUi.class, "STATE_", mState) + "]"; + } +} diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 3727c6eb0e6c..67c1b8cdf45a 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -21,9 +21,14 @@ import static com.android.server.autofill.Helper.sVerbose; import android.annotation.NonNull; import android.app.Dialog; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; import android.content.Context; +import android.content.Intent; import android.content.IntentSender; import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; import android.service.autofill.CustomDescription; import android.service.autofill.SaveInfo; import android.service.autofill.ValueFinder; @@ -31,15 +36,17 @@ import android.text.Html; import android.util.ArraySet; import android.util.Slog; import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; +import android.view.autofill.AutofillManager; +import android.view.autofill.IAutoFillManagerClient; import android.widget.RemoteViews; import android.widget.ScrollView; import android.widget.TextView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; import com.android.internal.R; import com.android.server.UiThread; @@ -109,12 +116,15 @@ final class SaveUi { private final CharSequence mTitle; private final CharSequence mSubTitle; + private final PendingUi mPendingUi; private boolean mDestroyed; - SaveUi(@NonNull Context context, @NonNull CharSequence providerLabel, @NonNull SaveInfo info, + SaveUi(@NonNull Context context, @NonNull PendingUi pendingUi, + @NonNull CharSequence providerLabel, @NonNull SaveInfo info, @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl, - @NonNull OnSaveListener listener) { + @NonNull IAutoFillManagerClient client, @NonNull OnSaveListener listener) { + mPendingUi= pendingUi; mListener = new OneTimeListener(listener); mOverlayControl = overlayControl; @@ -171,8 +181,49 @@ final class SaveUi { final RemoteViews presentation = customDescription.getPresentation(valueFinder); if (presentation != null) { + final RemoteViews.OnClickHandler handler = new RemoteViews.OnClickHandler() { + @Override + public boolean onClickHandler(View view, PendingIntent pendingIntent, + Intent intent) { + // We need to hide the Save UI before launching the pending intent, and + // restore back it once the activity is finished, and that's achieved by + // adding a custom extra in the activity intent. + if (pendingIntent != null) { + if (intent == null) { + Slog.w(TAG, + "remote view on custom description does not have intent"); + return false; + } + if (!pendingIntent.isActivity()) { + Slog.w(TAG, "ignoring custom description pending intent that's not " + + "for an activity: " + pendingIntent); + return false; + } + if (sVerbose) { + Slog.v(TAG, + "Intercepting custom description intent: " + intent); + } + final IBinder token = mPendingUi.getToken(); + intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); + try { + client.startIntentSender(pendingIntent.getIntentSender(), + intent); + mPendingUi.setState(PendingUi.STATE_PENDING); + if (sDebug) { + Slog.d(TAG, "hiding UI until restored with token " + token); + } + hide(); + } catch (RemoteException e) { + Slog.w(TAG, "error triggering pending intent: " + intent); + return false; + } + } + return true; + } + }; + try { - final View customSubtitleView = presentation.apply(context, null); + final View customSubtitleView = presentation.apply(context, null, handler); subtitleContainer = view.findViewById(R.id.autofill_save_custom_subtitle); subtitleContainer.addView(customSubtitleView); subtitleContainer.setVisibility(View.VISIBLE); @@ -202,7 +253,7 @@ final class SaveUi { } else { noButton.setText(R.string.autofill_save_no); } - View.OnClickListener cancelListener = + final View.OnClickListener cancelListener = (v) -> mListener.onCancel(info.getNegativeActionListener()); noButton.setOnClickListener(cancelListener); @@ -212,6 +263,9 @@ final class SaveUi { mDialog = new Dialog(context, R.style.Theme_DeviceDefault_Light_Panel); mDialog.setContentView(view); + // Dialog can be dismissed when touched outside. + mDialog.setOnDismissListener((d) -> mListener.onCancel(info.getNegativeActionListener())); + final Window window = mDialog.getWindow(); window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE @@ -227,9 +281,50 @@ final class SaveUi { params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; + show(); + } + + /** + * Update the pending UI, if any. + * + * @param operation how to update it. + * @param token token associated with the pending UI - if it doesn't match the pending token, + * the operation will be ignored. + */ + void onPendingUi(int operation, @NonNull IBinder token) { + if (!mPendingUi.matches(token)) { + Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of " + + mPendingUi.getToken()); + return; + } + switch (operation) { + case AutofillManager.PENDING_UI_OPERATION_RESTORE: + if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token); + show(); + break; + case AutofillManager.PENDING_UI_OPERATION_CANCEL: + if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token); + hide(); + break; + default: + Slog.w(TAG, "restore(): invalid operation " + operation); + } + mPendingUi.setState(PendingUi.STATE_FINISHED); + } + + private void show() { Slog.i(TAG, "Showing save dialog: " + mTitle); mDialog.show(); mOverlayControl.hideOverlays(); + } + + void hide() { + if (sVerbose) Slog.v(TAG, "Hiding save dialog."); + try { + mDialog.hide(); + } finally { + mOverlayControl.showOverlays(); + } } void destroy() { @@ -238,7 +333,6 @@ final class SaveUi { throwIfDestroyed(); mListener.onDestroy(); mHandler.removeCallbacksAndMessages(mListener); - if (sVerbose) Slog.v(TAG, "destroy(): dismissing dialog"); mDialog.dismiss(); mDestroyed = true; } finally { @@ -260,6 +354,7 @@ final class SaveUi { void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("title: "); pw.println(mTitle); pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle); + pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi); final View view = mDialog.getWindow().getDecorView(); final int[] loc = view.getLocationOnScreen(); |