diff options
| author | TreeHugger Robot <treehugger-gerrit@google.com> | 2018-02-05 09:55:29 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2018-02-05 09:55:29 +0000 |
| commit | 1bb9f29909016aa3be7ebe7ef52558d219696186 (patch) | |
| tree | 65b95a6eeea6173e8e23cb776653ae4b4496b91f /core/java/android | |
| parent | 1204834121070afea50fcc9b17a6604fcac4f3a8 (diff) | |
| parent | 24c90450fe3fe097a7bca51edd6a4cffd8fd13aa (diff) | |
Merge "Autofill compatibility mode."
Diffstat (limited to 'core/java/android')
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> <service android:name=".MyAutofillService" + * android:permission="android.permission.BIND_AUTOFILL_SERVICE"> + * <intent-filter> + * <action android:name="android.service.autofill.AutofillService" /> + * </intent-filter> + * <meta-data android:name="android.autofillservice" android:resource="@xml/autofillservice" /> + * </service></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> <autofill-service xmlns:android="http://schemas.android.com/apk/res/android"> + * <compatibility-package android:name="foo.bar.baz" android:maxLongVersionCode="1000000000"/> + * </autofill-service></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"); } |
