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));
+ }
}
}
} 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));
+ }
}
/**
}
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
+
+ final AutofillManager afm = getAutofillManager();
+ if (afm != null) {
+ afm.dump(prefix, writer);
+ }
}
/**
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;
/**
* 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) {
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;
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;
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;
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.
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 */
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();
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");
*/
public void onVisibleForAutofill() {
synchronized (mLock) {
- if (mEnabled && mSessionId != NO_SESSION && mTrackedViews != null) {
+ if (mEnabled && isActiveLocked() && mTrackedViews != null) {
mTrackedViews.onVisibleForAutofillLocked();
}
}
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);
}
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 {
synchronized (mLock) {
ensureServiceClientAddedIfNeededLocked();
- if (mEnabled && mSessionId != NO_SESSION) {
+ if (mEnabled && isActiveLocked()) {
final AutofillId id = getAutofillId(view);
// Update focus on existing session.
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) {
} else {
final AutofillId id = getAutofillId(view, virtualId);
- if (mSessionId == NO_SESSION) {
+ if (!isActiveLocked()) {
// Starts new session.
startSessionLocked(id, bounds, null, flags);
} else {
synchronized (mLock) {
ensureServiceClientAddedIfNeededLocked();
- if (mEnabled && mSessionId != NO_SESSION) {
+ if (mEnabled && isActiveLocked()) {
final AutofillId id = getAutofillId(view, virtualId);
// Update focus on existing session.
}
}
- if (!mEnabled || mSessionId == NO_SESSION) {
+ if (!mEnabled || !isActiveLocked()) {
return;
}
return;
}
synchronized (mLock) {
- if (!mEnabled || mSessionId == NO_SESSION) {
+ if (!mEnabled || !isActiveLocked()) {
return;
}
return;
}
synchronized (mLock) {
- if (!mEnabled && mSessionId == NO_SESSION) {
+ if (!mEnabled && !isActiveLocked()) {
return;
}
return;
}
synchronized (mLock) {
- if (!mEnabled && mSessionId == NO_SESSION) {
+ if (!mEnabled && !isActiveLocked()) {
return;
}
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);
@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();
}
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());
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());
private void resetSessionLocked() {
mSessionId = NO_SESSION;
+ mState = STATE_UNKNOWN;
mTrackedViews = null;
+ mFillableIds = null;
}
private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action,
Log.v(TAG, "updateSessionLocked(): id=" + id + ", bounds=" + bounds
+ ", value=" + value + ", action=" + action + ", flags=" + flags);
}
-
boolean restartIfNecessary = (flags & FLAG_MANUAL_REQUEST) != 0;
try {
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();
}
}
+ 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);
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) {
}
@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);
}
);
}
}
+
+ @Override
+ public void setSaveUiState(int sessionId, boolean shown) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() ->afm.setSaveUiState(sessionId, shown));
+ }
+ }
}
}
void disableOwnedAutofillServices(int userId);
boolean isServiceSupported(int userId);
boolean isServiceEnabled(int userId, String packageName);
+ void onPendingSaveUi(int operation, IBinder token);
}
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);
}
}
@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;
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;
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;
}
}
- /**
- * 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<>();
if (!isEnabled()) {
return 0;
}
+ if (sVerbose) Slog.v(TAG, "startSession(): token=" + activityToken + ", flags=" + flags);
// Occasionally clean up abandoned sessions
pruneAbandonedSessionsLocked();
}
}
+ 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()");
}
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();
}
}
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;
@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}.
mHandlerCaller.getHandler().post(() -> {
try {
synchronized (mLock) {
- mClient.startIntentSender(intentSender);
+ mClient.startIntentSender(intentSender, null);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
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;
}
// Remove the UI if the ViewState has changed.
if (mCurrentViewId != viewState.id) {
- hideFillUiIfOwnedByMe();
+ mUi.hideFillUi(this);
mCurrentViewId = viewState.id;
}
case ACTION_VIEW_EXITED:
if (mCurrentViewId == viewState.id) {
if (sVerbose) Slog.d(TAG, "Exiting view " + id);
- hideFillUiIfOwnedByMe();
+ mUi.hideFillUi(this);
mCurrentViewId = null;
}
break;
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) {
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);
}
if (!ids.isEmpty()) {
if (waitingDatasetAuth) {
- hideFillUiIfOwnedByMe();
+ mUi.hideFillUi(this);
}
if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
}
}
+ /**
+ * 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) {
}
}
+ 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
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;
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;
if (callback != mCallback) {
return;
}
- hideSaveUiUiThread(callback);
if (mFillUi != null) {
mFillUi.setFilterText(filterText);
}
*/
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;
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);
if (mCallback != null) {
mCallback.cancelSave();
}
+ destroySaveUiUiThread(sessionId, client);
}
@Override
}
/**
+ * 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 = " ";
+ ", 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);
--- /dev/null
+/*
+ * 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) + "]";
+ }
+}
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;
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;
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;
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);
} else {
noButton.setText(R.string.autofill_save_no);
}
- View.OnClickListener cancelListener =
+ final View.OnClickListener cancelListener =
(v) -> mListener.onCancel(info.getNegativeActionListener());
noButton.setOnClickListener(cancelListener);
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
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() {
throwIfDestroyed();
mListener.onDestroy();
mHandler.removeCallbacksAndMessages(mListener);
- if (sVerbose) Slog.v(TAG, "destroy(): dismissing dialog");
mDialog.dismiss();
mDestroyed = true;
} finally {
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();