1 /*
2  * Copyright (C) 2022 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.accessibilityservice.cts;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.fail;
22 
23 import android.accessibility.cts.common.InstrumentedAccessibilityService;
24 import android.accessibilityservice.AccessibilityServiceInfo;
25 import android.app.UiAutomation;
26 import android.os.SystemClock;
27 import android.view.MotionEvent;
28 
29 import androidx.annotation.NonNull;
30 
31 import com.android.compatibility.common.util.TestUtils;
32 
33 import java.util.concurrent.atomic.AtomicBoolean;
34 import java.util.function.Consumer;
35 
36 public class StubMotionInterceptingAccessibilityService extends InstrumentedAccessibilityService {
37     private Consumer<MotionEvent> mMotionEventListener;
38 
setMotionEventSources(int sources)39     public void setMotionEventSources(int sources) {
40         AccessibilityServiceInfo info = getServiceInfo();
41         info.setMotionEventSources(sources);
42         setServiceInfo(info);
43     }
44 
45     /** Sets the motion event sources to intercept but not consume events from. */
setObservedMotionEventSources(int sources)46     public void setObservedMotionEventSources(int sources) {
47         AccessibilityServiceInfo info = getServiceInfo();
48         info.setObservedMotionEventSources(sources);
49         setServiceInfo(info);
50     }
51 
52     /**
53      * Calls {@link AccessibilityServiceInfo#setMotionEventSources} and awaits confirmation
54      * that the input filter has been updated.
55      *
56      * <p>
57      * The AccessibilityInputFilter is updated asynchronously after the A11yService
58      * requests its list of interested motion event sources. For normal use this brief
59      * delay is inconsequential, but for testing we need a way to know when the filter
60      * is actually updated so that we can then inject a test event.
61      *
62      * <p>
63      * There are no public APIs to inspect the current InputFilter flags, so instead
64      * we send canary event(s) of type {@code canarySource} until we observe at least
65      * one. After a canary is observed, we know that the filter is installed, so tests
66      * can then safely send and await an event from {@code interestedSource}.
67      *
68      * @param canarySource     The source that is only used as a canary.
69      * @param interestedSource The source (different from canary) that is expected by the test.
70      */
setAndAwaitMotionEventSources(UiAutomation uiAutomation, int canarySource, int interestedSource, long timeoutMs)71     public void setAndAwaitMotionEventSources(UiAutomation uiAutomation, int canarySource,
72             int interestedSource, long timeoutMs) {
73         assertThat(canarySource).isNotEqualTo(interestedSource);
74         final int requestedSources = canarySource | interestedSource;
75         AccessibilityServiceInfo info = getServiceInfo();
76         info.setMotionEventSources(requestedSources);
77         setServiceInfo(info);
78         assertThat(getServiceInfo().getMotionEventSources()).isEqualTo(requestedSources);
79         final Object waitObject = new Object();
80         final AtomicBoolean foundCanaryEvent = new AtomicBoolean(false);
81         mMotionEventListener = motionEvent -> {
82             synchronized (waitObject) {
83                 if (motionEvent.getSource() == canarySource) {
84                     foundCanaryEvent.set(true);
85                 }
86                 waitObject.notifyAll();
87             }
88         };
89 
90         // Wait for the canary to signal that the filter has been updated.
91         final int maxAttempts = 3;
92         final String errorMessage = "Expected canary event from source " + canarySource;
93         for (int attempt = 0; attempt < maxAttempts; attempt++) {
94             uiAutomation.injectInputEventToInputFilter(createMotionEvent(canarySource));
95             try {
96                 TestUtils.waitOn(waitObject, foundCanaryEvent::get,
97                         timeoutMs, errorMessage);
98                 return;
99             } catch (AssertionError ignored) {
100                 // retry
101             }
102         }
103         fail(errorMessage);
104     }
105 
106     /**
107      * Injects an event to the AccessibilityInputFilter then awaits that the event
108      * is seen by {@link #onMotionEvent}.
109      */
injectAndAwaitMotionEvent(UiAutomation uiAutomation, int source, long timeoutMs)110     public void injectAndAwaitMotionEvent(UiAutomation uiAutomation, int source, long timeoutMs) {
111         final Object waitObject = new Object();
112         AtomicBoolean gotEvent = new AtomicBoolean(false);
113         mMotionEventListener = motionEvent -> {
114             synchronized (waitObject) {
115                 if (motionEvent.getSource() == source) {
116                     gotEvent.set(true);
117                 }
118                 waitObject.notifyAll();
119             }
120         };
121         uiAutomation.injectInputEventToInputFilter(createMotionEvent(source));
122         TestUtils.waitOn(waitObject, gotEvent::get, timeoutMs,
123                 "Expected single event from source " + source);
124     }
125 
setOnMotionEventListener(Consumer<MotionEvent> listener)126     public void setOnMotionEventListener(Consumer<MotionEvent> listener) {
127         mMotionEventListener = listener;
128     }
129 
130     @Override
onMotionEvent(@onNull MotionEvent event)131     public void onMotionEvent(@NonNull MotionEvent event) {
132         super.onMotionEvent(event);
133         mMotionEventListener.accept(event);
134     }
135 
createMotionEvent(int source)136     private MotionEvent createMotionEvent(int source) {
137         // Only source is used by these tests, so set other properties to valid defaults.
138         final long eventTime = SystemClock.uptimeMillis();
139         final MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
140         props.id = 0;
141         return MotionEvent.obtain(eventTime,
142                 eventTime,
143                 MotionEvent.ACTION_MOVE,
144                 1 /* pointerCount */,
145                 new MotionEvent.PointerProperties[]{props},
146                 new MotionEvent.PointerCoords[]{new MotionEvent.PointerCoords()},
147                 0 /* metaState */,
148                 0 /* buttonState */,
149                 0 /* xPrecision */,
150                 0 /* yPrecision */,
151                 1 /* deviceId */,
152                 0 /* edgeFlags */,
153                 source,
154                 0 /* flags */);
155     }
156 }
157