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 enableService( Class<T> clazz)171 public static <T extends InstrumentedAccessibilityService> T enableService( 172 Class<T> clazz) { 173 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 174 final String serviceName = clazz.getSimpleName(); 175 final Context context = instrumentation.getContext(); 176 final String enabledServices = 177 Settings.Secure.getString( 178 context.getContentResolver(), 179 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 180 if (enabledServices != null) { 181 assertFalse("Service is already enabled", enabledServices.contains(serviceName)); 182 } 183 final AccessibilityManager manager = 184 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 185 final List<AccessibilityServiceInfo> serviceInfos = 186 manager.getInstalledAccessibilityServiceList(); 187 for (AccessibilityServiceInfo serviceInfo : serviceInfos) { 188 final String serviceId = serviceInfo.getId(); 189 if (serviceId.endsWith(serviceName)) { 190 ShellCommandBuilder.create(instrumentation) 191 .putSecureSetting( 192 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 193 enabledServices + COMPONENT_NAME_SEPARATOR + serviceId) 194 .putSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED, "1") 195 .run(); 196 197 final T instance = getInstanceForClass(clazz, TIMEOUT_SERVICE_ENABLE); 198 if (instance == null) { 199 ShellCommandBuilder.create(instrumentation) 200 .putSecureSetting( 201 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices) 202 .run(); 203 throw new RuntimeException( 204 "Starting accessibility service " 205 + serviceName 206 + " took longer than " 207 + TIMEOUT_SERVICE_ENABLE 208 + "ms"); 209 } 210 return instance; 211 } 212 } 213 throw new RuntimeException("Accessibility service " + serviceName + " not found"); 214 } 215 getInstanceForClass( Class<T> clazz, long timeoutMillis)216 public static <T extends InstrumentedAccessibilityService> T getInstanceForClass( 217 Class<T> clazz, long timeoutMillis) { 218 final long timeoutTimeMillis = SystemClock.uptimeMillis() + timeoutMillis; 219 while (SystemClock.uptimeMillis() < timeoutTimeMillis) { 220 synchronized (sInstances) { 221 final T instance = getInstanceForClass(clazz); 222 if (instance != null) { 223 return instance; 224 } 225 try { 226 sInstances.wait(timeoutTimeMillis - SystemClock.uptimeMillis()); 227 } catch (InterruptedException e) { 228 return null; 229 } 230 } 231 } 232 return null; 233 } 234 getInstanceForClass( Class<T> clazz)235 static <T extends InstrumentedAccessibilityService> T getInstanceForClass( 236 Class<T> clazz) { 237 synchronized (sInstances) { 238 final WeakReference<InstrumentedAccessibilityService> ref = sInstances.get(clazz); 239 if (ref != null) { 240 final T instance = (T) ref.get(); 241 if (instance == null) { 242 sInstances.remove(clazz); 243 } else { 244 return instance; 245 } 246 } 247 } 248 return null; 249 } 250 disableAllServices()251 public static void disableAllServices() { 252 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 253 final Object waitLockForA11yOff = new Object(); 254 final Context context = instrumentation.getContext(); 255 final AccessibilityManager manager = 256 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 257 // Updates to manager.isEnabled() aren't synchronized 258 final AtomicBoolean accessibilityEnabled = new AtomicBoolean(manager.isEnabled()); 259 manager.addAccessibilityStateChangeListener( 260 b -> { 261 synchronized (waitLockForA11yOff) { 262 waitLockForA11yOff.notifyAll(); 263 accessibilityEnabled.set(b); 264 } 265 }); 266 final UiAutomation uiAutomation = instrumentation.getUiAutomation( 267 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); 268 ShellCommandBuilder.create(uiAutomation) 269 .deleteSecureSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) 270 .deleteSecureSetting(Settings.Secure.ACCESSIBILITY_ENABLED) 271 .run(); 272 uiAutomation.destroy(); 273 274 waitOn(waitLockForA11yOff, () -> !accessibilityEnabled.get(), TIMEOUT_SERVICE_ENABLE, 275 "Accessibility turns off"); 276 } 277 } 278