Merge "Allow to finish session when all views are gone" into oc-dev

This commit is contained in:
TreeHugger Robot
2017-04-17 20:44:57 +00:00
committed by Android (Google) Code Review
9 changed files with 449 additions and 5 deletions

View File

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

View File

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

View File

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

View File

@@ -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.

View File

@@ -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 */
@@ -193,6 +196,11 @@ public final class SaveInfo implements Parcelable {
return mType;
}
/** @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.
@@ -258,6 +267,21 @@ public final class SaveInfo implements Parcelable {
return this;
}
/**
* 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.
*
@@ -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();
}

View File

@@ -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();
}
}
}
}
/**
@@ -24491,6 +24519,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()}.

View File

@@ -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();
}
/**
@@ -259,6 +278,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
*
@@ -411,6 +445,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.
*
@@ -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);
@@ -968,6 +1039,195 @@ public final class AutofillManager {
return mService != null;
}
/**
* 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.
*
@@ -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)
);
}
}
}
}

View File

@@ -48,6 +48,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.
*/

View File

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