Voice input (#531)
This commit is contained in:
parent
7a19d6bcf7
commit
c64c8dac5c
39 changed files with 837 additions and 53 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ public class TraditionalT9 extends MainViewOps {
|
|||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
stopVoiceInput();
|
||||
onFinishTyping();
|
||||
suggestionOps.clear();
|
||||
setStatusIcon(mInputMode);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)._
|
||||
_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.
|
||||
11
fastlane/metadata/android/bg-BG/full_description.txt
Normal file
11
fastlane/metadata/android/bg-BG/full_description.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Traditional T9 е 12-клавишна клавиатура за устройства с копчета. Поддържа подскаващ текста на повече от 25 езика и бързи клавиши, а виртуалната клавиатура пресъздава доброто старо усещане за Нокия на съвременните телефони със сензорен екран. И най-хубавото е, че не ви шпионира.
|
||||
|
||||
Поддържани езици: арабски, български, хърватски, чешки, датски, холандски, английски, финландски, френски, немски, гръцки, иврит, унгарски, индонезийски, италиански, кисуахили, норвежки, полски, португалски (европейски и бразилски), румънски, руски, испански, шведски, турски, украински, идиш.
|
||||
|
||||
Философия и защита не личните данни:
|
||||
- Без реклами, специални или платени функции. Всичко е напълно безплатно.
|
||||
- Без шпиониране, следене, телеметрия и отчети. Без глупости!
|
||||
- Без връзка към интернет, освен когато е активно гласовото въвеждане.
|
||||
- Единствено си върши работата.
|
||||
- С отворен код, така че може да проверите горното и сами.
|
||||
- Създадена с помощта на цялата общност.
|
||||
1
fastlane/metadata/android/bg-BG/short_description.txt
Normal file
1
fastlane/metadata/android/bg-BG/short_description.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
T9 клавиатура за устройства с копчета.
|
||||
1
fastlane/metadata/android/bg-BG/title.txt
Normal file
1
fastlane/metadata/android/bg-BG/title.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Traditional T9
|
||||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue