summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
authorTreeHugger Robot <treehugger-gerrit@google.com>2018-02-05 09:55:29 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2018-02-05 09:55:29 +0000
commit1bb9f29909016aa3be7ebe7ef52558d219696186 (patch)
tree65b95a6eeea6173e8e23cb776653ae4b4496b91f /core/java/android
parent1204834121070afea50fcc9b17a6604fcac4f3a8 (diff)
parent24c90450fe3fe097a7bca51edd6a4cffd8fd13aa (diff)
Merge "Autofill compatibility mode."
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/accessibilityservice/AccessibilityServiceInfo.java20
-rw-r--r--core/java/android/app/Activity.java131
-rw-r--r--core/java/android/app/ActivityThread.java9
-rw-r--r--core/java/android/app/Application.java34
-rw-r--r--core/java/android/app/ContextImpl.java13
-rw-r--r--core/java/android/app/IApplicationThread.aidl2
-rw-r--r--core/java/android/app/assist/AssistStructure.java17
-rw-r--r--core/java/android/content/Context.java13
-rw-r--r--core/java/android/content/ContextWrapper.java16
-rw-r--r--core/java/android/provider/Settings.java11
-rw-r--r--core/java/android/service/autofill/AutofillService.java39
-rw-r--r--core/java/android/service/autofill/AutofillServiceInfo.java180
-rw-r--r--core/java/android/view/View.java91
-rw-r--r--core/java/android/view/ViewGroup.java21
-rw-r--r--core/java/android/view/ViewRootImpl.java87
-rw-r--r--core/java/android/view/accessibility/AccessibilityManager.java154
-rw-r--r--core/java/android/view/autofill/AutofillManager.java422
-rw-r--r--core/java/android/view/autofill/AutofillManagerInternal.java13
-rw-r--r--core/java/android/view/autofill/Helper.java16
19 files changed, 1057 insertions, 232 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index 06a9b0676d08..0b10a35b5d5e 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -16,6 +16,7 @@
package android.accessibilityservice;
+import android.annotation.IntDef;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -43,6 +44,8 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -346,6 +349,19 @@ public class AccessibilityServiceInfo implements Parcelable {
*/
public String[] packageNames;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FEEDBACK_" }, value = {
+ FEEDBACK_AUDIBLE,
+ FEEDBACK_GENERIC,
+ FEEDBACK_HAPTIC,
+ FEEDBACK_SPOKEN,
+ FEEDBACK_VISUAL,
+ FEEDBACK_BRAILLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FeedbackType {}
+
/**
* The feedback type an {@link AccessibilityService} provides.
* <p>
@@ -358,6 +374,7 @@ public class AccessibilityServiceInfo implements Parcelable {
* @see #FEEDBACK_VISUAL
* @see #FEEDBACK_BRAILLE
*/
+ @FeedbackType
public int feedbackType;
/**
@@ -818,7 +835,8 @@ public class AccessibilityServiceInfo implements Parcelable {
return stringBuilder.toString();
}
- private static void appendFeedbackTypes(StringBuilder stringBuilder, int feedbackTypes) {
+ private static void appendFeedbackTypes(StringBuilder stringBuilder,
+ @FeedbackType int feedbackTypes) {
stringBuilder.append("feedbackTypes:");
stringBuilder.append("[");
while (feedbackTypes != 0) {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index d3ee59086f9b..0bc510a13ba6 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -113,6 +113,7 @@ import android.view.Window.WindowControllerCallback;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityEvent;
+import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillManager.AutofillClient;
import android.view.autofill.AutofillPopupWindow;
@@ -125,7 +126,6 @@ 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.policy.DecorView;
import com.android.internal.policy.PhoneWindow;
import dalvik.system.VMRuntime;
@@ -5961,12 +5961,16 @@ public class Activity extends ContextThemeWrapper
*
* @return Returns the complete component name for this activity
*/
- @Override
- public ComponentName getComponentName()
- {
+ public ComponentName getComponentName() {
return mComponent;
}
+ /** @hide */
+ @Override
+ public final ComponentName autofillClientGetComponentName() {
+ return getComponentName();
+ }
+
/**
* Retrieve a {@link SharedPreferences} object for accessing preferences
* that are private to this activity. This simply calls the underlying
@@ -6262,7 +6266,6 @@ public class Activity extends ContextThemeWrapper
*
* @param action the action to run on the UI thread
*/
- @Override
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
@@ -6271,6 +6274,12 @@ public class Activity extends ContextThemeWrapper
}
}
+ /** @hide */
+ @Override
+ public final void autofillClientRunOnUiThread(Runnable action) {
+ runOnUiThread(action);
+ }
+
/**
* Standard implementation of
* {@link android.view.LayoutInflater.Factory#onCreateView} used when
@@ -7076,6 +7085,18 @@ public class Activity extends ContextThemeWrapper
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
+
+ setAutofillCompatibilityEnabled(application.isAutofillCompatibilityEnabled());
+ enableAutofillCompatibilityIfNeeded();
+ }
+
+ private void enableAutofillCompatibilityIfNeeded() {
+ if (isAutofillCompatibilityEnabled()) {
+ final AutofillManager afm = getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.enableCompatibilityMode();
+ }
+ }
}
/** @hide */
@@ -7572,7 +7593,7 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- final public void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
+ public final void autofillClientAuthenticate(int authenticationId, IntentSender intent,
Intent fillInIntent) {
try {
startIntentSenderForResultInner(intent, AUTO_FILL_AUTH_WHO_PREFIX,
@@ -7584,13 +7605,13 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- final public void autofillCallbackResetableStateAvailable() {
+ public final void autofillClientResetableStateAvailable() {
mAutoFillResetNeeded = true;
}
/** @hide */
@Override
- final public boolean autofillCallbackRequestShowFillUi(@NonNull View anchor, int width,
+ public final boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width,
int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter) {
final boolean wasShowing;
@@ -7607,7 +7628,7 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- final public boolean autofillCallbackRequestHideFillUi() {
+ public final boolean autofillClientRequestHideFillUi() {
if (mAutofillPopupWindow == null) {
return false;
}
@@ -7618,8 +7639,16 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- @NonNull public View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds) {
- final View[] views = new View[viewIds.length];
+ public final boolean autofillClientIsFillUiShowing() {
+ return mAutofillPopupWindow != null && mAutofillPopupWindow.isShowing();
+ }
+
+ /** @hide */
+ @Override
+ @NonNull
+ public final View[] autofillClientFindViewsByAutofillIdTraversal(
+ @NonNull AutofillId[] autofillId) {
+ final View[] views = new View[autofillId.length];
final ArrayList<ViewRootImpl> roots =
WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
@@ -7627,10 +7656,11 @@ public class Activity extends ContextThemeWrapper
final View rootView = roots.get(rootNum).getView();
if (rootView != null) {
- for (int viewNum = 0; viewNum < viewIds.length; viewNum++) {
+ final int viewCount = autofillId.length;
+ for (int viewNum = 0; viewNum < viewCount; viewNum++) {
if (views[viewNum] == null) {
views[viewNum] = rootView.findViewByAutofillIdTraversal(
- viewIds[viewNum]);
+ autofillId[viewNum].getViewId());
}
}
}
@@ -7641,14 +7671,15 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- @Nullable public View findViewByAutofillIdTraversal(int viewId) {
+ @Nullable
+ public final View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) {
final ArrayList<ViewRootImpl> roots =
WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
final View rootView = roots.get(rootNum).getView();
if (rootView != null) {
- final View view = rootView.findViewByAutofillIdTraversal(viewId);
+ final View view = rootView.findViewByAutofillIdTraversal(autofillId.getViewId());
if (view != null) {
return view;
}
@@ -7660,50 +7691,62 @@ public class Activity extends ContextThemeWrapper
/** @hide */
@Override
- @NonNull public boolean[] getViewVisibility(@NonNull int[] viewIds) {
- final boolean[] isVisible = new boolean[viewIds.length];
- final View views[] = findViewsByAutofillIdTraversal(viewIds);
-
- for (int i = 0; i < viewIds.length; i++) {
- View view = views[i];
- if (view == null) {
- isVisible[i] = false;
- continue;
- }
-
- isVisible[i] = true;
-
- // Check if the view is visible by checking all parents
- while (true) {
- if (view instanceof DecorView && view.getViewRootImpl() == view.getParent()) {
- break;
- }
-
- if (view.getVisibility() != View.VISIBLE) {
- isVisible[i] = false;
- break;
+ public final @NonNull boolean[] autofillClientGetViewVisibility(
+ @NonNull AutofillId[] autofillIds) {
+ final int autofillIdCount = autofillIds.length;
+ final boolean[] visible = new boolean[autofillIdCount];
+ for (int i = 0; i < autofillIdCount; i++) {
+ final AutofillId autofillId = autofillIds[i];
+ final View view = autofillClientFindViewByAutofillIdTraversal(autofillId);
+ if (view != null) {
+ if (!autofillId.isVirtual()) {
+ visible[i] = view.isVisibleToUser();
+ } else {
+ visible[i] = view.isVisibleToUserForAutofill(autofillId.getVirtualChildId());
}
+ }
+ }
+ return visible;
+ }
- if (view.getParent() instanceof View) {
- view = (View) view.getParent();
- } else {
- break;
+ /** @hide */
+ public final @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId,
+ int windowId) {
+ final ArrayList<ViewRootImpl> roots = WindowManagerGlobal.getInstance()
+ .getRootViews(getActivityToken());
+ for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+ final View rootView = roots.get(rootNum).getView();
+ if (rootView != null && rootView.getAccessibilityWindowId() == windowId) {
+ final View view = rootView.findViewByAccessibilityIdTraversal(viewId);
+ if (view != null) {
+ return view;
}
}
}
+ return null;
+ }
- return isVisible;
+ /** @hide */
+ @Override
+ public final @Nullable IBinder autofillClientGetActivityToken() {
+ return getActivityToken();
}
/** @hide */
@Override
- public boolean isVisibleForAutofill() {
+ public final boolean autofillClientIsVisibleForAutofill() {
return !mStopped;
}
/** @hide */
@Override
- public boolean isDisablingEnterExitEventForAutofill() {
+ public final boolean autofillIsCompatibilityModeEnabled() {
+ return isAutofillCompatibilityEnabled();
+ }
+
+ /** @hide */
+ @Override
+ public final boolean isDisablingEnterExitEventForAutofill() {
return mAutoFillIgnoreFirstResumePause || !mResumed;
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 42825f0ad8d4..3d088ff9e5c3 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -637,6 +637,8 @@ public final class ActivityThread extends ClientTransactionHandler {
/** Initial values for {@link Profiler}. */
ProfilerInfo initProfilerInfo;
+ boolean autofillCompatibilityEnabled;
+
public String toString() {
return "AppBindData{appInfo=" + appInfo + "}";
}
@@ -863,7 +865,7 @@ public final class ActivityThread extends ClientTransactionHandler {
boolean enableBinderTracking, boolean trackAllocation,
boolean isRestrictedBackupMode, boolean persistent, Configuration config,
CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
- String buildSerial) {
+ String buildSerial, boolean autofillCompatibilityEnabled) {
if (services != null) {
// Setup the service cache in the ServiceManager
@@ -889,6 +891,7 @@ public final class ActivityThread extends ClientTransactionHandler {
data.compatInfo = compatInfo;
data.initProfilerInfo = profilerInfo;
data.buildSerial = buildSerial;
+ data.autofillCompatibilityEnabled = autofillCompatibilityEnabled;
sendMessage(H.BIND_APPLICATION, data);
}
@@ -5840,6 +5843,10 @@ public final class ActivityThread extends ClientTransactionHandler {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
app = data.info.makeApplication(data.restrictedBackupMode, null);
+
+ // Propagate autofill compat state
+ app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled);
+
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java
index a13ac49bb553..81cbbcafe8c4 100644
--- a/core/java/android/app/Application.java
+++ b/core/java/android/app/Application.java
@@ -26,6 +26,7 @@ import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
+import android.view.autofill.AutofillManager;
/**
* Base class for maintaining global application state. You can provide your own
@@ -299,4 +300,37 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 {
}
}
}
+
+ /** @hide */
+ @Override
+ public AutofillManager.AutofillClient getAutofillClient() {
+ final AutofillManager.AutofillClient client = super.getAutofillClient();
+ if (client != null) {
+ return client;
+ }
+ // Okay, ppl use the application context when they should not. This breaks
+ // autofill among other things. We pick the focused activity since autofill
+ // interacts only with the currently focused activity and we need the fill
+ // client only if a call comes from the focused activity. Sigh...
+ final ActivityThread activityThread = ActivityThread.currentActivityThread();
+ if (activityThread == null) {
+ return null;
+ }
+ final int activityCount = activityThread.mActivities.size();
+ for (int i = 0; i < activityCount; i++) {
+ final ActivityThread.ActivityClientRecord record =
+ activityThread.mActivities.valueAt(i);
+ if (record == null) {
+ continue;
+ }
+ final Activity activity = record.activity;
+ if (activity == null) {
+ continue;
+ }
+ if (activity.getWindow().getDecorView().hasFocus()) {
+ return record.activity;
+ }
+ }
+ return null;
+ }
} \ No newline at end of file
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 5ead18b46217..4a9b2bcb16ed 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -187,6 +187,7 @@ class ContextImpl extends Context {
private @Nullable String mSplitName = null;
private AutofillClient mAutofillClient = null;
+ private boolean mIsAutofillCompatEnabled;
private final Object mSync = new Object();
@@ -2255,6 +2256,18 @@ class ContextImpl extends Context {
mAutofillClient = client;
}
+ /** @hide */
+ @Override
+ public boolean isAutofillCompatibilityEnabled() {
+ return mIsAutofillCompatEnabled;
+ }
+
+ /** @hide */
+ @Override
+ public void setAutofillCompatibilityEnabled(boolean autofillCompatEnabled) {
+ mIsAutofillCompatEnabled = autofillCompatEnabled;
+ }
+
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 893a41cc992b..9e99a78ccfbd 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -67,7 +67,7 @@ oneway interface IApplicationThread {
int debugMode, boolean enableBinderTracking, boolean trackAllocation,
boolean restrictedBackupMode, boolean persistent, in Configuration config,
in CompatibilityInfo compatInfo, in Map services,
- in Bundle coreSettings, in String buildSerial);
+ in Bundle coreSettings, in String buildSerial, boolean isAutofillCompatEnabled);
void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs);
void scheduleExit();
void scheduleServiceArgs(IBinder token, in ParceledListSlice args);
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index 87f227129586..8f49bc177dcb 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -4,6 +4,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.content.ComponentName;
+import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.net.Uri;
@@ -500,9 +501,8 @@ public class AssistStructure implements Parcelable {
ViewNodeBuilder builder = new ViewNodeBuilder(assist, mRoot, false);
if ((root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
if (forAutoFill) {
- final int autofillFlags = (flags & FillRequest.FLAG_MANUAL_REQUEST) != 0
- ? View.AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0;
- view.onProvideAutofillStructure(builder, autofillFlags);
+ final int viewFlags = resolveViewAutofillFlags(view.getContext(), flags);
+ view.onProvideAutofillStructure(builder, viewFlags);
} else {
// This is a secure window, so it doesn't want a screenshot, and that
// means we should also not copy out its view hierarchy for Assist
@@ -512,9 +512,8 @@ public class AssistStructure implements Parcelable {
}
}
if (forAutoFill) {
- final int autofillFlags = (flags & FillRequest.FLAG_MANUAL_REQUEST) != 0
- ? View.AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0;
- view.dispatchProvideAutofillStructure(builder, autofillFlags);
+ final int viewFlags = resolveViewAutofillFlags(view.getContext(), flags);
+ view.dispatchProvideAutofillStructure(builder, viewFlags);
} else {
view.dispatchProvideStructure(builder);
}
@@ -532,6 +531,12 @@ public class AssistStructure implements Parcelable {
mRoot = new ViewNode(reader, 0);
}
+ int resolveViewAutofillFlags(Context context, int fillRequestFlags) {
+ return (fillRequestFlags & FillRequest.FLAG_MANUAL_REQUEST) != 0
+ || context.isAutofillCompatibilityEnabled()
+ ? View.AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS : 0;
+ }
+
void writeSelfToParcel(Parcel out, PooledStringWriter pwriter, float[] tmpMatrix) {
out.writeInt(mX);
out.writeInt(mY);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 020db952768e..b85a3199881c 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4925,6 +4925,19 @@ public abstract class Context {
}
/**
+ * @hide
+ */
+ public boolean isAutofillCompatibilityEnabled() {
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public void setAutofillCompatibilityEnabled(boolean autofillCompatEnabled) {
+ }
+
+ /**
* Throws an exception if the Context is using system resources,
* which are non-runtime-overlay-themable and may show inconsistent UI.
* @hide
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 8c1293e5b298..a788989a7578 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -994,4 +994,20 @@ public class ContextWrapper extends Context {
public void setAutofillClient(AutofillClient client) {
mBase.setAutofillClient(client);
}
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean isAutofillCompatibilityEnabled() {
+ return mBase.isAutofillCompatibilityEnabled();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void setAutofillCompatibilityEnabled(boolean autofillCompatEnabled) {
+ mBase.setAutofillCompatibilityEnabled(autofillCompatEnabled);
+ }
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 881837f0b451..6f62f6baa962 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11333,6 +11333,15 @@ public final class Settings {
"chained_battery_attribution_enabled";
/**
+ * The packages whitelisted to be run in autofill compatibility mode.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String AUTOFILL_COMPAT_ALLOWED_PACKAGES =
+ "autofill_compat_allowed_packages";
+
+ /**
* Settings to backup. This is here so that it's in the same place as the settings
* keys and easy to update.
*
@@ -11874,8 +11883,6 @@ public final class Settings {
*/
public static final String MULTI_SIM_SMS_PROMPT = "multi_sim_sms_prompt";
-
-
/** User preferred subscriptions setting.
* This holds the details of the user selected subscription from the card and
* the activation status. Each settings string have the comma separated values
diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java
index 12aa64e4ecb7..7a304c7f55d2 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -490,8 +490,45 @@ import com.android.internal.os.SomeArgs;
* strong suspicious that it could. For example, if an activity has four or more fields and one of
* them is a list, chances are that these are address fields (like address, city, state, and
* zip code).
+ *
+ * <a name="CompatibilityMode"></a>
+ * <h3>Compatibility mode</h3>
+ *
+ * <p>Apps that use standard Android widgets support autofill out-of-the-box and need to do
+ * very little to improve their user experience (annotating autofillable views and providing
+ * autofill hints). However, some apps do their own rendering and the rendered content may
+ * contain semantic structure that needs to be surfaced to the autofill framework. The platform
+ * exposes APIs to achieve this, however it could take some time until these apps implement
+ * autofill support.
+ *
+ * <p>To enable autofill for such apps the platform provides a compatibility mode in which the
+ * platform would fall back to the accessibility APIs to generate the state reported to autofill
+ * services and fill data. This mode needs to be explicitly requested for a given package up
+ * to a specified max version code allowing clean migration path when the target app begins to
+ * support autofill natively. Note that enabling compatibility may degrade performance for the
+ * target package and should be used with caution. The platform supports whitelisting which packages
+ * can be targeted in compatibility mode to ensure this mode is used only when needed and as long
+ * as needed.
+ *
+ * <p>You can request compatibility mode for packages of interest in the meta-data resource
+ * associated with your service. Below is a sample service declaration:
+ *
+ * <pre> &lt;service android:name=".MyAutofillService"
+ * android:permission="android.permission.BIND_AUTOFILL_SERVICE"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.service.autofill.AutofillService" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;meta-data android:name="android.autofillservice" android:resource="@xml/autofillservice" /&gt;
+ * &lt;/service&gt;</pre>
+ *
+ * <P>In the XML file you can specify one or more packages for which to enable compatibility
+ * mode. Below is a sample meta-data declaration:
+ *
+ * <pre> &lt;autofill-service xmlns:android="http://schemas.android.com/apk/res/android"&gt;
+ * &lt;compatibility-package android:name="foo.bar.baz" android:maxLongVersionCode="1000000000"/&gt;
+ * &lt;/autofill-service&gt;</pre>
*/
-// TODO(b/70407264): add code snippets above???
+// TODO(b/70407264): add code snippets to field classification ???
public abstract class AutofillService extends Service {
private static final String TAG = "AutofillService";
diff --git a/core/java/android/service/autofill/AutofillServiceInfo.java b/core/java/android/service/autofill/AutofillServiceInfo.java
index 4f2f6cb36d87..29c6cea14980 100644
--- a/core/java/android/service/autofill/AutofillServiceInfo.java
+++ b/core/java/android/service/autofill/AutofillServiceInfo.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppGlobals;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
@@ -27,6 +28,8 @@ import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.metrics.LogMaker;
import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
@@ -35,11 +38,13 @@ import com.android.internal.R;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.XmlUtils;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
-
import java.io.IOException;
+import java.util.Map;
/**
* {@link ServiceInfo} and meta-data about an {@link AutofillService}.
@@ -49,6 +54,9 @@ import java.io.IOException;
public final class AutofillServiceInfo {
private static final String TAG = "AutofillServiceInfo";
+ private static final String TAG_AUTOFILL_SERVICE = "autofill-service";
+ private static final String TAG_COMPATIBILITY_PACKAGE = "compatibility-package";
+
private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, int userHandle)
throws PackageManager.NameNotFoundException {
try {
@@ -70,29 +78,15 @@ public final class AutofillServiceInfo {
@Nullable
private final String mSettingsActivity;
- public AutofillServiceInfo(PackageManager pm, ComponentName comp, int userHandle)
- throws PackageManager.NameNotFoundException {
- this(pm, getServiceInfoOrThrow(comp, userHandle));
- }
+ @Nullable
+ private final Map<String, Long> mCompatibilityPackages;
- public AutofillServiceInfo(PackageManager pm, ServiceInfo si) {
- mServiceInfo = si;
- final TypedArray metaDataArray = getMetaDataArray(pm, si);
- if (metaDataArray != null) {
- mSettingsActivity = metaDataArray
- .getString(R.styleable.AutofillService_settingsActivity);
- metaDataArray.recycle();
- } else {
- mSettingsActivity = null;
- }
+ public AutofillServiceInfo(Context context, ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ this(context, getServiceInfoOrThrow(comp, userHandle));
}
- /**
- * Gets the meta-data as a {@link TypedArray}, or {@code null} if not provided,
- * or throws if invalid.
- */
- @Nullable
- private static TypedArray getMetaDataArray(PackageManager pm, ServiceInfo si) {
+ public AutofillServiceInfo(Context context, ServiceInfo si) {
// Check for permissions.
if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission)) {
if (Manifest.permission.BIND_AUTOFILL.equals(si.permission)) {
@@ -111,45 +105,115 @@ public final class AutofillServiceInfo {
}
}
+ mServiceInfo = si;
+
// Get the AutoFill metadata, if declared.
- XmlResourceParser parser = si.loadXmlMetaData(pm, AutofillService.SERVICE_META_DATA);
+ final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(),
+ AutofillService.SERVICE_META_DATA);
if (parser == null) {
- return null;
+ mSettingsActivity = null;
+ mCompatibilityPackages = null;
+ return;
}
- // Parse service info and get the <autofill-service> tag as an AttributeSet.
- AttributeSet attrs;
+ String settingsActivity = null;
+ Map<String, Long> compatibilityPackages = null;
+
try {
- // Move the XML parser to the first tag.
- try {
- int type;
- while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
- && type != XmlPullParser.START_TAG) {
- }
- } catch (XmlPullParserException | IOException e) {
- Log.e(TAG, "Error parsing auto fill service meta-data", e);
- return null;
+ final Resources resources = context.getPackageManager().getResourcesForApplication(
+ si.applicationInfo);
+
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
}
- if (!"autofill-service".equals(parser.getName())) {
+ if (TAG_AUTOFILL_SERVICE.equals(parser.getName())) {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ TypedArray afsAttributes = null;
+ try {
+ afsAttributes = resources.obtainAttributes(allAttributes,
+ com.android.internal.R.styleable.AutofillService);
+ settingsActivity = afsAttributes.getString(
+ R.styleable.AutofillService_settingsActivity);
+ } finally {
+ if (afsAttributes != null) {
+ afsAttributes.recycle();
+ }
+ }
+ compatibilityPackages = parseCompatibilityPackages(parser, resources);
+ } else {
Log.e(TAG, "Meta-data does not start with autofill-service tag");
- return null;
}
- attrs = Xml.asAttributeSet(parser);
-
- // Get resources required to read the AttributeSet.
- Resources res;
- try {
- res = pm.getResourcesForApplication(si.applicationInfo);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Error getting application resources", e);
- return null;
+ } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
+ Log.e(TAG, "Error parsing auto fill service meta-data", e);
+ }
+
+ mSettingsActivity = settingsActivity;
+ mCompatibilityPackages = compatibilityPackages;
+ }
+
+ private Map<String, Long> parseCompatibilityPackages(XmlPullParser parser, Resources resources)
+ throws IOException, XmlPullParserException {
+ Map<String, Long> compatibilityPackages = null;
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
}
- return res.obtainAttributes(attrs, R.styleable.AutofillService);
- } finally {
- parser.close();
+ if (TAG_COMPATIBILITY_PACKAGE.equals(parser.getName())) {
+ TypedArray cpAttributes = null;
+ try {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+
+ cpAttributes = resources.obtainAttributes(allAttributes,
+ R.styleable.AutofillService_CompatibilityPackage);
+
+ final String name = cpAttributes.getString(
+ R.styleable.AutofillService_CompatibilityPackage_name);
+ if (TextUtils.isEmpty(name)) {
+ Log.e(TAG, "Invalid compatibility package:" + name);
+ break;
+ }
+
+ final String maxVersionCodeStr = cpAttributes.getString(
+ R.styleable.AutofillService_CompatibilityPackage_maxLongVersionCode);
+ final Long maxVersionCode;
+ if (maxVersionCodeStr != null) {
+ try {
+ maxVersionCode = Long.parseLong(maxVersionCodeStr);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid compatibility max version code:"
+ + maxVersionCodeStr);
+ break;
+ }
+ if (maxVersionCode < 0) {
+ Log.e(TAG, "Invalid compatibility max version code:"
+ + maxVersionCode);
+ break;
+ }
+ } else {
+ maxVersionCode = Long.MAX_VALUE;
+ }
+
+ if (compatibilityPackages == null) {
+ compatibilityPackages = new ArrayMap<>();
+ }
+ compatibilityPackages.put(name, maxVersionCode);
+ } finally {
+ XmlUtils.skipCurrentTag(parser);
+ if (cpAttributes != null) {
+ cpAttributes.recycle();
+ }
+ }
+ }
}
+
+ return compatibilityPackages;
}
public ServiceInfo getServiceInfo() {
@@ -161,8 +225,26 @@ public final class AutofillServiceInfo {
return mSettingsActivity;
}
+ @Nullable
+ public boolean isCompatibilityModeRequested(String packageName, long versionCode) {
+ if (mCompatibilityPackages == null) {
+ return false;
+ }
+ final Long maxVersionCode = mCompatibilityPackages.get(packageName);
+ if (maxVersionCode == null) {
+ return false;
+ }
+ return versionCode <= maxVersionCode;
+ }
+
@Override
public String toString() {
- return mServiceInfo == null ? "null" : mServiceInfo.toString();
+ final StringBuilder builder = new StringBuilder();
+ builder.append(getClass().getSimpleName());
+ builder.append("[").append(mServiceInfo);
+ builder.append(", settings:").append(mSettingsActivity);
+ builder.append(", hasCompatPckgs:").append(mCompatibilityPackages != null
+ && !mCompatibilityPackages.isEmpty()).append("]");
+ return builder.toString();
}
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c3e9e7387732..79fc13424bee 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -74,6 +74,7 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
+import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.FloatProperty;
@@ -7940,12 +7941,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* optimal implementation providing this data.
*/
public void onProvideVirtualStructure(ViewStructure structure) {
- AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ onProvideVirtualStructureCompat(structure, false);
+ }
+
+ /**
+ * Fallback implementation to populate a ViewStructure from accessibility state.
+ *
+ * @param structure The structure to populate.
+ * @param forAutofill Whether the structure is needed for autofill.
+ */
+ private void onProvideVirtualStructureCompat(ViewStructure structure, boolean forAutofill) {
+ final AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
if (provider != null) {
- AccessibilityNodeInfo info = createAccessibilityNodeInfo();
+ final AccessibilityNodeInfo info = createAccessibilityNodeInfo();
structure.setChildCount(1);
- ViewStructure root = structure.newChild(0);
- populateVirtualStructure(root, provider, info);
+ final ViewStructure root = structure.newChild(0);
+ populateVirtualStructure(root, provider, info, forAutofill);
info.recycle();
}
}
@@ -7974,6 +7985,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* <li>Call {@link android.view.autofill.AutofillManager#notifyViewEntered(View, int, Rect)}
* and/or {@link android.view.autofill.AutofillManager#notifyViewExited(View, int)}
* when the focused virtual child changed.
+ * <li>Override {@link #isVisibleToUserForAutofill(int)} to allow the platform to query
+ * whether a given virtual view is visible to the user in order to support triggering
+ * save when all views of interest go away.
* <li>Call
* {@link android.view.autofill.AutofillManager#notifyValueChanged(View, int, AutofillValue)}
* when the value of a virtual child changed.
@@ -8009,6 +8023,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @see #AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
*/
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
+ if (mContext.isAutofillCompatibilityEnabled()) {
+ onProvideVirtualStructureCompat(structure, true);
+ }
}
/**
@@ -8086,6 +8103,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @attr ref android.R.styleable#Theme_autofilledHighlight
*/
public void autofill(@NonNull @SuppressWarnings("unused") SparseArray<AutofillValue> values) {
+ if (!mContext.isAutofillCompatibilityEnabled()) {
+ return;
+ }
+ final AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider == null) {
+ return;
+ }
+ final int valueCount = values.size();
+ for (int i = 0; i < valueCount; i++) {
+ final AutofillValue value = values.valueAt(i);
+ if (value.isText()) {
+ final int virtualId = values.keyAt(i);
+ final CharSequence text = value.getTextValue();
+ final Bundle arguments = new Bundle();
+ arguments.putCharSequence(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
+ provider.performAction(virtualId, AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
+ }
+ }
}
/**
@@ -8339,7 +8375,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
private void populateVirtualStructure(ViewStructure structure,
- AccessibilityNodeProvider provider, AccessibilityNodeInfo info) {
+ AccessibilityNodeProvider provider, AccessibilityNodeInfo info,
+ boolean forAutofill) {
structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()),
null, null, null);
Rect rect = structure.getTempRect();
@@ -8374,21 +8411,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (info.isContextClickable()) {
structure.setContextClickable(true);
}
+ if (forAutofill) {
+ structure.setAutofillId(new AutofillId(getAutofillId(),
+ AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId())));
+ }
CharSequence cname = info.getClassName();
structure.setClassName(cname != null ? cname.toString() : null);
structure.setContentDescription(info.getContentDescription());
if ((info.getText() != null || info.getError() != null)) {
structure.setText(info.getText(), info.getTextSelectionStart(),
info.getTextSelectionEnd());
+ if (forAutofill) {
+ if (info.isEditable()) {
+ structure.setDataIsSensitive(true);
+ structure.setAutofillType(AUTOFILL_TYPE_TEXT);
+ final AutofillValue autofillValue = AutofillValue.forText(structure.getText());
+ structure.setAutofillValue(autofillValue);
+ if (info.isPassword()) {
+ structure.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ }
+ } else {
+ structure.setDataIsSensitive(false);
+ }
+ }
}
final int NCHILDREN = info.getChildCount();
if (NCHILDREN > 0) {
structure.setChildCount(NCHILDREN);
for (int i=0; i<NCHILDREN; i++) {
+ if (AccessibilityNodeInfo.getVirtualDescendantId(info.getChildNodeIds().get(i))
+ == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ Log.e(VIEW_LOG_TAG, "Virtual view pointing to its host. Ignoring");
+ continue;
+ }
AccessibilityNodeInfo cinfo = provider.createAccessibilityNodeInfo(
AccessibilityNodeInfo.getVirtualDescendantId(info.getChildId(i)));
ViewStructure child = structure.newChild(i);
- populateVirtualStructure(child, provider, cinfo);
+ populateVirtualStructure(child, provider, cinfo, forAutofill);
cinfo.recycle();
}
}
@@ -8717,6 +8776,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * Computes whether this virtual autofill view is visible to the user.
+ *
+ * @return Whether the view is visible on the screen.
+ */
+ public boolean isVisibleToUserForAutofill(int virtualId) {
+ if (mContext.isAutofillCompatibilityEnabled()) {
+ final AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider != null) {
+ final AccessibilityNodeInfo node = provider.createAccessibilityNodeInfo(virtualId);
+ if (node != null) {
+ return node.isVisibleToUser();
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
* Computes whether this view is visible to the user. Such a view is
* attached, visible, all its predecessors are visible, it is not clipped
* entirely by its predecessors, and has an alpha greater than zero.
@@ -8725,7 +8802,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @hide
*/
- protected boolean isVisibleToUser() {
+ public boolean isVisibleToUser() {
return isVisibleToUser(null);
}
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index a8bdb85660d0..273f097edc63 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -3555,13 +3555,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
public void dispatchProvideAutofillStructure(ViewStructure structure,
@AutofillFlags int flags) {
super.dispatchProvideAutofillStructure(structure, flags);
+
if (structure.getChildCount() != 0) {
return;
}
if (!isLaidOut()) {
- Log.v(VIEW_LOG_TAG, "dispatchProvideAutofillStructure(): not laid out, ignoring "
- + mChildrenCount + " children of " + getAutofillId());
+ if (Helper.sVerbose) {
+ Log.v(VIEW_LOG_TAG, "dispatchProvideAutofillStructure(): not laid out, ignoring "
+ + mChildrenCount + " children of " + getAutofillId());
+ }
return;
}
@@ -5120,6 +5123,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
// manually assembling the hierarchy, update the ancestor default-focus chain.
setDefaultFocus(child);
}
+
+ touchAccessibilityNodeProviderIfNeeded(child);
+ }
+
+ /**
+ * We may need to touch the provider to bring up the a11y layer. In a11y mode
+ * clients inspect the screen or the user touches it which triggers bringing up
+ * of the a11y infrastructure while in autofill mode we want the infra up and
+ * running from the beginning since we watch for a11y events to drive autofill.
+ */
+ private void touchAccessibilityNodeProviderIfNeeded(View child) {
+ if (mContext.isAutofillCompatibilityEnabled()) {
+ child.getAccessibilityNodeProvider();
+ }
}
private void addInArray(View child, int index) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7bd197e824db..810864eaac3c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -71,6 +71,7 @@ import android.os.Trace;
import android.util.AndroidRuntimeException;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.LongArray;
import android.util.MergedConfiguration;
import android.util.Slog;
import android.util.SparseArray;
@@ -114,6 +115,8 @@ import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
import java.util.concurrent.CountDownLatch;
/**
@@ -2569,6 +2572,10 @@ public final class ViewRootImpl implements ViewParent,
~WindowManager.LayoutParams
.SOFT_INPUT_IS_FORWARD_NAVIGATION;
mHasHadWindowFocus = true;
+
+ // Refocusing a window that has a focused view should fire a
+ // focus event for the view since the global focused view changed.
+ fireAccessibilityFocusEventIfHasFocusedNode();
} else {
if (mPointerCapture) {
handlePointerCaptureChanged(false);
@@ -2578,6 +2585,86 @@ public final class ViewRootImpl implements ViewParent,
mFirstInputStage.onWindowFocusChanged(hasWindowFocus);
}
+ private void fireAccessibilityFocusEventIfHasFocusedNode() {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return;
+ }
+ final View focusedView = mView.findFocus();
+ if (focusedView == null) {
+ return;
+ }
+ final AccessibilityNodeProvider provider = focusedView.getAccessibilityNodeProvider();
+ if (provider == null) {
+ focusedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ } else {
+ final AccessibilityNodeInfo focusedNode = findFocusedVirtualNode(provider);
+ if (focusedNode != null) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(
+ focusedNode.getSourceNodeId());
+ // This is a best effort since clearing and setting the focus via the
+ // provider APIs could have side effects. We don't have a provider API
+ // similar to that on View to ask a given event to be fired.
+ final AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ event.setSource(focusedView, virtualId);
+ event.setPackageName(focusedNode.getPackageName());
+ event.setChecked(focusedNode.isChecked());
+ event.setContentDescription(focusedNode.getContentDescription());
+ event.setPassword(focusedNode.isPassword());
+ event.getText().add(focusedNode.getText());
+ event.setEnabled(focusedNode.isEnabled());
+ focusedView.getParent().requestSendAccessibilityEvent(focusedView, event);
+ focusedNode.recycle();
+ }
+ }
+ }
+
+ private AccessibilityNodeInfo findFocusedVirtualNode(AccessibilityNodeProvider provider) {
+ AccessibilityNodeInfo focusedNode = provider.findFocus(
+ AccessibilityNodeInfo.FOCUS_INPUT);
+ if (focusedNode != null) {
+ return focusedNode;
+ }
+
+ if (!mContext.isAutofillCompatibilityEnabled()) {
+ return null;
+ }
+
+ // Unfortunately some provider implementations don't properly
+ // implement AccessibilityNodeProvider#findFocus
+ AccessibilityNodeInfo current = provider.createAccessibilityNodeInfo(
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+ if (current.isFocused()) {
+ return current;
+ }
+
+ final Queue<AccessibilityNodeInfo> fringe = new LinkedList<>();
+ fringe.offer(current);
+
+ while (!fringe.isEmpty()) {
+ current = fringe.poll();
+ final LongArray childNodeIds = current.getChildNodeIds();
+ if (childNodeIds== null || childNodeIds.size() <= 0) {
+ continue;
+ }
+ final int childCount = childNodeIds.size();
+ for (int i = 0; i < childCount; i++) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(
+ childNodeIds.get(i));
+ final AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(virtualId);
+ if (child != null) {
+ if (child.isFocused()) {
+ return child;
+ }
+ fringe.offer(child);
+ }
+ }
+ current.recycle();
+ }
+
+ return null;
+ }
+
private void handleOutOfResourcesException(Surface.OutOfResourcesException e) {
Log.e(mTag, "OutOfResourcesException initializing HW surface", e);
try {
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index b49b5bc02926..191c27073782 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -16,10 +16,9 @@
package android.view.accessibility;
-import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME;
-
import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
@@ -44,7 +43,7 @@ import android.util.Log;
import android.util.SparseArray;
import android.view.IWindow;
import android.view.View;
-
+import android.view.accessibility.AccessibilityEvent.EventType;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IntPair;
@@ -52,6 +51,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME;
+
/**
* System level service that serves as an event dispatch for {@link AccessibilityEvent}s,
* and provides facilities for querying the accessibility state of the system.
@@ -132,6 +133,8 @@ public final class AccessibilityManager {
boolean mIsHighTextContrastEnabled;
+ AccessibilityPolicy mAccessibilityPolicy;
+
private final ArrayMap<AccessibilityStateChangeListener, Handler>
mAccessibilityStateChangeListeners = new ArrayMap<>();
@@ -215,6 +218,60 @@ public final class AccessibilityManager {
void onHighTextContrastStateChanged(boolean enabled);
}
+ /**
+ * Policy to inject behavior into the accessibility manager.
+ *
+ * @hide
+ */
+ public interface AccessibilityPolicy {
+ /**
+ * Checks whether accessibility is enabled.
+ *
+ * @param accessibilityEnabled Whether the accessibility layer is enabled.
+ * @return whether accessibility is enabled.
+ */
+ boolean isEnabled(boolean accessibilityEnabled);
+
+ /**
+ * Notifies the policy for an accessibility event.
+ *
+ * @param event The event.
+ * @param accessibilityEnabled Whether the accessibility layer is enabled.
+ * @param relevantEventTypes The events relevant events.
+ * @return The event to dispatch or null.
+ */
+ @Nullable AccessibilityEvent onAccessibilityEvent(@NonNull AccessibilityEvent event,
+ boolean accessibilityEnabled, @EventType int relevantEventTypes);
+
+ /**
+ * Gets the list of relevant events.
+ *
+ * @param relevantEventTypes The relevant events.
+ * @return The relevant events to report.
+ */
+ @EventType int getRelevantEventTypes(@EventType int relevantEventTypes);
+
+ /**
+ * Gets the list of installed services to report.
+ *
+ * @param installedService The installed services.
+ * @return The services to report.
+ */
+ @NonNull List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
+ @Nullable List<AccessibilityServiceInfo> installedService);
+
+ /**
+ * Gets the list of enabled accessibility services.
+ *
+ * @param feedbackTypeFlags The feedback type to query for.
+ * @param enabledService The enabled services.
+ * @return The services to report.
+ */
+ @Nullable List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
+ @FeedbackType int feedbackTypeFlags,
+ @Nullable List<AccessibilityServiceInfo> enabledService);
+ }
+
private final IAccessibilityManagerClient.Stub mClient =
new IAccessibilityManagerClient.Stub() {
@Override
@@ -341,11 +398,8 @@ public final class AccessibilityManager {
*/
public boolean isEnabled() {
synchronized (mLock) {
- IAccessibilityManager service = getServiceLocked();
- if (service == null) {
- return false;
- }
- return mIsEnabled;
+ return mIsEnabled || (mAccessibilityPolicy != null
+ && mAccessibilityPolicy.isEnabled(mIsEnabled));
}
}
@@ -401,12 +455,23 @@ public final class AccessibilityManager {
public void sendAccessibilityEvent(AccessibilityEvent event) {
final IAccessibilityManager service;
final int userId;
+ final AccessibilityEvent dispatchedEvent;
synchronized (mLock) {
service = getServiceLocked();
if (service == null) {
return;
}
- if (!mIsEnabled) {
+ event.setEventTime(SystemClock.uptimeMillis());
+ if (mAccessibilityPolicy != null) {
+ dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event,
+ mIsEnabled, mRelevantEventTypes);
+ if (dispatchedEvent == null) {
+ return;
+ }
+ } else {
+ dispatchedEvent = event;
+ }
+ if (!isEnabled()) {
Looper myLooper = Looper.myLooper();
if (myLooper == Looper.getMainLooper()) {
throw new IllegalStateException(
@@ -420,9 +485,9 @@ public final class AccessibilityManager {
return;
}
}
- if ((event.getEventType() & mRelevantEventTypes) == 0) {
+ if ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) {
if (DEBUG) {
- Log.i(LOG_TAG, "Not dispatching irrelevant event: " + event
+ Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent
+ " that is not among "
+ AccessibilityEvent.eventTypeToString(mRelevantEventTypes));
}
@@ -431,23 +496,25 @@ public final class AccessibilityManager {
userId = mUserId;
}
try {
- event.setEventTime(SystemClock.uptimeMillis());
// it is possible that this manager is in the same process as the service but
// client using it is called through Binder from another process. Example: MMS
// app adds a SMS notification and the NotificationManagerService calls this method
long identityToken = Binder.clearCallingIdentity();
try {
- service.sendAccessibilityEvent(event, userId);
+ service.sendAccessibilityEvent(dispatchedEvent, userId);
} finally {
Binder.restoreCallingIdentity(identityToken);
}
if (DEBUG) {
- Log.i(LOG_TAG, event + " sent");
+ Log.i(LOG_TAG, dispatchedEvent + " sent");
}
} catch (RemoteException re) {
- Log.e(LOG_TAG, "Error during sending " + event + " ", re);
+ Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re);
} finally {
- event.recycle();
+ if (event != dispatchedEvent) {
+ event.recycle();
+ }
+ dispatchedEvent.recycle();
}
}
@@ -462,7 +529,7 @@ public final class AccessibilityManager {
if (service == null) {
return;
}
- if (!mIsEnabled) {
+ if (!isEnabled()) {
Looper myLooper = Looper.myLooper();
if (myLooper == Looper.getMainLooper()) {
throw new IllegalStateException(
@@ -532,6 +599,9 @@ public final class AccessibilityManager {
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
}
+ if (mAccessibilityPolicy != null) {
+ services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services);
+ }
if (services != null) {
return Collections.unmodifiableList(services);
} else {
@@ -574,6 +644,10 @@ public final class AccessibilityManager {
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
}
+ if (mAccessibilityPolicy != null) {
+ services = mAccessibilityPolicy.getEnabledAccessibilityServiceList(
+ feedbackTypeFlags, services);
+ }
if (services != null) {
return Collections.unmodifiableList(services);
} else {
@@ -783,6 +857,19 @@ public final class AccessibilityManager {
}
/**
+ * Sets the {@link AccessibilityPolicy} controlling this manager.
+ *
+ * @param policy The policy.
+ *
+ * @hide
+ */
+ public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) {
+ synchronized (mLock) {
+ mAccessibilityPolicy = policy;
+ }
+ }
+
+ /**
* Check if the accessibility volume stream is active.
*
* @return True if accessibility volume is active (i.e. some service has requested it). False
@@ -834,7 +921,7 @@ public final class AccessibilityManager {
final boolean highTextContrastEnabled =
(stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
- final boolean wasEnabled = mIsEnabled;
+ final boolean wasEnabled = isEnabled();
final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
@@ -843,7 +930,7 @@ public final class AccessibilityManager {
mIsTouchExplorationEnabled = touchExplorationEnabled;
mIsHighTextContrastEnabled = highTextContrastEnabled;
- if (wasEnabled != enabled) {
+ if (wasEnabled != isEnabled()) {
notifyAccessibilityStateChanged();
}
@@ -1052,16 +1139,15 @@ public final class AccessibilityManager {
if (mAccessibilityStateChangeListeners.isEmpty()) {
return;
}
- isEnabled = mIsEnabled;
+ isEnabled = isEnabled();
listeners = new ArrayMap<>(mAccessibilityStateChangeListeners);
}
- int numListeners = listeners.size();
+ final int numListeners = listeners.size();
for (int i = 0; i < numListeners; i++) {
- final AccessibilityStateChangeListener listener =
- mAccessibilityStateChangeListeners.keyAt(i);
- mAccessibilityStateChangeListeners.valueAt(i)
- .post(() -> listener.onAccessibilityStateChanged(isEnabled));
+ final AccessibilityStateChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).post(() ->
+ listener.onAccessibilityStateChanged(isEnabled));
}
}
@@ -1079,12 +1165,11 @@ public final class AccessibilityManager {
listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners);
}
- int numListeners = listeners.size();
+ final int numListeners = listeners.size();
for (int i = 0; i < numListeners; i++) {
- final TouchExplorationStateChangeListener listener =
- mTouchExplorationStateChangeListeners.keyAt(i);
- mTouchExplorationStateChangeListeners.valueAt(i)
- .post(() -> listener.onTouchExplorationStateChanged(isTouchExplorationEnabled));
+ final TouchExplorationStateChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).post(() ->
+ listener.onTouchExplorationStateChanged(isTouchExplorationEnabled));
}
}
@@ -1102,12 +1187,11 @@ public final class AccessibilityManager {
listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners);
}
- int numListeners = listeners.size();
+ final int numListeners = listeners.size();
for (int i = 0; i < numListeners; i++) {
- final HighTextContrastChangeListener listener =
- mHighTextContrastStateChangeListeners.keyAt(i);
- mHighTextContrastStateChangeListeners.valueAt(i)
- .post(() -> listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
+ final HighTextContrastChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).post(() ->
+ listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
}
}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index dac19987f9f2..63a9990cf839 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -16,10 +16,7 @@
package android.view.autofill;
-import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
-import static android.view.autofill.Helper.sDebug;
-import static android.view.autofill.Helper.sVerbose;
-
+import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +25,8 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.metrics.LogMaker;
import android.os.Bundle;
@@ -41,13 +40,22 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
+import android.view.Choreographer;
import android.view.View;
-
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityWindowInfo;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import org.xmlpull.v1.XmlPullParserException;
+import sun.misc.Cleaner;
+import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -58,8 +66,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
+import static android.view.autofill.Helper.sDebug;
+import static android.view.autofill.Helper.sVerbose;
+
// TODO: use java.lang.ref.Cleaner once Android supports Java 9
-import sun.misc.Cleaner;
/**
* The {@link AutofillManager} provides ways for apps and custom views to integrate with the
@@ -178,7 +189,6 @@ public final class AutofillManager {
private static final String STATE_TAG = "android:state";
private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData";
-
/** @hide */ public static final int ACTION_START_SESSION = 1;
/** @hide */ public static final int ACTION_VIEW_ENTERED = 2;
/** @hide */ public static final int ACTION_VIEW_EXITED = 3;
@@ -355,6 +365,10 @@ public final class AutofillManager {
@GuardedBy("mLock")
private boolean mSaveOnFinish;
+ /** If compatibility mode is enabled - this is a bridge to interact with a11y */
+ @GuardedBy("mLock")
+ private CompatibilityBridge mCompatibilityBridge;
+
/** @hide */
public interface AutofillClient {
/**
@@ -364,13 +378,13 @@ public final class AutofillManager {
* @param intent The authentication intent.
* @param fillInIntent The authentication fill-in intent.
*/
- void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
+ void autofillClientAuthenticate(int authenticationId, IntentSender intent,
Intent fillInIntent);
/**
* Tells the client this manager has state to be reset.
*/
- void autofillCallbackResetableStateAvailable();
+ void autofillClientResetableStateAvailable();
/**
* Request showing the autofill UI.
@@ -382,7 +396,7 @@ public final class AutofillManager {
* @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,
+ boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, int height,
@Nullable Rect virtualBounds, IAutofillWindowPresenter presenter);
/**
@@ -390,21 +404,28 @@ public final class AutofillManager {
*
* @return Whether the UI was hidden.
*/
- boolean autofillCallbackRequestHideFillUi();
+ boolean autofillClientRequestHideFillUi();
+
+ /**
+ * Gets whether the fill UI is currenlty being shown.
+ *
+ * @return Whether the fill UI is currently being shown
+ */
+ boolean autofillClientIsFillUiShowing();
/**
* Checks if views are currently attached and visible.
*
* @return And array with {@code true} iff the view is attached or visible
*/
- @NonNull boolean[] getViewVisibility(@NonNull int[] viewId);
+ @NonNull boolean[] autofillClientGetViewVisibility(@NonNull AutofillId[] autofillIds);
/**
* Checks is the client is currently visible as understood by autofill.
*
* @return {@code true} if the client is currently visible
*/
- boolean isVisibleForAutofill();
+ boolean autofillClientIsVisibleForAutofill();
/**
* Client might disable enter/exit event e.g. when activity is paused.
@@ -414,30 +435,51 @@ public final class AutofillManager {
/**
* Finds views by traversing the hierarchies of the client.
*
- * @param viewIds The autofill ids of the views to find
+ * @param autofillIds The autofill ids of the views to find
*
* @return And array containing the views (empty if no views found).
*/
- @NonNull View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds);
+ @NonNull View[] autofillClientFindViewsByAutofillIdTraversal(
+ @NonNull AutofillId[] autofillIds);
/**
* Finds a view by traversing the hierarchies of the client.
*
- * @param viewId The autofill id of the views to find
+ * @param autofillId The autofill id of the views to find
+ *
+ * @return The view, or {@code null} if not found
+ */
+ @Nullable View autofillClientFindViewByAutofillIdTraversal(@NonNull AutofillId autofillId);
+
+ /**
+ * Finds a view by a11y id in a given client window.
+ *
+ * @param viewId The accessibility id of the views to find
+ * @param windowId The accessibility window id where to search
*
* @return The view, or {@code null} if not found
*/
- @Nullable View findViewByAutofillIdTraversal(int viewId);
+ @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId);
/**
* Runs the specified action on the UI thread.
*/
- void runOnUiThread(Runnable action);
+ void autofillClientRunOnUiThread(Runnable action);
/**
* Gets the complete component name of this client.
*/
- ComponentName getComponentName();
+ ComponentName autofillClientGetComponentName();
+
+ /**
+ * Gets the activity token
+ */
+ @Nullable IBinder autofillClientGetActivityToken();
+
+ /**
+ * @return Whether compatibility mode is enabled.
+ */
+ boolean autofillIsCompatibilityModeEnabled();
}
/**
@@ -449,6 +491,19 @@ public final class AutofillManager {
}
/**
+ * @hide
+ */
+ public void enableCompatibilityMode() {
+ synchronized (mLock) {
+ // The accessibility manager is a singleton so we may need to plug
+ // different bridge based on which activity is currently focused
+ // in the current process. Since compat would be rarely used, just
+ // create and register a new instance every time.
+ mCompatibilityBridge = new CompatibilityBridge();
+ }
+ }
+
+ /**
* Restore state after activity lifecycle
*
* @param savedInstanceState The state to be restored
@@ -477,7 +532,8 @@ public final class AutofillManager {
if (client != null) {
try {
final boolean sessionWasRestored = mService.restoreSession(mSessionId,
- mContext.getActivityToken(), mServiceClient.asBinder());
+ client.autofillClientGetActivityToken(),
+ mServiceClient.asBinder());
if (!sessionWasRestored) {
Log.w(TAG, "Session " + mSessionId + " could not be restored");
@@ -488,7 +544,7 @@ public final class AutofillManager {
Log.d(TAG, "session " + mSessionId + " was restored");
}
- client.autofillCallbackResetableStateAvailable();
+ client.autofillClientResetableStateAvailable();
}
} catch (RemoteException e) {
Log.e(TAG, "Could not figure out if there was an autofill session", e);
@@ -501,22 +557,29 @@ public final class AutofillManager {
/**
* Called once the client becomes visible.
*
- * @see AutofillClient#isVisibleForAutofill()
+ * @see AutofillClient#autofillClientIsVisibleForAutofill()
*
* {@hide}
*/
public void onVisibleForAutofill() {
- synchronized (mLock) {
- if (mEnabled && isActiveLocked() && mTrackedViews != null) {
- mTrackedViews.onVisibleForAutofillLocked();
+ // This gets called when the client just got visible at which point the visibility
+ // of the tracked views may not have been computed (due to a pending layout, etc).
+ // While generally we have no way to know when the UI has settled. We will evaluate
+ // the tracked views state at the end of next frame to guarantee that everything
+ // that may need to be laid out is laid out.
+ Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> {
+ synchronized (mLock) {
+ if (mEnabled && isActiveLocked() && mTrackedViews != null) {
+ mTrackedViews.onVisibleForAutofillChangedLocked();
+ }
}
- }
+ }, null);
}
/**
* Called once the client becomes invisible.
*
- * @see AutofillClient#isVisibleForAutofill()
+ * @see AutofillClient#autofillClientIsVisibleForAutofill()
*
* {@hide}
*/
@@ -551,6 +614,15 @@ public final class AutofillManager {
}
/**
+ * @hide
+ */
+ public boolean isCompatibilityModeEnabled() {
+ synchronized (mLock) {
+ return mCompatibilityBridge != null;
+ }
+ }
+
+ /**
* Checks whether autofill is enabled for the current user.
*
* <p>Typically used to determine whether the option to explicitly request autofill should
@@ -653,7 +725,7 @@ public final class AutofillManager {
private boolean isClientVisibleForAutofillLocked() {
final AutofillClient client = getClient();
- return client != null && client.isVisibleForAutofill();
+ return client != null && client.autofillClientIsVisibleForAutofill();
}
private boolean isClientDisablingEnterExitEvent() {
@@ -1322,13 +1394,13 @@ public final class AutofillManager {
final AutofillClient client = getClient();
if (client == null) return; // NOTE: getClient() already logd it..
- mSessionId = mService.startSession(mContext.getActivityToken(),
+ mSessionId = mService.startSession(client.autofillClientGetActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
- mCallback != null, flags, client.getComponentName());
+ mCallback != null, flags, client.autofillClientGetComponentName());
if (mSessionId != NO_SESSION) {
mState = STATE_ACTIVE;
}
- client.autofillCallbackResetableStateAvailable();
+ client.autofillClientResetableStateAvailable();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1383,14 +1455,16 @@ public final class AutofillManager {
final AutofillClient client = getClient();
if (client == null) return; // NOTE: getClient() already logd it..
- final int newId = mService.updateOrRestartSession(mContext.getActivityToken(),
+ final int newId = mService.updateOrRestartSession(
+ client.autofillClientGetActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
- mCallback != null, flags, client.getComponentName(), mSessionId, action);
+ mCallback != null, flags, client.autofillClientGetComponentName(),
+ mSessionId, action);
if (newId != mSessionId) {
if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId);
mSessionId = newId;
mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE;
- client.autofillCallbackResetableStateAvailable();
+ client.autofillClientResetableStateAvailable();
}
} else {
mService.updateSession(mSessionId, id, bounds, value, action, flags,
@@ -1489,7 +1563,7 @@ public final class AutofillManager {
AutofillClient client = getClient();
if (client != null) {
- if (client.autofillCallbackRequestShowFillUi(anchor, width, height,
+ if (client.autofillClientRequestShowFillUi(anchor, width, height,
anchorBounds, presenter) && mCallback != null) {
callback = mCallback;
}
@@ -1516,7 +1590,7 @@ public final class AutofillManager {
// clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
// before onAuthenticationResult()
mOnInvisibleCalled = false;
- client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
+ client.autofillClientAuthenticate(authenticationId, intent, fillInIntent);
}
}
}
@@ -1587,7 +1661,8 @@ public final class AutofillManager {
final int itemCount = ids.size();
int numApplied = 0;
ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
- final View[] views = client.findViewsByAutofillIdTraversal(getViewIds(ids));
+ final View[] views = client.autofillClientFindViewsByAutofillIdTraversal(
+ Helper.toArray(ids));
for (int i = 0; i < itemCount; i++) {
final AutofillId id = ids.get(i);
@@ -1758,7 +1833,7 @@ public final class AutofillManager {
// service being uninstalled and the UI being dismissed.
AutofillClient client = getClient();
if (client != null) {
- if (client.autofillCallbackRequestHideFillUi() && mCallback != null) {
+ if (client.autofillClientRequestHideFillUi() && mCallback != null) {
callback = mCallback;
}
}
@@ -1807,35 +1882,6 @@ public final class AutofillManager {
}
/**
- * Get an array of viewIds from a List of {@link AutofillId}.
- *
- * @param autofillIds The autofill ids to convert
- *
- * @return The array of viewIds.
- */
- // TODO: move to Helper as static method
- @NonNull private int[] getViewIds(@NonNull AutofillId[] autofillIds) {
- final int numIds = autofillIds.length;
- final int[] viewIds = new int[numIds];
- for (int i = 0; i < numIds; i++) {
- viewIds[i] = autofillIds[i].getViewId();
- }
-
- return viewIds;
- }
-
- // TODO: move to Helper as static method
- @NonNull private int[] getViewIds(@NonNull List<AutofillId> autofillIds) {
- final int numIds = autofillIds.size();
- final int[] viewIds = new int[numIds];
- for (int i = 0; i < numIds; i++) {
- viewIds[i] = autofillIds.get(i).getViewId();
- }
-
- return viewIds;
- }
-
- /**
* Find a single view by its id.
*
* @param autofillId The autofill id of the view
@@ -1844,12 +1890,10 @@ public final class AutofillManager {
*/
private View findView(@NonNull AutofillId autofillId) {
final AutofillClient client = getClient();
-
- if (client == null) {
- return null;
+ if (client != null) {
+ return client.autofillClientFindViewByAutofillIdTraversal(autofillId);
}
-
- return client.findViewByAutofillIdTraversal(autofillId.getViewId());
+ return null;
}
/** @hide */
@@ -1895,6 +1939,7 @@ public final class AutofillManager {
pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds);
pw.print(pfx); pw.print("save trigger id: "); pw.println(mSaveTriggerId);
pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish);
+ pw.print(pfx); pw.print("compat mode enabled: "); pw.println(isCompatibilityModeEnabled());
pw.print(pfx); pw.print("debug: "); pw.print(sDebug);
pw.print(" verbose: "); pw.println(sVerbose);
}
@@ -1934,7 +1979,218 @@ public final class AutofillManager {
if (sVerbose) Log.v(TAG, "ignoring post() because client is null");
return;
}
- client.runOnUiThread(runnable);
+ client.autofillClientRunOnUiThread(runnable);
+ }
+
+ /**
+ * Implementation of the accessibility based compatibility.
+ */
+ private final class CompatibilityBridge implements AccessibilityManager.AccessibilityPolicy {
+ @GuardedBy("mLock")
+ private final Rect mFocusedBounds = new Rect();
+ @GuardedBy("mLock")
+ private final Rect mTempBounds = new Rect();
+
+ @GuardedBy("mLock")
+ private int mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ @GuardedBy("mLock")
+ private long mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+
+ // Need to report a fake service in case a11y clients check the service list
+ @NonNull
+ @GuardedBy("mLock")
+ AccessibilityServiceInfo mCompatServiceInfo;
+
+ CompatibilityBridge() {
+ final AccessibilityManager am = AccessibilityManager.getInstance(mContext);
+ am.setAccessibilityPolicy(this);
+ }
+
+ private AccessibilityServiceInfo getCompatServiceInfo() {
+ synchronized (mLock) {
+ if (mCompatServiceInfo != null) {
+ return mCompatServiceInfo;
+ }
+ final Intent intent = new Intent();
+ intent.setComponent(new ComponentName("android",
+ "com.android.server.autofill.AutofillCompatAccessibilityService"));
+ final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(
+ intent, PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
+ try {
+ mCompatServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Cannot find compat autofill service:" + intent);
+ throw new IllegalStateException("Cannot find compat autofill service");
+ }
+ return mCompatServiceInfo;
+ }
+ }
+
+ @Override
+ public boolean isEnabled(boolean accessibilityEnabled) {
+ return true;
+ }
+
+ @Override
+ public int getRelevantEventTypes(int relevantEventTypes) {
+ return relevantEventTypes | AccessibilityEvent.TYPE_VIEW_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
+ }
+
+ @Override
+ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
+ List<AccessibilityServiceInfo> installedServices) {
+ if (installedServices == null) {
+ installedServices = new ArrayList<>();
+ }
+ installedServices.add(getCompatServiceInfo());
+ return installedServices;
+ }
+
+ @Override
+ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
+ int feedbackTypeFlags, List<AccessibilityServiceInfo> enabledService) {
+ if (enabledService == null) {
+ enabledService = new ArrayList<>();
+ }
+ enabledService.add(getCompatServiceInfo());
+ return enabledService;
+ }
+
+ @Override
+ public AccessibilityEvent onAccessibilityEvent(AccessibilityEvent event,
+ boolean accessibilityEnabled, int relevantEventTypes) {
+ switch (event.getEventType()) {
+ case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
+ synchronized (mLock) {
+ if (mFocusedWindowId == event.getWindowId()
+ && mFocusedNodeId == event.getSourceNodeId()) {
+ return event;
+ }
+ if (mFocusedWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
+ && mFocusedNodeId != AccessibilityNodeInfo.UNDEFINED_NODE_ID) {
+ notifyViewExited(mFocusedWindowId, mFocusedNodeId);
+ mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+ mFocusedNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
+ mFocusedBounds.set(0, 0, 0, 0);
+ }
+ final int windowId = event.getWindowId();
+ final long nodeId = event.getSourceNodeId();
+ if (notifyViewEntered(windowId, nodeId, mFocusedBounds)) {
+ mFocusedWindowId = windowId;
+ mFocusedNodeId = nodeId;
+ }
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: {
+ synchronized (mLock) {
+ if (mFocusedWindowId == event.getWindowId()
+ && mFocusedNodeId == event.getSourceNodeId()) {
+ notifyValueChanged(event.getWindowId(), event.getSourceNodeId());
+ }
+ }
+ } break;
+
+ case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
+ final AutofillClient client = getClient();
+ if (client != null) {
+ synchronized (mLock) {
+ if (client.autofillClientIsFillUiShowing()) {
+ notifyViewEntered(mFocusedWindowId, mFocusedNodeId, mFocusedBounds);
+ }
+ updateTrackedViewsLocked();
+ }
+ }
+ } break;
+ }
+
+ return accessibilityEnabled ? event : null;
+ }
+
+ private boolean notifyViewEntered(int windowId, long nodeId, Rect focusedBounds) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return false;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return false;
+ }
+ final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
+ if (node == null) {
+ return false;
+ }
+ if (!node.isEditable()) {
+ return false;
+ }
+ final Rect newBounds = mTempBounds;
+ node.getBoundsInScreen(newBounds);
+ if (newBounds.equals(focusedBounds)) {
+ return false;
+ }
+ focusedBounds.set(newBounds);
+ AutofillManager.this.notifyViewEntered(view, virtualId, newBounds);
+ return true;
+ }
+
+ private void notifyViewExited(int windowId, long nodeId) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return;
+ }
+ AutofillManager.this.notifyViewExited(view, virtualId);
+ }
+
+ private void notifyValueChanged(int windowId, long nodeId) {
+ final int virtualId = AccessibilityNodeInfo.getVirtualDescendantId(nodeId);
+ if (!isVirtualNode(virtualId)) {
+ return;
+ }
+ final View view = findViewByAccessibilityId(windowId, nodeId);
+ if (view == null) {
+ return;
+ }
+ final AccessibilityNodeInfo node = findVirtualNodeByAccessibilityId(view, virtualId);
+ if (node == null) {
+ return;
+ }
+ AutofillManager.this.notifyValueChanged(view, virtualId,
+ AutofillValue.forText(node.getText()));
+ }
+
+ private void updateTrackedViewsLocked() {
+ if (mTrackedViews != null) {
+ mTrackedViews.onVisibleForAutofillChangedLocked();
+ }
+ }
+
+ private View findViewByAccessibilityId(int windowId, long nodeId) {
+ final AutofillClient client = getClient();
+ if (client == null) {
+ return null;
+ }
+ final int viewId = AccessibilityNodeInfo.getAccessibilityViewId(nodeId);
+ return client.autofillClientFindViewByAccessibilityIdTraversal(viewId, windowId);
+ }
+
+ private AccessibilityNodeInfo findVirtualNodeByAccessibilityId(View view, int virtualId) {
+ final AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
+ if (provider == null) {
+ return null;
+ }
+ return provider.createAccessibilityNodeInfo(virtualId);
+ }
+
+ private boolean isVirtualNode(int nodeId) {
+ return nodeId != AccessibilityNodeProvider.HOST_VIEW_ID
+ && nodeId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
+ }
}
/**
@@ -2013,11 +2269,11 @@ public final class AutofillManager {
*/
TrackedViews(@Nullable AutofillId[] trackedIds) {
final AutofillClient client = getClient();
- if (trackedIds != null && client != null) {
+ if (!ArrayUtils.isEmpty(trackedIds) && client != null) {
final boolean[] isVisible;
- if (client.isVisibleForAutofill()) {
- isVisible = client.getViewVisibility(getViewIds(trackedIds));
+ if (client.autofillClientIsVisibleForAutofill()) {
+ isVisible = client.autofillClientGetViewVisibility(trackedIds);
} else {
// All false
isVisible = new boolean[trackedIds.length];
@@ -2054,7 +2310,7 @@ public final class AutofillManager {
*/
void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) {
if (sDebug) {
- Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible="
+ Log.d(TAG, "notifyViewVisibilityChangedLocked(): id=" + id + " isVisible="
+ isVisible);
}
@@ -2083,9 +2339,9 @@ public final class AutofillManager {
/**
* Called once the client becomes visible.
*
- * @see AutofillClient#isVisibleForAutofill()
+ * @see AutofillClient#autofillClientIsVisibleForAutofill()
*/
- void onVisibleForAutofillLocked() {
+ void onVisibleForAutofillChangedLocked() {
// The visibility of the views might have changed while the client was not be visible,
// hence update the visibility state for all views.
AutofillClient client = getClient();
@@ -2095,8 +2351,8 @@ public final class AutofillManager {
if (mInvisibleTrackedIds != null) {
final ArrayList<AutofillId> orderedInvisibleIds =
new ArrayList<>(mInvisibleTrackedIds);
- final boolean[] isVisible = client.getViewVisibility(
- getViewIds(orderedInvisibleIds));
+ final boolean[] isVisible = client.autofillClientGetViewVisibility(
+ Helper.toArray(orderedInvisibleIds));
final int numInvisibleTrackedIds = orderedInvisibleIds.size();
for (int i = 0; i < numInvisibleTrackedIds; i++) {
@@ -2116,8 +2372,8 @@ public final class AutofillManager {
if (mVisibleTrackedIds != null) {
final ArrayList<AutofillId> orderedVisibleIds =
new ArrayList<>(mVisibleTrackedIds);
- final boolean[] isVisible = client.getViewVisibility(
- getViewIds(orderedVisibleIds));
+ final boolean[] isVisible = client.autofillClientGetViewVisibility(
+ Helper.toArray(orderedVisibleIds));
final int numVisibleTrackedIds = orderedVisibleIds.size();
for (int i = 0; i < numVisibleTrackedIds; i++) {
diff --git a/core/java/android/view/autofill/AutofillManagerInternal.java b/core/java/android/view/autofill/AutofillManagerInternal.java
index fc5d306ddcef..155fe721311c 100644
--- a/core/java/android/view/autofill/AutofillManagerInternal.java
+++ b/core/java/android/view/autofill/AutofillManagerInternal.java
@@ -15,6 +15,9 @@
*/
package android.view.autofill;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+
/**
* Autofill Manager local system service interface.
*
@@ -26,4 +29,14 @@ public abstract class AutofillManagerInternal {
* Notifies the manager that the back key was pressed.
*/
public abstract void onBackKeyPressed();
+
+ /**
+ * Gets whether compatibility mode is enabled for a package
+ *
+ * @param packageName The package for which to query.
+ * @param versionCode The package version code.
+ * @param userId The user id for which to query.
+ */
+ public abstract boolean isCompatibilityModeRequested(@NonNull String packageName,
+ long versionCode, @UserIdInt int userId);
}
diff --git a/core/java/android/view/autofill/Helper.java b/core/java/android/view/autofill/Helper.java
index 4b2c53c7eb84..48d0dbb3e02b 100644
--- a/core/java/android/view/autofill/Helper.java
+++ b/core/java/android/view/autofill/Helper.java
@@ -19,6 +19,8 @@ package android.view.autofill;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import java.util.Collection;
+
/** @hide */
public final class Helper {
@@ -59,6 +61,20 @@ public final class Helper {
builder.append(" ]");
}
+ /**
+ * Convers a collaction of {@link AutofillId AutofillIds} to an array.
+ * @param collection The collection.
+ * @return The array.
+ */
+ public static @NonNull AutofillId[] toArray(Collection<AutofillId> collection) {
+ if (collection == null) {
+ return new AutofillId[0];
+ }
+ final AutofillId[] array = new AutofillId[collection.size()];
+ collection.toArray(array);
+ return array;
+ }
+
private Helper() {
throw new UnsupportedOperationException("contains static members only");
}