Merge "Allow to finish session when all views are gone" into oc-dev
diff --git a/api/current.txt b/api/current.txt
index 3733cb9..0cdc8f2 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -37160,6 +37160,7 @@
method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence);
method public android.service.autofill.SaveInfo.Builder setNegativeAction(java.lang.CharSequence, android.content.IntentSender);
method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]);
+ method public android.service.autofill.SaveInfo.Builder setSaveOnAllViewsInvisible(boolean);
}
public final class SaveRequest implements android.os.Parcelable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 5fff061..a10806b3 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -40270,6 +40270,7 @@
method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence);
method public android.service.autofill.SaveInfo.Builder setNegativeAction(java.lang.CharSequence, android.content.IntentSender);
method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]);
+ method public android.service.autofill.SaveInfo.Builder setSaveOnAllViewsInvisible(boolean);
}
public final class SaveRequest implements android.os.Parcelable {
diff --git a/api/test-current.txt b/api/test-current.txt
index 5231ada..a861e9d 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -37313,6 +37313,7 @@
method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence);
method public android.service.autofill.SaveInfo.Builder setNegativeAction(java.lang.CharSequence, android.content.IntentSender);
method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]);
+ method public android.service.autofill.SaveInfo.Builder setSaveOnAllViewsInvisible(boolean);
}
public final class SaveRequest implements android.os.Parcelable {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index b36a160..169dcb0 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -16,21 +16,16 @@
package android.app;
-import android.metrics.LogMaker;
import android.graphics.Rect;
import android.os.SystemClock;
import android.view.ViewRootImpl.ActivityConfigCallback;
-import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillPopupWindow;
-import android.view.autofill.AutofillValue;
import android.view.autofill.IAutofillWindowPresenter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.app.ToolbarActionBar;
import com.android.internal.app.WindowDecorActionBar;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.policy.PhoneWindow;
import android.annotation.CallSuper;
@@ -1234,6 +1229,13 @@
mFragments.doLoaderStart();
getApplication().dispatchActivityStarted(this);
+
+ if (mAutoFillResetNeeded) {
+ AutofillManager afm = getAutofillManager();
+ if (afm != null) {
+ afm.onVisibleForAutofill();
+ }
+ }
}
/**
@@ -7407,6 +7409,54 @@
return true;
}
+ /** @hide */
+ @Override
+ public boolean getViewVisibility(int viewId) {
+ Window window = getWindow();
+ if (window == null) {
+ Log.i(TAG, "no window");
+ return false;
+ }
+
+ View decorView = window.peekDecorView();
+ if (decorView == null) {
+ Log.i(TAG, "no decorView");
+ return false;
+ }
+
+ View view = decorView.findViewByAccessibilityIdTraversal(viewId);
+ if (view == null) {
+ Log.i(TAG, "cannot find view");
+ return false;
+ }
+
+ // Check if the view is visible by checking all parents
+ while (view != null) {
+ if (view == decorView) {
+ break;
+ }
+
+ if (view.getVisibility() != View.VISIBLE) {
+ Log.i(TAG, view + " is not visible");
+ return false;
+ }
+
+ if (view.getParent() instanceof View) {
+ view = (View) view.getParent();
+ } else {
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ public boolean isVisibleForAutofill() {
+ return !mStopped;
+ }
+
/**
* If set to true, this indicates to the system that it should never take a
* screenshot of the activity to be used as a representation while it is not in a started state.
diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java
index 258d257..7f960df 100644
--- a/core/java/android/service/autofill/SaveInfo.java
+++ b/core/java/android/service/autofill/SaveInfo.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.assist.AssistStructure;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.Parcel;
@@ -158,6 +159,7 @@
private final AutofillId[] mRequiredIds;
private final AutofillId[] mOptionalIds;
private final CharSequence mDescription;
+ private final boolean mSaveOnAllViewsInvisible;
private SaveInfo(Builder builder) {
mType = builder.mType;
@@ -166,6 +168,7 @@
mRequiredIds = builder.mRequiredIds;
mOptionalIds = builder.mOptionalIds;
mDescription = builder.mDescription;
+ mSaveOnAllViewsInvisible = builder.mSaveOnAllViewsInvisible;
}
/** @hide */
@@ -194,6 +197,11 @@
}
/** @hide */
+ public boolean saveOnAllViewsInvisible() {
+ return mSaveOnAllViewsInvisible;
+ }
+
+ /** @hide */
public CharSequence getDescription() {
return mDescription;
}
@@ -211,6 +219,7 @@
private AutofillId[] mOptionalIds;
private CharSequence mDescription;
private boolean mDestroyed;
+ private boolean mSaveOnAllViewsInvisible;
/**
* Creates a new builder.
@@ -259,6 +268,21 @@
}
/**
+ * Usually {@link AutofillService#onSaveRequest(AssistStructure, Bundle, SaveCallback)}
+ * is called once the activity finishes. If this property is set it is called once all
+ * autofillable or saved views become invisible.
+ *
+ * @param saveOnAllViewsInvisible Set to {@code true} if the data should be saved once
+ * all the views become invisible.
+ * @return This builder.
+ */
+ public @NonNull Builder setSaveOnAllViewsInvisible(boolean saveOnAllViewsInvisible) {
+ throwIfDestroyed();
+ mSaveOnAllViewsInvisible = saveOnAllViewsInvisible;
+ return this;
+ }
+
+ /**
* Sets the ids of additional, optional views the service would be interested to save.
*
* <p>See {@link SaveInfo} for more info.
@@ -354,6 +378,7 @@
.append(", requiredIds=").append(Arrays.toString(mRequiredIds))
.append(", optionalIds=").append(Arrays.toString(mOptionalIds))
.append(", description=").append(mDescription)
+ .append(", saveOnNoVisibleTrackedViews=").append(mSaveOnAllViewsInvisible)
.append("]").toString();
}
@@ -374,6 +399,7 @@
parcel.writeParcelable(mNegativeActionListener, flags);
parcel.writeParcelableArray(mOptionalIds, flags);
parcel.writeCharSequence(mDescription);
+ parcel.writeBoolean(mSaveOnAllViewsInvisible);
}
public static final Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() {
@@ -387,6 +413,7 @@
builder.setNegativeAction(parcel.readCharSequence(), parcel.readParcelable(null));
builder.setOptionalIds(parcel.readParcelableArray(null, AutofillId.class));
builder.setDescription(parcel.readCharSequence());
+ builder.setSaveOnAllViewsInvisible(parcel.readBoolean());
return builder.build();
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 172ad8d..7d2d77e 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -66,6 +66,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
@@ -4380,6 +4381,9 @@
@Nullable
private RoundScrollbarRenderer mRoundScrollbarRenderer;
+ /** Used to delay visibility updates sent to the autofill manager */
+ private Handler mVisibilityChangeForAutofillHandler;
+
/**
* Simple constructor to use when creating a view from code.
*
@@ -11696,6 +11700,30 @@
if (fg != null && isVisible != fg.isVisible()) {
fg.setVisible(isVisible, false);
}
+
+ if (isAutofillable()) {
+ AutofillManager afm = getAutofillManager();
+
+ if (afm != null && getAccessibilityViewId() > LAST_APP_ACCESSIBILITY_ID) {
+ if (mVisibilityChangeForAutofillHandler != null) {
+ mVisibilityChangeForAutofillHandler.removeMessages(0);
+ }
+
+ // If the view is in the background but still part of the hierarchy this is called
+ // with isVisible=false. Hence visibility==false requires further checks
+ if (isVisible) {
+ afm.notifyViewVisibilityChange(this, true);
+ } else {
+ if (mVisibilityChangeForAutofillHandler == null) {
+ mVisibilityChangeForAutofillHandler =
+ new VisibilityChangeForAutofillHandler(afm, this);
+ }
+ // Let current operation (e.g. removal of the view from the hierarchy)
+ // finish before checking state
+ mVisibilityChangeForAutofillHandler.obtainMessage(0, this).sendToTarget();
+ }
+ }
+ }
}
/**
@@ -24492,6 +24520,27 @@
}
/**
+ * When a view becomes invisible checks if autofill considers the view invisible too. This
+ * happens after the regular removal operation to make sure the operation is finished by the
+ * time this is called.
+ */
+ private static class VisibilityChangeForAutofillHandler extends Handler {
+ private final AutofillManager mAfm;
+ private final View mView;
+
+ private VisibilityChangeForAutofillHandler(@NonNull AutofillManager afm,
+ @NonNull View view) {
+ mAfm = afm;
+ mView = view;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ mAfm.notifyViewVisibilityChange(mView, mView.isShown());
+ }
+ }
+
+ /**
* Base class for derived classes that want to save and restore their own
* state in {@link android.view.View#onSaveInstanceState()}.
*/
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index ec6559c..f9f400d 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -32,6 +32,7 @@
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
@@ -143,6 +144,10 @@
@GuardedBy("mLock")
@Nullable private ParcelableMap mLastAutofilledData;
+ /** If view tracking is enabled, contains the tracking state */
+ @GuardedBy("mLock")
+ @Nullable private TrackedViews mTrackedViews;
+
/** @hide */
public interface AutofillClient {
/**
@@ -177,6 +182,20 @@
* @return Whether the UI was hidden.
*/
boolean autofillCallbackRequestHideFillUi();
+
+ /**
+ * Checks if the view is currently attached and visible.
+ *
+ * @return {@code true} iff the view is attached or visible
+ */
+ boolean getViewVisibility(int viewId);
+
+ /**
+ * Checks is the client is currently visible as understood by autofill.
+ *
+ * @return {@code true} if the client is currently visible
+ */
+ boolean isVisibleForAutofill();
}
/**
@@ -260,6 +279,21 @@
}
/**
+ * Called once the client becomes visible.
+ *
+ * @see AutofillClient#isVisibleForAutofill()
+ *
+ * {@hide}
+ */
+ public void onVisibleForAutofill() {
+ synchronized (mLock) {
+ if (mEnabled && mSessionId != NO_SESSION && mTrackedViews != null) {
+ mTrackedViews.onVisibleForAutofill();
+ }
+ }
+ }
+
+ /**
* Save state before activity lifecycle
*
* @param outState Place to store the state
@@ -412,6 +446,22 @@
}
/**
+ * Called when a {@link View view's} visibility changes.
+ *
+ * @param view {@link View} that was exited.
+ * @param isVisible visible if the view is visible in the view hierarchy.
+ *
+ * @hide
+ */
+ public void notifyViewVisibilityChange(@NonNull View view, boolean isVisible) {
+ synchronized (mLock) {
+ if (mEnabled && mSessionId != NO_SESSION && mTrackedViews != null) {
+ mTrackedViews.notifyViewVisibilityChange(view, isVisible);
+ }
+ }
+ }
+
+ /**
* Called when a virtual view that supports autofill is entered.
*
* @param view the {@link View} whose descendant is the virtual view.
@@ -669,6 +719,7 @@
throw e.rethrowFromSystemServer();
}
+ mTrackedViews = null;
mSessionId = NO_SESSION;
}
@@ -683,6 +734,7 @@
throw e.rethrowFromSystemServer();
}
+ mTrackedViews = null;
mSessionId = NO_SESSION;
}
@@ -903,6 +955,25 @@
}
}
+ /**
+ * Set the tracked views.
+ *
+ * @param trackedIds The views to be tracked
+ * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible.
+ */
+ private void setTrackedViews(int sessionId, List<AutofillId> trackedIds,
+ boolean saveOnAllViewsInvisible) {
+ synchronized (mLock) {
+ if (mEnabled && mSessionId == sessionId) {
+ if (saveOnAllViewsInvisible) {
+ mTrackedViews = new TrackedViews(trackedIds);
+ } else {
+ mTrackedViews = null;
+ }
+ }
+ }
+ }
+
private void requestHideFillUi(int sessionId, IBinder windowToken, AutofillId id) {
final View anchor = findAchorView(windowToken, id);
@@ -969,6 +1040,195 @@
}
/**
+ * View tracking information. Once all tracked views become invisible the session is finished.
+ */
+ private class TrackedViews {
+ /** Visible tracked views */
+ @Nullable private ArraySet<AutofillId> mVisibleTrackedIds;
+
+ /** Invisible tracked views */
+ @Nullable private ArraySet<AutofillId> mInvisibleTrackedIds;
+
+ /**
+ * Check if set is null or value is in set.
+ *
+ * @param set The set or null (== empty set)
+ * @param value The value that might be in the set
+ *
+ * @return {@code true} iff set is not empty and value is in set
+ */
+ private <T> boolean isInSet(@Nullable ArraySet<T> set, T value) {
+ return set != null && set.contains(value);
+ }
+
+ /**
+ * Add a value to a set. If set is null, create a new set.
+ *
+ * @param set The set or null (== empty set)
+ * @param valueToAdd The value to add
+ *
+ * @return The set including the new value. If set was {@code null}, a set containing only
+ * the new value.
+ */
+ @NonNull
+ private <T> ArraySet<T> addToSet(@Nullable ArraySet<T> set, T valueToAdd) {
+ if (set == null) {
+ set = new ArraySet<>(1);
+ }
+
+ set.add(valueToAdd);
+
+ return set;
+ }
+
+ /**
+ * Remove a value from a set.
+ *
+ * @param set The set or null (== empty set)
+ * @param valueToRemove The value to remove
+ *
+ * @return The set without the removed value. {@code null} if set was null, or is empty
+ * after removal.
+ */
+ @Nullable
+ private <T> ArraySet<T> removeFromSet(@Nullable ArraySet<T> set, T valueToRemove) {
+ if (set == null) {
+ return null;
+ }
+
+ set.remove(valueToRemove);
+
+ if (set.isEmpty()) {
+ return null;
+ }
+
+ return set;
+ }
+
+ /**
+ * Set the tracked views.
+ *
+ * @param trackedIds The views to be tracked
+ */
+ TrackedViews(@NonNull List<AutofillId> trackedIds) {
+ mVisibleTrackedIds = null;
+ mInvisibleTrackedIds = null;
+
+ AutofillClient client = getClientLocked();
+ if (trackedIds != null) {
+ int numIds = trackedIds.size();
+ for (int i = 0; i < numIds; i++) {
+ AutofillId id = trackedIds.get(i);
+
+ boolean isVisible = true;
+ if (client != null && client.isVisibleForAutofill()) {
+ isVisible = client.getViewVisibility(id.getViewId());
+ }
+
+ if (isVisible) {
+ mVisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+ } else {
+ mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+ }
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "TrackedViews(trackedIds=" + trackedIds + "): "
+ + " mVisibleTrackedIds=" + mVisibleTrackedIds
+ + " mInvisibleTrackedIds=" + mInvisibleTrackedIds);
+ }
+
+ if (mVisibleTrackedIds == null) {
+ finishSessionLocked();
+ }
+ }
+
+ /**
+ * Called when a {@link View view's} visibility changes.
+ *
+ * @param view {@link View} that was exited.
+ * @param isVisible visible if the view is visible in the view hierarchy.
+ */
+ void notifyViewVisibilityChange(@NonNull View view, boolean isVisible) {
+ AutofillId id = getAutofillId(view);
+ AutofillClient client = getClientLocked();
+
+ if (DEBUG) {
+ Log.d(TAG, "notifyViewVisibilityChange(): id=" + id + " isVisible="
+ + isVisible);
+ }
+
+ if (client != null && client.isVisibleForAutofill()) {
+ if (isVisible) {
+ if (isInSet(mInvisibleTrackedIds, id)) {
+ mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
+ mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
+ }
+ } else {
+ if (isInSet(mVisibleTrackedIds, id)) {
+ mVisibleTrackedIds = removeFromSet(mVisibleTrackedIds, id);
+ mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+ }
+ }
+ }
+
+ if (mVisibleTrackedIds == null) {
+ finishSessionLocked();
+ }
+ }
+
+ /**
+ * Called once the client becomes visible.
+ *
+ * @see AutofillClient#isVisibleForAutofill()
+ */
+ void onVisibleForAutofill() {
+ // The visibility of the views might have changed while the client was not started,
+ // hence update the visibility state for all views.
+ AutofillClient client = getClientLocked();
+ ArraySet<AutofillId> updatedVisibleTrackedIds = null;
+ ArraySet<AutofillId> updatedInvisibleTrackedIds = null;
+ if (client != null) {
+ if (mInvisibleTrackedIds != null) {
+ for (AutofillId id : mInvisibleTrackedIds) {
+ if (client.getViewVisibility(id.getViewId())) {
+ updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
+
+ if (DEBUG) {
+ Log.i(TAG, "onVisibleForAutofill() " + id + " became visible");
+ }
+ } else {
+ updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
+ }
+ }
+ }
+
+ if (mVisibleTrackedIds != null) {
+ for (AutofillId id : mVisibleTrackedIds) {
+ if (client.getViewVisibility(id.getViewId())) {
+ updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
+ } else {
+ updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
+
+ if (DEBUG) {
+ Log.i(TAG, "onVisibleForAutofill() " + id + " became invisible");
+ }
+ }
+ }
+ }
+
+ mInvisibleTrackedIds = updatedInvisibleTrackedIds;
+ mVisibleTrackedIds = updatedVisibleTrackedIds;
+ }
+
+ if (mVisibleTrackedIds == null) {
+ finishSessionLocked();
+ }
+ }
+ }
+
+ /**
* Callback for auto-fill related events.
*
* <p>Typically used for applications that display their own "auto-complete" views, so they can
@@ -1106,5 +1366,16 @@
});
}
}
+
+ @Override
+ public void setTrackedViews(int sessionId, List<AutofillId> ids,
+ boolean saveOnAllViewsInvisible) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.mContext.getMainThreadHandler().post(
+ () -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible)
+ );
+ }
+ }
}
}
diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
index 56f91ed..1a6bad2 100644
--- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl
+++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
@@ -49,6 +49,13 @@
void authenticate(int sessionId, in IntentSender intent, in Intent fillInIntent);
/**
+ * Sets the views to track. If saveOnAllViewsInvisible is set and all these view are invisible
+ * the session is finished automatically.
+ */
+ void setTrackedViews(int sessionId, in List<AutofillId> ids,
+ boolean saveOnAllViewsInvisible);
+
+ /**
* Requests showing the fill UI.
*/
void requestShowFillUi(int sessionId, in IBinder windowToken, in AutofillId id, int width,
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 5feb81d..7c3f324 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -68,6 +68,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
@@ -220,6 +221,9 @@
synchronized (mLock) {
mActivityToken = newActivity;
mClient = IAutoFillManagerClient.Stub.asInterface(newClient);
+
+ // The tracked id are not persisted in the client, hence update them
+ updateTrackedIdsLocked();
}
}
@@ -749,6 +753,38 @@
}
}
+ private void updateTrackedIdsLocked() {
+ if (mResponses == null || mResponses.size() == 0) {
+ return;
+ }
+
+ // Only track the views of the last response as only those are reported back to the
+ // service, see #showSaveLocked
+ ArrayList<AutofillId> trackedViews = new ArrayList<>();
+ boolean saveOnAllViewsInvisible = false;
+ SaveInfo saveInfo = mResponses.valueAt(getLastResponseIndex()).getSaveInfo();
+ if (saveInfo != null) {
+ saveOnAllViewsInvisible = saveInfo.saveOnAllViewsInvisible();
+
+ // We only need to track views if we want to save once they become invisible.
+ if (saveOnAllViewsInvisible) {
+ if (saveInfo.getRequiredIds() != null) {
+ Collections.addAll(trackedViews, saveInfo.getRequiredIds());
+ }
+
+ if (saveInfo.getOptionalIds() != null) {
+ Collections.addAll(trackedViews, saveInfo.getOptionalIds());
+ }
+ }
+ }
+
+ try {
+ mClient.setTrackedViews(id, trackedViews, saveOnAllViewsInvisible);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Cannot set tracked ids", e);
+ }
+ }
+
private void processResponseLocked(FillResponse response, int requestId) {
if (DEBUG) {
Slog.d(TAG, "processResponseLocked(mCurrentViewId=" + mCurrentViewId + "):" + response);
@@ -763,6 +799,7 @@
}
setViewStatesLocked(response, ViewState.STATE_FILLABLE);
+ updateTrackedIdsLocked();
if (mCurrentViewId == null) {
return;