diff options
| author | Philip P. Moltmann <moltmann@google.com> | 2017-04-11 10:13:33 -0700 |
|---|---|---|
| committer | Philip P. Moltmann <moltmann@google.com> | 2017-04-16 21:36:25 -0700 |
| commit | 494c3f5da2c467ad07f50b4e1ad01065a8e3aa4f (patch) | |
| tree | a19730d180908b5749aad27938bba1dea3d9f779 /core/java | |
| parent | d8837b6fca23847fcd4dd295ccbb33ef9d4edcec (diff) | |
Allow to finish session when all views are gone
An service can option to finish the session once all views that it
declared as important. Views that are important are all autofillable
views of any partition and the saveable fields of the last partition.
Test: CtsAutoFillServiceTestCases
Fixes: 35708237
Change-Id: I0ccade8ebb427e5d8928697ef0007c75d3f83df0
Diffstat (limited to 'core/java')
| -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 |
5 files changed, 409 insertions, 5 deletions
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, |
