blob: b4d2c6bfe016737762746e5dacbe7ae79fd78482 [file] [log] [blame]
/*
* 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 android.view.autofill;
import static android.view.autofill.Helper.DEBUG;
import static android.view.autofill.Helper.VERBOSE;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
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.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.WindowManagerGlobal;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* App entry point to the AutoFill Framework.
*/
// TODO(b/33197203): improve this javadoc
//TODO(b/33197203): restrict manager calls to activity
public final class AutofillManager {
private static final String TAG = "AutofillManager";
/**
* Intent extra: The assist structure which captures the filled screen.
* <p>
* Type: {@link android.app.assist.AssistStructure}
* </p>
*/
public static final String EXTRA_ASSIST_STRUCTURE =
"android.view.autofill.extra.ASSIST_STRUCTURE";
/**
* Intent extra: The result of an authentication operation. It is
* either a fully populated {@link android.service.autofill.FillResponse}
* or a fully populated {@link android.service.autofill.Dataset} if
* a response or a dataset is being authenticated respectively.
*
* <p>
* Type: {@link android.service.autofill.FillResponse} or a
* {@link android.service.autofill.Dataset}
* </p>
*/
public static final String EXTRA_AUTHENTICATION_RESULT =
"android.view.autofill.extra.AUTHENTICATION_RESULT";
// Public flags start from the lowest bit
/**
* Indicates autofill was explicitly requested by the user.
*/
public static final int FLAG_MANUAL_REQUEST = 0x1;
// Private flags start from the highest bit
/** @hide */ public static final int FLAG_START_SESSION = 0x80000000;
/** @hide */ public static final int FLAG_VIEW_ENTERED = 0x40000000;
/** @hide */ public static final int FLAG_VIEW_EXITED = 0x20000000;
/** @hide */ public static final int FLAG_VALUE_CHANGED = 0x10000000;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
private final IAutoFillManager mService;
private IAutoFillManagerClient mServiceClient;
private AutofillCallback mCallback;
private Context mContext;
private boolean mHasSession;
private boolean mEnabled;
/** @hide */
public interface AutofillClient {
/**
* Asks the client to start an authentication flow.
*
* @param intent The authentication intent.
* @param fillInIntent The authentication fill-in intent.
*/
void autofillCallbackAuthenticate(IntentSender intent, Intent fillInIntent);
/**
* Tells the client this manager has state to be reset.
*/
void autofillCallbackResetableStateAvailable();
/**
* Request showing the autofill UI.
*
* @param anchor The real view the UI needs to anchor to.
* @param width The width of the fill UI content.
* @param height The height of the fill UI content.
* @param virtualBounds The bounds of the virtual decendant of the anchor.
* @param presenter The presenter that controls the fill UI window.
* @return Whether the UI was shown.
*/
boolean autofillCallbackRequestShowFillUi(@NonNull View anchor, int width, int height,
@Nullable Rect virtualBounds, IAutofillWindowPresenter presenter);
/**
* Request hiding the autofill UI.
*
* @return Whether the UI was hidden.
*/
boolean autofillCallbackRequestHideFillUi();
}
/**
* @hide
*/
public AutofillManager(Context context, IAutoFillManager service) {
mContext = context;
mService = service;
}
/**
* Checkes whether autofill is enabled for the current user.
*
* <p>Typically used to determine whether the option to explicitly request autofill should
* be offered - see {@link #requestAutofill(View)}.
*
* @return whether autofill is enabled for the current user.
*/
public boolean isEnabled() {
ensureServiceClientAddedIfNeeded();
return mEnabled;
}
/**
* Explicitly requests a new autofill context.
*
* <p>Normally, the autofill context is automatically started when autofillable views are
* focused, but this method should be used in the cases where it must be explicitly requested,
* like a view that provides a contextual menu allowing users to autofill the activity.
*
* @param view view requesting the new autofill context.
*/
public void requestAutofill(@NonNull View view) {
ensureServiceClientAddedIfNeeded();
if (!mEnabled) {
return;
}
final AutofillId id = getAutofillId(view);
final AutofillValue value = view.getAutofillValue();
startSession(id, view.getWindowToken(), null, value, FLAG_MANUAL_REQUEST);
}
/**
* Explicitly requests a new autofill context for virtual views.
*
* <p>Normally, the autofill context is automatically started when autofillable views are
* focused, but this method should be used in the cases where it must be explicitly requested,
* like a virtual view that provides a contextual menu allowing users to autofill the activity.
*
* @param view the {@link View} whose descendant is the virtual view.
* @param childId id identifying the virtual child inside the view.
* @param bounds child boundaries, relative to the top window.
*/
public void requestAutofill(@NonNull View view, int childId, @NonNull Rect bounds) {
ensureServiceClientAddedIfNeeded();
if (!mEnabled) {
return;
}
final AutofillId id = getAutofillId(view, childId);
startSession(id, view.getWindowToken(), bounds, null, FLAG_MANUAL_REQUEST);
}
/**
* Called when a {@link View} that supports autofill is entered.
*
* @param view {@link View} that was entered.
*/
public void notifyViewEntered(@NonNull View view) {
ensureServiceClientAddedIfNeeded();
if (!mEnabled) {
if (mCallback != null) {
mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
}
return;
}
final AutofillId id = getAutofillId(view);
final AutofillValue value = view.getAutofillValue();
if (!mHasSession) {
// Starts new session.
startSession(id, view.getWindowToken(), null, value, 0);
} else {
// Update focus on existing session.
updateSession(id, null, value, FLAG_VIEW_ENTERED);
}
}
/**
* Called when a {@link View} that supports autofill is exited.
*
* @param view {@link View} that was exited.
*/
public void notifyViewExited(@NonNull View view) {
ensureServiceClientAddedIfNeeded();
if (mEnabled && mHasSession) {
final AutofillId id = getAutofillId(view);
// Update focus on existing session.
updateSession(id, null, null, FLAG_VIEW_EXITED);
}
}
/**
* Called when a virtual view that supports autofill is entered.
*
* @param view the {@link View} whose descendant is the virtual view.
* @param childId id identifying the virtual child inside the view.
* @param bounds child boundaries, relative to the top window.
*/
public void notifyViewEntered(@NonNull View view, int childId, @NonNull Rect bounds) {
ensureServiceClientAddedIfNeeded();
if (!mEnabled) {
if (mCallback != null) {
mCallback.onAutofillEvent(view, childId, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
}
return;
}
final AutofillId id = getAutofillId(view, childId);
if (!mHasSession) {
// Starts new session.
startSession(id, view.getWindowToken(), bounds, null, 0);
} else {
// Update focus on existing session.
updateSession(id, bounds, null, FLAG_VIEW_ENTERED);
}
}
/**
* Called when a virtual view that supports autofill is exited.
*
* @param view the {@link View} whose descendant is the virtual view.
* @param childId id identifying the virtual child inside the view.
*/
public void notifyViewExited(@NonNull View view, int childId) {
ensureServiceClientAddedIfNeeded();
if (mEnabled && mHasSession) {
final AutofillId id = getAutofillId(view, childId);
// Update focus on existing session.
updateSession(id, null, null, FLAG_VIEW_EXITED);
}
}
/**
* Called to indicate the value of an autofillable {@link View} changed.
*
* @param view view whose value changed.
*/
public void notifyValueChanged(View view) {
if (!mEnabled || !mHasSession) {
return;
}
final AutofillId id = getAutofillId(view);
final AutofillValue value = view.getAutofillValue();
updateSession(id, null, value, FLAG_VALUE_CHANGED);
}
/**
* Called to indicate the value of an autofillable virtual {@link View} changed.
*
* @param view the {@link View} whose descendant is the virtual view.
* @param childId id identifying the virtual child inside the parent view.
* @param value new value of the child.
*/
public void notifyValueChanged(View view, int childId, AutofillValue value) {
if (!mEnabled || !mHasSession) {
return;
}
final AutofillId id = getAutofillId(view, childId);
updateSession(id, null, value, FLAG_VALUE_CHANGED);
}
/**
* Called to indicate the current autofill context should be commited.
*
* <p>For example, when a virtual view is rendering an {@code HTML} page with a form, it should
* call this method after the form is submitted and another page is rendered.
*/
public void commit() {
if (!mEnabled && !mHasSession) {
return;
}
finishSession();
}
/**
* Called to indicate the current autofill context should be cancelled.
*
* <p>For example, when a virtual view is rendering an {@code HTML} page with a form, it should
* call this method if the user does not post the form but moves to another form in this page.
*/
public void cancel() {
if (!mEnabled && !mHasSession) {
return;
}
cancelSession();
}
private AutofillClient getClient() {
if (mContext instanceof AutofillClient) {
return (AutofillClient) mContext;
}
return null;
}
/** @hide */
public void onAuthenticationResult(Intent data) {
// TODO(b/33197203): the result code is being ignored, so this method is not reliably
// handling the cases where it's not RESULT_OK: it works fine if the service does not
// set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the
// service set the extra and returned RESULT_CANCELED...
if (DEBUG) Log.d(TAG, "onAuthenticationResult(): d=" + data);
if (data == null) {
return;
}
final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
try {
mService.setAuthenticationResult(responseData,
mContext.getActivityToken(), mContext.getUserId());
} catch (RemoteException e) {
Log.e(TAG, "Error delivering authentication result", e);
}
}
private static AutofillId getAutofillId(View view) {
return new AutofillId(view.getAccessibilityViewId());
}
private static AutofillId getAutofillId(View parent, int childId) {
return new AutofillId(parent.getAccessibilityViewId(), childId);
}
private void startSession(@NonNull AutofillId id, @NonNull IBinder windowToken,
@NonNull Rect bounds, @NonNull AutofillValue value, int flags) {
if (DEBUG) {
Log.d(TAG, "startSession(): id=" + id + ", bounds=" + bounds + ", value=" + value
+ ", flags=" + flags);
}
try {
mService.startSession(mContext.getActivityToken(), windowToken,
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
mCallback != null, flags, mContext.getOpPackageName());
AutofillClient client = getClient();
if (client != null) {
client.autofillCallbackResetableStateAvailable();
}
mHasSession = true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void finishSession() {
if (DEBUG) {
Log.d(TAG, "finishSession()");
}
mHasSession = false;
try {
mService.finishSession(mContext.getActivityToken(), mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void cancelSession() {
if (DEBUG) {
Log.d(TAG, "cancelSession()");
}
mHasSession = false;
try {
mService.cancelSession(mContext.getActivityToken(), mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void updateSession(AutofillId id, Rect bounds, AutofillValue value, int flags) {
if (DEBUG) {
if (VERBOSE || (flags & FLAG_VIEW_EXITED) != 0) {
Log.d(TAG, "updateSession(): id=" + id + ", bounds=" + bounds + ", value=" + value
+ ", flags=" + flags);
}
}
try {
mService.updateSession(mContext.getActivityToken(), id, bounds, value, flags,
mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void ensureServiceClientAddedIfNeeded() {
if (getClient() == null) {
return;
}
if (mServiceClient == null) {
mServiceClient = new AutofillManagerClient(this);
try {
mEnabled = mService.addClient(mServiceClient, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
/**
* Registers a {@link AutofillCallback} to receive autofill events.
*
* @param callback callback to receive events.
*/
public void registerCallback(@Nullable AutofillCallback callback) {
if (callback == null) return;
final boolean hadCallback = mCallback != null;
mCallback = callback;
if (mHasSession && !hadCallback) {
try {
mService.setHasCallback(mContext.getActivityToken(), mContext.getUserId(), true);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
/**
* Unregisters a {@link AutofillCallback} to receive autofill events.
*
* @param callback callback to stop receiving events.
*/
public void unregisterCallback(@Nullable AutofillCallback callback) {
if (callback == null || mCallback == null || callback != mCallback) return;
mCallback = null;
if (mHasSession) {
try {
mService.setHasCallback(mContext.getActivityToken(), mContext.getUserId(), false);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
private void requestShowFillUi(IBinder windowToken, AutofillId id, int width, int height,
Rect anchorBounds, IAutofillWindowPresenter presenter) {
final View anchor = findAchorView(windowToken, id);
if (anchor == null) {
return;
}
if (getClient().autofillCallbackRequestShowFillUi(anchor, width, height,
anchorBounds, presenter) && mCallback != null) {
if (id.isVirtual()) {
mCallback.onAutofillEvent(anchor, id.getVirtualChildId(),
AutofillCallback.EVENT_INPUT_SHOWN);
} else {
mCallback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_SHOWN);
}
}
}
private void handleAutofill(IBinder windowToken, List<AutofillId> ids,
List<AutofillValue> values) {
final View root = WindowManagerGlobal.getInstance().getWindowView(windowToken);
if (root == null) {
return;
}
final int itemCount = ids.size();
int numApplied = 0;
ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
for (int i = 0; i < itemCount; i++) {
final AutofillId id = ids.get(i);
final AutofillValue value = values.get(i);
final int viewId = id.getViewId();
final View view = root.findViewByAccessibilityIdTraversal(viewId);
if (view == null) {
Log.w(TAG, "autofill(): no View with id " + viewId);
continue;
}
if (id.isVirtual()) {
if (virtualValues == null) {
// Most likely there will be just one view with virtual children.
virtualValues = new ArrayMap<>(1);
}
SparseArray<AutofillValue> valuesByParent = virtualValues.get(view);
if (valuesByParent == null) {
// We don't know the size yet, but usually it will be just a few fields...
valuesByParent = new SparseArray<>(5);
virtualValues.put(view, valuesByParent);
}
valuesByParent.put(id.getVirtualChildId(), value);
} else {
if (view.autofill(value)) {
numApplied++;
}
}
}
if (virtualValues != null) {
for (int i = 0; i < virtualValues.size(); i++) {
final View parent = virtualValues.keyAt(i);
final SparseArray<AutofillValue> childrenValues = virtualValues.valueAt(i);
if (parent.autofill(childrenValues)) {
numApplied += childrenValues.size();
}
}
}
final LogMaker log = new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_DATASET_APPLIED);
log.addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount);
log.addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied);
mMetricsLogger.write(log);
}
private void requestHideFillUi(IBinder windowToken, AutofillId id) {
if (getClient().autofillCallbackRequestHideFillUi() && mCallback != null) {
final View anchor = findAchorView(windowToken, id);
if (id.isVirtual()) {
mCallback.onAutofillEvent(anchor, id.getVirtualChildId(),
AutofillCallback.EVENT_INPUT_HIDDEN);
} else {
mCallback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_HIDDEN);
}
}
}
private void notifyNoFillUi(IBinder windowToken, AutofillId id) {
if (mCallback != null) {
final View anchor = findAchorView(windowToken, id);
if (id.isVirtual()) {
mCallback.onAutofillEvent(anchor, id.getVirtualChildId(),
AutofillCallback.EVENT_INPUT_UNAVAILABLE);
} else {
mCallback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
}
}
}
private View findAchorView(IBinder windowToken, AutofillId id) {
final View root = WindowManagerGlobal.getInstance().getWindowView(windowToken);
if (root == null) {
Log.w(TAG, "no window with token " + windowToken);
return null;
}
final View view = root.findViewByAccessibilityIdTraversal(id.getViewId());
if (view == null) {
Log.w(TAG, "no view with id " + id);
return null;
}
return view;
}
/**
* Callback for auto-fill related events.
*
* <p>Typically used for applications that display their own "auto-complete" views, so they can
* enable / disable such views when the auto-fill UI affordance is shown / hidden.
*/
public abstract static class AutofillCallback {
/** @hide */
@IntDef({EVENT_INPUT_SHOWN, EVENT_INPUT_HIDDEN})
@Retention(RetentionPolicy.SOURCE)
public @interface AutofillEventType {}
/**
* The auto-fill input UI affordance associated with the view was shown.
*
* <p>If the view provides its own auto-complete UI affordance and its currently shown, it
* should be hidden upon receiving this event.
*/
public static final int EVENT_INPUT_SHOWN = 1;
/**
* The auto-fill input UI affordance associated with the view was hidden.
*
* <p>If the view provides its own auto-complete UI affordance that was hidden upon a
* {@link #EVENT_INPUT_SHOWN} event, it could be shown again now.
*/
public static final int EVENT_INPUT_HIDDEN = 2;
/**
* The auto-fill input UI affordance associated with the view won't be shown because
* autofill is not available.
*
* <p>If the view provides its own auto-complete UI affordance but was not displaying it
* to avoid flickering, it could shown it upon receiving this event.
*/
public static final int EVENT_INPUT_UNAVAILABLE = 3;
/**
* Called after a change in the autofill state associated with a view.
*
* @param view view associated with the change.
*
* @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}.
*/
public void onAutofillEvent(@NonNull View view, @AutofillEventType int event) {
}
/**
* Called after a change in the autofill state associated with a virtual view.
*
* @param view parent view associated with the change.
* @param childId id identifying the virtual child inside the parent view.
*
* @param event currently either {@link #EVENT_INPUT_SHOWN} or {@link #EVENT_INPUT_HIDDEN}.
*/
public void onAutofillEvent(@NonNull View view, int childId, @AutofillEventType int event) {
}
}
private static final class AutofillManagerClient extends IAutoFillManagerClient.Stub {
private final WeakReference<AutofillManager> mAfm;
AutofillManagerClient(AutofillManager autofillManager) {
mAfm = new WeakReference<>(autofillManager);
}
@Override
public void setState(boolean enabled) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(() -> afm.mEnabled = enabled);
}
}
@Override
public void autofill(IBinder windowToken, List<AutofillId> ids,
List<AutofillValue> values) {
// TODO(b/33197203): must keep the dataset so subsequent calls pass the same
// dataset.extras to service
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(() ->
afm.handleAutofill(windowToken, ids, values));
}
}
@Override
public void authenticate(IntentSender intent, Intent fillInIntent) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(() -> {
if (afm.getClient() != null) {
afm.getClient().autofillCallbackAuthenticate(intent, fillInIntent);
}
});
}
}
@Override
public void requestShowFillUi(IBinder windowToken, AutofillId id,
int width, int height, Rect anchorBounds, IAutofillWindowPresenter presenter) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(() -> {
if (afm.getClient() != null) {
afm.requestShowFillUi(windowToken, id, width,
height, anchorBounds, presenter);
}
});
}
}
@Override
public void requestHideFillUi(IBinder windowToken, AutofillId id) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(() -> {
if (afm.getClient() != null) {
afm.requestHideFillUi(windowToken, id);
}
});
}
}
@Override
public void notifyNoFillUi(IBinder windowToken, AutofillId id) {
final AutofillManager afm = mAfm.get();
if (afm != null) {
afm.mContext.getMainThreadHandler().post(() -> {
if (afm.getClient() != null) {
afm.notifyNoFillUi(windowToken, id);
}
});
}
}
}
}