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