From a4f4a831d61f4a891cbed95bf9df3ac291826ae2 Mon Sep 17 00:00:00 2001 From: sallyyuen Date: Fri, 26 Aug 2022 09:59:11 -0700 Subject: [PATCH] [EXO] Create a service connection for proxies A proxy will give a privileged app access to a11y data typically available to AccessibilityServices. (Refer to the design doc linked in the comments for more information.) A proxy will easily be able to set modify a list of service infos depending on what a11y services/features are running on the client device. Introduce the system connection for a proxy: - The service connection needs to maintain a single info that represents this proxy-supplied list, so merge list values. - Populate properties for each listed info that are default populated for services installed on the device. This will prevent exceptions if an app accesses these properties. - Make setAccessibilityTool a SystemAPI, so a proxy can set this if it needs to, since it cannot set this before runtime. Test: builds, atest ProxyAccessibilityServiceConnectionTest Bug: 241429275 Change-Id: Id99e2fa08be567738751d7a9a84f0014279924a1 --- core/api/system-current.txt | 8 + core/api/test-current.txt | 1 - .../AccessibilityServiceInfo.java | 10 +- .../ProxyAccessibilityServiceConnection.java | 472 ++++++++++++++++++ ...oxyAccessibilityServiceConnectionTest.java | 211 ++++++++ 5 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java create mode 100644 services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 134b71a492724..01d7a1ac1920f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -462,6 +462,14 @@ package android { } +package android.accessibilityservice { + + public class AccessibilityServiceInfo implements android.os.Parcelable { + method public void setAccessibilityTool(boolean); + } + +} + package android.accounts { public class AccountManager { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index bdd9ec7614b4e..958dab314a842 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -89,7 +89,6 @@ package android.accessibilityservice { public class AccessibilityServiceInfo implements android.os.Parcelable { method @NonNull public android.content.ComponentName getComponentName(); - method public void setAccessibilityTool(boolean); } } diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 0a99c367c80b3..295eaafedfd33 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -24,6 +24,7 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -809,6 +810,13 @@ public class AccessibilityServiceInfo implements Parcelable { mComponentName = component; } + /** + * @hide + */ + public void setResolveInfo(@NonNull ResolveInfo resolveInfo) { + mResolveInfo = resolveInfo; + } + /** * @hide */ @@ -1128,7 +1136,7 @@ public class AccessibilityServiceInfo implements Parcelable { * * @hide */ - @TestApi + @SystemApi public void setAccessibilityTool(boolean isAccessibilityTool) { mIsAccessibilityTool = isAccessibilityTool; } diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java new file mode 100644 index 0000000000000..934b665d7dae0 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.AccessibilityTrace; +import android.accessibilityservice.MagnificationConfig; +import android.annotation.NonNull; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ParceledListSlice; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.graphics.Region; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.annotation.Nullable; + +import com.android.server.wm.WindowManagerInternal; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Represents the system connection to an {@link android.view.accessibility.AccessibilityProxy}. + * + *

Most methods are no-ops since this connection does not need to capture input or listen to + * hardware-related changes. + * + * TODO(241429275): Initialize this when a proxy is registered. + */ +public class ProxyAccessibilityServiceConnection extends AccessibilityServiceConnection { + // Names used to populate ComponentName and ResolveInfo + private static final String PROXY_COMPONENT_PACKAGE_NAME = "ProxyPackage"; + private static final String PROXY_COMPONENT_CLASS_NAME = "ProxyClass"; + + private int mDisplayId; + private List mInstalledAndEnabledServices; + + ProxyAccessibilityServiceConnection( + Context context, + ComponentName componentName, + AccessibilityServiceInfo accessibilityServiceInfo, int id, + Handler mainHandler, Object lock, + AccessibilitySecurityPolicy securityPolicy, + SystemSupport systemSupport, AccessibilityTrace trace, + WindowManagerInternal windowManagerInternal, + AccessibilityWindowManager awm, int displayId) { + super(/* userState= */null, context, componentName, accessibilityServiceInfo, id, + mainHandler, lock, securityPolicy, systemSupport, trace, windowManagerInternal, + /* systemActionPerformer= */ null, awm, /* activityTaskManagerService= */ null); + mDisplayId = displayId; + } + + /** + * Keeps mAccessibilityServiceInfo in sync with the proxy's list of AccessibilityServiceInfos. + * + *

This also sets the properties that are assumed to be populated by installed packages. + * + * @param infos the list of enabled and installed services. + */ + @Override + public void setInstalledAndEnabledServices(List infos) { + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + mInstalledAndEnabledServices = infos; + final AccessibilityServiceInfo proxyInfo = mAccessibilityServiceInfo; + // Reset values. + proxyInfo.flags = 0; + proxyInfo.eventTypes = 0; + proxyInfo.notificationTimeout = 0; + final Set packageNames = new HashSet<>(); + boolean hasNullPackagesNames = false; + boolean isAccessibilityTool = false; + int interactiveUiTimeout = 0; + int nonInteractiveUiTimeout = 0; + + // Go through and set properties that are relevant to the proxy. This bypasses + // A11yServiceInfo.updateDynamicallyConfigurableProperties since the proxy has + // higher security privileges as a SystemAPI and has to set values at runtime. + for (AccessibilityServiceInfo info : infos) { + isAccessibilityTool = isAccessibilityTool | info.isAccessibilityTool(); + if (info.packageNames == null || info.packageNames.length == 0) { + hasNullPackagesNames = true; + } else if (!hasNullPackagesNames) { + packageNames.addAll(Arrays.asList(info.packageNames)); + } + interactiveUiTimeout = Math.max(interactiveUiTimeout, + info.getInteractiveUiTimeoutMillis()); + nonInteractiveUiTimeout = Math.max(nonInteractiveUiTimeout, + info.getNonInteractiveUiTimeoutMillis()); + proxyInfo.notificationTimeout = Math.max(proxyInfo.notificationTimeout, + info.notificationTimeout); + proxyInfo.eventTypes |= info.eventTypes; + proxyInfo.feedbackType |= info.feedbackType; + proxyInfo.flags |= info.flags; + // For each info, populate default properties like ResolveInfo. + setDefaultPropertiesIfNullLocked(info); + } + + proxyInfo.setAccessibilityTool(isAccessibilityTool); + proxyInfo.setInteractiveUiTimeoutMillis(interactiveUiTimeout); + proxyInfo.setNonInteractiveUiTimeoutMillis(nonInteractiveUiTimeout); + + // If any one service info doesn't set package names, i.e. if it's interested in all + // apps, the proxy shouldn't filter by package name even if some infos specify this. + if (hasNullPackagesNames) { + proxyInfo.packageNames = null; + } else { + proxyInfo.packageNames = packageNames.toArray(new String[0]); + } + + // Update connection with mAccessibilityServiceInfo values. + setDynamicallyConfigurableProperties(proxyInfo); + // Notify manager service. + mSystemSupport.onClientChangeLocked(true); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private void setDefaultPropertiesIfNullLocked(AccessibilityServiceInfo info) { + final String componentClassDisplayName = PROXY_COMPONENT_CLASS_NAME + mDisplayId; + // Populate the properties that can't be null, since this may cause crashes in apps that + // assume these are populated by an installed package. + if (info.getResolveInfo() == null) { + final ResolveInfo resolveInfo = new ResolveInfo(); + final ServiceInfo serviceInfo = new ServiceInfo(); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + + serviceInfo.packageName = PROXY_COMPONENT_PACKAGE_NAME; + serviceInfo.name = componentClassDisplayName; + + applicationInfo.processName = PROXY_COMPONENT_PACKAGE_NAME; + applicationInfo.className = componentClassDisplayName; + + resolveInfo.serviceInfo = serviceInfo; + serviceInfo.applicationInfo = applicationInfo; + info.setResolveInfo(resolveInfo); + } + + if (info.getComponentName() == null) { + info.setComponentName(new ComponentName(PROXY_COMPONENT_PACKAGE_NAME, + componentClassDisplayName)); + } + } + + @Override + public List getInstalledAndEnabledServices() { + synchronized (mLock) { + return mInstalledAndEnabledServices; + } + } + + @Override + public void binderDied() { + } + + @Override + protected boolean supportsFlagForNotImportantViews(AccessibilityServiceInfo info) { + // Don't need to check for earlier APIs. + return true; + } + + @Override + protected boolean hasRightsToCurrentUserLocked() { + // TODO(250929565): Proxy access is not currently determined by user. Adjust in refactoring. + return true; + } + + /** @throws UnsupportedOperationException since a proxy does not need key events */ + @Override + public boolean onKeyEvent(KeyEvent keyEvent, int sequenceNumber) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("onKeyEvent is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need fingerprint hardware */ + @Override + public boolean isCapturingFingerprintGestures() throws UnsupportedOperationException { + throw new UnsupportedOperationException("isCapturingFingerprintGestures is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need fingerprint hardware */ + @Override + public void onFingerprintGestureDetectionActiveChanged(boolean active) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("onFingerprintGestureDetectionActiveChanged is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need fingerprint hardware */ + @Override + public void onFingerprintGesture(int gesture) throws UnsupportedOperationException { + throw new UnsupportedOperationException("onFingerprintGesture is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need fingerprint hardware */ + @Override + public boolean isFingerprintGestureDetectionAvailable() throws UnsupportedOperationException { + throw new UnsupportedOperationException("isFingerprintGestureDetectionAvailable is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy is not a Service */ + @Override + public void onServiceConnected(ComponentName name, IBinder service) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("onServiceConnected is not supported"); + + } + + /** @throws UnsupportedOperationException since a proxy is not a Service */ + @Override + public void onServiceDisconnected(ComponentName name) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("onServiceDisconnected is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy should use + * setInstalledAndEnabledServices*/ + @Override + public void setServiceInfo(AccessibilityServiceInfo info) + throws UnsupportedOperationException { + // TODO(241429275): Ensure getServiceInfo is called appropriately for a proxy or is a no-op. + throw new UnsupportedOperationException("setServiceInfo is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy should use A11yManager#unregister */ + @Override + public void disableSelf() throws UnsupportedOperationException { + // A proxy uses A11yManager#unregister to turn itself off. + throw new UnsupportedOperationException("disableSelf is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not have global system access */ + @Override + public boolean performGlobalAction(int action) throws UnsupportedOperationException { + throw new UnsupportedOperationException("performGlobalAction is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need key events */ + @Override + public void setOnKeyEventResult(boolean handled, int sequence) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setOnKeyEventResult is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not have global system access */ + @Override + public @NonNull List getSystemActions() + throws UnsupportedOperationException { + throw new UnsupportedOperationException("getSystemActions is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Nullable + @Override + public MagnificationConfig getMagnificationConfig(int displayId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("getMagnificationConfig is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public float getMagnificationScale(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("getMagnificationScale is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public float getMagnificationCenterX(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("getMagnificationCenterX is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public float getMagnificationCenterY(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("getMagnificationCenterY is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public Region getMagnificationRegion(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("getMagnificationRegion is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public Region getCurrentMagnificationRegion(int displayId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("getCurrentMagnificationRegion is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public boolean resetMagnification(int displayId, boolean animate) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("resetMagnification is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public boolean resetCurrentMagnification(int displayId, boolean animate) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("resetCurrentMagnification is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public boolean setMagnificationConfig(int displayId, + @androidx.annotation.NonNull MagnificationConfig config, boolean animate) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setMagnificationConfig is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public void setMagnificationCallbackEnabled(int displayId, boolean enabled) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setMagnificationCallbackEnabled is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @Override + public boolean isMagnificationCallbackEnabled(int displayId) { + throw new UnsupportedOperationException("isMagnificationCallbackEnabled is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need IME access*/ + @Override + public boolean setSoftKeyboardShowMode(int showMode) throws UnsupportedOperationException { + throw new UnsupportedOperationException("setSoftKeyboardShowMode is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need IME access */ + @Override + public int getSoftKeyboardShowMode() throws UnsupportedOperationException { + throw new UnsupportedOperationException("getSoftKeyboardShowMode is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need IME access */ + @Override + public void setSoftKeyboardCallbackEnabled(boolean enabled) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setSoftKeyboardCallbackEnabled is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need IME access */ + @Override + public boolean switchToInputMethod(String imeId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("switchToInputMethod is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need IME access */ + @Override + public int setInputMethodEnabled(String imeId, boolean enabled) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setInputMethodEnabled is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need access to the shortcut */ + @Override + public boolean isAccessibilityButtonAvailable() throws UnsupportedOperationException { + throw new UnsupportedOperationException("isAccessibilityButtonAvailable is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need gestures/input access */ + @Override + public void sendGesture(int sequence, ParceledListSlice gestureSteps) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("sendGesture is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need gestures/input access */ + @Override + public void dispatchGesture(int sequence, ParceledListSlice gestureSteps, int displayId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("dispatchGesture is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need access to screenshots */ + @Override + public void takeScreenshot(int displayId, RemoteCallback callback) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("takeScreenshot is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need gestures/input access */ + @Override + public void setGestureDetectionPassthroughRegion(int displayId, Region region) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setGestureDetectionPassthroughRegion is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need gestures/input access */ + @Override + public void setTouchExplorationPassthroughRegion(int displayId, Region region) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setTouchExplorationPassthroughRegion is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need gestures/input access */ + @Override + public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("setServiceDetectsGesturesEnabled is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need touch input access */ + @Override + public void requestTouchExploration(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("requestTouchExploration is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need touch input access */ + @Override + public void requestDragging(int displayId, int pointerId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("requestDragging is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need touch input access */ + @Override + public void requestDelegating(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("requestDelegating is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need touch input access */ + @Override + public void onDoubleTap(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("onDoubleTap is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need touch input access */ + @Override + public void onDoubleTapAndHold(int displayId) throws UnsupportedOperationException { + throw new UnsupportedOperationException("onDoubleTapAndHold is not supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need touch input access */ + @Override + public void setAnimationScale(float scale) throws UnsupportedOperationException { + throw new UnsupportedOperationException("setAnimationScale is not supported"); + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java new file mode 100644 index 0000000000000..c84c2c287679c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyAccessibilityServiceConnectionTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_AUDIBLE; +import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.AccessibilityTrace; +import android.content.ComponentName; +import android.content.Context; +import android.os.Handler; +import android.view.accessibility.AccessibilityEvent; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verify; + +import com.android.server.wm.WindowManagerInternal; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +public class ProxyAccessibilityServiceConnectionTest { + private static final int DISPLAY_ID = 1000; + private static final int CONNECTION_ID = 1000; + private static final ComponentName COMPONENT_NAME = new ComponentName( + "com.android.server.accessibility", ".ProxyAccessibilityServiceConnectionTest"); + public static final int NON_INTERACTIVE_UI_TIMEOUT_100MS = 100; + public static final int NON_INTERACTIVE_UI_TIMEOUT_200MS = 200; + public static final int INTERACTIVE_UI_TIMEOUT_100MS = 100; + public static final int INTERACTIVE_UI_TIMEOUT_200MS = 200; + public static final int NOTIFICATION_TIMEOUT_100MS = 100; + public static final int NOTIFICATION_TIMEOUT_200MS = 200; + public static final String PACKAGE_1 = "package 1"; + public static final String PACKAGE_2 = "package 2"; + public static final String PACKAGE_3 = "package 3"; + + @Mock + Context mMockContext; + @Mock + Object mMockLock; + @Mock + AccessibilitySecurityPolicy mMockSecurityPolicy; + @Mock + AccessibilityWindowManager mMockA11yWindowManager; + @Mock AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport; + @Mock + AccessibilityTrace mMockA11yTrace; + @Mock + WindowManagerInternal mMockWindowManagerInternal; + ProxyAccessibilityServiceConnection mProxyConnection; + AccessibilityServiceInfo mAccessibilityServiceInfo; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mAccessibilityServiceInfo = new AccessibilityServiceInfo(); + mProxyConnection = new ProxyAccessibilityServiceConnection(mMockContext, COMPONENT_NAME, + mAccessibilityServiceInfo, CONNECTION_ID , new Handler( + getInstrumentation().getContext().getMainLooper()), + mMockLock, mMockSecurityPolicy, mMockSystemSupport, mMockA11yTrace, + mMockWindowManagerInternal, mMockA11yWindowManager, DISPLAY_ID); + } + + @Test + public void testSetInstalledAndEnabledServices_clientChanged() { + final List infos = new ArrayList<>(); + final AccessibilityServiceInfo info1 = new AccessibilityServiceInfo(); + infos.add(info1); + + mProxyConnection.setInstalledAndEnabledServices(infos); + + verify(mMockSystemSupport).onClientChangeLocked(true); + } + + @Test + public void testSetInstalledAndEnabledServices_returnList() { + final List infos = new ArrayList<>(); + final AccessibilityServiceInfo info1 = new AccessibilityServiceInfo(); + final AccessibilityServiceInfo info2 = new AccessibilityServiceInfo(); + infos.add(info1); + infos.add(info2); + + mProxyConnection.setInstalledAndEnabledServices(infos); + final List infoList = + mProxyConnection.getInstalledAndEnabledServices(); + + assertThat(infoList.size()).isEqualTo(2); + assertThat(infoList.get(0)).isEqualTo(info1); + assertThat(infoList.get(1)).isEqualTo(info2); + } + + @Test + public void testSetInstalledAndEnabledServices_defaultNamesPopulated() { + final List infos = new ArrayList<>(); + final AccessibilityServiceInfo info1 = new AccessibilityServiceInfo(); + infos.add(info1); + + mProxyConnection.setInstalledAndEnabledServices(infos); + final List infoList = + mProxyConnection.getInstalledAndEnabledServices(); + + assertThat(infoList.get(0).getComponentName()).isNotNull(); + assertThat(infoList.get(0).getResolveInfo()).isNotNull(); + } + + @Test + public void testSetInstalledAndEnabledServices_connectionInfoIsUnion() { + final List infos = new ArrayList<>(); + final AccessibilityServiceInfo info1 = new AccessibilityServiceInfo(); + info1.setAccessibilityTool(true); + info1.packageNames = new String[]{PACKAGE_1, PACKAGE_2}; + info1.setInteractiveUiTimeoutMillis(INTERACTIVE_UI_TIMEOUT_200MS); + info1.setNonInteractiveUiTimeoutMillis(NON_INTERACTIVE_UI_TIMEOUT_100MS); + info1.notificationTimeout = NOTIFICATION_TIMEOUT_100MS; + info1.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED; + info1.feedbackType = FEEDBACK_AUDIBLE; + info1.flags = FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + infos.add(info1); + + final AccessibilityServiceInfo info2 = new AccessibilityServiceInfo(); + info2.packageNames = new String[]{PACKAGE_2, PACKAGE_3}; + info2.setInteractiveUiTimeoutMillis(INTERACTIVE_UI_TIMEOUT_100MS); + info2.setNonInteractiveUiTimeoutMillis(NON_INTERACTIVE_UI_TIMEOUT_200MS); + info2.notificationTimeout = NOTIFICATION_TIMEOUT_200MS; + info2.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; + info2.feedbackType = FEEDBACK_GENERIC; + info2.flags = FLAG_REQUEST_TOUCH_EXPLORATION_MODE | FLAG_REPORT_VIEW_IDS; + infos.add(info2); + + mProxyConnection.setInstalledAndEnabledServices(infos); + + assertThat(mAccessibilityServiceInfo.isAccessibilityTool()).isTrue(); + // Package 1, 2, 3 + assertThat(mAccessibilityServiceInfo.packageNames).asList().containsExactly( + PACKAGE_1, PACKAGE_2, PACKAGE_3); + assertThat(mAccessibilityServiceInfo.getInteractiveUiTimeoutMillis()).isEqualTo( + INTERACTIVE_UI_TIMEOUT_200MS); + assertThat(mAccessibilityServiceInfo.getNonInteractiveUiTimeoutMillis()).isEqualTo( + NON_INTERACTIVE_UI_TIMEOUT_200MS); + assertThat(mAccessibilityServiceInfo.notificationTimeout).isEqualTo( + NOTIFICATION_TIMEOUT_200MS); + assertThat(mAccessibilityServiceInfo.eventTypes).isEqualTo( + AccessibilityEvent.TYPES_ALL_MASK); + assertThat(mAccessibilityServiceInfo.feedbackType).isEqualTo(FEEDBACK_AUDIBLE + | FEEDBACK_GENERIC); + assertThat(mAccessibilityServiceInfo.flags).isEqualTo(FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + | FLAG_REQUEST_TOUCH_EXPLORATION_MODE | FLAG_REPORT_VIEW_IDS); + } + + @Test + public void testSetInstalledAndEnabledServices_emptyPackageNames_packageNamesIsNull() { + final List infos = new ArrayList<>(); + final AccessibilityServiceInfo info1 = new AccessibilityServiceInfo(); + info1.packageNames = new String[]{PACKAGE_1, PACKAGE_2}; + infos.add(info1); + final AccessibilityServiceInfo info2 = new AccessibilityServiceInfo(); + infos.add(info2); + + mProxyConnection.setInstalledAndEnabledServices(infos); + + final String[] packageNames = mAccessibilityServiceInfo.packageNames; + assertThat(packageNames).isNull(); + } + + @Test + public void testSetServiceInfo_setIllegalOperationExceptionThrown_() { + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> mProxyConnection.setServiceInfo(new AccessibilityServiceInfo())); + + assertThat(thrown).hasMessageThat().contains("setServiceInfo is not supported"); + } + + @Test + public void testDisableSelf_setIllegalOperationExceptionThrown_() { + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> mProxyConnection.disableSelf()); + + assertThat(thrown).hasMessageThat().contains("disableSelf is not supported"); + } +}