aboutsummaryrefslogtreecommitdiff
path: root/RotaryIME
diff options
context:
space:
mode:
authorDanny Epstein <depstein@google.com>2020-06-30 19:00:40 -0700
committerDanny Epstein <depstein@google.com>2020-07-06 10:57:13 -0700
commit577fab74809d3ea85e09dd586036f18ec795be57 (patch)
treeda2131ef05122d5d585c4c8f5d2ae70e0f9edb2e /RotaryIME
parent49aed455d306f0d487b146648acb561fe3795710 (diff)
downloadtests-577fab74809d3ea85e09dd586036f18ec795be57.tar.gz
Rotary IME
A demo rotary IME. Bug: 131421840 Bug: 152439508 Test: manual Change-Id: I81e943ca0bdee9b8b83b67c8be884d8392e9aa10
Diffstat (limited to 'RotaryIME')
-rw-r--r--RotaryIME/Android.bp35
-rw-r--r--RotaryIME/AndroidManifest.xml31
-rw-r--r--RotaryIME/readme.md25
-rw-r--r--RotaryIME/res/color/key_background.xml21
-rw-r--r--RotaryIME/res/layout/horizontal_keyboard.xml196
-rw-r--r--RotaryIME/res/values/colors.xml21
-rw-r--r--RotaryIME/res/values/strings.xml67
-rw-r--r--RotaryIME/res/values/styles.xml28
-rw-r--r--RotaryIME/res/xml/ime.xml22
-rw-r--r--RotaryIME/src/com/android/car/rotaryime/RotaryIme.java181
10 files changed, 627 insertions, 0 deletions
diff --git a/RotaryIME/Android.bp b/RotaryIME/Android.bp
new file mode 100644
index 0000000..bef2697
--- /dev/null
+++ b/RotaryIME/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_app {
+ name: "RotaryIME",
+
+ srcs: ["src/**/*.java"],
+ resource_dirs: ["res"],
+
+ sdk_version: "system_current",
+
+ static_libs: [
+ "car-ui-lib",
+ ],
+
+ owner: "google",
+ certificate: "shared",
+
+ optimize: {
+ enabled: false,
+ }
+}
diff --git a/RotaryIME/AndroidManifest.xml b/RotaryIME/AndroidManifest.xml
new file mode 100644
index 0000000..420175a
--- /dev/null
+++ b/RotaryIME/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.rotaryime">
+
+ <application android:label="@string/ime_name">
+ <service
+ android:name=".RotaryIme"
+ android:label="@string/ime_name"
+ android:permission="android.permission.BIND_INPUT_METHOD">
+ <intent-filter>
+ <action android:name="android.view.InputMethod" />
+ </intent-filter>
+ <meta-data android:name="android.view.im" android:resource="@xml/ime" />
+ </service>
+ </application>
+</manifest>
diff --git a/RotaryIME/readme.md b/RotaryIME/readme.md
new file mode 100644
index 0000000..50fcd16
--- /dev/null
+++ b/RotaryIME/readme.md
@@ -0,0 +1,25 @@
+# Rotary IME: Sample input method for rotary controller
+
+This is a sample IME for use with a rotary controller. It is intentionally very basic so that it's
+easy to understand the code. It doesn't support multiple locales / layouts. It doesn't support
+password fields, numeric fields, etc.
+
+## Building
+```
+make RotaryIME
+```
+
+## Installing
+```
+adb install out/target/product/[hardware]/system/app/RotaryIME/RotaryIME.apk
+```
+
+## Using
+
+Once installed, configure the `rotary_input_method` string resource in the
+`CarRotaryController` package to refer to this IME:
+```
+ <string name="rotary_input_method" translatable="false">com.android.car.rotaryime/.RotaryIme</string>
+```
+Then build and install `CarRotaryController`. There is no need to enable this
+IME or select it; the `RotaryService` will select it automatically in rotary mode.
diff --git a/RotaryIME/res/color/key_background.xml b/RotaryIME/res/color/key_background.xml
new file mode 100644
index 0000000..1a4a2c4
--- /dev/null
+++ b/RotaryIME/res/color/key_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:color="@color/key_background_pressed" />
+ <item android:state_focused="true" android:color="@color/key_background_focused" />
+ <item android:color="@color/key_background_normal" />
+</selector>
diff --git a/RotaryIME/res/layout/horizontal_keyboard.xml b/RotaryIME/res/layout/horizontal_keyboard.xml
new file mode 100644
index 0000000..f76b6b4
--- /dev/null
+++ b/RotaryIME/res/layout/horizontal_keyboard.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="#2c2c2c">
+ <com.android.car.ui.FocusParkingView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <com.android.car.ui.FocusArea
+ android:id="@+id/letters"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/a"
+ android:text="@string/key_label_a"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_b"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_c"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_d"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_e"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_f"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_g"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_h"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_i"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_j"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_k"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_l"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_m"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_n"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_o"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_p"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_q"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_r"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_s"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_t"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_u"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_v"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_w"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_x"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_y"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_z"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/delete"
+ android:text="@string/key_label_delete"
+ style="@style/Key" />
+ </com.android.car.ui.FocusArea>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/other_keys"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/close"
+ android:text="@string/key_close_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/dash"
+ android:text="@string/key_dash_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/quote"
+ android:text="@string/key_quote_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/apostrophe"
+ android:text="@string/key_apostropohe_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/exclamation_mark"
+ android:text="@string/key_exclamation_mark_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/question_mark"
+ android:text="@string/key_question_mark_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/semicolon"
+ android:text="@string/key_semicolon_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/colon"
+ android:text="@string/key_colon_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/comma"
+ android:text="@string/key_comma_label"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/period"
+ android:text="@string/key_period_label"
+ style="@style/Key" />
+ <TextView
+ android:layout_width="120dp"
+ android:id="@+id/space"
+ style="@style/Key" />
+ <TextView
+ android:id="@+id/zero"
+ android:text="@string/key_label_0"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_1"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_2"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_3"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_4"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_5"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_6"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_7"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_8"
+ style="@style/Key" />
+ <TextView
+ android:text="@string/key_label_9"
+ style="@style/Key" />
+ </com.android.car.ui.FocusArea>
+</LinearLayout>
diff --git a/RotaryIME/res/values/colors.xml b/RotaryIME/res/values/colors.xml
new file mode 100644
index 0000000..e2bc73d
--- /dev/null
+++ b/RotaryIME/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <color name="key_background_normal">#404040</color>
+ <color name="key_background_focused">#6080c0</color>
+ <color name="key_background_pressed">#a0c0ff</color>
+</resources>
diff --git a/RotaryIME/res/values/strings.xml b/RotaryIME/res/values/strings.xml
new file mode 100644
index 0000000..8ff29a7
--- /dev/null
+++ b/RotaryIME/res/values/strings.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <string name="ime_name">Rotary</string>
+ <string name="label_subtype_generic">%s</string>
+ <string name="key_label_a">A</string>
+ <string name="key_label_b">B</string>
+ <string name="key_label_c">C</string>
+ <string name="key_label_d">D</string>
+ <string name="key_label_e">E</string>
+ <string name="key_label_f">F</string>
+ <string name="key_label_g">G</string>
+ <string name="key_label_h">H</string>
+ <string name="key_label_i">I</string>
+ <string name="key_label_j">J</string>
+ <string name="key_label_k">K</string>
+ <string name="key_label_l">L</string>
+ <string name="key_label_m">M</string>
+ <string name="key_label_n">N</string>
+ <string name="key_label_o">O</string>
+ <string name="key_label_p">P</string>
+ <string name="key_label_q">Q</string>
+ <string name="key_label_r">R</string>
+ <string name="key_label_s">S</string>
+ <string name="key_label_t">T</string>
+ <string name="key_label_u">U</string>
+ <string name="key_label_v">V</string>
+ <string name="key_label_w">W</string>
+ <string name="key_label_x">X</string>
+ <string name="key_label_y">Y</string>
+ <string name="key_label_z">Z</string>
+ <string name="key_label_delete">⌫</string>
+ <string name="key_close_label">▼</string>
+ <string name="key_dash_label">-</string>
+ <string name="key_quote_label">\"</string>
+ <string name="key_apostropohe_label">\'</string>
+ <string name="key_exclamation_mark_label">!</string>
+ <string name="key_question_mark_label">\?</string>
+ <string name="key_semicolon_label">;</string>
+ <string name="key_colon_label">:</string>
+ <string name="key_comma_label">,</string>
+ <string name="key_period_label">.</string>
+ <string name="key_label_0">0</string>
+ <string name="key_label_1">1</string>
+ <string name="key_label_2">2</string>
+ <string name="key_label_3">3</string>
+ <string name="key_label_4">4</string>
+ <string name="key_label_5">5</string>
+ <string name="key_label_6">6</string>
+ <string name="key_label_7">7</string>
+ <string name="key_label_8">8</string>
+ <string name="key_label_9">9</string>
+</resources>
diff --git a/RotaryIME/res/values/styles.xml b/RotaryIME/res/values/styles.xml
new file mode 100644
index 0000000..6604d6f
--- /dev/null
+++ b/RotaryIME/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <style name="Key">
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:layout_margin">2dp</item>
+ <item name="android:gravity">center</item>
+ <item name="android:background">@color/key_background</item>
+ <item name="android:textColor">#ffffff</item>
+ <item name="android:textSize">30dp</item>
+ </style>
+</resources>
diff --git a/RotaryIME/res/xml/ime.xml b/RotaryIME/res/xml/ime.xml
new file mode 100644
index 0000000..41e3eec
--- /dev/null
+++ b/RotaryIME/res/xml/ime.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<input-method xmlns:android="http://schemas.android.com/apk/res/android">
+ <subtype
+ android:label="@string/label_subtype_generic"
+ android:imeSubtypeLocale="en_US"
+ android:imeSubtypeMode="keyboard"/>
+</input-method>
diff --git a/RotaryIME/src/com/android/car/rotaryime/RotaryIme.java b/RotaryIME/src/com/android/car/rotaryime/RotaryIme.java
new file mode 100644
index 0000000..4d7f3e4
--- /dev/null
+++ b/RotaryIME/src/com/android/car/rotaryime/RotaryIme.java
@@ -0,0 +1,181 @@
+package com.android.car.rotaryime;
+
+import static android.view.KeyEvent.KEYCODE_1;
+import static android.view.KeyEvent.KEYCODE_A;
+import static android.view.KeyEvent.KEYCODE_APOSTROPHE;
+import static android.view.KeyEvent.KEYCODE_COMMA;
+import static android.view.KeyEvent.KEYCODE_DEL;
+import static android.view.KeyEvent.KEYCODE_PERIOD;
+import static android.view.KeyEvent.KEYCODE_SEMICOLON;
+import static android.view.KeyEvent.KEYCODE_SLASH;
+import static android.view.KeyEvent.KEYCODE_SPACE;
+import static android.view.KeyEvent.META_SHIFT_ON;
+
+import android.inputmethodservice.InputMethodService;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+
+/**
+ * Sample IME for rotary controllers. This is intentionally very basic so that it's easy to
+ * understand the code. It doesn't support multiple locales / layouts. It doesn't support password
+ * fields, numeric fields, etc.
+ */
+public class RotaryIme extends InputMethodService {
+
+ /** Message requesting that the view in the {@code obj} field have its pressed state cleared. */
+ private static final int MSG_CLEAR_PRESSED = 1;
+
+ /** How many milliseconds a key should remain pressed after the user clicks it. */
+ private static final long PRESSED_MS = 200;
+
+ /** A handler for clearing the pressed state shortly after a key is pressed. */
+ private final Handler mHandler = new ImeHandler(Looper.getMainLooper());
+
+ @Override
+ public boolean onEvaluateFullscreenMode() {
+ // Don't go full-screen, even in landscape. This IME is very short so it fits easily.
+ return false;
+ }
+
+ @Override
+ public View onCreateInputView() {
+ ViewGroup rootView = (ViewGroup) getLayoutInflater().inflate(R.layout.horizontal_keyboard,
+ /* root= */ null);
+
+ // Since the IME isn't in the application window, when the user presses the center button on the
+ // rotary controller to press the focused key on this keyboard, ACTION_CLICK will be performed
+ // on the key which will invoke its click handler. Long press will similarly result in the key's
+ // long click handler being invoked.
+
+ // The first row contains letters and the delete key.
+ ViewGroup letters = rootView.findViewById(R.id.letters);
+ int aIndex = findChild(letters, rootView.findViewById(R.id.a));
+ for (int i = 0; i < 26; i++) {
+ TextView keyTextView = (TextView) letters.getChildAt(aIndex + i);
+ int keyCode = KEYCODE_A + i;
+ keyTextView.setOnClickListener(view -> handleKeyClick(view, keyCode));
+ keyTextView.setOnLongClickListener(view -> handleKeyLongClick(view, keyCode));
+ }
+
+ rootView.findViewById(R.id.delete).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_DEL));
+
+ // The second row contains the close key, symbols, space, and digits.
+ rootView.findViewById(R.id.close).setOnClickListener(
+ view -> requestHideSelf(/* flags= */ 0));
+ rootView.findViewById(R.id.dash).setOnClickListener(
+ view -> handleKeyClick(view, KeyEvent.KEYCODE_MINUS));
+ rootView.findViewById(R.id.quote).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_APOSTROPHE, META_SHIFT_ON));
+ rootView.findViewById(R.id.apostrophe).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_APOSTROPHE));
+ rootView.findViewById(R.id.exclamation_mark).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_1, META_SHIFT_ON));
+ rootView.findViewById(R.id.question_mark).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_SLASH, META_SHIFT_ON));
+ rootView.findViewById(R.id.semicolon).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_SEMICOLON));
+ rootView.findViewById(R.id.colon).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_SEMICOLON, META_SHIFT_ON));
+ rootView.findViewById(R.id.comma).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_COMMA));
+ rootView.findViewById(R.id.period).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_PERIOD));
+ rootView.findViewById(R.id.space).setOnClickListener(
+ view -> handleKeyClick(view, KEYCODE_SPACE));
+ ViewGroup otherKeys = rootView.findViewById(R.id.other_keys);
+ int zeroIndex = findChild(otherKeys, rootView.findViewById(R.id.zero));
+ for (int i = 0; i < 10; i++) {
+ int keyCode = KeyEvent.KEYCODE_0 + i;
+ otherKeys.getChildAt(zeroIndex + i).setOnClickListener(
+ view -> handleKeyClick(view, keyCode));
+ }
+
+ return rootView;
+ }
+
+ /** Returns {@code child}'s index within {@code parent} or -1 if not found. */
+ private static int findChild(ViewGroup parent, View child) {
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ if (parent.getChildAt(i) == child) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Handles a click on the key {@code view} by sending events with the given {@code keyCode}. Use
+ * this convenience method for unshifted keys.
+ */
+ private void handleKeyClick(View view, int keyCode) {
+ handleKeyClick(view, keyCode, /* metaState= */ 0);
+ }
+
+ /**
+ * Handles a click on the key {@code view} by sending events with the given {@code keyCode}
+ * and {@code metaState}. Use {@link KeyEvent#META_SHIFT_ON} to access shifted keys such as
+ * question mark (shift slash). Use zero for unshifted keys.
+ */
+ private void handleKeyClick(View view, int keyCode, int metaState) {
+ animatePressed(view);
+ sendDownUpKeyEvents(keyCode, metaState);
+ }
+
+ /**
+ * Handles a long click on the key {@code view} by sending events with the given {@code keyCode}
+ * and {@link KeyEvent#META_SHIFT_ON} to produce shifted keys such as capital letters.
+ */
+ private boolean handleKeyLongClick(View view, int keyCode) {
+ animatePressed(view);
+ sendDownUpKeyEvents(keyCode, META_SHIFT_ON);
+ return true;
+ }
+
+ /**
+ * Sends an {@link KeyEvent#ACTION_DOWN} event followed by an {@link KeyEvent#ACTION_UP} event
+ * with the given {@code keyCode} and {@code metaState}.
+ */
+ private void sendDownUpKeyEvents(int keyCode, int metaState) {
+ long uptimeMillis = SystemClock.uptimeMillis();
+ KeyEvent downEvent = new KeyEvent(uptimeMillis, uptimeMillis, KeyEvent.ACTION_DOWN,
+ keyCode, /* repeat= */ 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, /* scancode= */ 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD, InputDevice.SOURCE_KEYBOARD);
+ KeyEvent upEvent = new KeyEvent(uptimeMillis, uptimeMillis, KeyEvent.ACTION_UP,
+ keyCode, /* repeat= */ 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, /* scancode= */ 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD, InputDevice.SOURCE_KEYBOARD);
+ getCurrentInputConnection().sendKeyEvent(downEvent);
+ getCurrentInputConnection().sendKeyEvent(upEvent);
+ }
+
+ /** Sets {@code view}'s pressed state and clears it {@link #PRESSED_MS} later. */
+ private void animatePressed(View view) {
+ view.setPressed(true);
+ Message message = mHandler.obtainMessage(MSG_CLEAR_PRESSED, view);
+ mHandler.removeMessages(MSG_CLEAR_PRESSED);
+ mHandler.sendMessageDelayed(message, PRESSED_MS);
+ }
+
+ /** A handler for clearing the pressed state shortly after a key is pressed. */
+ private static class ImeHandler extends Handler {
+ ImeHandler(@NonNull Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ if (msg.what == MSG_CLEAR_PRESSED) {
+ ((View) msg.obj).setPressed(false);
+ }
+ }
+ }
+}