Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ Please check the GitHub repository to report issues: [https://github.com/futo-or

The source code is hosted on our [internal GitLab](https://gitlab.futo.org/keyboard/latinime) and mirrored to [GitHub](https://github.com/futo-org/android-keyboard/). As registration is closed on our internal GitLab, we use GitHub instead for issues and pull requests.

If you want to help translate the app, please do so via our Pontoon instance: https://i18n-keyboard.futo.org/

Due to custom license, pull requests to this repository require signing a [CLA](https://cla.futo.org/) which you can do after opening a PR. Contributions to the [layouts repo](https://github.com/futo-org/futo-keyboard-layouts) don't require CLA as they're Apache-2.0

If you want to help translate the app, please do so via our Pontoon instance: https://i18n-keyboard.futo.org/
Please do not submit AI-generated pull requests, as these tend to make the codebase more difficult to understand. Pull requests made with clear heavy use of AI may be closed without comment. Please also avoid using AI to write issues, and try to explain it in your own words instead.

## Layouts

Expand Down
15 changes: 15 additions & 0 deletions java/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@
</intent-filter>
</activity>


<activity android:name=".uix.settings.VoiceInputSwitchActivity"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:launchMode="singleTask"
android:noHistory="false"
android:configChanges="orientation|screenLayout|screenSize|keyboardHidden|keyboard|uiMode|density"
android:exported="true">
<intent-filter>
<action android:name="org.futo.inputmethod.latin.action.VoiceInputSwitch" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<activity android:name=".uix.ImportResourceActivity"
android:label="@string/language_settings_import_dictionary_or_model"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
Expand Down
9 changes: 9 additions & 0 deletions java/res/values/strings-uix.xml
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,15 @@
<!-- Voice input settings menu -->
<string name="voice_input_settings_title">Voice Input</string>
<string name="voice_input_settings_builtin_disabled_notice">Built-in voice input is disabled!</string>

<string name="voice_input_settings_builtin_disabled_notice2">Using external engine: %s</string>
<string name="voice_input_settings_backend_system">External engine</string>
<string name="voice_input_settings_backend_system_internal">None (built-in)</string>
<string name="voice_input_settings_backend_system_external_warning">Note: the %s app will record your speech and may send it to online servers, it is not covered by our privacy policy.</string>
<string name="voice_input_settings_backend_system_confirm">Switch voice input engine to %s?</string>
<string name="voice_input_settings_backend_system_confirm_yes">Switch</string>
<string name="voice_input_settings_backend_system_confirm_no">Cancel</string>

<string name="voice_input_settings_disable_builtin_voice_input">Disable built-in voice input</string>
<string name="voice_input_settings_disable_builtin_voice_input_subtitle">Use voice input provided by external app</string>
<string name="voice_input_settings_indication_sounds">Indication sounds</string>
Expand Down
7 changes: 7 additions & 0 deletions java/src/org/futo/inputmethod/latin/LatinIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,13 @@ class LatinIME : InputMethodServiceCompose(), LatinIMELegacy.SuggestionStripCont
// TODO: Spell checker service
}

/// Switch to voice IME or to internal one if it's no longer present.
fun trySwitchToShortcutIMEorInternal() {
if(!latinIMELegacy.mRichImm.switchToShortcutIme(this)) {
uixManager.activateInternalVoiceIME()
}
}

override val foldState: FoldingOptions
get() = uixManager.foldingOptions.value

Expand Down
4 changes: 2 additions & 2 deletions java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public interface SuggestionStripController {
private View mComposeInputView;
private InsetsUpdater mInsetsUpdater;

private RichInputMethodManager mRichImm;
public RichInputMethodManager mRichImm;
public final KeyboardSwitcher mKeyboardSwitcher;
private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;

Expand Down Expand Up @@ -727,7 +727,7 @@ public void onCodeInput(final int codePoint, final int x, final int y,
// completely replace #onCodeInput.
public void onEvent(@Nonnull final Event event) {
if (Constants.CODE_SHORTCUT == event.mKeyCode) {
mRichImm.switchToShortcutIme(mInputMethodService);
getLatinIME().trySwitchToShortcutIMEorInternal();
}
/*final InputTransaction completeInputTransaction =
mInputLogic.onCodeInput(mSettings.getCurrent(), event,
Expand Down
66 changes: 49 additions & 17 deletions java/src/org/futo/inputmethod/latin/RichInputMethodManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import org.futo.inputmethod.annotations.UsedForTesting;
import org.futo.inputmethod.compat.InputMethodManagerCompatWrapper;
import org.futo.inputmethod.latin.settings.Settings;
import org.futo.inputmethod.latin.uix.DataStoreHelper;
import org.futo.inputmethod.latin.uix.PreferenceUtils;
import org.futo.inputmethod.latin.uix.SettingsKt;
import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils;
import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils;

Expand All @@ -40,6 +42,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nonnull;
Expand All @@ -63,6 +66,8 @@ private RichInputMethodManager() {
private InputMethodManagerCompatWrapper mImmWrapper;
private InputMethodInfoCache mInputMethodInfoCache;
private RichInputMethodSubtype mCurrentRichInputMethodSubtype;

private String mShortcutPackage;
private InputMethodInfo mShortcutInputMethodInfo;
private InputMethodSubtype mShortcutSubtype;

Expand Down Expand Up @@ -279,40 +284,67 @@ private void updateShortcutIme() {
+ (mShortcutSubtype == null ? "<null>" : (
mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
}
final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype;
final Locale systemLocale = mContext.getResources().getConfiguration().locale;

// TODO: Update an icon for shortcut IME
final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
getInputMethodManager().getShortcutInputMethodsAndSubtypes();
mShortcutPackage = DataStoreHelper.getSetting(SettingsKt.getSYSTEM_VOICE_INPUT_PACKAGE());
if(mShortcutPackage.isEmpty()) {
mShortcutPackage = null;
}



mShortcutInputMethodInfo = null;
mShortcutSubtype = null;
for (final InputMethodInfo imi : shortcuts.keySet()) {
final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
// TODO: Returns the first found IMI for now. Should handle all shortcuts as
// appropriate.
mShortcutInputMethodInfo = imi;
// TODO: Pick up the first found subtype for now. Should handle all subtypes
// as appropriate.
mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
break;

if(mShortcutPackage != null) {
final List<InputMethodInfo> inputMethodList = getInputMethodManager().getEnabledInputMethodList();
for (final InputMethodInfo imi : inputMethodList) {
if(!mShortcutPackage.equals(imi.getPackageName())) continue;

mShortcutInputMethodInfo = imi;
final List<InputMethodSubtype> subtypes = getInputMethodManager()
.getEnabledInputMethodSubtypeList(imi, true);

for(int i=0; i<subtypes.size(); i++) {
final InputMethodSubtype subtype = imi.getSubtypeAt(i);
if ("voice".equals(subtype.getMode())) {
mShortcutSubtype = subtype;
break;
}
}

break;
}
} else {
// Fallback to first one. If package no longer exists, we do not set any shortcut at all
final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
getInputMethodManager().getShortcutInputMethodsAndSubtypes();
for (final InputMethodInfo imi : shortcuts.keySet()) {
final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
mShortcutInputMethodInfo = imi;
mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
break;
}
}


if (DEBUG) {
Log.d(TAG, "Update shortcut IME to : "
+ (mShortcutInputMethodInfo == null
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ (mShortcutSubtype == null ? "<null>" : (
mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))
+ " tgt=" + (mShortcutPackage == null ? "<null>" : mShortcutPackage));
}
}

public void switchToShortcutIme(final InputMethodService context) {
public boolean switchToShortcutIme(final InputMethodService context) {
if (mShortcutInputMethodInfo == null) {
return;
return false;
}

final String imiId = mShortcutInputMethodInfo.getId();
switchToTargetIME(imiId, mShortcutSubtype, context);
return true;
}

private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
Expand Down
5 changes: 5 additions & 0 deletions java/src/org/futo/inputmethod/latin/uix/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ val USE_SYSTEM_VOICE_INPUT = SettingsKey(
default = false
)

val SYSTEM_VOICE_INPUT_PACKAGE = SettingsKey(
key = stringPreferencesKey("systemVoiceInputPackage"),
default = ""
)

val USE_TRANSFORMER_FINETUNING = SettingsKey(
key = booleanPreferencesKey("useTransformerFinetuning2"),
default = false
Expand Down
17 changes: 10 additions & 7 deletions java/src/org/futo/inputmethod/latin/uix/UixManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -648,12 +648,13 @@ class UixManager(private val latinIME: LatinIME) {

val isMainKeyboardHidden get() = mainKeyboardHidden.value

private fun onActionActivatedInternal(rawAction: Action) {
private fun onActionActivatedInternal(rawAction: Action, skipOverride: Boolean = false) {
resizers.hideResizer()

val action = runBlocking {
val action = if(skipOverride)
rawAction
else
ActionRegistry.getActionOverride(latinIME, rawAction)
}

if (action.windowImpl != null) {
enterActionWindowView(action)
Expand All @@ -664,11 +665,13 @@ class UixManager(private val latinIME: LatinIME) {
}
}

private fun onActionAltActivatedInternal(rawAction: Action) {
val action = runBlocking {
ActionRegistry.getActionOverride(latinIME, rawAction)
}
/// public function to skip action override for voice input
fun activateInternalVoiceIME() {
onActionActivatedInternal(org.futo.inputmethod.latin.uix.actions.VoiceInputAction, skipOverride = true)
}

private fun onActionAltActivatedInternal(rawAction: Action) {
val action = ActionRegistry.getActionOverride(latinIME, rawAction)
action.altPressImpl?.invoke(keyboardManagerForAction, persistentStates[action])
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private fun List<Action>.verifyNamesAreUnique(): List<Action> {
}

object ActionRegistry {
suspend fun getActionOverride(context: Context, action: Action): Action {
fun getActionOverride(context: Context, action: Action): Action {
return if(action == VoiceInputAction || action == SystemVoiceInputAction) {
val useSystemVoiceInput = context.getSetting(USE_SYSTEM_VOICE_INPUT)
if(useSystemVoiceInput) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.futo.inputmethod.latin.uix.settings

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.runBlocking
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.SYSTEM_VOICE_INPUT_PACKAGE
import org.futo.inputmethod.latin.uix.USE_SYSTEM_VOICE_INPUT
import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.latin.uix.setSetting
import org.futo.inputmethod.latin.uix.theme.UixThemeAuto

class VoiceInputSwitchActivity : ComponentActivity() {
private var targetPackage: String? = null
private val label = mutableStateOf("?")

private fun switch() {
runBlocking {
setSetting(SYSTEM_VOICE_INPUT_PACKAGE, targetPackage!!)
setSetting(USE_SYSTEM_VOICE_INPUT, true)
}
}

private fun updateContent() {
setContent {
DataStoreCacheProvider {
UixThemeAuto {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Surface(Modifier.width(380.dp).height(240.dp).padding(8.dp), shape = RoundedCornerShape(16.dp)) {
Column(Modifier.padding(8.dp)) {
Text(stringResource(R.string.voice_input_settings_backend_system_confirm, label.value))

Row(Modifier.align(Alignment.End), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = {
setResult(RESULT_CANCELED)
finish()
}) {
Text(stringResource(R.string.voice_input_settings_backend_system_confirm_no))
}

TextButton(onClick = {
switch()
setResult(65)
finish()
}) {
Text(stringResource(R.string.voice_input_settings_backend_system_confirm_yes))
}
}
}
}
}
}
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val targetPackage = intent.getStringExtra("targetPackage")
this.targetPackage = targetPackage

if(targetPackage == null) {
Log.e("VoiceInputSwitchActivity", "Called without a targetPackage.")
finish()
return
}

val mode = intent.getStringExtra("mode")
when(mode) {
"popup" -> {
label.value = try {
packageManager.getPackageInfo(targetPackage, 0)?.let {
packageManager.getApplicationLabel(it.applicationInfo!!)
}!!.toString()
}catch(e: Exception) {
e.printStackTrace()
setResult(RESULT_CANCELED)
finish()
return
}

updateContent()
}
"switch" -> {
if(targetPackage.startsWith("org.futo.")) {
switch()
setResult(66)
finish()
} else {
setResult(RESULT_CANCELED)
finish()
}
}
"check" -> {
val current = getSetting(SYSTEM_VOICE_INPUT_PACKAGE)
if(current == targetPackage) {
setResult(67)
finish()
} else {
setResult(68)
finish()
}
}
else -> {
setResult(RESULT_CANCELED)
finish()
}
}

}
}
Loading