diff --git a/app/USER_EXPERIENCE.md b/app/USER_EXPERIENCE.md new file mode 100644 index 0000000..5e29686 --- /dev/null +++ b/app/USER_EXPERIENCE.md @@ -0,0 +1,98 @@ +# Titanboard user experience + +Keyboards are an integral part of the functioning of any computer, including the smartphone. Care +should then be taken to design keyboards to be as intuitive, easy to use, and unobstructed as +possible. + +The Unihertz Titan already has a physical keyboard. It's poorly designed in many aspects. Mainly, it +isn't very reminiscent of a regular QWERTY keyboard which people are used to: + +1. The worst part is arguably the shift and alt keys being at the top of the keyboard, instead of on + the bottom. +2. The spacebar key is on the same row as the bottom QWERTY alphabet keys. +3. The numerical keys, which are some of the most commonly used keys in phones (passwords, PIN + codes) + and in regular language are right below the alt key, which makes typing them very awkward. One + could + also argue that the numpad is in a "reversed" layout, but this is consistent with the usual + layout + found on phones. +4. It lacks some commonly used symbols: ';', '&', '=', '<', '>', '$'. Other than that, it lacks some + other + symbols from regular QWERTY keyboards. +5. There is no extra alt-gr key to type in localized letters. +6. In my opinion, it also lacks a control key, but that is unnecessary on smartphones, unless you + use the terminal or modify spreadsheets. However, providing appropriate UI elements for the + actions + possible on regular computers should be the responsibility of the application. Smartphones are + simply different devices, with smaller screens and less space for complexity. +7. Emojis. + +However, when designing a keyboard app for it one should already use all pre-existing quirks and +decisions which materialized in the physical keyboard. For example, the alt key should be used for +the alternate characters printed on the keyboard, the shift key should give us uppercase characters +and so on. + +# Solutions + +Here are my solutions for the above listed problems: + +1. This is solvable with shift/alt latching. This makes it so when you press these keys once, the + next key pressed will be either uppercased or the alternate key visible on the physical button. + Additionally, the shift and alt key can be toggled by double-clicking in a fast succession, + similar + to regular visual keyboards on Android. +2. This is unfortunately unsolvable with software. +3. See 1., latching the keys solves this issue. +4. This requires a visual keyboard, the UI of which is discussed later. Why does this require a + visual keyboard, and not some alternate "symbol" mode, perhaps accessible with some keyboard + shortcuts? That would make the learning curve of the keyboard higher, and it would be less + intuitive. One should make sure not to complicate the physical keyboard any more than it already + is, + and use all of its idiosyncrasies but add no more. +5. Similar to regular visual keyboards on Android, a diacritic/alternate key is assigned to each key + according to the language. Long pressing a key will print it. Ideally, there should also be a + popup + menu allowing for several characters under one key. Cycling through the keys would use the + shift/alt + keys (so that no matter what key you use, the other hand can cycle through) and selecting it + would + happen on release of the key. +6. I have no plans on adding a control key, as it is largely unnecessary on phones. +7. I do not really use emojis, but I am aware that they are used by the majority of people. Most + IM apps provide their own emoji list, so this isn't a priority. Designing a good UX for it is + also + hard on the Titan. + +Taking into account typing ergonomics and pre-existing UX choices of other keyboard designers, this +input method should make typing on the Unihertz Titan a pleasant experience. Other than fixing the +fundamental issues, the keyboard should also: + +1. Provide a way of managing the clipboard, or at the very least pasting things with an UI element. + This is to make the usage intuitive and not introduce any additional keys such as a ctrl key. +2. Autocorrect, language switching, auto caps and periods. +3. Some minimal customization, but just enough not to overwhelm the user. + +For the Unihertz Titan Pocket/Slim, the sym key will be used as the alt+gr key, providing diacritics +and such. This is because in the official documentation and implementation the alt key is designed +for the alternate keys printed on the buttons. + +# User interface + +Google recommends that visual keyboards should not occupy more than half of the screen estate. +However, most phones have long, vertical screens, which means essentially that the keyboard will +be a square, and so will the remaining view. On the Titan, the screen is already square, so the +remaining space will be proportionally much smaller. Therefore, the keyboard should occupy very +minimal screen estate, as to allow the user to see the application they are interacting with. + +For this reason, I make sure the keyboard occupies 20–25 % screen estate. This is enough for a +symbol bar and a suggestions bar, as well as some additional buttons such as language change or +settings. + +Both the symbol bar and suggestions bar are scrollable. The suggestions being scrollable is a +feature on some visual keyboards already, however, I have come to the decision to make this for the +symbol bar as well. An expandable menu would make the keyboard occupy half, or more of the screen. +A scrollable bar is more intuitive and can hold an indefinite amount of characters, possibly also +user-defined. + +A simple divider between the suggestion and symbol bar is added to avoid accidental clicks. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 92285ee..51983c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdk 19 targetSdk 34 versionCode 1 - versionName "1.0" + versionName "0.1" } buildTypes { @@ -28,3 +28,4 @@ android { } + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a853978..2758b9f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,26 +3,36 @@ xmlns:tools="http://schemas.android.com/tools"> + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/TitanboardIME.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/IME.java similarity index 74% rename from app/src/main/java/cfpi_fpsi/eu/titanboard/TitanboardIME.java rename to app/src/main/java/cfpi_fpsi/eu/titanboard/IME.java index 923194e..54b1f38 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/TitanboardIME.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/IME.java @@ -1,19 +1,42 @@ +/* + * An input method for the Unihertz Titan/physical keyboard phones + * Copyright (C) 2024 Emil Kosz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package cfpi_fpsi.eu.titanboard; import android.annotation.SuppressLint; import android.inputmethodservice.InputMethodService; import android.os.Build; +import android.os.Vibrator; import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; -public class TitanboardIME extends InputMethodService { - private final MetaKeyManager mMetaKeyManager = new MetaKeyManager(); +public class IME extends InputMethodService { + private MetaKeyManager mMetaKeyManager; private Keymap mKeymap; + private LinearLayout mInputView; + private Vibrator mVibrator; @Override public void onCreate() { @@ -23,6 +46,8 @@ public class TitanboardIME extends InputMethodService { // Unihertz phones: the Pocket and the Slim. However, as I don't have them, it'll be guesswork. // Sym key support is already implemented. mKeymap = new Keymap(getResources().getXml(R.xml.keymap_titan)); + mVibrator = (Vibrator) getBaseContext().getSystemService(VIBRATOR_SERVICE); + mMetaKeyManager = new MetaKeyManager(mVibrator); } @SuppressLint("InflateParams") @@ -50,17 +75,20 @@ public class TitanboardIME extends InputMethodService { } switch (keyCode) { - // The back key behaviour is handled by the IMS already. + // If the keyboard is visible, hide it, otherwise, let the system handle the key. + case KeyEvent.KEYCODE_BACK: + if (isInputViewShown()) { + requestHideSelf(0); + return true; + } + return false; // If it's the Enter key, we let the application do it's job. case KeyEvent.KEYCODE_ENTER: return false; // If it's the backspace key, we remove one character. case KeyEvent.KEYCODE_DEL: + conn.deleteSurroundingText(1, 0); mMetaKeyManager.adjustMetaStateAfterKeypress(); - if (conn.getSelectedText(0) == null) { - conn.deleteSurroundingText(1, 0); - return true; - } return false; case KeyEvent.KEYCODE_SPACE: conn.commitText(" ", 1); @@ -68,18 +96,19 @@ public class TitanboardIME extends InputMethodService { return true; default: String key = getKey(keyCode); - if (key != null) { - conn.commitText(getKey(keyCode), 1); - mMetaKeyManager.adjustMetaStateAfterKeypress(); - return true; - } else { + + if (key == null) { return false; } + + conn.commitText(getKey(keyCode), 1); + mMetaKeyManager.adjustMetaStateAfterKeypress(); + return true; } } public boolean onKeyUp(int keyCode, KeyEvent event) { - mMetaKeyManager.updateMetaStateUp(keyCode); + mMetaKeyManager.updateMetaStateUp(keyCode, event); updateMetaStateTextView(); return false; } @@ -93,9 +122,6 @@ public class TitanboardIME extends InputMethodService { public void onFinishInput() { mMetaKeyManager.resetMetaKeyState(); updateMetaStateTextView(); - mInputView.findViewById(R.id.extra_view).setVisibility(View.GONE); - Button key = mInputView.findViewById(R.id.expand_button); - key.setText("▲"); } private String getKey(int keyCode) { @@ -144,20 +170,9 @@ public class TitanboardIME extends InputMethodService { } } - /** @noinspection unused*/ - public void onInputViewExtraKeyClick(View v) { - int visibility = mInputView.findViewById(R.id.extra_view).getVisibility(); - Button key = (Button) v; - if (visibility == View.VISIBLE) { - mInputView.findViewById(R.id.extra_view).setVisibility(View.GONE); - key.setText("▲"); - } else { - mInputView.findViewById(R.id.extra_view).setVisibility(View.VISIBLE); - key.setText("▼"); - } - } - - /** @noinspection unused*/ + /** + * @noinspection unused + */ public void onInputViewKeyClick(View v) { Button key = (Button) v; InputConnection conn = getCurrentInputConnection(); diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/Keymap.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/Keymap.java index c34fd5f..64bd364 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/Keymap.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/Keymap.java @@ -1,3 +1,22 @@ +/* + * An input method for the Unihertz Titan/physical keyboard phones + * Copyright (C) 2024 Emil Kosz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package cfpi_fpsi.eu.titanboard; import org.xmlpull.v1.XmlPullParser; diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java index 6054c11..0325da3 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/MetaKeyManager.java @@ -1,62 +1,93 @@ +/* + * An input method for the Unihertz Titan/physical keyboard phones + * Copyright (C) 2024 Emil Kosz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package cfpi_fpsi.eu.titanboard; +import android.os.Vibrator; import android.view.KeyEvent; public class MetaKeyManager { - private static final int mRepeatCount = 5; private MetaKey mMetaKeyPressed = MetaKey.NONE; private MetaKey mMetaKeyLatched = MetaKey.NONE; private MetaKey mMetaKeyLocked = MetaKey.NONE; private boolean mMetaKeyUsed = false; + private long mKeyUpTime; + + private Vibrator mVibrator; + + public MetaKeyManager(Vibrator vibrator) { + mVibrator = vibrator; + } public void updateMetaStateDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: - updateMetaKeyDown(MetaKey.ALT, event.getRepeatCount()); + updateMetaKeyDown(MetaKey.ALT, event); break; case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: - updateMetaKeyDown(MetaKey.SHIFT, event.getRepeatCount()); + updateMetaKeyDown(MetaKey.SHIFT, event); break; case KeyEvent.KEYCODE_SYM: - updateMetaKeyDown(MetaKey.SYM, event.getRepeatCount()); + updateMetaKeyDown(MetaKey.SYM, event); break; default: break; } } - void updateMetaKeyDown(MetaKey key, int repeatCount) { + void updateMetaKeyDown(MetaKey key, KeyEvent event) { // Perhaps this function should be renamed to something... mMetaKeyUsed = false; mMetaKeyPressed = key; mMetaKeyLatched = mMetaKeyLocked != MetaKey.NONE ? mMetaKeyLocked : mMetaKeyLatched; - mMetaKeyLocked = repeatCount > mRepeatCount ? key : MetaKey.NONE; + mMetaKeyLocked = MetaKey.NONE; } - public void updateMetaStateUp(int keyCode) { + public void updateMetaStateUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: - updateMetaKeyUp(MetaKey.ALT); + updateMetaKeyUp(MetaKey.ALT, event); break; case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: - updateMetaKeyUp(MetaKey.SHIFT); + updateMetaKeyUp(MetaKey.SHIFT, event); break; case KeyEvent.KEYCODE_SYM: - updateMetaKeyUp(MetaKey.SYM); + updateMetaKeyUp(MetaKey.SYM, event); break; default: break; } } - public void updateMetaKeyUp(MetaKey key) { + public void updateMetaKeyUp(MetaKey key, KeyEvent event) { + if (event.getEventTime() - mKeyUpTime < 400 && mMetaKeyLatched == key) { + mMetaKeyLocked = key; + mVibrator.vibrate(30); + } + + mKeyUpTime = event.getEventTime(); mMetaKeyPressed = MetaKey.NONE; - if (mMetaKeyLatched == MetaKey.NONE && mMetaKeyLocked == MetaKey.NONE - && !mMetaKeyUsed) { + if (mMetaKeyLatched == MetaKey.NONE && mMetaKeyLocked == MetaKey.NONE && !mMetaKeyUsed) { mMetaKeyLatched = key; } else { mMetaKeyLatched = MetaKey.NONE; @@ -67,7 +98,9 @@ public class MetaKeyManager { if (mMetaKeyLatched != MetaKey.NONE) { mMetaKeyLatched = MetaKey.NONE; mMetaKeyUsed = false; - } else if (mMetaKeyPressed != MetaKey.NONE) { + } + + if (mMetaKeyPressed != MetaKey.NONE) { mMetaKeyUsed = true; } } @@ -79,6 +112,11 @@ public class MetaKeyManager { mMetaKeyUsed = false; } + public void setMetaKeyLatched(MetaKey key) { + resetMetaKeyState(); + mMetaKeyLatched = key; + } + public MetaKey getMetaKeyPressed() { return mMetaKeyPressed; } diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java new file mode 100644 index 0000000..cbd8e38 --- /dev/null +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/PreferencesActivity.java @@ -0,0 +1,31 @@ +/* + * An input method for the Unihertz Titan/physical keyboard phones + * Copyright (C) 2024 Emil Kosz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package cfpi_fpsi.eu.titanboard; + +import android.app.Activity; +import android.os.Bundle; + +public class PreferencesActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.preferences); + } +} diff --git a/app/src/main/res/drawable/key.xml b/app/src/main/res/drawable/key.xml index bf4dc14..cfa2023 100644 --- a/app/src/main/res/drawable/key.xml +++ b/app/src/main/res/drawable/key.xml @@ -2,8 +2,10 @@ - - + + diff --git a/app/src/main/res/layout/input.xml b/app/src/main/res/layout/input.xml index 3630bf5..b639ef3 100644 --- a/app/src/main/res/layout/input.xml +++ b/app/src/main/res/layout/input.xml @@ -14,264 +14,127 @@ android:orientation="horizontal" android:visibility="visible"> -