diff options
| -rw-r--r-- | api/current.txt | 1 | ||||
| -rw-r--r-- | api/system-current.txt | 1 | ||||
| -rw-r--r-- | api/test-current.txt | 1 | ||||
| -rw-r--r-- | core/java/android/app/Activity.java | 60 | ||||
| -rw-r--r-- | core/java/android/service/autofill/SaveInfo.java | 27 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 49 | ||||
| -rw-r--r-- | core/java/android/view/autofill/AutofillManager.java | 271 | ||||
| -rw-r--r-- | core/java/android/view/autofill/IAutoFillManagerClient.aidl | 7 | ||||
| -rw-r--r-- | services/autofill/java/com/android/server/autofill/Session.java | 37 |
9 files changed, 449 insertions, 5 deletions
diff --git a/api/current.txt b/api/current.txt index 3733cb9fd2a1..0cdc8f2ef806 100644 --- a/api/current.txt +++ b/api/current.txt @@ -37160,6 +37160,7 @@ package android.service.autofill { 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 5fff0610f483..a10806b35981 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -40270,6 +40270,7 @@ package android.service.autofill { 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 5231ada16694..a861e9d39aa7 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -37313,6 +37313,7 @@ package android.service.autofill { 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 b36a1600bae5..169dcb01c90a 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 @@ public class Activity extends ContextThemeWrapper mFragments.doLoaderStart(); getApplication().dispatchActivityStarted(this); + + if (mAutoFillResetNeeded) { + AutofillManager afm = getAutofillManager(); + if (afm != null) { + afm.onVisibleForAutofill(); + } + } } /** @@ -7407,6 +7409,54 @@ public class Activity extends ContextThemeWrapper 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 258d257813ca..7f960dff0bdf 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -21,6 +21,7 @@ import static android.view.autofill.Helper.DEBUG; 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 @@ public final class SaveInfo implements Parcelable { 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 @@ public final class SaveInfo implements Parcelable { mRequiredIds = builder.mRequiredIds; mOptionalIds = builder.mOptionalIds; mDescription = builder.mDescription; + mSaveOnAllViewsInvisible = builder.mSaveOnAllViewsInvisible; } /** @hide */ @@ -194,6 +197,11 @@ public final class SaveInfo implements Parcelable { } /** @hide */ + public boolean saveOnAllViewsInvisible() { + return mSaveOnAllViewsInvisible; + } + + /** @hide */ public CharSequence getDescription() { return mDescription; } @@ -211,6 +219,7 @@ public final class SaveInfo implements Parcelable { private AutofillId[] mOptionalIds; private CharSequence mDescription; private boolean mDestroyed; + private boolean mSaveOnAllViewsInvisible; /** * Creates a new builder. @@ -259,6 +268,21 @@ public final class SaveInfo implements Parcelable { } /** + * 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 @@ public final class SaveInfo implements Parcelable { .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 @@ public final class SaveInfo implements Parcelable { 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 @@ public final class SaveInfo implements Parcelable { 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 172ad8da5381..7d2d77e251a9 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -66,6 +66,7 @@ import android.os.Build; 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * 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 ec6559cba39b..f9f400d83b09 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -32,6 +32,7 @@ import android.os.IBinder; 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 @@ public final class AutofillManager { @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 @@ public final class AutofillManager { * @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 @@ public final class AutofillManager { } /** + * 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 @@ public final class AutofillManager { } /** + * 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 @@ public final class AutofillManager { throw e.rethrowFromSystemServer(); } + mTrackedViews = null; mSessionId = NO_SESSION; } @@ -683,6 +734,7 @@ public final class AutofillManager { throw e.rethrowFromSystemServer(); } + mTrackedViews = null; mSessionId = NO_SESSION; } @@ -903,6 +955,25 @@ public final class AutofillManager { } } + /** + * 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 @@ public final class AutofillManager { } /** + * 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 @@ public final class AutofillManager { }); } } + + @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 56f91ed6de9b..1a6bad2d1cd6 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -49,6 +49,13 @@ oneway interface IAutoFillManagerClient { 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 5feb81db4a0b..7c3f3245461c 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 com.android.server.autofill.ui.AutoFillUI; 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } setViewStatesLocked(response, ViewState.STATE_FILLABLE); + updateTrackedIdsLocked(); if (mCurrentViewId == null) { return; |