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