core/java/android/os/storage/IObbActionListener.aidl \
core/java/android/security/IKeystoreService.aidl \
core/java/android/security/keymaster/IKeyAttestationApplicationIdProvider.aidl \
+ core/java/android/service/autofill/IAutoFillCallback.aidl \
core/java/android/service/autofill/IAutoFillManagerService.aidl \
core/java/android/service/autofill/IAutoFillService.aidl \
core/java/android/service/carrier/ICarrierService.aidl \
public static class AssistStructure.ViewNode {
method public float getAlpha();
+ method public int getAutoFillId();
method public android.app.assist.AssistStructure.ViewNode getChildAt(int);
method public int getChildCount();
method public java.lang.String getClassName();
public abstract class AutoFillService extends android.app.Service {
ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
- method public void onNewSession(java.lang.String, android.os.Bundle, int, android.app.assist.AssistStructure);
+ method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.CancellationSignal, android.service.autofill.FillCallback);
method public void onReady();
- method public void onSessionFinished(java.lang.String);
method public void onShutdown();
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
}
+ public final class FillCallback {
+ method public void onFailure(java.lang.CharSequence);
+ method public void onSuccess(android.service.autofill.FillCallback.FillData);
+ }
+
+ public static final class FillCallback.FillData {
+ }
+
+ public static class FillCallback.FillData.Builder {
+ ctor public FillCallback.FillData.Builder();
+ method public android.service.autofill.FillCallback.FillData build();
+ method public android.service.autofill.FillCallback.FillData.Builder setTextField(int, java.lang.String);
+ }
+
}
package android.service.carrier {
public static class AssistStructure.ViewNode {
method public float getAlpha();
+ method public int getAutoFillId();
method public android.app.assist.AssistStructure.ViewNode getChildAt(int);
method public int getChildCount();
method public java.lang.String getClassName();
public abstract class AutoFillService extends android.app.Service {
ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
- method public void onNewSession(java.lang.String, android.os.Bundle, int, android.app.assist.AssistStructure);
+ method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.CancellationSignal, android.service.autofill.FillCallback);
method public void onReady();
- method public void onSessionFinished(java.lang.String);
method public void onShutdown();
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
}
+ public final class FillCallback {
+ method public void onFailure(java.lang.CharSequence);
+ method public void onSuccess(android.service.autofill.FillCallback.FillData);
+ }
+
+ public static final class FillCallback.FillData {
+ }
+
+ public static class FillCallback.FillData.Builder {
+ ctor public FillCallback.FillData.Builder();
+ method public android.service.autofill.FillCallback.FillData build();
+ method public android.service.autofill.FillCallback.FillData.Builder setTextField(int, java.lang.String);
+ }
+
}
package android.service.carrier {
public static class AssistStructure.ViewNode {
method public float getAlpha();
+ method public int getAutoFillId();
method public android.app.assist.AssistStructure.ViewNode getChildAt(int);
method public int getChildCount();
method public java.lang.String getClassName();
public abstract class AutoFillService extends android.app.Service {
ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
- method public void onNewSession(java.lang.String, android.os.Bundle, int, android.app.assist.AssistStructure);
+ method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.CancellationSignal, android.service.autofill.FillCallback);
method public void onReady();
- method public void onSessionFinished(java.lang.String);
method public void onShutdown();
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
}
+ public final class FillCallback {
+ method public void onFailure(java.lang.CharSequence);
+ method public void onSuccess(android.service.autofill.FillCallback.FillData);
+ }
+
+ public static final class FillCallback.FillData {
+ }
+
+ public static class FillCallback.FillData.Builder {
+ ctor public FillCallback.FillData.Builder();
+ method public android.service.autofill.FillCallback.FillData build();
+ method public android.service.autofill.FillCallback.FillData.Builder setTextField(int, java.lang.String);
+ }
+
}
package android.service.carrier {
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
-import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.media.session.MediaController;
import android.net.Uri;
import android.os.StrictMode;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.service.autofill.FillableInputField;
+import android.service.autofill.AutoFillService;
+import android.service.autofill.IAutoFillCallback;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.ContextThemeWrapper;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
-import android.view.InputDevice;
-import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
import android.view.KeyboardShortcutInfo;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
+import android.widget.EditText;
import android.widget.Toast;
import android.widget.Toolbar;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.app.ToolbarActionBar;
import com.android.internal.app.WindowDecorActionBar;
private boolean mReleased;
private boolean mUpdated;
}
- private final ArrayList<ManagedCursor> mManagedCursors =
- new ArrayList<ManagedCursor>();
- // protected by synchronized (this)
+ @GuardedBy("mManagedCursors")
+ private final ArrayList<ManagedCursor> mManagedCursors = new ArrayList<>();
+
+ @GuardedBy("this")
int mResultCode = RESULT_CANCELED;
+ @GuardedBy("this")
Intent mResultData = null;
private TranslucentConversionListener mTranslucentCallback;
private boolean mHasCurrentPermissionsRequest;
private boolean mEatKeyUpEvent;
+ @GuardedBy("this")
+ private IAutoFillCallback mAutoFillCallback;
+
private static native String getDlWarning();
/** Return the intent that started this activity. */
}
/**
+ * Lazily gets the {@code IAutoFillCallback} for this activitity.
+ *
+ * <p>This callback is used by the {@link AutoFillService} app to auto-fill the activity fields.
+ */
+ IAutoFillCallback getAutoFillCallback() {
+ synchronized (this) {
+ if (mAutoFillCallback == null) {
+ mAutoFillCallback = new IAutoFillCallback.Stub() {
+ @Override
+ public void autofill(@SuppressWarnings("rawtypes") List fields)
+ throws RemoteException {
+ runOnUiThread(() -> {
+ final View root = getWindow().getDecorView().getRootView();
+ for (Object field : fields) {
+ if (!(field instanceof FillableInputField)) {
+ Slog.w(TAG, "autofill(): invalid type " + field.getClass());
+ continue;
+ }
+ FillableInputField autoFillField = (FillableInputField) field;
+ final int viewId = autoFillField.getId();
+ final View view = root.findViewByAccessibilityIdTraversal(viewId);
+ // TODO: should handle other types of view as well, but that will
+ // require:
+ // - a new interface like AutoFillable
+ // - a way for the views to define the type of the autofield value
+ if ((view instanceof EditText)) {
+ ((EditText) view).setText(autoFillField.getValue());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void showError(String message) {
+ runOnUiThread(() -> {
+ // TODO: temporary show a toast until it uses the Snack bar.
+ Toast.makeText(Activity.this, "Auto-fill request failed: " + message,
+ Toast.LENGTH_LONG).show();
+ });
+ }
+ };
+ }
+ }
+ return mAutoFillCallback;
+ }
+
+ /**
* Request the Keyboard Shortcuts screen to show up. This will trigger
* {@link #onProvideKeyboardShortcuts} to retrieve the shortcuts for the foreground activity.
*/
getWindow().peekDecorView().getViewRootImpl().dump(prefix, fd, writer, args);
}
+ if (mAutoFillCallback != null) {
+ writer.print(prefix); writer.print("mAutoFillCallback: " );
+ writer.println(mAutoFillCallback);
+ }
+
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
}
/** @hide requestType for assist context: generate full AssistStructure. */
public static final int ASSIST_CONTEXT_FULL = 1;
+ /** @hide requestType for assist context: generate full AssistStructure for auto-fill. */
+ public static final int ASSIST_CONTEXT_AUTOFILL = 2;
+
/** @hide Flag for registerUidObserver: report changes in process state. */
public static final int UID_OBSERVER_PROCSTATE = 1<<0;
static public void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
ActivityManager.noteAlarmFinish(ps, sourceUid, tag);
}
-}
\ No newline at end of file
+}
import android.provider.Settings;
import android.security.NetworkSecurityPolicy;
import android.security.net.config.NetworkSecurityConfigProvider;
+import android.service.autofill.IAutoFillCallback;
+import android.service.voice.VoiceInteractionSession;
import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
mLastAssistStructures.remove(i);
}
}
+ // Filling for auto-fill has a few differences:
+ // - it does not need an AssistContent
+ // - it does not call onProvideAssistData()
+ // - it needs an IAutoFillCallback
+ boolean forAutofill = cmd.requestType == ActivityManager.ASSIST_CONTEXT_AUTOFILL;
+
Bundle data = new Bundle();
AssistStructure structure = null;
- AssistContent content = new AssistContent();
+ AssistContent content = forAutofill ? null : new AssistContent();
ActivityClientRecord r = mActivities.get(cmd.activityToken);
Uri referrer = null;
if (r != null) {
r.activity.getApplication().dispatchOnProvideAssistData(r.activity, data);
- r.activity.onProvideAssistData(data);
+ if (!forAutofill) {
+ r.activity.onProvideAssistData(data);
+ }
referrer = r.activity.onProvideReferrer();
- if (cmd.requestType == ActivityManager.ASSIST_CONTEXT_FULL) {
+ if (cmd.requestType == ActivityManager.ASSIST_CONTEXT_FULL || forAutofill) {
structure = new AssistStructure(r.activity);
Intent activityIntent = r.activity.getIntent();
if (activityIntent != null && (r.window == null ||
intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION));
intent.removeUnsafeExtras();
- content.setDefaultIntent(intent);
+ if (forAutofill) {
+ IAutoFillCallback autoFillCallback = r.activity.getAutoFillCallback();
+ data.putBinder(VoiceInteractionSession.KEY_AUTO_FILL_CALLBACK,
+ autoFillCallback.asBinder());
+ } else {
+ content.setDefaultIntent(intent);
+ }
} else {
- content.setDefaultIntent(new Intent());
+ if (!forAutofill) {
+ content.setDefaultIntent(new Intent());
+ }
+ }
+ if (!forAutofill) {
+ r.activity.onProvideAssistContent(content);
}
- r.activity.onProvideAssistContent(content);
}
}
if (structure == null) {
void moveStackToDisplay(int stackId, int displayId) = 403;
void enterPictureInPictureModeWithAspectRatio(in IBinder token, float aspectRatio) = 404;
void setPictureInPictureAspectRatio(in IBinder token, float aspectRatio) = 405;
+ boolean requestAutoFillData(in IResultReceiver receiver, in Bundle receiverExtras,
+ in IBinder activityToken) = 406;
// Please keep these transaction codes the same -- they are also
// sent by C++ code. when a new method is added, use the next available transaction id.
String mIdPackage;
String mIdType;
String mIdEntry;
+ int mAutoFillId = View.NO_ID;
int mX;
int mY;
int mScrollX;
static final int FLAGS_ACTIVATED = 0x00002000;
static final int FLAGS_CONTEXT_CLICKABLE = 0x00004000;
+ static final int FLAGS_HAS_AUTO_FILL_ID = 0x80000000;
static final int FLAGS_HAS_MATRIX = 0x40000000;
static final int FLAGS_HAS_ALPHA = 0x20000000;
static final int FLAGS_HAS_ELEVATION = 0x10000000;
}
}
}
+ if ((flags&FLAGS_HAS_AUTO_FILL_ID) != 0) {
+ mAutoFillId = in.readInt();
+ }
if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
mX = in.readInt();
mY = in.readInt();
if (mId != View.NO_ID) {
flags |= FLAGS_HAS_ID;
}
+ if (mAutoFillId != View.NO_ID) {
+ flags |= FLAGS_HAS_AUTO_FILL_ID;
+ }
if ((mX&~0x7fff) != 0 || (mY&~0x7fff) != 0
|| (mWidth&~0x7fff) != 0 | (mHeight&~0x7fff) != 0) {
flags |= FLAGS_HAS_LARGE_COORDS;
}
}
}
+ if ((flags&FLAGS_HAS_AUTO_FILL_ID) != 0) {
+ out.writeInt(mAutoFillId);
+ }
if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
out.writeInt(mX);
out.writeInt(mY);
}
/**
+ * Returns the id that can be used to auto-fill the view.
+ *
+ * <p>It's only set when the {@link AssistStructure} is used for auto-filling purposes, not
+ * for assist.
+ */
+ public int getAutoFillId() {
+ return mAutoFillId;
+ }
+
+ /**
* Returns the left edge of this view, in pixels, relative to the left edge of its parent.
*/
public int getLeft() {
public Rect getTempRect() {
return mAssist.mTmpRect;
}
+
+ @Override
+ public void setAutoFillId(int autoFillId) {
+ mNode.mAutoFillId = autoFillId;
+ }
}
/** @hide */
*/
package android.service.autofill;
+import android.annotation.IntDef;
import android.annotation.SdkConstant;
+import android.app.Activity;
import android.app.Service;
import android.app.assist.AssistStructure;
import android.content.Intent;
import android.os.Bundle;
+import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
+import android.service.voice.VoiceInteractionSession;
import android.util.Log;
import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.IResultReceiver;
import com.android.internal.os.SomeArgs;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Top-level service of the current auto-fill service for a given user.
+ *
+ * <p>Apps providing auto-fill capabilities must extend this service.
*/
-// TODO: expand documentation
public abstract class AutoFillService extends Service {
- private static final String TAG = "AutoFillService";
- private static final boolean DEBUG = true; // TODO: set to false once stable
+ static final String TAG = "AutoFillService";
+ static final boolean DEBUG = true; // TODO: set to false once stable
/**
* The {@link Intent} that must be declared as handled by the service.
public static final String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
private static final int MSG_READY = 1;
- private static final int MSG_NEW_SESSION = 2;
- private static final int MSG_SESSION_FINISHED = 3;
- private static final int MSG_SHUTDOWN = 4;
+ private static final int MSG_AUTO_FILL = 2;
+ private static final int MSG_SHUTDOWN = 3;
- // TODO: add metadata?
+ private final IResultReceiver mAssistReceiver = new IResultReceiver.Stub() {
+ @Override
+ public void send(int resultCode, Bundle resultData) throws RemoteException {
+ final AssistStructure structure = resultData
+ .getParcelable(VoiceInteractionSession.KEY_STRUCTURE);
+
+ final IBinder binder = resultData
+ .getBinder(VoiceInteractionSession.KEY_AUTO_FILL_CALLBACK);
+
+ mHandlerCaller
+ .obtainMessageOO(MSG_AUTO_FILL, structure, binder).sendToTarget();
+ }
+
+ };
private final IAutoFillService mInterface = new IAutoFillService.Stub() {
@Override
}
@Override
- public void newSession(String token, Bundle data, int flags,
- AssistStructure structure) {
- mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO(MSG_NEW_SESSION,
- flags, token, data, structure));
- }
-
- @Override
- public void finishSession(String token) {
- mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_SESSION_FINISHED, token));
+ public IResultReceiver getAssistReceiver() {
+ return mAssistReceiver;
}
@Override
case MSG_READY: {
onReady();
break;
- } case MSG_NEW_SESSION: {
+ } case MSG_AUTO_FILL: {
final SomeArgs args = (SomeArgs) msg.obj;
- final int flags = args.argi1;
- final String token = (String) args.arg1;
- final Bundle data = (Bundle) args.arg2;
- final AssistStructure assistStructure = (AssistStructure) args.arg3;
- onNewSession(token, data, flags, assistStructure);
- break;
- } case MSG_SESSION_FINISHED: {
- final String token = (String) msg.obj;
- onSessionFinished(token);
+ final AssistStructure structure = (AssistStructure) args.arg1;
+ final IBinder binder = (IBinder) args.arg2;
+ autoFillActivity(structure, binder);
break;
} case MSG_SHUTDOWN: {
onShutdown();
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return mInterface.asBinder();
}
+ Log.w(TAG, "Tried to bind to wrong intent: " + intent);
return null;
}
* to receive interaction from it.
*
* <p>You should generally do initialization here rather than in {@link #onCreate}.
- *
- * <p>Sub-classes should call it first, since it sets the reference to the sytem-server service.
*/
- // TODO: rename to onConnected() / add onDisconnected()?
+ // TODO: rename to onConnect() / update javadoc
public void onReady() {
if (DEBUG) Log.d(TAG, "onReady()");
}
/**
- * Called to receive data from the application that the user was requested auto-fill for.
+ * Handles an auto-fill request.
*
- * @param token unique token identifying the auto-fill session, it should be used when providing
- * the auto-filled fields.
- * @param data Arbitrary data supplied by the app through
- * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}.
- * May be {@code null} if data has been disabled by the user or device policy.
- * @param startFlags currently always 0.
- * @param structure If available, the structure definition of all windows currently
- * displayed by the app. May be {@code null} if auto-fill data has been disabled by the user
- * or device policy; will be an empty stub if the application has disabled auto-fill
- * by marking its window as secure.
+ * @param structure {@link Activity}'s view structure .
+ * @param cancellationSignal signal for observing cancel requests.
+ * @param callback object used to fulllfill the request.
*/
- @SuppressWarnings("unused")
- // TODO: take the factory approach where this method return a session, and move the callback
- // methods (like autofill()) to the session.
- public void onNewSession(String token, Bundle data, int startFlags, AssistStructure structure) {
- if (DEBUG) Log.d(TAG, "onNewSession(): token=" + token);
- }
+ public abstract void onFillRequest(AssistStructure structure,
+ CancellationSignal cancellationSignal, FillCallback callback);
- /**
- * Called when an auto-fill session is finished.
- */
- @SuppressWarnings("unused")
- public void onSessionFinished(String token) {
- if (DEBUG) Log.d(TAG, "onSessionFinished(): token=" + token);
+ private void autoFillActivity(AssistStructure structure, IBinder binder) {
+ final FillCallback callback = new FillCallback(binder);
+ // TODO: hook up the cancelationSignal
+ onFillRequest(structure, new CancellationSignal(), callback);
}
/**
*
* <p> At this point this service may no longer be an active {@link AutoFillService}.
*/
+ // TODO: rename to onDisconnected() / update javadoc
public void onShutdown() {
if (DEBUG) Log.d(TAG, "onShutdown()");
}
+
}
--- /dev/null
+/*
+ * Copyright (C) 2016 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.service.autofill;
+
+import static android.service.autofill.AutoFillService.DEBUG;
+import static android.service.autofill.AutoFillService.TAG;
+
+import android.app.Activity;
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Handles auto-fill requests from the {@link AutoFillService} into the {@link Activity} being
+ * auto-filled.
+ */
+public final class FillCallback {
+
+ private final IAutoFillCallback mCallback;
+
+ /** @hide */
+ FillCallback(IBinder binder) {
+ mCallback = IAutoFillCallback.Stub.asInterface(binder);
+ }
+
+ /**
+ * Auto-fills the {@link Activity}.
+ *
+ * @throws RuntimeException if an error occurred while auto-filling it.
+ */
+ public void onSuccess(FillData data) {
+ if (DEBUG) Log.d(TAG, "onSuccess(): data=" + data);
+
+ Preconditions.checkArgument(data != null, "data cannot be null");
+
+ try {
+ mCallback.autofill(data.asList());
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Notifies the activity that the auto-fill request failed.
+ */
+ public void onFailure(CharSequence message) {
+ if (DEBUG) Log.d(TAG, "onFailure(): message=" + message);
+
+ Preconditions.checkArgument(message != null, "message cannot be null");
+
+ try {
+ mCallback.showError(message.toString());
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Data used to fill the fields of an {@link Activity}.
+ *
+ * <p>This class is immutable.
+ */
+ public static final class FillData {
+
+ private final List<FillableInputField> mList;
+
+ private FillData(Builder builder) {
+ final int size = builder.mFields.size();
+ final List<FillableInputField> list = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ list.add(builder.mFields.valueAt(i));
+ }
+ mList = Collections.unmodifiableList(list);
+ // TODO: use FastImmutableArraySet or a similar structure instead?
+ }
+
+ /**
+ * Gets the response as a {@code List} so it can be used in a binder call.
+ */
+ List<FillableInputField> asList() {
+ return mList;
+ }
+
+ @Override
+ public String toString() {
+ return "[AutoFillResponse: " + mList + "]";
+ }
+
+ /**
+ * Builder for {@link FillData} objects.
+ *
+ * <p>Typical usage:
+ *
+ * <pre class="prettyprint">
+ * FillCallback.FillData data = new FillCallback.FillData.Builder()
+ * .setTextField(id1, "value 1")
+ * .setTextField(id2, "value 2")
+ * .build()
+ * </pre>
+ */
+ public static class Builder {
+ private final SparseArray<FillableInputField> mFields = new SparseArray<>();
+
+ /**
+ * Auto-fills a text field.
+ *
+ * @param id view id as returned by {@link ViewNode#getAutoFillId()}.
+ * @param text text to be auto-filled.
+ * @return same builder so it can be chained.
+ */
+ public Builder setTextField(int id, String text) {
+ mFields.put(id, FillableInputField.forText(id, text));
+ return this;
+ }
+
+ /**
+ * Builds a new {@link FillData} instance.
+ */
+ public FillData build() {
+ return new FillData(this);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 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.service.autofill;
+
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a view field that can be auto-filled.
+ *
+ * <p>Currently only text-fields are supported, so the value of the field can be obtained through
+ * {@link #getValue()}.
+ *
+ * @hide
+ */
+public final class FillableInputField implements Parcelable {
+
+ private final int mId;
+ private final String mValue;
+
+ private FillableInputField(int id, String value) {
+ mId = id;
+ mValue = value;
+ }
+
+ private FillableInputField(Parcel parcel) {
+ mId = parcel.readInt();
+ mValue = parcel.readString();
+ }
+
+ /**
+ * Gets the view id as returned by {@link ViewNode#getAutoFillId()}.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets the value of this field.
+ */
+ public String getValue() {
+ return mValue;
+
+ }
+
+ @Override
+ public String toString() {
+ return "[AutoFillField: " + mId + "=" + mValue + "]";
+ }
+
+ /**
+ * Creates an {@code AutoFillField} for a text field.
+ *
+ * @param id view id as returned by {@link ViewNode#getAutoFillId()}.
+ * @param text value to be auto-filled.
+ */
+ public static FillableInputField forText(int id, String text) {
+ return new FillableInputField(id, text);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mId);
+ parcel.writeString(mValue);
+ }
+
+ public static final Parcelable.Creator<FillableInputField> CREATOR =
+ new Parcelable.Creator<FillableInputField>() {
+ @Override
+ public FillableInputField createFromParcel(Parcel source) {
+ return new FillableInputField(source);
+ }
+
+ @Override
+ public FillableInputField[] newArray(int size) {
+ return new FillableInputField[size];
+ }
+ };
+}
--- /dev/null
+/*
+ * Copyright (C) 2016 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.service.autofill;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+interface IAutoFillCallback {
+ void autofill(in List values);
+ void showError(String message);
+}
import android.os.Bundle;
/**
- * Intermediator between apps being auto-filled and auto-fill service implementations.
+ * Mediator between apps being auto-filled and auto-fill service implementations.
*
* {@hide}
*/
interface IAutoFillManagerService {
/**
- * Starts an auto-fill session for the top activities for a given user.
- *
- * It's used to start a new session from system affordances.
+ * Request auto-fill on the top activity of a given user.
*
* @param userId user handle.
- * @param args the bundle to pass as arguments to the voice interaction session.
- * @param flags flags indicating optional session behavior.
* @param activityToken optional token of activity that needs to be on top.
*
- * @return session token, or null if session was not created (for example, if the activity's
- * user does not have an auto-fill service associated with).
- */
- // TODO: pass callback providing an onAutoFill() method
- String startSession(int userId, in Bundle args, int flags, IBinder activityToken);
-
- /**
- * Finishes an auto-fill session.
- *
- * @param userId user handle.
- * @param token session token.
- *
- * @return true if session existed and was finished.
+ * @return whether the request succeeded (for example, if the activity's
+ * user does not have an auto-fill service associated with, it will return false).
*/
- boolean finishSession(int userId, String token);
-
+ boolean requestAutoFill(int userId, IBinder activityToken);
}
package android.service.autofill;
-import android.os.Bundle;
import android.app.assist.AssistStructure;
+import android.os.Bundle;
+import android.service.autofill.IAutoFillCallback;
+import com.android.internal.os.IResultReceiver;
/**
* @hide
*/
-oneway interface IAutoFillService {
+interface IAutoFillService {
+ // TODO: rename to onConnected() / onDisconnected()
void ready();
- void newSession(String token, in Bundle data, int flags, in AssistStructure structure);
- void finishSession(String token);
void shutdown();
+ IResultReceiver getAssistReceiver();
}
public static final String KEY_CONTENT = "content";
/** @hide */
public static final String KEY_RECEIVER_EXTRAS = "receiverExtras";
+ /** @hide */
+ public static final String KEY_AUTO_FILL_CALLBACK = "autoFillCallback";
final Context mContext;
final HandlerCaller mHandlerCaller;
} else {
structure.setId(id, null, null, null);
}
+
+ // The auto-fill id needs to be unique, but its value doesn't matter, so it's better to
+ // reuse the accessibility id to save space.
+ structure.setAutoFillId(getAccessibilityViewId());
+
structure.setDimens(mLeft, mTop, mScrollX, mScrollY, mRight - mLeft, mBottom - mTop);
if (!hasIdentityMatrix()) {
structure.setTransformation(getMatrix());
/** @hide */
public abstract Rect getTempRect();
+
+ /** @hide */
+ public abstract void setAutoFillId(int autoFillId);
}
final class AutoFillManagerServiceStub extends IAutoFillManagerService.Stub {
@Override
- public String startSession(int userId, Bundle args, int flags, IBinder activityToken) {
+ public boolean requestAutoFill(int userId, IBinder activityToken) {
mContext.enforceCallingPermission(MANAGE_AUTO_FILL, TAG);
synchronized (mLock) {
- return getImplOrThrowLocked(userId).startSession(args, flags, activityToken);
- }
- }
-
- @Override
- public boolean finishSession(int userId, String token) {
- mContext.enforceCallingPermission(MANAGE_AUTO_FILL, TAG);
-
- synchronized (mLock) {
- return getImplOrThrowLocked(userId).finishSessionLocked(token);
+ return getImplOrThrowLocked(userId).requestAutoFill(activityToken);
}
}
+ ", uid=" + Binder.getCallingUid());
return;
}
- if (args.length > 0) {
- if ("--sessions".equals(args[0])) {
- dumpSessions(pw);
- return;
- }
- }
synchronized (mLock) {
pw.print("mEnableService: "); pw.println(mEnableService);
pw.print("mSafeMode: "); pw.println(mSafeMode);
}
}
- private void dumpSessions(PrintWriter pw) {
- boolean foundOne = false;
- synchronized (mLock) {
- final int size = mImplByUser.size();
- for (int i = 0; i < size; i++) {
- final AutoFillManagerServiceImpl impl = mImplByUser.valueAt(i);
- foundOne |= impl.dumpSessionsLocked("", pw);
- }
- }
- if (!foundOne) {
- pw.println("No active sessions");
- }
- }
-
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
package com.android.server.autofill;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.app.IActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
+import android.icu.text.DateFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import com.android.server.autofill.AutoFillManagerService.AutoFillManagerServiceStub;
import java.io.PrintWriter;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
-import java.util.Map;
-import java.util.UUID;
/**
* Bridge between the {@code system_server}'s {@link AutoFillManagerService} and the
* app's {@link IAutoFillService} implementation.
*
- * <p>It keeps a list of auto-fill sessions for a specifc user.
*/
final class AutoFillManagerServiceImpl {
final ComponentName mComponent;
private final Context mContext;
+ private final IActivityManager mAm;
private final Object mLock;
private final AutoFillManagerServiceStub mServiceStub;
private final AutoFillServiceInfo mInfo;
- // Map of sessions keyed by session tokens.
- @GuardedBy("mLock")
- private Map<String, AutoFillSession> mSessions = new HashMap<>();
+ // TODO: improve its usage
+ // - set maximum number of entries
+ // - disable on low-memory devices.
+ private final List<String> mRequestHistory = new ArrayList<>();
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
final String reason = intent.getStringExtra("reason");
if (DEBUG) Slog.d(TAG, "close system dialogs: " + reason);
- // TODO: close any pending UI like account selection
+ // TODO: close any pending UI like account selection (or remove this receiver)
}
}
};
mServiceStub = stub;
mUser = user;
mComponent = component;
+ mAm = ActivityManager.getService();
- AutoFillServiceInfo info;
+ final AutoFillServiceInfo info;
try {
info = new AutoFillServiceInfo(component, mUser);
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) Slog.d(TAG, "Bound to " + mComponent);
}
- String startSession(Bundle args, int flags, IBinder activityToken) {
-
+ boolean requestAutoFill(IBinder activityToken) {
if (!mBound) {
// TODO: should it bind on demand? Or perhaps always run when on on low-memory?
- Slog.w(TAG, "startSession() failed because it's not bound to service");
- return null;
+ Slog.w(TAG, "requestAutoFill() failed because it's not bound to service");
+ return false;
}
- // TODO: session should have activity ids, so same session is reused when called again
- // for the same activity.
-
// TODO: activityToken should probably not be null, but we need to wait until the UI is
- // triggering the call (for now it's trough 'adb shell cmd autofill start session'
+ // triggering the call (for now it's trough 'adb shell cmd autofill request'
if (activityToken == null) {
// Let's get top activities from all visible stacks.
Slog.d(TAG, "Top activities (" + topActivities.size() + "): " + topActivities);
if (topActivities.isEmpty()) {
Slog.w(TAG, "Could not get top activity");
- return null;
+ return false;
}
activityToken = topActivities.get(0);
}
synchronized (mLock) {
- return startSessionLocked(args, flags, activityToken);
+ return requestAutoFillLocked(activityToken);
}
}
- // TODO: remove args and flags if not needed?
- private String startSessionLocked(Bundle args, int flags, IBinder activityToken) {
-
- final String sessionToken = UUID.randomUUID().toString();
-
- if (DEBUG) Slog.d(TAG, "Starting session for user " + mUser
- + ": sessionToken=" + sessionToken + ", activityToken=" + activityToken);
+ private boolean requestAutoFillLocked(IBinder activityToken) {
+ mRequestHistory.add(
+ DateFormat.getDateTimeInstance().format(new Date()) + " - " + activityToken);
+ if (DEBUG) Slog.d(TAG, "Requesting for user " + mUser + " and activity " + activityToken);
- final AutoFillSession session =
- new AutoFillSession(mService, mLock, sessionToken, activityToken);
- session.startLocked();
- mSessions.put(sessionToken, session);
-
- return sessionToken;
- }
+ // Sanity check
+ if (mService == null) {
+ Slog.w(TAG, "requestAutoFillLocked(: service is null");
+ return false;
+ }
- // TODO: need a way to automatically call it when the activity is destroyed.
- boolean finishSessionLocked(String token) {
- if (DEBUG) Slog.d(TAG, "Removing session " + token + " for user " + mUser);
- final AutoFillSession session = mSessions.remove(token);
- if (session != null) {
- session.finishLocked();
+ /*
+ * TODO: apply security checks below:
+ * - checks if disabled by secure settings / device policy
+ * - log operation using noteOp()
+ * - check flags
+ * - display disclosure if needed
+ */
+ try {
+ // TODO: add MetricsLogger call
+ if (!mAm.requestAutoFillData(mService.getAssistReceiver(), null, activityToken)) {
+ return false;
+ }
+ } catch (RemoteException e) {
+ // Should happen, it's a local call.
}
- return session != null;
+ return true;
}
void shutdownLocked() {
mInfo.getServiceInfo().dump(new PrintWriterPrinter(pw), prefix + prefix);
}
- if (!dumpSessionsLocked(prefix, pw)) {
- pw.print(prefix); pw.print("No active sessions for user "); pw.println(mUser);
- }
- }
-
- boolean dumpSessionsLocked(String prefix, PrintWriter pw) {
- if (mSessions.isEmpty()) {
- return false;
- }
-
- pw.print(mSessions.size());pw.println(" active sessions:");
- final String sessionPrefix = prefix + prefix;
- for (AutoFillSession session : mSessions.values()) {
- pw.println();
- session.dumpLocked(sessionPrefix, pw);
+ if (mRequestHistory.isEmpty()) {
+ pw.print(prefix); pw.println("No history");
+ } else {
+ pw.print(prefix); pw.println("History:");
+ final String prefix2 = prefix + prefix;
+ for (int i = 0; i < mRequestHistory.size(); i++) {
+ pw.print(prefix2); pw.print(i); pw.print(": "); pw.println(mRequestHistory.get(i));
+ }
}
- return true;
}
@Override
final PrintWriter pw = getOutPrintWriter();
try {
switch (cmd) {
- case "start":
- return runStart(pw);
- case "finish":
- return runFinish(pw);
+ case "request":
+ return requestAutoFill();
default:
return handleDefaultCommands(cmd);
}
pw.println(" help");
pw.println(" Prints this help text.");
pw.println("");
- pw.println(" start session [--user USER_ID]");
- pw.println(" Starts an auto-fill session. "
- + "Prints 'token:SESSION_TOKEN if successful, or error message");
+ pw.println(" request [--user USER_ID]");
+ pw.println(" Request auto-fill on the top activity. ");
pw.println("");
- pw.println(" finish session <TOKEN> [--user USER_ID]");
- pw.println(" Finishes a session with the given TOKEN. "
- + "Prints empty string if successful, or error message.");
- pw.println("");
- }
- }
-
- private int runStart(PrintWriter pw) throws RemoteException {
- final String type = getNextArg();
- if (type == null) {
- pw.println("Error: didn't specify type of data to start");
- return -1;
- }
- switch (type) {
- case "session":
- return startAutoFillSession(pw);
- }
- pw.println("Error: unknown start type '" + type + "'");
- return -1;
- }
-
- private int runFinish(PrintWriter pw) throws RemoteException {
- final String type = getNextArg();
- if (type == null) {
- pw.println("Error: didn't specify type of data to finish");
- return -1;
- }
- switch (type) {
- case "session":
- return finishAutoFillSession(pw);
}
- pw.println("Error: unknown finish type '" + type + "'");
- return -1;
- }
-
- private int startAutoFillSession(PrintWriter pw) throws RemoteException {
- final int userId = getUserIdFromArgs();
- final String token = mService.startSession(userId, null, 0, null);
- pw.print("token:"); pw.println(token);
- return 0;
}
- private int finishAutoFillSession(PrintWriter pw) throws RemoteException {
- final String token = getNextArgRequired();
+ private int requestAutoFill() throws RemoteException {
final int userId = getUserIdFromArgs();
-
- boolean finished = mService.finishSession(userId, token);
- if (!finished) {
- pw.println("No such session");
- return 1;
- }
- return 0;
+ final boolean ok = mService.requestAutoFill(userId, null);
+ return ok ? 0 : 1;
}
private int getUserIdFromArgs() {
+++ /dev/null
-/*
- * Copyright (C) 2016 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 com.android.server.autofill;
-
-import android.app.ActivityManager;
-import android.app.ActivityManagerNative;
-import android.app.IActivityManager;
-import android.app.assist.AssistStructure;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.service.autofill.AutoFillService;
-import android.service.autofill.IAutoFillService;
-import android.service.voice.VoiceInteractionSession;
-import android.util.Slog;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.os.IResultReceiver;
-
-import java.io.PrintWriter;
-
-/**
- * An auto-fill session between the system's {@link AutoFillManagerServiceImpl} and the provider's
- * {@link AutoFillService} implementation.
- */
-final class AutoFillSession {
-
- private static final String TAG = "AutoFillSession";
-
- private static final boolean FOCUSED = true;
- private static final boolean NEW_SESSION_ID = true;
-
- private final IAutoFillService mService;
- private final String mSessionToken;
- private final IBinder mActivityToken;
- private final Object mLock;
- private final IActivityManager mAm;
-
- private final IResultReceiver mAssistReceiver = new IResultReceiver.Stub() {
- @Override
- public void send(int resultCode, Bundle resultData) throws RemoteException {
- synchronized (mLock) {
- mPendingResponse = false;
- mAssistResponse = resultData;
- deliverSessionDataLocked();
- }
- }
- };
-
- // Assist data is filled asynchronously.
- @GuardedBy("mLock")
- private Bundle mAssistResponse;
- @GuardedBy("mLock")
- private boolean mPendingResponse;
-
- AutoFillSession(IAutoFillService service, Object lock, String sessionToken,
- IBinder activityToken) {
- mService = service;
- mSessionToken = sessionToken;
- mActivityToken = activityToken;
- mLock = lock;
- mAm = ActivityManagerNative.getDefault();
- }
-
- void startLocked() {
- /*
- * TODO: apply security checks below:
- * - checks if disabled by secure settings / device policy
- * - log operation using noteOp()
- * - check flags
- * - display disclosure if needed
- */
- mAssistResponse = null;
- mPendingResponse = true;
- try {
- // TODO: add MetricsLogger call
- if (!mAm.requestAssistContextExtras(ActivityManager.ASSIST_CONTEXT_FULL,
- mAssistReceiver, (Bundle) null, mActivityToken, FOCUSED, NEW_SESSION_ID)) {
- mPendingResponse = false;
- Slog.w(TAG, "requestAssistContextExtras() rejected");
- }
- } catch (RemoteException e) {
- // Should happen, it's a local call.
- }
- }
-
- void finishLocked() {
- try {
- mService.finishSession(mSessionToken);
- } catch (RemoteException e) {
- Slog.e(TAG, "auto-fill service failed to finish session " + mSessionToken, e);
- }
- }
-
- private void deliverSessionDataLocked() {
- if (mAssistResponse == null) {
- Slog.w(TAG, "No assist data for session " + mSessionToken);
- return;
- }
-
- final Bundle assistData = mAssistResponse.getBundle(VoiceInteractionSession.KEY_DATA);
- final AssistStructure structure =
- mAssistResponse.getParcelable(VoiceInteractionSession.KEY_STRUCTURE);
- try {
- mService.newSession(mSessionToken, assistData, 0, structure);
- } catch (RemoteException e) {
- Slog.e(TAG, "auto-fill service failed to start session " + mSessionToken, e);
- } finally {
- mPendingResponse = false;
- // We could set mAssistResponse to null here, but we don't so it's shown on dump()
- }
- }
-
- void dumpLocked(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.print("mSessionToken="); pw.println(mSessionToken);
- pw.print(prefix); pw.print("mActivityToken="); pw.println(mActivityToken);
- pw.print(prefix); pw.print("mPendingResponse="); pw.println(mPendingResponse);
- pw.print(prefix); pw.print("mAssistResponse="); pw.println(mAssistResponse);
- }
-
-}
!= null;
}
+ @Override
+ public boolean requestAutoFillData(IResultReceiver receiver, Bundle receiverExtras,
+ IBinder activityToken) {
+ return enqueueAssistContext(ActivityManager.ASSIST_CONTEXT_AUTOFILL, null, null, receiver,
+ receiverExtras, activityToken, true, true,
+ UserHandle.getCallingUserId(), null, PENDING_ASSIST_EXTRAS_LONG_TIMEOUT)
+ != null;
+ }
+
private PendingAssistExtras enqueueAssistContext(int requestType, Intent intent, String hint,
IResultReceiver receiver, Bundle receiverExtras, IBinder activityToken,
boolean focused, boolean newSessionId, int userHandle, Bundle args, long timeout) {
sendBundle.putParcelable(VoiceInteractionSession.KEY_CONTENT, pae.content);
sendBundle.putBundle(VoiceInteractionSession.KEY_RECEIVER_EXTRAS,
pae.receiverExtras);
+ IBinder autoFillCallback =
+ extras.getBinder(VoiceInteractionSession.KEY_AUTO_FILL_CALLBACK);
+ if (autoFillCallback != null) {
+ sendBundle.putBinder(VoiceInteractionSession.KEY_AUTO_FILL_CALLBACK,
+ autoFillCallback);
+ }
}
}
if (sendReceiver != null) {