1
0
Fork 0

Voice input (#531)

This commit is contained in:
Dimo Karaivanov 2024-06-10 09:57:37 +03:00 committed by GitHub
parent 7a19d6bcf7
commit c64c8dac5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 837 additions and 53 deletions

View file

@ -94,7 +94,7 @@ Thanks to your donations, a brand new testing device is available, a Sonim XP380
## 💪 Privacy Policy and Philosophy
- No ads, no premium or paid features. It's all free.
- No spying, no tracking, no telemetry or reports. No nothing!
- No network connectivity.
- No network connectivity, except when voice input is active.
- It only does its job.
- Open-source, so you can verify all the above yourself.
- Created with help from the entire community.

View file

@ -1,12 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
android:versionCode="548"
android:versionName="32.5"
android:versionCode="555"
android:versionName="32.12"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- allows displaying notifications on Android >= 13 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <!-- allows voice input -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- allows words exporting on Android < 10 -->
<queries>
<intent>
<action android:name="android.speech.RecognitionService" /> <!-- allows voice input on Android >= 11 -->
</intent>
</queries>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
@ -34,5 +41,10 @@
android:label=""
android:name="io.github.sspanak.tt9.ui.dialogs.PopupDialogActivity"
android:theme="@style/alertDialog" />
<activity
android:excludeFromRecents="true"
android:label=""
android:name="io.github.sspanak.tt9.ui.dialogs.RequestPermissionDialog" />
</application>
</manifest>

View file

@ -24,6 +24,7 @@ abstract public class AbstractHandler extends InputMethodService {
// UI
abstract protected void createSuggestionBar(View mainView);
abstract protected void resetStatus();
abstract protected InputMode getInputMode();

View file

@ -8,12 +8,18 @@ import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.ui.UI;
import io.github.sspanak.tt9.ui.dialogs.AddWordDialog;
abstract class CommandHandler extends TypingHandler {
abstract public class CommandHandler extends VoiceHandler {
@Override
protected boolean onBack() {
if (super.onBack()) {
return true;
}
if (mainView.isCommandPaletteShown()) {
mainView.hideCommandPalette();
statusBar.setText(mInputMode);
if (!voiceInputOps.isListening()) {
resetStatus();
}
return true;
}
@ -39,6 +45,10 @@ abstract class CommandHandler extends TypingHandler {
@Override
protected boolean onNumber(int key, boolean hold, int repeat) {
if (statusBar.isErrorShown()) {
resetStatus();
}
if (!shouldBeOff() && mainView.isCommandPaletteShown()) {
onCommand(key);
return true;
@ -63,16 +73,26 @@ abstract class CommandHandler extends TypingHandler {
showSettings();
break;
case 2:
mainView.hideCommandPalette();
statusBar.setText(mInputMode);
addWord();
break;
case 3:
toggleVoiceInput();
break;
}
}
protected void resetStatus() {
if (mainView.isCommandPaletteShown()) {
statusBar.setText(R.string.commands_select_command);
} else {
statusBar.setText(mInputMode);
}
}
public void addWord() {
if (mInputMode.isNumeric()) {
if (mInputMode.isNumeric() || voiceInputOps.isListening()) {
return;
}
@ -83,6 +103,8 @@ abstract class CommandHandler extends TypingHandler {
suggestionOps.cancelDelayedAccept();
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
mainView.hideCommandPalette();
resetStatus();
String word = textField.getSurroundingWord(mLanguage);
if (word.isEmpty()) {
@ -95,12 +117,13 @@ abstract class CommandHandler extends TypingHandler {
public void changeKeyboard() {
suggestionOps.cancelDelayedAccept();
stopVoiceInput();
UI.showChangeKeyboardDialog(this);
}
protected void nextInputMode() {
if (mInputMode.isPassthrough()) {
if (mInputMode.isPassthrough() || voiceInputOps.isListening()) {
return;
} else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) {
mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, InputMode.MODE_123) : mInputMode;
@ -130,6 +153,8 @@ abstract class CommandHandler extends TypingHandler {
protected void nextLang() {
stopVoiceInput();
// select the next language
int previous = mEnabledLanguages.indexOf(mLanguage.getId());
int next = (previous + 1) % mEnabledLanguages.size();
@ -171,6 +196,17 @@ abstract class CommandHandler extends TypingHandler {
public void showSettings() {
suggestionOps.cancelDelayedAccept();
stopVoiceInput();
UI.showSettingsScreen(this);
}
public void showCommandPalette() {
suggestionOps.cancelDelayedAccept();
suggestionOps.acceptIncomplete();
mInputMode.reset();
mainView.showCommandPalette();
resetStatus();
}
}

View file

@ -4,7 +4,6 @@ import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryLoader;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.modes.ModePredictive;
@ -231,12 +230,7 @@ public abstract class HotkeyHandler extends CommandHandler {
}
if (!validateOnly) {
suggestionOps.cancelDelayedAccept();
suggestionOps.acceptIncomplete();
mInputMode.reset();
mainView.showCommandPalette();
statusBar.setText(getString(R.string.commands_select_command));
showCommandPalette();
forceShowWindow();
}

View file

@ -2,6 +2,7 @@ package io.github.sspanak.tt9.ime;
import androidx.annotation.Nullable;
import io.github.sspanak.tt9.ime.voice.VoiceInputOps;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
@ -27,6 +28,10 @@ abstract public class MainViewOps extends HotkeyHandler {
return mInputMode.is123() && inputType.isPhoneNumber();
}
public boolean isVoiceInputMissing() {
return !(new VoiceInputOps(this, null, null, null)).isAvailable();
}
@Nullable
public Language getLanguage() {
return mLanguage;

View file

@ -139,6 +139,7 @@ public class TraditionalT9 extends MainViewOps {
@Override
protected void onStop() {
stopVoiceInput();
onFinishTyping();
suggestionOps.clear();
setStatusIcon(mInputMode);

View file

@ -0,0 +1,82 @@
package io.github.sspanak.tt9.ime;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.voice.VoiceInputError;
import io.github.sspanak.tt9.ime.voice.VoiceInputOps;
import io.github.sspanak.tt9.ui.dialogs.RequestPermissionDialog;
import io.github.sspanak.tt9.util.Logger;
abstract class VoiceHandler extends TypingHandler {
private final static String LOG_TAG = VoiceHandler.class.getSimpleName();
protected VoiceInputOps voiceInputOps;
@Override
protected void onInit() {
super.onInit();
voiceInputOps = new VoiceInputOps(
this,
this::onVoiceInputStarted,
this::onVoiceInputStopped,
this::onVoiceInputError
);
}
@Override
protected boolean onBack() {
stopVoiceInput();
return false; // we don't want to abort other operations, we just silently stop voice input
}
@Override
protected boolean onNumber(int key, boolean hold, int repeat) {
stopVoiceInput();
return super.onNumber(key, hold, repeat);
}
public void toggleVoiceInput() {
if (voiceInputOps.isListening() || !voiceInputOps.isAvailable()) {
stopVoiceInput();
return;
}
statusBar.setText(R.string.loading);
suggestionOps.cancelDelayedAccept();
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
voiceInputOps.listen(mLanguage);
}
protected void stopVoiceInput() {
if (voiceInputOps.isListening()) {
statusBar.setText(R.string.voice_input_stopping);
voiceInputOps.stop();
}
}
private void onVoiceInputStarted() {
statusBar.setText(voiceInputOps);
}
private void onVoiceInputStopped(String text) {
onText(text, false);
resetStatus();
}
private void onVoiceInputError(VoiceInputError error) {
if (error.isIrrelevantToUser()) {
Logger.i(LOG_TAG, "Ignoring voice input. " + error.debugMessage);
resetStatus();
} else {
Logger.e(LOG_TAG, "Failed to listen. " + error.debugMessage);
statusBar.setError(error.toString());
if (error.isNoPermission()) {
RequestPermissionDialog.show(this);
}
}
}
}

View file

@ -0,0 +1,135 @@
package io.github.sspanak.tt9.ime.voice;
import android.content.Context;
import android.os.Build;
import android.speech.SpeechRecognizer;
import androidx.annotation.NonNull;
import io.github.sspanak.tt9.R;
public class VoiceInputError {
public final static int ERROR_NOT_AVAILABLE = 101;
public final static int ERROR_INVALID_LANGUAGE = 102;
public final int code;
public final String message;
public final String debugMessage;
public VoiceInputError(Context context, int errorCode) {
code = errorCode;
debugMessage = codeToDebugString(errorCode);
message = codeToString(context, errorCode);
}
public boolean isNoPermission() {
return code == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS;
}
public boolean isIrrelevantToUser() {
return
code == SpeechRecognizer.ERROR_NO_MATCH
|| code == SpeechRecognizer.ERROR_SPEECH_TIMEOUT
|| code == SpeechRecognizer.ERROR_AUDIO
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && code == SpeechRecognizer.ERROR_CANNOT_LISTEN_TO_DOWNLOAD_EVENTS)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && code == SpeechRecognizer.ERROR_CANNOT_CHECK_SUPPORT)
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && code == SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
}
@NonNull
@Override
public String toString() {
return message;
}
@NonNull
private static String codeToString(Context context, int code) {
switch (code) {
case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
return context.getString(R.string.voice_input_error_no_permissions);
case SpeechRecognizer.ERROR_LANGUAGE_NOT_SUPPORTED:
return context.getString(R.string.voice_input_error_language_not_supported);
case SpeechRecognizer.ERROR_NETWORK:
return context.getString(R.string.voice_input_error_no_network);
case ERROR_NOT_AVAILABLE:
return context.getString(R.string.voice_input_error_not_available);
case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
case SpeechRecognizer.ERROR_SERVER:
case SpeechRecognizer.ERROR_SERVER_DISCONNECTED:
case SpeechRecognizer.ERROR_TOO_MANY_REQUESTS:
return context.getString(R.string.voice_input_error_network_failed);
default:
return context.getString(R.string.voice_input_error_generic);
}
}
private static String codeToDebugString(int code) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && code == SpeechRecognizer.ERROR_CANNOT_LISTEN_TO_DOWNLOAD_EVENTS) {
return "Cannot listen to download events.";
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && code == SpeechRecognizer.ERROR_CANNOT_CHECK_SUPPORT) {
return "Cannot check voice input support.";
}
String message = codeToDebugString31(code);
message = message != null ? message : codeToDebugStringCommon(code);
message = message != null ? message : "Unknown voice input error code: " + code;
return message;
}
private static String codeToDebugString31(int code) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
switch (code) {
case SpeechRecognizer.ERROR_TOO_MANY_REQUESTS:
return "Server overloaded. Try again later.";
case SpeechRecognizer.ERROR_SERVER_DISCONNECTED:
return "Lost connection to the server.";
case SpeechRecognizer.ERROR_LANGUAGE_NOT_SUPPORTED:
return "Language not supported.";
case SpeechRecognizer.ERROR_LANGUAGE_UNAVAILABLE:
return "Language missing. Try again later.";
}
}
return null;
}
private static String codeToDebugStringCommon(int code) {
switch (code) {
case SpeechRecognizer.ERROR_AUDIO:
return "Audio capture error.";
case SpeechRecognizer.ERROR_CLIENT:
return "Speech recognition client error.";
case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
return "No microphone permissions.";
case SpeechRecognizer.ERROR_NETWORK:
return "No network connection.";
case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
return "Network timeout.";
case SpeechRecognizer.ERROR_NO_MATCH:
return "No match.";
case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
return "Voice input service is busy.";
case SpeechRecognizer.ERROR_SERVER:
return "Speech recognition server error.";
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
return "No speech detected.";
case ERROR_NOT_AVAILABLE:
return "Voice input is not available.";
case ERROR_INVALID_LANGUAGE:
return "Invalid language for voice input.";
default:
return null;
}
}
}

View file

@ -0,0 +1,136 @@
package io.github.sspanak.tt9.ime.voice;
import android.content.Intent;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.util.ConsumerCompat;
import io.github.sspanak.tt9.util.Logger;
public class VoiceInputOps {
private final boolean isOnDeviceRecognitionAvailable;
private final boolean isRecognitionAvailable;
private final InputMethodService ims;
private Language language;
private SpeechRecognizer speechRecognizer;
private final VoiceListener listener;
private final ConsumerCompat<String> onStopListening;
private final ConsumerCompat<VoiceInputError> onListeningError;
public VoiceInputOps(
@NonNull InputMethodService ims,
Runnable onStart,
ConsumerCompat<String> onStop,
ConsumerCompat<VoiceInputError> onError
) {
isOnDeviceRecognitionAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && SpeechRecognizer.isOnDeviceRecognitionAvailable(ims);
isRecognitionAvailable = SpeechRecognizer.isRecognitionAvailable(ims);
listener = new VoiceListener(ims, onStart, this::onStop, this::onError);
onStopListening = onStop != null ? onStop : result -> {};
onListeningError = onError != null ? onError : error -> {};
this.ims = ims;
}
private void createRecognizer() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isOnDeviceRecognitionAvailable) {
speechRecognizer = SpeechRecognizer.createOnDeviceSpeechRecognizer(ims);
} else if (isRecognitionAvailable) {
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(ims);
} else {
return;
}
speechRecognizer.setRecognitionListener(listener);
}
public boolean isAvailable() {
return isRecognitionAvailable || isOnDeviceRecognitionAvailable;
}
public boolean isListening() {
return listener.isListening();
}
public void listen(Language language) {
if (language == null) {
onListeningError.accept(new VoiceInputError(ims, VoiceInputError.ERROR_INVALID_LANGUAGE));
return;
}
if (!isAvailable()) {
onListeningError.accept(new VoiceInputError(ims, VoiceInputError.ERROR_NOT_AVAILABLE));
return;
}
if (isListening()) {
onListeningError.accept(new VoiceInputError(ims, SpeechRecognizer.ERROR_RECOGNIZER_BUSY));
return;
}
createRecognizer();
this.language = language;
String locale = language.getLocale().toString().replace("_", "-");
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, locale);
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, toString());
speechRecognizer.startListening(intent);
Logger.d(getClass().getSimpleName(), "SpeechRecognizer started for locale: " + locale);
}
public void stop() {
this.language = null;
if (isAvailable() && listener.isListening()) {
speechRecognizer.stopListening();
}
}
private void destroy() {
this.language = null;
if (speechRecognizer != null) {
speechRecognizer.destroy();
speechRecognizer = null;
Logger.d(getClass().getSimpleName(), "SpeechRecognizer destroyed");
}
}
private void onStop(ArrayList<String> results) {
destroy();
onStopListening.accept(results.isEmpty() ? null : results.get(0));
}
private void onError(VoiceInputError error) {
destroy();
onListeningError.accept(error);
}
@NonNull
@Override
public String toString() {
String languageSuffix = language == null ? "" : " / " + language.getName();
return ims.getString(R.string.voice_input_listening) + languageSuffix;
}
}

View file

@ -0,0 +1,65 @@
package io.github.sspanak.tt9.ime.voice;
import android.content.Context;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.SpeechRecognizer;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import io.github.sspanak.tt9.util.ConsumerCompat;
class VoiceListener implements RecognitionListener {
private boolean listening = false;
@NonNull private final Context context;
private final Runnable onStart;
private final ConsumerCompat<ArrayList<String>> onStop;
private final ConsumerCompat<VoiceInputError> onError;
VoiceListener(
@NonNull Context context,
Runnable onStart,
ConsumerCompat<ArrayList<String>> onStop,
ConsumerCompat<VoiceInputError> onError
) {
this.context = context;
this.onStart = onStart != null ? onStart : () -> {};
this.onStop = onStop != null ? onStop : (t) -> {};
this.onError = onError != null ? onError : (e) -> {};
}
public boolean isListening() {
return listening;
}
@Override
public void onReadyForSpeech(Bundle params) {
listening = true;
onStart.run();
}
@Override
public void onError(int error) {
listening = false;
onError.accept(new VoiceInputError(context, error));
}
@Override
public void onResults(Bundle resultsRaw) {
listening = false;
ArrayList<String> results = resultsRaw.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
onStop.accept(results == null ? new ArrayList<>() : results);
}
// we don't care about these, but the interface requires us to implement them
@Override public void onPartialResults(Bundle results) {}
@Override public void onBeginningOfSpeech() {}
@Override public void onEndOfSpeech() {}
@Override public void onEvent(int e, Bundle b) {}
@Override public void onRmsChanged(float r) {}
@Override public void onBufferReceived(byte[] b) {}
}

View file

@ -3,6 +3,7 @@ package io.github.sspanak.tt9.ui.dialogs;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import io.github.sspanak.tt9.ime.TraditionalT9;
@ -51,4 +52,10 @@ public class PopupDialogActivity extends AppCompatActivity {
intent.putExtra(PopupDialog.INTENT_CLOSE, message);
startService(intent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
onDialogClose(null);
}
}

View file

@ -0,0 +1,64 @@
package io.github.sspanak.tt9.ui.dialogs;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.ui.UI;
import io.github.sspanak.tt9.util.Permissions;
public class RequestPermissionDialog extends AppCompatActivity {
private final Permissions permissions;
public RequestPermissionDialog() {
super();
permissions = new Permissions(this);
}
@Override
protected void onCreate(Bundle savedData) {
super.onCreate(savedData);
// currently there is only one permission to request, so we don't ovecomplicate it
permissions.requestRecordAudio();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
showPermissionRequiredMessage(permissions, grantResults);
finish();
reviveMain();
}
private void reviveMain() {
Intent intent = new Intent(this, TraditionalT9.class);
intent.putExtra(PopupDialog.INTENT_CLOSE, "");
startService(intent);
}
private void showPermissionRequiredMessage(@NonNull String[] permissions, @NonNull int[] grantResults) {
if (permissions.length == 0) {
return;
}
if (permissions[0].equals(Manifest.permission.RECORD_AUDIO) && grantResults[0] == PackageManager.PERMISSION_DENIED) {
UI.toastLong(this, R.string.voice_input_mic_permission_is_needed);
}
}
public static void show(InputMethodService ims) {
Intent intent = new Intent(ims, RequestPermissionDialog.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
ims.startActivity(intent);
}
}

View file

@ -4,17 +4,9 @@ import android.content.Context;
import android.util.AttributeSet;
public class SoftCommandKey extends SoftNumberKey {
public SoftCommandKey(Context context) {
super(context);
}
public SoftCommandKey(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftCommandKey(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SoftCommandKey(Context context) { super(context);}
public SoftCommandKey(Context context, AttributeSet attrs) { super(context, attrs);}
public SoftCommandKey(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);}
@Override
protected String getTitle() {
@ -40,4 +32,13 @@ public class SoftCommandKey extends SoftNumberKey {
return null;
}
@Override
public void render() {
if (tt9 != null && tt9.isVoiceInputMissing() && getNumber(getId()) == 3) {
setVisibility(GONE);
} else {
super.render();
}
}
}

View file

@ -0,0 +1,42 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
public class SoftFilterKey extends SoftKey {
public SoftFilterKey(Context context) { super(context); setFontSize(); }
public SoftFilterKey(Context context, AttributeSet attrs) { super(context, attrs); setFontSize(); }
public SoftFilterKey(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setFontSize(); }
private void setFontSize() {
complexLabelTitleSize = SettingsStore.SOFT_KEY_COMPLEX_LABEL_TITLE_SIZE / 0.85f;
complexLabelSubTitleSize = SettingsStore.SOFT_KEY_COMPLEX_LABEL_SUB_TITLE_SIZE / 0.85f;
}
@Override
protected boolean handleHold() {
if (!validateTT9Handler()) {
return false;
}
return tt9.onKeyFilterClear(false);
}
@Override
protected boolean handleRelease() {
boolean multiplePress = getLastPressedKey() == getId();
return tt9.onKeyFilterSuggestions(false, multiplePress);
}
@Override
protected String getTitle() {
return "CLR";
}
@Override
protected String getSubTitle() {
return "FLTR";
}
}

View file

@ -3,16 +3,16 @@ package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
public class SoftKeyInputMode extends SoftKey {
public SoftKeyInputMode(Context context) {
public class SoftInputModeKey extends SoftKey {
public SoftInputModeKey(Context context) {
super(context);
}
public SoftKeyInputMode(Context context, AttributeSet attrs) {
public SoftInputModeKey(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftKeyInputMode(Context context, AttributeSet attrs, int defStyleAttr) {
public SoftInputModeKey(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

View file

@ -130,6 +130,10 @@ public class SoftKey extends androidx.appcompat.widget.AppCompatButton implement
repeatHandler.removeCallbacks(this::repeatOnLongPress);
}
protected static int getLastPressedKey() {
return lastPressedKey;
}
protected boolean handlePress() {
return false;
}
@ -144,16 +148,14 @@ public class SoftKey extends androidx.appcompat.widget.AppCompatButton implement
}
int keyId = getId();
boolean multiplePress = lastPressedKey == keyId;
if (keyId == R.id.soft_key_add_word) { tt9.addWord(); return true; }
if (keyId == R.id.soft_key_command_palette) return tt9.onKeyCommandPalette(false);
if (keyId == R.id.soft_key_filter_suggestions) return tt9.onKeyFilterSuggestions(false, multiplePress);
if (keyId == R.id.soft_key_clear_filter) return tt9.onKeyFilterClear(false);
if (keyId == R.id.soft_key_left_arrow) return tt9.onKeyScrollSuggestion(false, true);
if (keyId == R.id.soft_key_right_arrow) return tt9.onKeyScrollSuggestion(false, false);
if (keyId == R.id.soft_key_language) return tt9.onKeyNextLanguage(false);
if (keyId == R.id.soft_key_settings) { tt9.showSettings(); return true; }
if (keyId == R.id.soft_key_voice_input) { tt9.toggleVoiceInput(); return true; }
return false;
}
@ -186,6 +188,7 @@ public class SoftKey extends androidx.appcompat.widget.AppCompatButton implement
return null;
}
/**
* render
* Sets the key label using "getTitle()" and "getSubtitle()" or if they both
@ -206,6 +209,8 @@ public class SoftKey extends androidx.appcompat.widget.AppCompatButton implement
return;
}
int titleLength = title.length();
SpannableStringBuilder sb = new SpannableStringBuilder(title);
sb.append('\n');
sb.append(subtitle);
@ -215,10 +220,10 @@ public class SoftKey extends androidx.appcompat.widget.AppCompatButton implement
padding /= 10;
}
sb.setSpan(new RelativeSizeSpan(complexLabelTitleSize), 0, 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
sb.setSpan(new StyleSpan(Typeface.ITALIC), 0, 1, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
sb.setSpan(new RelativeSizeSpan(padding), 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
sb.setSpan(new RelativeSizeSpan(complexLabelSubTitleSize), 2, sb.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
sb.setSpan(new RelativeSizeSpan(complexLabelTitleSize), 0, titleLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
sb.setSpan(new StyleSpan(Typeface.ITALIC), 0, titleLength, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
sb.setSpan(new RelativeSizeSpan(padding), titleLength, titleLength + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
sb.setSpan(new RelativeSizeSpan(complexLabelSubTitleSize), titleLength + 1, sb.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
setText(sb);
}

View file

@ -0,0 +1,24 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
public class SoftVoiceInputKey extends SoftKey {
public SoftVoiceInputKey(Context context) { super(context); }
public SoftVoiceInputKey(Context context, AttributeSet attrs) { super(context, attrs); }
public SoftVoiceInputKey(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
@Override
protected String getTitle() {
return "🎤";
}
@Override
public void render() {
if (tt9 != null && tt9.isVoiceInputMissing()) {
setVisibility(INVISIBLE);
} else {
super.render();
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.core.content.ContextCompat;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.ime.voice.VoiceInputOps;
import io.github.sspanak.tt9.util.Logger;
public class StatusBar {
@ -20,6 +21,21 @@ public class StatusBar {
}
public boolean isErrorShown() {
return statusText != null && statusText.startsWith("");
}
public void setError(String error) {
setText("" + error);
}
public void setText(int stringResourceId) {
setText(statusView.getContext().getString(stringResourceId));
}
public void setText(String text) {
statusText = text;
this.render();
@ -31,6 +47,11 @@ public class StatusBar {
}
public void setText(VoiceInputOps voiceInputOps) {
setText("[ " + voiceInputOps.toString() + " ]");
}
public void setDarkTheme(boolean darkTheme) {
if (statusView == null) {
return;

View file

@ -6,13 +6,15 @@ import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.annotation.NonNull;
import java.util.HashMap;
public class Permissions {
private static final HashMap<String, Boolean> firstTimeAsking = new HashMap<>();
private final Activity activity;
@NonNull private final Activity activity;
public Permissions(Activity activity) {
public Permissions(@NonNull Activity activity) {
this.activity = activity;
}
@ -33,6 +35,11 @@ public class Permissions {
}
}
public void requestRecordAudio() {
requestPermission(Manifest.permission.RECORD_AUDIO);
}
public boolean noWriteStorage() {
return
Build.VERSION.SDK_INT < Build.VERSION_CODES.R

View file

@ -185,13 +185,12 @@
android:id="@+id/separator_2_2"
style="@style/numSeparator" />
<io.github.sspanak.tt9.ui.main.keys.SoftKey
<io.github.sspanak.tt9.ui.main.keys.SoftFilterKey
android:id="@+id/soft_key_filter_suggestions"
style="@android:style/Widget.Holo.Button.Borderless"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="@dimen/numpad_control_key_layout_weight"
android:text="Fltr" />
android:layout_weight="@dimen/numpad_control_key_layout_weight"/>
</LinearLayout>
@ -202,7 +201,7 @@
android:layoutDirection="ltr"
tools:ignore="HardcodedText">
<io.github.sspanak.tt9.ui.main.keys.SoftKeyInputMode
<io.github.sspanak.tt9.ui.main.keys.SoftInputModeKey
android:id="@+id/soft_key_input_mode"
style="@android:style/Widget.Holo.Button.Borderless"
android:layout_width="0dp"
@ -244,13 +243,12 @@
android:id="@+id/separator_3_2"
style="@style/numSeparator" />
<io.github.sspanak.tt9.ui.main.keys.SoftKey
android:id="@+id/soft_key_clear_filter"
<io.github.sspanak.tt9.ui.main.keys.SoftVoiceInputKey
android:id="@+id/soft_key_voice_input"
style="@android:style/Widget.Holo.Button.Borderless"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="@dimen/numpad_control_key_layout_weight"
android:text="Clr" />
android:layout_weight="@dimen/numpad_control_key_layout_weight" />
</LinearLayout>

View file

@ -3,6 +3,7 @@
<string name="app_settings">Настройки на TT9</string>
<string name="pref_font_size_large">Голям</string>
<string name="completed">Завършено</string>
<string name="loading">Зареждане…</string>
<string name="no_language">Няма език</string>
<string name="error_unexpected">Възникна неочаквана грешка.</string>
<string name="failed_loading_language_definitions">Неуспешно зареждане на езиковите дефиниции.</string>
@ -130,4 +131,13 @@
<string name="key_yellow">Жълт бутон</string>
<string name="key_blue">Син бутон</string>
<string name="key_volume_mute">Заглушаване на звук</string>
<string name="voice_input_listening">Говорете</string>
<string name="voice_input_mic_permission_is_needed">Трябва да разрешите достъпа до микрофона, за да използвате гласовото въвеждане.</string>
<string name="voice_input_error_generic">Неуспешно гласово въвеждане</string>
<string name="voice_input_error_no_permissions">Няма достъп до микрофона</string>
<string name="voice_input_error_language_not_supported">Езикът не се поддържа</string>
<string name="voice_input_error_not_available">Не е налично въвеждане с глас</string>
<string name="voice_input_error_no_network">Няма връзка с интернет</string>
<string name="voice_input_error_network_failed">Проблем с мрежовата връзка</string>
<string name="voice_input_stopping">Изключване на микрофона…</string>
</resources>

View file

@ -120,4 +120,13 @@
<string name="key_yellow">Gelber Knopf</string>
<string name="key_blue">Blauer Knopf</string>
<string name="key_volume_mute">Stummschalttaste</string>
<string name="voice_input_listening">Sprechen</string>
<string name="voice_input_mic_permission_is_needed">Sie müssen dem Mikrofon die Erlaubnis erteilen, die Spracheingabe zu verwenden.</string>
<string name="voice_input_error_generic">Fehler bei der Spracheingabe</string>
<string name="voice_input_error_no_permissions">Keine Mikrofonberechtigung</string>
<string name="voice_input_error_language_not_supported">Sprache nicht unterstützt</string>
<string name="voice_input_error_network_failed">Netzwerkverbindung fehlgeschlagen</string>
<string name="voice_input_error_no_network">Keine Internetverbindung</string>
<string name="voice_input_error_not_available">Spracheingabe ist nicht verfügbar</string>
<string name="voice_input_stopping">Mikrofon ausschalten…</string>
</resources>

View file

@ -130,4 +130,13 @@
<string name="key_yellow">Botón amarillo</string>
<string name="key_blue">Botón azul</string>
<string name="key_volume_mute">Botón de silencio</string>
<string name="voice_input_listening">Hable</string>
<string name="voice_input_mic_permission_is_needed">Debe otorgar permiso al micrófono para usar la entrada de voz.</string>
<string name="voice_input_error_generic">Error de entrada de voz</string>
<string name="voice_input_error_no_permissions">Sin permiso para el micrófono</string>
<string name="voice_input_error_language_not_supported">Idioma no compatible</string>
<string name="voice_input_error_network_failed">Conexión de red fallida</string>
<string name="voice_input_error_no_network">Sin conexión a Internet</string>
<string name="voice_input_error_not_available">La entrada de voz no está disponible</string>
<string name="voice_input_stopping">Apagando el micrófono…</string>
</resources>

View file

@ -127,4 +127,13 @@
<string name="key_yellow">Bouton jaune</string>
<string name="key_blue">Bouton bleu</string>
<string name="key_volume_mute">Muet</string>
<string name="voice_input_listening">Parlez</string>
<string name="voice_input_mic_permission_is_needed">Vous devez accorder l\'autorisation au microphone pour utiliser la saisie vocale.</string>
<string name="voice_input_error_generic">Erreur de saisie vocale</string>
<string name="voice_input_error_no_permissions">Pas d\'autorisation pour le microphone</string>
<string name="voice_input_error_language_not_supported">Langue non prise en charge</string>
<string name="voice_input_error_network_failed">Échec de la connexion réseau</string>
<string name="voice_input_error_no_network">Pas de connexion Internet</string>
<string name="voice_input_error_not_available">La saisie vocale n\'est pas disponible</string>
<string name="voice_input_stopping">Désactivation du microphone…</string>
</resources>

View file

@ -120,5 +120,14 @@
<string name="key_yellow">Pulsante giallo</string>
<string name="key_blue">Pulsante blu</string>
<string name="key_volume_mute">Muto</string>
<string name="voice_input_listening">Parli</string>
<string name="voice_input_mic_permission_is_needed">Devi concedere l\'autorizzazione al microfono per utilizzare l\'input vocale.</string>
<string name="voice_input_error_generic">Errore di input vocale</string>
<string name="voice_input_error_no_permissions">Nessuna autorizzazione per il microfono</string>
<string name="voice_input_error_language_not_supported">Lingua non supportata</string>
<string name="voice_input_error_network_failed">Connessione di rete fallita</string>
<string name="voice_input_error_no_network">Nessuna connessione Internet</string>
<string name="voice_input_error_not_available">L\'input vocale non è disponibile</string>
<string name="voice_input_stopping">Spegnimento del microfono…</string>
</resources>

View file

@ -133,4 +133,13 @@
<string name="key_yellow">כפתור צהוב</string>
<string name="key_blue">כפתור כחול</string>
<string name="key_volume_mute">כפתור השתק</string>
<string name="voice_input_listening">האזנה</string>
<string name="voice_input_mic_permission_is_needed">עליך להעניק למיקרופון הרשאה להשתמש בקלט קולי.</string>
<string name="voice_input_error_generic">שגיאת קלט קולי</string>
<string name="voice_input_error_no_permissions">אין הרשאת מיקרופון</string>
<string name="voice_input_error_language_not_supported">השפה אינה נתמכת</string>
<string name="voice_input_error_network_failed">חיבור הרשת נכשל</string>
<string name="voice_input_error_no_network">אין חיבור לאינטרנט</string>
<string name="voice_input_error_not_available">קלט קולי אינו זמין</string>
<string name="voice_input_stopping">מכבה את המיקרופון…</string>
</resources>

View file

@ -139,4 +139,13 @@
<string name="key_yellow">Geltonas mygtukas</string>
<string name="key_blue">Mėlynas mygtukas</string>
<string name="key_volume_mute">Nutildymo mygt.</string>
<string name="voice_input_listening">Kalbėkite</string>
<string name="voice_input_mic_permission_is_needed">Turite suteikti mikrofonui leidimą naudoti balso įvestį.</string>
<string name="voice_input_error_generic">Balso įvesties klaida</string>
<string name="voice_input_error_no_permissions">Nėra mikrofono leidimo</string>
<string name="voice_input_error_language_not_supported">Kalba nepalaikoma</string>
<string name="voice_input_error_network_failed">Tinklo ryšys nepavyko</string>
<string name="voice_input_error_no_network">Nėra interneto ryšio</string>
<string name="voice_input_error_not_available">Balso įvestis nėra prieinama</string>
<string name="voice_input_stopping">Išjungiamas mikrofonas…</string>
</resources>

View file

@ -118,4 +118,13 @@
<string name="key_yellow">Gele knop</string>
<string name="key_blue">Blauwe knop</string>
<string name="key_volume_mute">Stilteknop</string>
<string name="voice_input_listening">Spreek</string>
<string name="voice_input_mic_permission_is_needed">U moet de microfoon toestemming geven om spraakopvoer te gebruiken.</string>
<string name="voice_input_error_generic">Fout bij spraakopvoer</string>
<string name="voice_input_error_no_permissions">Geen microfoontoestemming</string>
<string name="voice_input_error_language_not_supported">Taal niet ondersteund</string>
<string name="voice_input_error_network_failed">Netwerkverbinding mislukt</string>
<string name="voice_input_error_no_network">Geen internetverbinding</string>
<string name="voice_input_error_not_available">Spraakopvoer is niet beschikbaar</string>
<string name="voice_input_stopping">Microfoon uitschakelen…</string>
</resources>

View file

@ -133,4 +133,13 @@
<string name="key_yellow">Botão amarelo</string>
<string name="key_blue">Botão azul</string>
<string name="key_volume_mute">Mudo</string>
<string name="voice_input_listening">Fale</string>
<string name="voice_input_mic_permission_is_needed">Você deve conceder permissão ao microfone para usar a entrada de voz.</string>
<string name="voice_input_error_generic">Erro de entrada de voz</string>
<string name="voice_input_error_no_permissions">Sem permissão para o microfone</string>
<string name="voice_input_error_language_not_supported">Idioma não suportado</string>
<string name="voice_input_error_network_failed">Falha na conexão de rede</string>
<string name="voice_input_error_no_network">Sem conexão com a Internet</string>
<string name="voice_input_error_not_available">A entrada de voz não está disponível</string>
<string name="voice_input_stopping">Desligando o microfone…</string>
</resources>

View file

@ -130,4 +130,13 @@
<string name="key_yellow">Жёлтая кнопка</string>
<string name="key_blue">Синяя кнопка</string>
<string name="key_volume_mute">Выключения звука</string>
<string name="voice_input_listening">Говорите</string>
<string name="voice_input_mic_permission_is_needed">Вы должны предоставить микрофону разрешение на использование голосового ввода.</string>
<string name="voice_input_error_generic">Ошибка голосового ввода</string>
<string name="voice_input_error_no_permissions">Нет разрешения на использование микрофона</string>
<string name="voice_input_error_language_not_supported">Язык не поддерживается</string>
<string name="voice_input_error_network_failed">Сбой сетевого подключения</string>
<string name="voice_input_error_no_network">Нет подключения к Интернету</string>
<string name="voice_input_error_not_available">Голосовой ввод недоступен</string>
<string name="voice_input_stopping">Отключение микрофона…</string>
</resources>

View file

@ -130,4 +130,13 @@
<string name="key_yellow">Sarı düğme</string>
<string name="key_blue">Mavi düğme</string>
<string name="key_volume_mute">Sessiz tuşu</string>
<string name="voice_input_listening">Konuşun</string>
<string name="voice_input_mic_permission_is_needed">Sesli giriş kullanmak için mikrofona izin vermelisiniz.</string>
<string name="voice_input_error_generic">Sesli giriş hatası</string>
<string name="voice_input_error_no_permissions">Mikrofon izni yok</string>
<string name="voice_input_error_language_not_supported">Dil desteklenmiyor</string>
<string name="voice_input_error_network_failed">Ağ bağlantısı başarısız</string>
<string name="voice_input_error_no_network">İnternet bağlantısı yok</string>
<string name="voice_input_error_not_available">Sesli giriş kullanılamıyor</string>
<string name="voice_input_stopping">Mikrofon kapatılıyor…</string>
</resources>

View file

@ -141,4 +141,13 @@
<string name="key_yellow">Жовта кнопка</string>
<string name="key_blue">Синя кнопка</string>
<string name="key_volume_mute">Вимкнення звуку</string>
<string name="voice_input_listening">Диктуйте</string>
<string name="voice_input_mic_permission_is_needed">Ви повинні надати мікрофону дозвіл на використання голосового введення.</string>
<string name="voice_input_error_generic">Помилка голосового введення</string>
<string name="voice_input_error_no_permissions">Немає дозволу на використання мікрофона</string>
<string name="voice_input_error_language_not_supported">Мова не підтримується</string>
<string name="voice_input_error_network_failed">Помилка підключення до мережі</string>
<string name="voice_input_error_no_network">Немає підключення до Інтернету</string>
<string name="voice_input_error_not_available">Голосовий ввід недоступний</string>
<string name="voice_input_stopping">Вимикання мікрофона…</string>
</resources>

View file

@ -6,6 +6,7 @@
<string name="app_settings">TT9 Settings</string>
<string name="completed">Completed</string>
<string name="error">Error</string>
<string name="loading">Loading…</string>
<string name="no_language">No Language</string>
<string name="error_unexpected">Unexpected error occurred.</string>
@ -162,4 +163,14 @@
<string name="char_dot" translatable="false">.</string>
<string name="char_newline">New Line</string>
<string name="char_space">Space</string>
<string name="voice_input_listening">Speak</string>
<string name="voice_input_stopping">Turning off the microphone…</string>
<string name="voice_input_mic_permission_is_needed">You must allow the microphone permission to use voice input.</string>
<string name="voice_input_error_generic">Voice input error</string>
<string name="voice_input_error_no_permissions">No microphone permission</string>
<string name="voice_input_error_language_not_supported">Language not supported</string>
<string name="voice_input_error_network_failed">Network connection failed</string>
<string name="voice_input_error_no_network">No Internet connection</string>
<string name="voice_input_error_not_available">Voice input is not available</string>
</resources>

View file

@ -115,17 +115,30 @@ Many phones have only two or three "free" buttons that can be used as hotkeys. B
Below is a list of the possible commands:
- **Show the Settings Screen (Default Combo: ✱, 1-key).** On the Settings screen, you can choose languages for typing, configure the keypad hotkeys, change the application appearance, or improve compatibility with your phone.
- **Add a Word (Default Combo: ✱, 2-key).** Add a new word to the dictionary for the current language. You can also add new emojis and then access them by pressing 1-1-3. Regardless of the currently selected language, all emojis will be available in all languages.
- **Voice Input (Default Combo: ✱, 3-key).** Activate the voice input on the phones that support it. See [below](#voice-input) for more info.
- **Select a Different Keyboard (Default Combo: ✱, 0-key).** Open the Android Change Keyboard dialog allowing you to select between all installed keyboards.
_This key does not do anything when the Screen Layout is set to "Virtual Keyboard" because all keys for all possible functions are already available on the screen._
If you do have a hardware keypad and prefer having more screen space, disable the software keys from the Settings → Appearance.
## Voice Input
The voice input function allows for speech-to-text input, similar to Gboard. Like all other keyboards, Traditional T9 does not perform speech recognition by itself, but it asks your phone to do it.
_The Voice Input button is hidden on devices that do not support it._
### Supported Devices
On devices with Google Services, it will use the Google Cloud infrastructure to convert your words to text. You must connect to a Wi-Fi network or enable mobile data for this method to work.
On devices without Google, if the device has a voice assistant app or the native keyboard supports voice input, whichever is available will be used for speech recognition. Note that this method is considerably less capable than Google. It will not work in a noisy environment and will usually recognize only simple phrases, such as: "open calendar" or "play music" and similar. The advantage is that it will work offline.
Other phones without Google will generally not support voice input. Chinese phones do not have speech recognition capabilities due to Chinese security policies. On these phones, it may be possible to enable voice input support by installing the Google application, package name: "com.google.android.googlequicksearchbox".
## On-screen Keypad
On touchscreen-only phones, a fully functional on-screen keypad is available and it will be enabled automatically. If, for some reason, your phone was not detected to have a touchscreen, enable it by going to Settings → Appearance → On-Screen Layout, and selecting "Virtual numpad".
It is also recommended to disable the special behavior of the "Back" key working as "Backspace". It is useful only for a hardware keypad. To do so, go to Settings → Keypad → Select Hotkeys → Backspace key, then select the "--" option.
If you do have a hardware keypad and prefer having more screen space, disable the software keys from the Settings → Appearance.
## Settings Screen
On the Settings screen, you can choose languages for typing, configure the keypad hotkeys, change the application appearance, or improve compatibility with your phone.
@ -243,9 +256,11 @@ To mitigate this problem, go to Settings → Appearance, and enable "Status Icon
**Long explanation.** Qin F21 Pro (and possibly F22, too), has a hotkey application that allows assigning Volume Up and Volume Down functions to number keys. By default, the hotkey manager is enabled, and holding 2-key increases the volume, holding 8-key decreases it. However, when there is no status icon, the manager assumes no keyboard is active and adjusts the volume, instead of letting Traditional T9 handle the key and type a number. So, enabling the icon just bypasses the hotkey manager and everything works fine.
#### General problems on Xiaomi phones
Xiaomi has introduced several non-standard permissions on their phones, which prevent Traditional T9's virtual on-screen keyboard from working properly. More precisely, the "Show Settings" and the "Add Word" keys may not perform their respective functions. To fix this, you must grant the "Display pop-up window" and "Display pop-up window while running in the background" permissions to TT9 from your phone's settings. [This guide](https://parental-control.flashget.com/how-to-enable-display-pop-up-windows-while-running-in-the-background-on-flashget-kids-on-xiaomi) for another application explains how to do it.
It is also highly recommended to grant the "Permanent notification" permission. This is similar to the "Notifications" permission introduced in Android 13. See [above](#notes-for-android-13-or-higher) for more information on why you need it.
_The Xiaomi problems have been discussed in [this GitHub issue](https://github.com/sspanak/tt9/issues/490)._
#### Voice Input takes a very long time to stop
It is [a known problem](https://issuetracker.google.com/issues/158198432) on Android 10 that Google never fixed. It is not possible to mitigate it on the TT9 side. To stop the Voice Input operation, stay quiet for a couple of seconds. Android turns off the microphone automatically when it can not detect any speech.

View file

@ -0,0 +1,11 @@
Traditional T9 е 12-клавишна клавиатура за устройства с копчета. Поддържа подскаващ текста на повече от 25 езика и бързи клавиши, а виртуалната клавиатура пресъздава доброто старо усещане за Нокия на съвременните телефони със сензорен екран. И най-хубавото е, че не ви шпионира.
Поддържани езици: арабски, български, хърватски, чешки, датски, холандски, английски, финландски, френски, немски, гръцки, иврит, унгарски, индонезийски, италиански, кисуахили, норвежки, полски, португалски (европейски и бразилски), румънски, руски, испански, шведски, турски, украински, идиш.
Философия и защита не личните данни:
- Без реклами, специални или платени функции. Всичко е напълно безплатно.
- Без шпиониране, следене, телеметрия и отчети. Без глупости!
- Без връзка към интернет, освен когато е активно гласовото въвеждане.
- Единствено си върши работата.
- С отворен код, така че може да проверите горното и сами.
- Създадена с помощта на цялата общност.

View file

@ -0,0 +1 @@
T9 клавиатура за устройства с копчета.

View file

@ -0,0 +1 @@
Traditional T9

View file

@ -5,7 +5,7 @@ Supported languages: Arabic, Bulgarian, Croatian, Czech, Danish, Dutch, English,
Privacy Policy and Philosophy:
- No ads, no premium or paid features. It's all free.
- No spying, no tracking, no telemetry or reports. No nothing!
- No network connectivity.
- No network connectivity, except when voice input is active.
- It only does its job.
- Open-source, so you can verify all the above yourself.
- Created with help from the entire community.