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