1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.accessibilityservice.cts; 18 19 import android.accessibilityservice.AccessibilityService; 20 import android.accessibilityservice.AccessibilityServiceInfo; 21 import android.app.Instrumentation; 22 import android.content.Context; 23 import android.os.Handler; 24 import android.os.SystemClock; 25 import android.provider.Settings; 26 import androidx.annotation.CallSuper; 27 import android.test.InstrumentationTestCase; 28 import android.view.accessibility.AccessibilityEvent; 29 import android.view.accessibility.AccessibilityManager; 30 31 import java.lang.ref.WeakReference; 32 import java.util.HashMap; 33 import java.util.List; 34 import java.util.concurrent.CountDownLatch; 35 import java.util.concurrent.TimeUnit; 36 37 import static junit.framework.Assert.assertFalse; 38 import static junit.framework.Assert.assertTrue; 39 40 public class InstrumentedAccessibilityService extends AccessibilityService { 41 42 private static final boolean DEBUG = false; 43 44 // Match com.android.server.accessibility.AccessibilityManagerService#COMPONENT_NAME_SEPARATOR 45 private static final String COMPONENT_NAME_SEPARATOR = ":"; 46 47 // Timeout disabled in #DEBUG mode to prevent breakpoint-related failures 48 private static final int TIMEOUT_SERVICE_ENABLE = DEBUG ? Integer.MAX_VALUE : 10000; 49 private static final int TIMEOUT_SERVICE_PERFORM_SYNC = DEBUG ? Integer.MAX_VALUE : 5000; 50 51 private static final HashMap<Class, WeakReference<InstrumentedAccessibilityService>> 52 sInstances = new HashMap<>(); 53 54 private final Handler mHandler = new Handler(); 55 final Object mInterruptWaitObject = new Object(); 56 public boolean mOnInterruptCalled; 57 58 59 @Override 60 @CallSuper 61 protected void onServiceConnected() { 62 synchronized (sInstances) { 63 sInstances.put(getClass(), new WeakReference<>(this)); 64 sInstances.notifyAll(); 65 } 66 } 67 68 @Override 69 public void onDestroy() { 70 synchronized (sInstances) { 71 sInstances.remove(getClass()); 72 } 73 } 74 75 @Override 76 public void onAccessibilityEvent(AccessibilityEvent event) { 77 // Stub method. 78 } 79 80 @Override 81 public void onInterrupt() { 82 synchronized (mInterruptWaitObject) { 83 mOnInterruptCalled = true; 84 mInterruptWaitObject.notifyAll(); 85 } 86 } 87 88 public void disableSelfAndRemove() { 89 disableSelf(); 90 91 synchronized (sInstances) { 92 sInstances.remove(getClass()); 93 } 94 } 95 96 public void runOnServiceSync(Runnable runner) { 97 final SyncRunnable sr = new SyncRunnable(runner, TIMEOUT_SERVICE_PERFORM_SYNC); 98 mHandler.post(sr); 99 assertTrue("Timed out waiting for runOnServiceSync()", sr.waitForComplete()); 100 } 101 102 public boolean wasOnInterruptCalled() { 103 synchronized (mInterruptWaitObject) { 104 return mOnInterruptCalled; 105 } 106 } 107 108 public Object getInterruptWaitObject() { 109 return mInterruptWaitObject; 110 } 111 112 private static final class SyncRunnable implements Runnable { 113 private final CountDownLatch mLatch = new CountDownLatch(1); 114 private final Runnable mTarget; 115 private final long mTimeout; 116 117 public SyncRunnable(Runnable target, long timeout) { 118 mTarget = target; 119 mTimeout = timeout; 120 } 121 122 public void run() { 123 mTarget.run(); 124 mLatch.countDown(); 125 } 126 127 public boolean waitForComplete() { 128 try { 129 return mLatch.await(mTimeout, TimeUnit.MILLISECONDS); 130 } catch (InterruptedException e) { 131 return false; 132 } 133 } 134 } 135 136 protected static <T extends InstrumentedAccessibilityService> T enableService( 137 Instrumentation instrumentation, Class<T> clazz) { 138 final String serviceName = clazz.getSimpleName(); 139 final Context context = instrumentation.getContext(); 140 final String enabledServices = Settings.Secure.getString( 141 context.getContentResolver(), 142 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 143 if (enabledServices != null) { 144 assertFalse("Service is already enabled", enabledServices.contains(serviceName)); 145 } 146 final AccessibilityManager manager = (AccessibilityManager) context.getSystemService( 147 Context.ACCESSIBILITY_SERVICE); 148 final List<AccessibilityServiceInfo> serviceInfos = 149 manager.getInstalledAccessibilityServiceList(); 150 for (AccessibilityServiceInfo serviceInfo : serviceInfos) { 151 final String serviceId = serviceInfo.getId(); 152 if (serviceId.endsWith(serviceName)) { 153 ShellCommandBuilder.create(instrumentation) 154 .putSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 155 enabledServices + COMPONENT_NAME_SEPARATOR + serviceId) 156 .putSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED, "1") 157 .run(); 158 159 final T instance = getInstanceForClass(clazz, TIMEOUT_SERVICE_ENABLE); 160 if (instance == null) { 161 ShellCommandBuilder.create(instrumentation) 162 .putSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 163 enabledServices) 164 .run(); 165 throw new RuntimeException("Starting accessibility service " + serviceName 166 + " took longer than " + TIMEOUT_SERVICE_ENABLE + "ms"); 167 } 168 return instance; 169 } 170 } 171 throw new RuntimeException("Accessibility service " + serviceName + " not found"); 172 } 173 174 private static <T extends InstrumentedAccessibilityService> T getInstanceForClass(Class clazz, 175 long timeoutMillis) { 176 final long timeoutTimeMillis = SystemClock.uptimeMillis() + timeoutMillis; 177 while (SystemClock.uptimeMillis() < timeoutTimeMillis) { 178 synchronized (sInstances) { 179 final WeakReference<InstrumentedAccessibilityService> ref = sInstances.get(clazz); 180 if (ref != null) { 181 final T instance = (T) ref.get(); 182 if (instance == null) { 183 sInstances.remove(clazz); 184 } else { 185 return instance; 186 } 187 } 188 try { 189 sInstances.wait(timeoutTimeMillis - SystemClock.uptimeMillis()); 190 } catch (InterruptedException e) { 191 return null; 192 } 193 } 194 } 195 return null; 196 } 197 198 public static void disableAllServices(Instrumentation instrumentation) { 199 final Object waitLockForA11yOff = new Object(); 200 final Context context = instrumentation.getContext(); 201 final AccessibilityManager manager = 202 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 203 manager.addAccessibilityStateChangeListener(b -> { 204 synchronized (waitLockForA11yOff) { 205 waitLockForA11yOff.notifyAll(); 206 } 207 }); 208 209 ShellCommandBuilder.create(instrumentation) 210 .deleteSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) 211 .deleteSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED) 212 .run(); 213 214 final long timeoutTimeMillis = SystemClock.uptimeMillis() + TIMEOUT_SERVICE_ENABLE; 215 while (SystemClock.uptimeMillis() < timeoutTimeMillis) { 216 synchronized (waitLockForA11yOff) { 217 if (manager.getEnabledAccessibilityServiceList( 218 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty()) { 219 return; 220 } 221 try { 222 waitLockForA11yOff.wait(timeoutTimeMillis - SystemClock.uptimeMillis()); 223 } catch (InterruptedException e) { 224 // Ignored; loop again 225 } 226 } 227 } 228 throw new RuntimeException("Disabling all accessibility services took longer than " 229 + TIMEOUT_SERVICE_ENABLE + "ms"); 230 } 231 } 232