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