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