Merge "Hides the Save UI while handling a pending intent from CustomDescription." into oc-mr1-dev

This commit is contained in:
TreeHugger Robot
2017-08-29 03:13:57 +00:00
committed by Android (Google) Code Review
11 changed files with 575 additions and 83 deletions

View File

@@ -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));
}
}
}
@@ -5500,6 +5510,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));
}
}
/**
@@ -6234,6 +6251,11 @@ public class Activity extends ContextThemeWrapper
}
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
final AutofillManager afm = getAutofillManager();
if (afm != null) {
afm.dump(prefix, writer);
}
}
/**

View File

@@ -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) {

View File

@@ -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;
@@ -174,6 +184,44 @@ public final class AutofillManager {
/** @hide The index for an undefined data set */
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.
*
@@ -232,6 +280,9 @@ public final class AutofillManager {
@GuardedBy("mLock")
private int mSessionId = NO_SESSION;
@GuardedBy("mLock")
private int mState = STATE_UNKNOWN;
@GuardedBy("mLock")
private boolean mEnabled;
@@ -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));
}
}
}
}

View File

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

View File

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

View File

@@ -654,6 +654,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;

View File

@@ -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();
}
}

View File

@@ -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);
}
private void hideFillUiIfOwnedByMe() {
mUi.hideFillUi(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);
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

View File

@@ -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
@@ -303,6 +308,19 @@ 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.
*/
@@ -310,6 +328,14 @@ public final class AutoFillUI {
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,11 +369,40 @@ 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);

View File

@@ -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) + "]";
}
}

View File

@@ -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.Window;
import android.view.WindowManager;
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 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 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();