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