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.accessibility.cts.common; 18 19 import static com.android.compatibility.common.util.TestUtils.waitOn; 20 21 import static junit.framework.Assert.assertFalse; 22 import static junit.framework.Assert.assertTrue; 23 24 import android.accessibilityservice.AccessibilityService; 25 import android.accessibilityservice.AccessibilityServiceInfo; 26 import android.app.Instrumentation; 27 import android.app.UiAutomation; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.os.Handler; 31 import android.os.SystemClock; 32 import android.provider.Settings; 33 import android.util.Log; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.view.accessibility.AccessibilityManager; 36 37 import androidx.annotation.CallSuper; 38 import androidx.test.platform.app.InstrumentationRegistry; 39 40 import java.lang.ref.WeakReference; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.concurrent.Callable; 44 import java.util.concurrent.CountDownLatch; 45 import java.util.concurrent.TimeUnit; 46 import java.util.concurrent.atomic.AtomicBoolean; 47 import java.util.concurrent.atomic.AtomicReference; 48 49 public class InstrumentedAccessibilityService extends AccessibilityService { 50 private static final String LOG_TAG = "InstrumentedA11yService"; 51 52 private static final boolean DEBUG = false; 53 54 // Match com.android.server.accessibility.AccessibilityManagerService#COMPONENT_NAME_SEPARATOR 55 private static final String COMPONENT_NAME_SEPARATOR = ":"; 56 private static final int TIMEOUT_SERVICE_PERFORM_SYNC = DEBUG ? Integer.MAX_VALUE : 10000; 57 58 private static final HashMap<Class, WeakReference<InstrumentedAccessibilityService>> 59 sInstances = new HashMap<>(); 60 61 private final Handler mHandler = new Handler(); 62 final Object mInterruptWaitObject = new Object(); 63 64 public boolean mOnInterruptCalled; 65 66 // Timeout disabled in #DEBUG mode to prevent breakpoint-related failures 67 public static final int TIMEOUT_SERVICE_ENABLE = DEBUG ? Integer.MAX_VALUE : 10000; 68 69 @Override 70 @CallSuper onServiceConnected()71 protected void onServiceConnected() { 72 synchronized (sInstances) { 73 sInstances.put(getClass(), new WeakReference<>(this)); 74 sInstances.notifyAll(); 75 } 76 Log.v(LOG_TAG, "onServiceConnected [" + this + "]"); 77 } 78 79 @Override onUnbind(Intent intent)80 public boolean onUnbind(Intent intent) { 81 Log.v(LOG_TAG, "onUnbind [" + this + "]"); 82 return false; 83 } 84 85 @Override onDestroy()86 public void onDestroy() { 87 synchronized (sInstances) { 88 sInstances.remove(getClass()); 89 } 90 Log.v(LOG_TAG, "onDestroy [" + this + "]"); 91 } 92 93 @Override onAccessibilityEvent(AccessibilityEvent event)94 public void onAccessibilityEvent(AccessibilityEvent event) { 95 // Stub method. 96 } 97 98 @Override onInterrupt()99 public void onInterrupt() { 100 synchronized (mInterruptWaitObject) { 101 mOnInterruptCalled = true; 102 mInterruptWaitObject.notifyAll(); 103 } 104 } 105 disableSelfAndRemove()106 public void disableSelfAndRemove() { 107 disableSelf(); 108 109 synchronized (sInstances) { 110 sInstances.remove(getClass()); 111 } 112 } 113 runOnServiceSync(Runnable runner)114 public void runOnServiceSync(Runnable runner) { 115 final SyncRunnable sr = new SyncRunnable(runner, TIMEOUT_SERVICE_PERFORM_SYNC); 116 mHandler.post(sr); 117 assertTrue("Timed out waiting for runOnServiceSync()", sr.waitForComplete()); 118 } 119 getOnService(Callable<T> callable)120 public <T extends Object> T getOnService(Callable<T> callable) { 121 AtomicReference<T> returnValue = new AtomicReference<>(null); 122 AtomicReference<Throwable> throwable = new AtomicReference<>(null); 123 runOnServiceSync( 124 () -> { 125 try { 126 returnValue.set(callable.call()); 127 } catch (Throwable e) { 128 throwable.set(e); 129 } 130 }); 131 if (throwable.get() != null) { 132 throw new RuntimeException(throwable.get()); 133 } 134 return returnValue.get(); 135 } 136 wasOnInterruptCalled()137 public boolean wasOnInterruptCalled() { 138 synchronized (mInterruptWaitObject) { 139 return mOnInterruptCalled; 140 } 141 } 142 getInterruptWaitObject()143 public Object getInterruptWaitObject() { 144 return mInterruptWaitObject; 145 } 146 147 private static final class SyncRunnable implements Runnable { 148 private final CountDownLatch mLatch = new CountDownLatch(1); 149 private final Runnable mTarget; 150 private final long mTimeout; 151 SyncRunnable(Runnable target, long timeout)152 public SyncRunnable(Runnable target, long timeout) { 153 mTarget = target; 154 mTimeout = timeout; 155 } 156 run()157 public void run() { 158 mTarget.run(); 159 mLatch.countDown(); 160 } 161 waitForComplete()162 public boolean waitForComplete() { 163 try { 164 return mLatch.await(mTimeout, TimeUnit.MILLISECONDS); 165 } catch (InterruptedException e) { 166 return false; 167 } 168 } 169 } 170 171 /** 172 * Enables the service. 173 * 174 * <p> This behaves like {@link #enableService(Class)} except it simply runs the shell command 175 * to enable the service and does not wait for {@link AccessibilityService#onServiceConnected()} 176 * to be called. 177 */ enableServiceWithoutWait(Class clazz, Instrumentation instrumentation, String enabledServices)178 public static void enableServiceWithoutWait(Class clazz, Instrumentation instrumentation, 179 String enabledServices) { 180 final String serviceName = clazz.getSimpleName(); 181 if (enabledServices != null) { 182 assertFalse("Service is already enabled", enabledServices.contains(serviceName)); 183 } 184 final AccessibilityManager manager = 185 (AccessibilityManager) instrumentation.getContext() 186 .getSystemService(Context.ACCESSIBILITY_SERVICE); 187 final List<AccessibilityServiceInfo> serviceInfos = 188 manager.getInstalledAccessibilityServiceList(); 189 for (AccessibilityServiceInfo serviceInfo : serviceInfos) { 190 final String serviceId = serviceInfo.getId(); 191 if (serviceId.endsWith(serviceName)) { 192 ShellCommandBuilder.create(instrumentation) 193 .putSecureSetting( 194 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 195 enabledServices + COMPONENT_NAME_SEPARATOR + serviceId) 196 .putSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED, "1") 197 .run(); 198 return; 199 } 200 } 201 throw new RuntimeException("Accessibility service " + serviceName + " not found"); 202 } 203 204 /** 205 * Enables and returns the service. 206 */ enableService( Class<T> clazz)207 public static <T extends InstrumentedAccessibilityService> T enableService( 208 Class<T> clazz) { 209 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 210 final String enabledServices = 211 Settings.Secure.getString( 212 instrumentation.getContext().getContentResolver(), 213 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 214 215 enableServiceWithoutWait(clazz, instrumentation, enabledServices); 216 217 final T instance = getInstanceForClass(clazz, TIMEOUT_SERVICE_ENABLE); 218 if (instance == null) { 219 ShellCommandBuilder.create(instrumentation) 220 .putSecureSetting( 221 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices) 222 .run(); 223 throw new RuntimeException( 224 "Starting accessibility service " 225 + clazz.getSimpleName() 226 + " took longer than " 227 + TIMEOUT_SERVICE_ENABLE 228 + "ms"); 229 } 230 return instance; 231 } 232 getInstanceForClass( Class<T> clazz, long timeoutMillis)233 public static <T extends InstrumentedAccessibilityService> T getInstanceForClass( 234 Class<T> clazz, long timeoutMillis) { 235 final long timeoutTimeMillis = SystemClock.uptimeMillis() + timeoutMillis; 236 while (SystemClock.uptimeMillis() < timeoutTimeMillis) { 237 synchronized (sInstances) { 238 final T instance = getInstanceForClass(clazz); 239 if (instance != null) { 240 return instance; 241 } 242 try { 243 sInstances.wait(timeoutTimeMillis - SystemClock.uptimeMillis()); 244 } catch (InterruptedException e) { 245 return null; 246 } 247 } 248 } 249 return null; 250 } 251 getInstanceForClass( Class<T> clazz)252 static <T extends InstrumentedAccessibilityService> T getInstanceForClass( 253 Class<T> clazz) { 254 synchronized (sInstances) { 255 final WeakReference<InstrumentedAccessibilityService> ref = sInstances.get(clazz); 256 if (ref != null) { 257 final T instance = (T) ref.get(); 258 if (instance == null) { 259 sInstances.remove(clazz); 260 } else { 261 return instance; 262 } 263 } 264 } 265 return null; 266 } 267 disableAllServices()268 public static void disableAllServices() { 269 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 270 final Object waitLockForA11yOff = new Object(); 271 final Context context = instrumentation.getContext(); 272 final AccessibilityManager manager = 273 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 274 // Updates to manager.isEnabled() aren't synchronized 275 final AtomicBoolean accessibilityEnabled = new AtomicBoolean(manager.isEnabled()); 276 manager.addAccessibilityStateChangeListener( 277 b -> { 278 synchronized (waitLockForA11yOff) { 279 waitLockForA11yOff.notifyAll(); 280 accessibilityEnabled.set(b); 281 } 282 }); 283 final UiAutomation uiAutomation = instrumentation.getUiAutomation( 284 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); 285 ShellCommandBuilder.create(uiAutomation) 286 .deleteSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) 287 .deleteSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED) 288 .run(); 289 uiAutomation.destroy(); 290 291 waitOn(waitLockForA11yOff, () -> !accessibilityEnabled.get(), TIMEOUT_SERVICE_ENABLE, 292 "Accessibility turns off"); 293 } 294 } 295