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

@ -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>