Voice input (#531)
This commit is contained in:
parent
7a19d6bcf7
commit
c64c8dac5c
39 changed files with 837 additions and 53 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue