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 package com.example.android.sampleinputmethodaccessibilityservice;
17 
18 import android.accessibilityservice.AccessibilityService;
19 import android.accessibilityservice.InputMethod;
20 import android.os.SystemClock;
21 import android.util.Log;
22 import android.util.Pair;
23 import android.view.Gravity;
24 import android.view.KeyCharacterMap;
25 import android.view.KeyEvent;
26 import android.view.WindowManager;
27 import android.view.WindowMetrics;
28 import android.view.accessibility.AccessibilityEvent;
29 import android.view.inputmethod.EditorInfo;
30 import android.widget.Button;
31 import android.widget.HorizontalScrollView;
32 import android.widget.LinearLayout;
33 import android.widget.ScrollView;
34 import android.widget.TextView;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import java.util.List;
40 import java.util.function.BiConsumer;
41 
42 /**
43  * A sample {@link AccessibilityService} to demo how to use IME APIs.
44  */
45 public final class SampleInputMethodAccessibilityService extends AccessibilityService {
46     private static final String TAG = "SampleImeA11yService";
47 
48     private EventMonitor mEventMonitor;
49 
50     private final class InputMethodImpl extends InputMethod {
InputMethodImpl(AccessibilityService service)51         InputMethodImpl(AccessibilityService service) {
52             super(service);
53         }
54 
55         @Override
onStartInput(EditorInfo attribute, boolean restarting)56         public void onStartInput(EditorInfo attribute, boolean restarting) {
57             Log.d(TAG, String.format("onStartInput(%s,%b)", attribute, restarting));
58             mEventMonitor.onStartInput(attribute, restarting);
59         }
60 
61         @Override
onFinishInput()62         public void onFinishInput() {
63             Log.d(TAG, "onFinishInput()");
64             mEventMonitor.onFinishInput();
65         }
66 
67         @Override
onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)68         public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
69                 int newSelEnd, int candidatesStart, int candidatesEnd) {
70             Log.d(TAG, String.format("onUpdateSelection(%d,%d,%d,%d,%d,%d)", oldSelStart, oldSelEnd,
71                     newSelStart, newSelEnd, candidatesStart, candidatesEnd));
72             mEventMonitor.onUpdateSelection(oldSelStart, oldSelEnd,
73                     newSelStart, newSelEnd, candidatesStart, candidatesEnd);
74         }
75     }
76 
item(@onNull CharSequence label, @Nullable T value)77     private static <T> Pair<CharSequence, T> item(@NonNull CharSequence label, @Nullable T value) {
78         return Pair.create(label, value);
79     }
80 
addButtons(@onNull LinearLayout parentView, @NonNull String headerText, @NonNull List<Pair<CharSequence, T>> items, @NonNull BiConsumer<T, InputMethod.AccessibilityInputConnection> action)81     private <T> void addButtons(@NonNull LinearLayout parentView, @NonNull String headerText,
82             @NonNull List<Pair<CharSequence, T>> items,
83             @NonNull BiConsumer<T, InputMethod.AccessibilityInputConnection> action) {
84         final LinearLayout layout = new LinearLayout(this);
85         layout.setOrientation(LinearLayout.VERTICAL);
86         {
87             final TextView headerTextView = new TextView(this, null,
88                     android.R.attr.listSeparatorTextViewStyle);
89             headerTextView.setAllCaps(false);
90             headerTextView.setText(headerText);
91             layout.addView(headerTextView);
92         }
93         {
94             final LinearLayout itemLayout = new LinearLayout(this);
95             itemLayout.setOrientation(LinearLayout.HORIZONTAL);
96             for (Pair<CharSequence, T> item : items) {
97                 final Button button = new Button(this, null, android.R.attr.buttonStyleSmall);
98                 button.setAllCaps(false);
99                 button.setText(item.first);
100                 button.setOnClickListener(view -> {
101                     final InputMethod ime = getInputMethod();
102                     if (ime == null) {
103                         return;
104                     }
105                     final InputMethod.AccessibilityInputConnection ic =
106                             ime.getCurrentInputConnection();
107                     if (ic == null) {
108                         return;
109                     }
110                     action.accept(item.second, ic);
111                 });
112                 itemLayout.addView(button);
113             }
114             final HorizontalScrollView scrollView = new HorizontalScrollView(this);
115             scrollView.addView(itemLayout);
116             layout.addView(scrollView);
117         }
118         parentView.addView(layout);
119     }
120 
121     @Override
onServiceConnected()122     protected void onServiceConnected() {
123         super.onServiceConnected();
124 
125         final WindowManager windowManager = getSystemService(WindowManager.class);
126         final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
127 
128         // Create a monitor window.
129         {
130             final TextView textView = new TextView(this);
131             mEventMonitor = new EventMonitor(textView::setText);
132 
133             final LinearLayout monitorWindowContent = new LinearLayout(this);
134             monitorWindowContent.setOrientation(LinearLayout.VERTICAL);
135             monitorWindowContent.setPadding(10, 10, 10, 10);
136 
137             monitorWindowContent.addView(textView);
138 
139             OverlayWindowBuilder.from(monitorWindowContent)
140                     .setSize((metrics.getBounds().width() * 3) / 4,
141                             WindowManager.LayoutParams.WRAP_CONTENT)
142                     .setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
143                     .setBackgroundColor(0xeed2e3fc)
144                     .show();
145         }
146 
147         final LinearLayout contentView = new LinearLayout(this);
148         contentView.setOrientation(LinearLayout.VERTICAL);
149         {
150             final TextView textView = new TextView(this, null, android.R.attr.windowTitleStyle);
151             textView.setGravity(Gravity.CENTER);
152             textView.setText("A11Y IME");
153             contentView.addView(textView);
154         }
155         {
156             final LinearLayout buttonLayout = new LinearLayout(this);
157             buttonLayout.setBackgroundColor(0xfffeefc3);
158             buttonLayout.setPadding(10, 10, 10, 10);
159             buttonLayout.setOrientation(LinearLayout.VERTICAL);
160 
161             addButtons(buttonLayout,
162                     "commitText", List.of(
163                             item("A", "A"),
164                             item("Hello World", "Hello World"),
165                             item("\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F",
166                                     "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F")),
167                     (value, ic) -> ic.commitText(value, 1, null));
168 
169             addButtons(buttonLayout,
170                     "sendKeyEvent", List.of(
171                             item("A", KeyEvent.KEYCODE_A),
172                             item("DEL", KeyEvent.KEYCODE_DEL),
173                             item("DPAD_LEFT", KeyEvent.KEYCODE_DPAD_LEFT),
174                             item("DPAD_RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT),
175                             item("COPY", KeyEvent.KEYCODE_COPY),
176                             item("CUT", KeyEvent.KEYCODE_CUT),
177                             item("PASTE", KeyEvent.KEYCODE_PASTE)),
178                     (keyCode, ic) -> {
179                         final long eventTime = SystemClock.uptimeMillis();
180                         ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
181                                 KeyEvent.ACTION_DOWN, keyCode, 0, 0,
182                                 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
183                                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
184                         ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(),
185                                 KeyEvent.ACTION_UP, keyCode, 0, 0,
186                                 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
187                                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
188                     });
189 
190             addButtons(buttonLayout,
191                     "performEditorAction", List.of(
192                             item("UNSPECIFIED", EditorInfo.IME_ACTION_UNSPECIFIED),
193                             item("NONE", EditorInfo.IME_ACTION_NONE),
194                             item("GO", EditorInfo.IME_ACTION_GO),
195                             item("SEARCH", EditorInfo.IME_ACTION_SEARCH),
196                             item("SEND", EditorInfo.IME_ACTION_SEND),
197                             item("NEXT", EditorInfo.IME_ACTION_NEXT),
198                             item("DONE", EditorInfo.IME_ACTION_DONE),
199                             item("PREVIOUS", EditorInfo.IME_ACTION_PREVIOUS)),
200                     (action, ic) -> ic.performEditorAction(action));
201 
202             addButtons(buttonLayout,
203                     "performContextMenuAction", List.of(
204                             item("selectAll", android.R.id.selectAll),
205                             item("startSelectingText", android.R.id.startSelectingText),
206                             item("stopSelectingText", android.R.id.stopSelectingText),
207                             item("cut", android.R.id.cut),
208                             item("copy", android.R.id.copy),
209                             item("paste", android.R.id.paste),
210                             item("copyUrl", android.R.id.copyUrl),
211                             item("switchInputMethod", android.R.id.switchInputMethod)),
212                     (action, ic) -> ic.performContextMenuAction(action));
213 
214             addButtons(buttonLayout,
215                     "setSelection", List.of(
216                             item("(0,0)", Pair.create(0, 0)),
217                             item("(0,1)", Pair.create(0, 1)),
218                             item("(1,1)", Pair.create(1, 1)),
219                             item("(0,999)", Pair.create(0, 999))),
220                     (pair, ic) -> ic.setSelection(pair.first, pair.second));
221 
222             addButtons(buttonLayout,
223                     "deleteSurroundingText", List.of(
224                             item("(0,0)", Pair.create(0, 0)),
225                             item("(0,1)", Pair.create(0, 1)),
226                             item("(1,0)", Pair.create(1, 0)),
227                             item("(1,1)", Pair.create(1, 1)),
228                             item("(999,0)", Pair.create(999, 0)),
229                             item("(0,999)", Pair.create(0, 999))),
230                     (pair, ic) -> ic.deleteSurroundingText(pair.first, pair.second));
231 
232             final ScrollView scrollView = new ScrollView(this);
233             scrollView.addView(buttonLayout);
234             contentView.addView(scrollView);
235 
236             // Set margin
237             {
238                 final LinearLayout.LayoutParams lp =
239                         ((LinearLayout.LayoutParams) scrollView.getLayoutParams());
240                 lp.leftMargin = lp.rightMargin = lp.bottomMargin = 20;
241                 scrollView.setLayoutParams(lp);
242             }
243         }
244 
245         OverlayWindowBuilder.from(contentView)
246                 .setSize((metrics.getBounds().width() * 3) / 4,
247                         metrics.getBounds().height() / 5)
248                 .setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
249                 .setRelativePosition(300, 300)
250                 .setBackgroundColor(0xfffcc934)
251                 .show();
252     }
253 
254     @Override
onAccessibilityEvent(AccessibilityEvent event)255     public void onAccessibilityEvent(AccessibilityEvent event) {
256     }
257 
258     @Override
onInterrupt()259     public void onInterrupt() {
260     }
261 
262     @Override
onCreateInputMethod()263     public InputMethod onCreateInputMethod() {
264         Log.d(TAG, "onCreateInputMethod");
265         return new InputMethodImpl(this);
266     }
267 }
268