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