Switch to new language settings (#89)

* add language settings
* move to settings instead of language selection at end of setup wizard
* allow storing enabled subtypes in preferences
* make language selection and input method picker work with new system
* deal with weird issue of getSystemLocales returning inconsistent locales
* add details text to language settings
* make usused settings inaccessible
* better deal with "zz" subtypes, move hungarian (qwerty) from method.xml so a separate aditional subtype
* scrape some strings+translations from android system + latinime
* rename strings, add comment for unused string
This commit is contained in:
Helium314 2023-08-25 17:33:29 +02:00 committed by GitHub
parent e0c054ce09
commit f32395366d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 1816 additions and 207 deletions

View file

@ -24,6 +24,7 @@ import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import androidx.annotation.NonNull;
@ -593,4 +594,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
return mKeyboardLayoutSet.getScriptId();
}
public void switchToSubtype(InputMethodSubtype subtype) {
Log.i("test1", "switch to "+subtype.getLocale());
mLatinIME.switchToSubtype(subtype);
}
}

View file

@ -45,6 +45,7 @@ import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils;
@ -75,6 +76,7 @@ import org.dslul.openboard.inputmethod.latin.permissions.PermissionsManager;
import org.dslul.openboard.inputmethod.latin.personalization.PersonalizationHelper;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SettingsActivity;
import org.dslul.openboard.inputmethod.latin.settings.SubtypeSettingsKt;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripView;
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
@ -82,6 +84,7 @@ import org.dslul.openboard.inputmethod.latin.touchinputconsumer.GestureConsumer;
import org.dslul.openboard.inputmethod.latin.utils.ApplicationUtils;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
import org.dslul.openboard.inputmethod.latin.utils.DialogUtils;
import org.dslul.openboard.inputmethod.latin.utils.InputMethodPickerKt;
import org.dslul.openboard.inputmethod.latin.utils.IntentUtils;
import org.dslul.openboard.inputmethod.latin.utils.JniUtils;
import org.dslul.openboard.inputmethod.latin.utils.LeakGuardHandlerWrapper;
@ -578,7 +581,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
static final class SubtypeState {
final class SubtypeState {
private InputMethodSubtype mLastActiveSubtype;
private boolean mCurrentSubtypeHasBeenUsed;
@ -586,9 +589,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mCurrentSubtypeHasBeenUsed = true;
}
public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) {
final InputMethodSubtype currentSubtype = richImm.getInputMethodManager()
.getCurrentInputMethodSubtype();
public void switchSubtype(final RichInputMethodManager richImm) {
final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
final boolean currentSubtypeHasBeenUsed = mCurrentSubtypeHasBeenUsed;
if (currentSubtypeHasBeenUsed) {
@ -598,10 +600,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (currentSubtypeHasBeenUsed
&& richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
&& !currentSubtype.equals(lastActiveSubtype)) {
richImm.setInputMethodAndSubtype(token, lastActiveSubtype);
switchToSubtype(lastActiveSubtype);
return;
}
richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
// switchSubtype is called only for internal switching, so let's just switch to the next subtype
switchToSubtype(richImm.getNextSubtypeInThisIme(true));
}
}
@ -624,6 +627,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void onCreate() {
Settings.init(this);
DebugFlags.init(DeviceProtectedUtils.getSharedPreferences(this));
SubtypeSettingsKt.init(this);
RichInputMethodManager.init(this);
mRichImm = RichInputMethodManager.getInstance();
AudioAndHapticFeedbackManager.init(this);
@ -802,6 +806,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onConfigurationChanged(final Configuration conf) {
SettingsValues settingsValues = mSettings.getCurrent();
SubtypeSettingsKt.reloadSystemLocales(this);
if (settingsValues.mDisplayOrientation != conf.orientation) {
mHandler.startOrientationChanging();
mInputLogic.onOrientationChange(mSettings.getCurrent());
@ -918,6 +923,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
loadKeyboard();
}
/** alias to onCurrentInputMethodSubtypeChanged with a better name, as it's also used for internal switching */
public void switchToSubtype(final InputMethodSubtype subtype) {
onCurrentInputMethodSubtypeChanged(subtype);
}
void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
super.onStartInput(editorInfo, restarting);
@ -1425,8 +1435,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
switch (requestCode) {
case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
mRichImm.getInputMethodManager().showInputMethodPicker();
return true;
InputMethodPickerKt.showInputMethodPicker(this, mRichImm, mKeyboardSwitcher.getMainKeyboardView().getWindowToken());
return true; // todo: don't show and return if dialog already shown? but how can this happen?
}
return false;
}
@ -1478,19 +1488,42 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return mOptionsDialog != null && mOptionsDialog.isShowing();
}
// todo: remove, this is really not necessary
public void switchLanguage(final InputMethodSubtype subtype) {
final IBinder token = getWindow().getWindow().getAttributes().token;
mRichImm.setInputMethodAndSubtype(token, subtype);
switchToSubtype(subtype);
}
// TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
public void switchToNextSubtype() {
final IBinder token = getWindow().getWindow().getAttributes().token;
if (shouldSwitchToOtherInputMethods()) {
mRichImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
// todo: this is the old behavior, is this actually wanted?
// maybe make the language switch key more configurable
boolean moreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
final InputMethodSubtype nextSubtype = mRichImm.getNextSubtypeInThisIme(moreThanOneSubtype);
if (nextSubtype != null) {
switchToSubtype(nextSubtype);
} else {
// we are at end of the internal subtype list, switch to next input method
// (for now) don't care about which input method and subtype exactly, let the system choose
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToNextInputMethod(false);
} else {
final IBinder token = getWindow().getWindow().getAttributes().token;
mRichImm.getInputMethodManager().switchToNextInputMethod(token, false);
}
}
return;
}
mSubtypeState.switchSubtype(token, mRichImm);
mSubtypeState.switchSubtype(mRichImm);
}
public void switchInputMethodAndSubtype(final InputMethodInfo imi, final InputMethodSubtype subtype) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchInputMethod(imi.getId(), subtype);
} else {
final IBinder token = getWindow().getWindow().getAttributes().token;
mRichImm.getInputMethodManager().setInputMethodAndSubtype(token, imi.getId(), subtype);
}
}
// TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
@ -2038,15 +2071,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
public boolean shouldSwitchToOtherInputMethods() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
return shouldOfferSwitchingToNextInputMethod();
// TODO: Revisit here to reorganize the settings. Probably we can/should use different
// strategy once the implementation of
// {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well.
final boolean fallbackValue = mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList;
final IBinder token = getWindow().getWindow().getAttributes().token;
if (token == null) {
return fallbackValue;
return mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList;
}
return mRichImm.shouldOfferSwitchingToNextInputMethod(token, fallbackValue);
return mRichImm.getInputMethodManager().shouldOfferSwitchingToNextInputMethod(token);
}
public boolean shouldShowLanguageSwitchKey() {

View file

@ -31,6 +31,7 @@ import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
import org.dslul.openboard.inputmethod.compat.InputMethodManagerCompatWrapper;
import org.dslul.openboard.inputmethod.compat.InputMethodSubtypeCompatUtils;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SubtypeSettingsKt;
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
import org.dslul.openboard.inputmethod.latin.utils.LanguageOnSpacebarUtils;
@ -100,17 +101,16 @@ public class RichInputMethodManager {
mInputMethodInfoCache = new InputMethodInfoCache(
mImmWrapper.mImm, context.getPackageName());
// Initialize additional subtypes.
// Initialize subtype utils.
SubtypeLocaleUtils.init(context);
final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes();
mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
getInputMethodIdOfThisIme(), additionalSubtypes);
// Initialize the current input method subtype and the shortcut IME.
refreshSubtypeCaches();
}
public InputMethodSubtype[] getAdditionalSubtypes() {
public InputMethodSubtype[] getAdditionalSubtypes() { // todo: can be removed
// todo: this should read the enabled subtypes setting
// either use or remove the default additional subtypes
final SharedPreferences prefs = DeviceProtectedUtils.getSharedPreferences(mContext);
final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
prefs, mContext.getResources());
@ -129,7 +129,8 @@ public class RichInputMethodManager {
}
public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
// todo: don't want this any more, and actually mImmWrapper.switchToNextInputMethod can be removed
if (false && mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
return true;
}
// Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
@ -140,10 +141,27 @@ public class RichInputMethodManager {
return switchToNextInputMethodAndSubtype(token);
}
public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) {
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(true);
final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
if (currentIndex == INDEX_NOT_FOUND) {
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
+ SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
if (onlyCurrentIme) return enabledSubtypes.get(0); // just return first enabled subtype
else return null;
}
final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
if (nextIndex <= currentIndex && !onlyCurrentIme) {
// The current subtype is the last or only enabled one and it needs to switch to next IME.
return null;
}
return enabledSubtypes.get(nextIndex);
}
private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
final boolean onlyCurrentIme) {
final InputMethodManager imm = mImmWrapper.mImm;
final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
true /* allowsImplicitlySelectedSubtypes */);
final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
@ -159,7 +177,7 @@ public class RichInputMethodManager {
return false;
}
final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
setInputMethodAndSubtype(token, nextSubtype);
setInputMethodAndSubtype(token, nextSubtype); // todo: not working any more, but switchToNextInputSubtypeInThisIme isn't called anyway...
return true;
}
@ -266,8 +284,14 @@ public class RichInputMethodManager {
if (cachedList != null) {
return cachedList;
}
final List<InputMethodSubtype> result = mImm.getEnabledInputMethodSubtypeList(
imi, allowsImplicitlySelectedSubtypes);
final List<InputMethodSubtype> result;
if (imi == getInputMethodOfThisIme()) {
// allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled,
// use it to fall back to system locales or en_US to avoid returning an empty list
result = SubtypeSettingsKt.getEnabledSubtypes(DeviceProtectedUtils.getSharedPreferences(sInstance.mContext), allowsImplicitlySelectedSubtypes);
} else {
result = mImm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes);
}
cache.put(imi, result);
return result;
}
@ -309,6 +333,7 @@ public class RichInputMethodManager {
private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
final List<InputMethodSubtype> subtypes) {
// todo: why not simply subtypes.indexOf(subtype)? should do exactly the same, even return the same value -1 if not found
final int count = subtypes.size();
for (int index = 0; index < count; index++) {
final InputMethodSubtype ims = subtypes.get(index);
@ -470,10 +495,13 @@ public class RichInputMethodManager {
}
public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) {
mImmWrapper.mImm.setInputMethodAndSubtype(
token, getInputMethodIdOfThisIme(), subtype);
// todo: mImm stuff doesn't work any more, need sth like notifySubtypeChanged to actually trigger a reload
// try calling this instead, probably best before loading keyboard
// essentially it should do that update thing?
onSubtypeChanged(subtype);
}
// todo: remove together with additional subtype settings
public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) {
mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
getInputMethodIdOfThisIme(), subtypes);
@ -482,7 +510,7 @@ public class RichInputMethodManager {
refreshSubtypeCaches();
}
private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
final boolean allowsImplicitlySelectedSubtypes) {
return mInputMethodInfoCache.getEnabledInputMethodSubtypeList(
imi, allowsImplicitlySelectedSubtypes);
@ -490,10 +518,12 @@ public class RichInputMethodManager {
public void refreshSubtypeCaches() {
mInputMethodInfoCache.clear();
updateCurrentSubtype(mImmWrapper.mImm.getCurrentInputMethodSubtype());
SharedPreferences prefs = DeviceProtectedUtils.getSharedPreferences(mContext);
updateCurrentSubtype(SubtypeSettingsKt.getSelectedSubtype(prefs));
updateShortcutIme();
}
// todo: remove
public boolean shouldOfferSwitchingToNextInputMethod(final IBinder binder,
boolean defaultValue) {
// Use the default value instead on Jelly Bean MR2 and previous where
@ -505,6 +535,7 @@ public class RichInputMethodManager {
return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder);
}
// todo: remove?
public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
final Locale systemLocale = mContext.getResources().getConfiguration().locale;
final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>();
@ -530,10 +561,12 @@ public class RichInputMethodManager {
return true;
}
private void updateCurrentSubtype(@Nullable final InputMethodSubtype subtype) {
private void updateCurrentSubtype(final InputMethodSubtype subtype) {
SubtypeSettingsKt.setSelectedSubtype(DeviceProtectedUtils.getSharedPreferences(mContext), subtype);
mCurrentRichInputMethodSubtype = RichInputMethodSubtype.getRichInputMethodSubtype(subtype);
}
// todo: what is shortcutIme? the voice input? if yes, rename it and other things like mHasShortcutKey
private void updateShortcutIme() {
if (DEBUG) {
Log.d(TAG, "Update shortcut IME from : "

View file

@ -26,7 +26,6 @@ import android.content.pm.PackageManager;
import android.os.Process;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
@ -66,12 +65,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
final String intentAction = intent.getAction();
if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intentAction)) {
Log.i(TAG, "Package has been replaced: " + context.getPackageName());
// Need to restore additional subtypes because system always clears additional
// subtypes when the package is replaced.
RichInputMethodManager.init(context);
final RichInputMethodManager richImm = RichInputMethodManager.getInstance();
final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes();
richImm.setAdditionalInputMethodSubtypes(additionalSubtypes);
toggleAppIcon(context);
} else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) {
Log.i(TAG, "Boot has been completed");

View file

@ -101,8 +101,6 @@ class AppearanceSettingsFragment : SubScreenFragment(), Preference.OnPreferenceC
super.onResume()
updateThemePreferencesState()
updateAfterPreferenceChanged()
CustomInputStyleSettingsFragment.updateCustomInputStylesSummary(
findPreference(Settings.PREF_CUSTOM_INPUT_STYLES))
}
override fun onPause() {

View file

@ -29,6 +29,7 @@ import java.io.File
import java.io.IOException
import java.util.*
@Suppress("deprecation")
class DictionarySettingsFragment : SubScreenFragment() {
// dict for which dialog is currently open (if any)
@ -348,7 +349,7 @@ class DictionarySettingsFragment : SubScreenFragment() {
private const val DICTIONARY_REQUEST_CODE = 96834
private const val DICTIONARY_URL =
"https://codeberg.org/Helium314/aosp-dictionaries"
private const val USER_DICTIONARY_SUFFIX = "user.dict"
const val USER_DICTIONARY_SUFFIX = "user.dict"
private const val DICT_INTERNAL_AND_USER = 2
private const val DICT_INTERNAL_ONLY = 1

View file

@ -0,0 +1,146 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.content.Context
import android.graphics.Rect
import android.preference.Preference
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.view.doOnLayout
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.RecyclerView
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.utils.*
class LanguageFilterListPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs) {
private var preferenceView: View? = null
private val adapter = LanguageAdapter(emptyList(), context)
private val sortedSubtypes = mutableListOf<MutableList<SubtypeInfo>>()
fun setSettingsFragment(newFragment: LanguageSettingsFragment?) {
adapter.fragment = newFragment
}
override fun onBindView(view: View?) {
super.onBindView(view)
preferenceView = view
preferenceView?.findViewById<RecyclerView>(R.id.language_list)?.adapter = adapter
val searchField = preferenceView?.findViewById<EditText>(R.id.search_field)!!
searchField.doAfterTextChanged { text ->
adapter.list = sortedSubtypes.filter { it.first().displayName.startsWith(text.toString(), ignoreCase = true) }
}
view?.doOnLayout {
// set correct height for recycler view, so there is no scrolling of the outside view happening
// not sure how, but probably this can be achieved in xml...
val windowFrame = Rect()
it.getWindowVisibleDisplayFrame(windowFrame) // rect the app has, we want the bottom (above screen bottom/navbar/keyboard)
val globalRect = Rect()
it.getGlobalVisibleRect(globalRect) // rect the view takes, we want the top (below the system language preference)
val recycler = it.findViewById<RecyclerView>(R.id.language_list)
val newHeight = windowFrame.bottom - globalRect.top - it.findViewById<View>(R.id.search_container).height
recycler.layoutParams = recycler.layoutParams.apply { height = newHeight }
}
}
fun setLanguages(list: Collection<MutableList<SubtypeInfo>>, disableSwitches: Boolean) {
sortedSubtypes.clear()
sortedSubtypes.addAll(list)
adapter.disableSwitches = disableSwitches
adapter.list = sortedSubtypes
}
}
class LanguageAdapter(list: List<MutableList<SubtypeInfo>> = listOf(), context: Context) :
RecyclerView.Adapter<LanguageAdapter.ViewHolder>() {
var disableSwitches = false
private val prefs = DeviceProtectedUtils.getSharedPreferences(context)
var fragment: LanguageSettingsFragment? = null
var list: List<MutableList<SubtypeInfo>> = list
set(value) {
field = value
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.onBind(list[position])
}
override fun getItemCount() = list.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LanguageAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.language_list_item, parent, false)
return ViewHolder(v)
}
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun onBind(infos: MutableList<SubtypeInfo>) {
fun setupDetailsTextAndSwitch() {
// this is unrelated -> rename it
view.findViewById<TextView>(R.id.language_details).apply {
// input styles if more than one in infos
val sb = StringBuilder()
if (infos.size > 1) {
sb.append(infos.joinToString(", ") {// separator ok? because for some languages it might not be...
SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(it.subtype)
?: SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(it.subtype)
})
}
val secondaryLocales = Settings.getSecondaryLocales(prefs, infos.first().subtype.locale)
if (secondaryLocales.isNotEmpty()) {
if (sb.isNotEmpty())
sb.append("\n")
sb.append(Settings.getSecondaryLocales(prefs, infos.first().subtype.locale)
.joinToString(", ") {
it.getDisplayName(context.resources.configuration.locale)
})
}
text = sb.toString()
if (text.isBlank()) isGone = true
else isVisible = true
}
view.findViewById<Switch>(R.id.language_switch).apply {
isEnabled = !disableSwitches && infos.size == 1
// take care: isChecked changes if the language is scrolled out of view and comes back!
// disable the change listener when setting the checked status on scroll
// so it's only triggered on user interactions
setOnCheckedChangeListener(null)
isChecked = disableSwitches || infos.any { it.isEnabled }
setOnCheckedChangeListener { _, b ->
if (b) {
if (infos.size == 1) {
addEnabledSubtype(prefs, infos.first().subtype)
infos.single().isEnabled = true
} else {
LanguageSettingsDialog(view.context, infos, fragment, disableSwitches, { setupDetailsTextAndSwitch() }).show()
}
} else {
if (infos.size == 1) {
removeEnabledSubtype(prefs, infos.first().subtype)
infos.single().isEnabled = false
} else {
LanguageSettingsDialog(view.context, infos, fragment, disableSwitches, { setupDetailsTextAndSwitch() }).show()
}
}
}
}
}
view.findViewById<TextView>(R.id.language_name).text = infos.first().displayName
view.findViewById<LinearLayout>(R.id.language_text).setOnClickListener {
LanguageSettingsDialog(view.context, infos, fragment, disableSwitches, { setupDetailsTextAndSwitch() }).show()
}
setupDetailsTextAndSwitch()
}
}
}

View file

@ -0,0 +1,401 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.core.view.isGone
import androidx.core.view.isVisible
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.FileUtils
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.makedict.DictionaryHeader
import org.dslul.openboard.inputmethod.latin.utils.*
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.collections.HashSet
@Suppress("deprecation")
class LanguageSettingsDialog(
context: Context,
private val subtypes: MutableList<SubtypeInfo>,
private val fragment: LanguageSettingsFragment?,
private val disableSwitches: Boolean,
private val onSubtypesChanged: () -> Unit
) : AlertDialog(ContextThemeWrapper(context, R.style.platformDialogTheme)), LanguageSettingsFragment.Listener {
private val context = ContextThemeWrapper(context, R.style.platformDialogTheme)
private val prefs = DeviceProtectedUtils.getSharedPreferences(context)!!
private val view = LayoutInflater.from(context).inflate(R.layout.locale_settings_dialog, null)
private val mainLocaleString = subtypes.first().subtype.locale
private val mainLocale = mainLocaleString.toLocale()
private val cachedDictionaryFile by lazy { File(context.cacheDir.path + File.separator + "temp_dict") }
init {
setTitle(subtypes.first().displayName)
setView(ScrollView(context).apply { addView(view) })
setButton(BUTTON_NEGATIVE, context.getString(R.string.dialog_close)) { _, _ ->
dismiss()
}
fillSubtypesView(view.findViewById(R.id.subtypes))
fillSecondaryLocaleView(view.findViewById(R.id.secondary_languages))
fillDictionariesView(view.findViewById(R.id.dictionaries))
}
override fun onStart() {
super.onStart()
fragment?.setListener(this)
}
override fun onStop() {
super.onStop()
fragment?.setListener(null)
}
private fun fillSubtypesView(subtypesView: LinearLayout) {
if (subtypes.any { it.subtype.isAsciiCapable }) { // currently can only add subtypes for latin keyboards
subtypesView.findViewById<ImageView>(R.id.add_subtype).setOnClickListener {
val layouts = context.resources.getStringArray(R.array.predefined_layouts)
.filterNot { layoutName -> subtypes.any { SubtypeLocaleUtils.getKeyboardLayoutSetName(it.subtype) == layoutName } }
val displayNames = layouts.map { SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(it) }
Builder(context)
.setTitle(R.string.keyboard_layout_set)
.setItems(displayNames.toTypedArray()) { di, i ->
di.dismiss()
val newSubtype = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(mainLocaleString, layouts[i])
val newSubtypeInfo = newSubtype.toSubtypeInfo(mainLocale, context.resources, true) // enabled by default, because why else add them
addSubtypeToView(newSubtypeInfo, subtypesView)
val oldAdditionalSubtypesString = Settings.readPrefAdditionalSubtypes(prefs, context.resources)
val oldAdditionalSubtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(oldAdditionalSubtypesString).toHashSet()
val newAdditionalSubtypesString = AdditionalSubtypeUtils.createPrefSubtypes((oldAdditionalSubtypes + newSubtype).toTypedArray())
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
addEnabledSubtype(prefs, newSubtype)
subtypes.add(newSubtypeInfo)
onSubtypesChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
} else
subtypesView.findViewById<View>(R.id.add_subtype).isGone = true
// add subtypes
subtypes.sortedBy { it.displayName }.forEach {
addSubtypeToView(it, subtypesView)
}
}
private fun addSubtypeToView(subtype: SubtypeInfo, subtypesView: LinearLayout) {
val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView)
row.findViewById<TextView>(R.id.language_name).text =
SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype.subtype)
?: SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype.subtype)
row.findViewById<View>(R.id.language_details).isGone = true
row.findViewById<Switch>(R.id.language_switch).apply {
isChecked = subtype.isEnabled
isEnabled = !disableSwitches
setOnCheckedChangeListener { _, b ->
if (b)
addEnabledSubtype(prefs, subtype.subtype)
else
removeEnabledSubtype(prefs, subtype.subtype)
subtype.isEnabled = b
onSubtypesChanged()
}
}
if (isAdditionalSubtype(subtype.subtype)) {
row.findViewById<Switch>(R.id.language_switch).isEnabled = true
row.findViewById<ImageView>(R.id.delete_button).apply {
isVisible = true
setOnClickListener {
// can be re-added easily, no need for confirmation dialog
subtypesView.removeView(row)
subtypes.remove(subtype)
val oldAdditionalSubtypesString = Settings.readPrefAdditionalSubtypes(prefs, context.resources)
val oldAdditionalSubtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(oldAdditionalSubtypesString)
val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != subtype.subtype }
val newAdditionalSubtypesString = AdditionalSubtypeUtils.createPrefSubtypes(newAdditionalSubtypes.toTypedArray())
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
removeEnabledSubtype(prefs, subtype.subtype)
onSubtypesChanged()
}
}
}
subtypesView.addView(row)
}
private fun fillSecondaryLocaleView(secondaryLocalesView: LinearLayout) {
// can only use multilingual typing if there is more than one dictionary available
val availableSecondaryLocales = getAvailableDictionaryLocales(
context,
mainLocaleString,
subtypes.first().subtype.isAsciiCapable
)
val selectedSecondaryLocales = Settings.getSecondaryLocales(prefs, mainLocaleString)
selectedSecondaryLocales.forEach {
addSecondaryLocaleView(it, secondaryLocalesView)
}
if (availableSecondaryLocales.isNotEmpty()) {
secondaryLocalesView.findViewById<ImageView>(R.id.add_secondary_language).apply {
isVisible = true
setOnClickListener {
val locales = (availableSecondaryLocales - Settings.getSecondaryLocales(prefs, mainLocaleString).map { it.toString() }).sorted()
val localeNames = locales.map { it.toLocale().getDisplayName(context.resources.configuration.locale) }.toTypedArray()
Builder(context)
.setTitle(R.string.language_selection_title)
.setItems(localeNames) { di, i ->
val locale = locales[i]
val localeStrings = Settings.getSecondaryLocales(prefs, mainLocaleString).map { it.toString() }
Settings.setSecondaryLocales(prefs, mainLocaleString, localeStrings + locale)
addSecondaryLocaleView(locale.toLocale(), secondaryLocalesView)
di.dismiss()
}
.show()
}
}
} else if (selectedSecondaryLocales.isEmpty())
secondaryLocalesView.isGone = true
}
private fun addSecondaryLocaleView(locale: Locale, secondaryLocalesView: LinearLayout) {
val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView)
row.findViewById<Switch>(R.id.language_switch).isGone = true
row.findViewById<Switch>(R.id.language_details).isGone = true
row.findViewById<TextView>(R.id.language_name).text = locale.displayName
row.findViewById<ImageView>(R.id.delete_button).apply {
isVisible = true
setOnClickListener {
val localeStrings = Settings.getSecondaryLocales(prefs, mainLocaleString).map { it.toString() }
Settings.setSecondaryLocales(prefs, mainLocaleString, localeStrings - locale.toString())
secondaryLocalesView.removeView(row)
}
}
secondaryLocalesView.addView(row)
}
private fun fillDictionariesView(dictionariesView: LinearLayout) {
dictionariesView.findViewById<ImageView>(R.id.add_dictionary).setOnClickListener {
val link = "<a href='$DICTIONARY_URL'>" + context.getString(R.string.dictionary_link_text) + "</a>"
val message = Html.fromHtml(context.getString(R.string.add_dictionary, link))
val dialog = Builder(context)
.setTitle(R.string.add_new_dictionary_title)
.setMessage(message)
.setPositiveButton(R.string.user_dict_settings_add_menu_title) { _, _ -> fragment?.requestDictionary() }
.setNegativeButton(android.R.string.cancel, null)
.create()
dialog.show()
(dialog.findViewById<View>(android.R.id.message) as? TextView)?.movementMethod = LinkMovementMethod.getInstance()
}
val (userDicts, hasInternalDict) = getUserAndInternalDictionaries(context, mainLocaleString)
if (hasInternalDict) {
dictionariesView.addView(TextView(context, null, R.style.PreferenceCategoryTitleText).apply {
setText(R.string.internal_dictionary_summary)
textSize *= 0.8f
setPadding((context.resources.displayMetrics.scaledDensity * 16).toInt(), 0, 0, 0)
isEnabled = userDicts.none { it.name == "${DictionaryInfoUtils.MAIN_DICT_PREFIX}${DictionarySettingsFragment.USER_DICTIONARY_SUFFIX}" }
})
}
userDicts.sorted().forEach {
addDictionaryToView(it, dictionariesView)
}
}
override fun onNewDictionary(uri: Uri?) {
if (uri == null)
return onDictionaryLoadingError(R.string.dictionary_load_error)
cachedDictionaryFile.delete()
try {
FileUtils.copyStreamToNewFile(
context.contentResolver.openInputStream(uri),
cachedDictionaryFile
)
} catch (e: IOException) {
return onDictionaryLoadingError(R.string.dictionary_load_error)
}
val newHeader = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(cachedDictionaryFile, 0, cachedDictionaryFile.length())
?: return onDictionaryLoadingError(R.string.dictionary_file_error)
val locale = newHeader.mLocaleString.toLocale()
// ScriptUtils.getScriptFromSpellCheckerLocale may return latin when it should not,
// e.g. for Persian or Chinese. But at least fail when dictionary certainly is incompatible
if (ScriptUtils.getScriptFromSpellCheckerLocale(locale) != ScriptUtils.getScriptFromSpellCheckerLocale(mainLocale))
return onDictionaryLoadingError(R.string.dictionary_file_wrong_script)
if (locale != mainLocale) {
val message = context.resources.getString(
R.string.dictionary_file_wrong_locale,
locale.getDisplayName(context.resources.configuration.locale),
mainLocale.getDisplayName(context.resources.configuration.locale)
)
Builder(context)
.setMessage(message)
.setNegativeButton(android.R.string.cancel) { _, _ -> cachedDictionaryFile.delete() }
.setPositiveButton(R.string.dictionary_file_wrong_locale_ok) { _, _ ->
addDictAndAskToReplace(newHeader)
}
.show()
return
}
addDictAndAskToReplace(newHeader)
}
private fun addDictAndAskToReplace(header: DictionaryHeader) {
val dictionaryType = header.mIdString.substringBefore(":")
val dictFilename = DictionaryInfoUtils.getCacheDirectoryForLocale(mainLocaleString, context) +
File.separator + dictionaryType + "_" + DictionarySettingsFragment.USER_DICTIONARY_SUFFIX
val dictFile = File(dictFilename)
fun moveDict(replaced: Boolean) {
if (!cachedDictionaryFile.renameTo(dictFile)) {
return onDictionaryLoadingError(R.string.dictionary_load_error)
}
if (dictionaryType == DictionaryInfoUtils.DEFAULT_MAIN_DICT) {
// replaced main dict, remove the one created from internal data
val internalMainDictFilename = DictionaryInfoUtils.getCacheDirectoryForLocale(this.toString(), context) +
File.separator + DictionaryInfoUtils.getMainDictFilename(this.toString())
File(internalMainDictFilename).delete()
}
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
fragment?.activity?.sendBroadcast(newDictBroadcast)
if (!replaced)
addDictionaryToView(dictFile, view.findViewById(R.id.dictionaries))
}
if (!dictFile.exists()) {
return moveDict(false)
}
confirmDialog(context, context.getString(R.string.replace_dictionary_message2, dictionaryType), context.getString(
R.string.replace_dictionary)) {
moveDict(true)
}
}
private fun onDictionaryLoadingError(messageId: Int) = onDictionaryLoadingError(context.getString(messageId))
private fun onDictionaryLoadingError(message: String) {
cachedDictionaryFile.delete()
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
private fun addDictionaryToView(dictFile: File, dictionariesView: LinearLayout) {
val dictType = dictFile.name.substringBefore("_${DictionarySettingsFragment.USER_DICTIONARY_SUFFIX}")
val row = LayoutInflater.from(context).inflate(R.layout.language_list_item, listView)
row.findViewById<TextView>(R.id.language_name).text = dictType
row.findViewById<TextView>(R.id.language_details).apply {
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dictFile, 0, dictFile.length())
if (header?.description == null) {
isGone = true
} else {
// what would potentially be interesting? locale? description? version? timestamp?
text = header.description
}
}
row.findViewById<Switch>(R.id.language_switch).isGone = true
row.findViewById<ImageView>(R.id.delete_button).apply {
isVisible = true
setOnClickListener {
confirmDialog(context, context.getString(R.string.remove_dictionary_message2, dictType), context.getString(
R.string.delete_dict)) {
val parent = dictFile.parentFile
dictFile.delete()
if (parent?.list()?.isEmpty() == true)
parent.delete()
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
fragment?.activity?.sendBroadcast(newDictBroadcast)
dictionariesView.removeView(row)
}
}
}
dictionariesView.addView(row)
}
}
fun confirmDialog(context: Context, message: String, confirmButton: String, onConfirmed: (() -> Unit)) {
AlertDialog.Builder(context)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(confirmButton) { _, _ -> onConfirmed() }
.show()
}
/** @return list of user dictionary files and whether an internal dictionary exists */
fun getUserAndInternalDictionaries(context: Context, locale: String): Pair<List<File>, Boolean> {
val localeString = locale.lowercase() // internal files and folders always use lowercase
val userDicts = mutableListOf<File>()
var hasInternalDict = false
val userLocaleDir = File(DictionaryInfoUtils.getWordListCacheDirectory(context), localeString)
if (userLocaleDir.exists() && userLocaleDir.isDirectory) {
userLocaleDir.listFiles()?.forEach {
if (it.name.endsWith(DictionarySettingsFragment.USER_DICTIONARY_SUFFIX))
userDicts.add(it)
else if (it.name.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX))
hasInternalDict = true
}
}
if (hasInternalDict)
return userDicts to true
BinaryDictionaryGetter.getAssetsDictionaryList(context)?.forEach { dictFile ->
BinaryDictionaryGetter.extractLocaleFromAssetsDictionaryFile(dictFile)?.let {
if (it == localeString)
return userDicts to true
}
}
return userDicts to false
}
// get locales with same script as main locale, but different language
private fun getAvailableDictionaryLocales(context: Context, mainLocaleString: String, asciiCapable: Boolean): Set<String> {
val mainLocale = mainLocaleString.toLocale()
val locales = HashSet<String>()
val mainScript = if (asciiCapable) ScriptUtils.SCRIPT_LATIN
else ScriptUtils.getScriptFromSpellCheckerLocale(mainLocale)
// ScriptUtils.getScriptFromSpellCheckerLocale may return latin when it should not
// e.g. for persian or chinese
// workaround: don't allow secondary locales for these locales
if (!asciiCapable && mainScript == ScriptUtils.SCRIPT_LATIN) return locales
// get cached dictionaries: extracted or user-added dictionaries
val cachedDirectoryList = DictionaryInfoUtils.getCachedDirectoryList(context)
if (cachedDirectoryList != null) {
for (directory in cachedDirectoryList) {
if (!directory.isDirectory) continue
val dirLocale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name)
if (dirLocale == mainLocaleString) continue
val locale = dirLocale.toLocale()
if (locale.language == mainLocale.language) continue
val localeScript = ScriptUtils.getScriptFromSpellCheckerLocale(locale)
if (localeScript != mainScript) continue
locales.add(locale.toString())
}
}
// get assets dictionaries
val assetsDictionaryList = BinaryDictionaryGetter.getAssetsDictionaryList(context)
if (assetsDictionaryList != null) {
for (dictionary in assetsDictionaryList) {
val dictLocale =
BinaryDictionaryGetter.extractLocaleFromAssetsDictionaryFile(dictionary)
?: continue
if (dictLocale == mainLocaleString) continue
val locale = dictLocale.toLocale()
if (locale.language == mainLocale.language) continue
val localeScript = ScriptUtils.getScriptFromSpellCheckerLocale(locale)
if (localeScript != mainScript) continue
locales.add(locale.toString())
}
}
return locales
}
private fun String.toLocale() = LocaleUtils.constructLocaleFromString(this)
private const val DICTIONARY_URL = "https://codeberg.org/Helium314/aosp-dictionaries"

View file

@ -0,0 +1,188 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.app.Activity
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.os.Bundle
import android.preference.TwoStatePreference
import android.view.inputmethod.InputMethodSubtype
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
import java.util.Locale
@Suppress("Deprecation") // yes everything here is deprecated, but only work on this if really necessary
class LanguageSettingsFragment : SubScreenFragment() {
private val sortedSubtypes = LinkedHashMap<String, MutableList<SubtypeInfo>>()
private val enabledSubtypes = mutableListOf<InputMethodSubtype>()
private val systemLocales = mutableListOf<Locale>()
private val languageFilterListPreference by lazy { findPreference("pref_language_filter") as LanguageFilterListPreference }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.prefs_screen_languages);
SubtypeLocaleUtils.init(activity)
enabledSubtypes.addAll(getExplicitlyEnabledSubtypes())
systemLocales.addAll(getSystemLocales())
val systemLocalesSwitch = findPreference(Settings.PREF_USE_SYSTEM_LOCALES) as TwoStatePreference
systemLocalesSwitch.setOnPreferenceChangeListener { _, b ->
loadSubtypes(b as Boolean)
true
}
loadSubtypes(systemLocalesSwitch.isChecked)
}
override fun onResume() {
super.onResume()
languageFilterListPreference.setSettingsFragment(this)
}
override fun onPause() {
super.onPause()
languageFilterListPreference.setSettingsFragment(null)
}
private fun loadSubtypes(systemOnly: Boolean) {
sortedSubtypes.clear()
val allSubtypes = getAllAvailableSubtypes().toMutableList()
// maybe make use of the map used by SubtypeSettings for performance reasons?
fun List<Locale>.sortedAddToSubtypesAndRemoveFromAllSubtypes() {
val subtypesToAdd = mutableListOf<SubtypeInfo>()
forEach { locale ->
val localeString = locale.toString()
val iter = allSubtypes.iterator()
var added = false
while (iter.hasNext()) {
val subtype = iter.next()
if (subtype.locale == localeString) {
subtypesToAdd.add(subtype.toSubtypeInfo(locale))
iter.remove()
added = true
}
}
if (!added && locale.country.isNotEmpty()) {
// try again, but with language only
val languageString = locale.language
val iter = allSubtypes.iterator()
while (iter.hasNext()) {
val subtype = iter.next()
if (subtype.locale == languageString) {
subtypesToAdd.add(subtype.toSubtypeInfo(LocaleUtils.constructLocaleFromString(languageString)))
iter.remove()
added = true
}
}
}
// special treatment for the known languages with _ZZ types
// todo: later: make it a bit less weird... and probably faster
// consider that more _ZZ languages might be added (e.g. hinglish)
if (!added && locale.language == "sr") {
val languageString = locale.language
val iter = allSubtypes.iterator()
while (iter.hasNext()) {
val subtype = iter.next()
if (subtype.locale.substringBefore("_") == languageString) {
subtypesToAdd.add(subtype.toSubtypeInfo(LocaleUtils.constructLocaleFromString(subtype.locale)))
iter.remove()
}
}
}
}
subtypesToAdd.sortedBy { it.displayName }.addToSortedSubtypes()
}
if (systemOnly) {
systemLocales.sortedAddToSubtypesAndRemoveFromAllSubtypes()
languageFilterListPreference.setLanguages(sortedSubtypes.values, systemOnly)
return
}
// add enabled subtypes
enabledSubtypes.map { it.toSubtypeInfo(LocaleUtils.constructLocaleFromString(it.locale), true) }
.sortedBy { it.displayName }.addToSortedSubtypes()
allSubtypes.removeAll(enabledSubtypes)
// add subtypes that have a dictionary
val localesWithDictionary = DictionaryInfoUtils.getCachedDirectoryList(activity)?.mapNotNull { dir ->
if (!dir.isDirectory)
return@mapNotNull null
if (dir.list()?.any { it.endsWith(DictionarySettingsFragment.USER_DICTIONARY_SUFFIX) } == true)
LocaleUtils.constructLocaleFromString(dir.name)
else null
}
localesWithDictionary?.sortedAddToSubtypesAndRemoveFromAllSubtypes()
// add subtypes for device locales
systemLocales.sortedAddToSubtypesAndRemoveFromAllSubtypes()
// add the remaining ones
allSubtypes.map { it.toSubtypeInfo(LocaleUtils.constructLocaleFromString(it.locale)) }
.sortedBy { if (it.subtype.locale.equals("zz", true))
"zz" // "No language (Alphabet)" should be last
else it.displayName
}.addToSortedSubtypes()
// set languages
languageFilterListPreference.setLanguages(sortedSubtypes.values, systemOnly)
}
private fun InputMethodSubtype.toSubtypeInfo(locale: Locale, isEnabled: Boolean = false) =
toSubtypeInfo(locale, resources, isEnabled)
private fun List<SubtypeInfo>.addToSortedSubtypes() {
forEach {
sortedSubtypes.getOrPut(it.displayName) { mutableListOf() }.add(it)
}
}
interface Listener {
fun onNewDictionary(uri: Uri?)
}
private var listener: Listener? = null
fun setListener(newListener: Listener?) {
listener = newListener
}
fun requestDictionary() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/octet-stream")
startActivityForResult(intent, DICTIONARY_REQUEST_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == DICTIONARY_REQUEST_CODE)
listener?.onNewDictionary(resultData?.data)
}
}
class SubtypeInfo(val displayName: String, val subtype: InputMethodSubtype, var isEnabled: Boolean) {
override fun equals(other: Any?): Boolean {
if (other !is SubtypeInfo) return false
return subtype == other.subtype
}
override fun hashCode(): Int {
return subtype.hashCode()
}
}
fun InputMethodSubtype.toSubtypeInfo(locale: Locale, resources: Resources, isEnabled: Boolean): SubtypeInfo {
val displayName = if (locale.toString().equals("zz", true)) // no language
SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(locale.toString())
else if (locale.toString().endsWith("zz", true)) // serbian (latin), maybe others in the future
SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this)
else
locale.getDisplayName(resources.configuration.locale)
return SubtypeInfo(displayName, this, isEnabled)
}
private const val DICTIONARY_REQUEST_CODE = 96834

View file

@ -109,7 +109,7 @@ public final class SecondaryLocaleSettingsFragment extends SubScreenFragment {
locale = "";
final Set<String> encodedLocales = new HashSet<>();
boolean updated = false;
for (String encodedLocale : getSharedPreferences().getStringSet(Settings.PREF_SECONDARY_LOCALES, new HashSet<>())) {
for (String encodedLocale : getSharedPreferences().getStringSet(Settings.PREF_SECONDARY_LOCALES_PREFIX, new HashSet<>())) {
String[] locs = encodedLocale.split("§");
if (locs.length == 2 && locs[0].equals(mainLocale)) {
if (!locale.isEmpty())
@ -121,7 +121,7 @@ public final class SecondaryLocaleSettingsFragment extends SubScreenFragment {
}
if (!updated)
encodedLocales.add(mainLocale + "§" + locale);
getSharedPreferences().edit().putStringSet(Settings.PREF_SECONDARY_LOCALES, encodedLocales).apply();
getSharedPreferences().edit().putStringSet(Settings.PREF_SECONDARY_LOCALES_PREFIX, encodedLocales).apply();
final Intent newDictBroadcast = new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
getActivity().sendBroadcast(newDictBroadcast);
resetKeyboardLocales();

View file

@ -43,7 +43,6 @@ import org.dslul.openboard.inputmethod.latin.utils.StatsUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@ -146,11 +145,13 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_ENABLE_CLIPBOARD_HISTORY = "pref_enable_clipboard_history";
public static final String PREF_CLIPBOARD_HISTORY_RETENTION_TIME = "pref_clipboard_history_retention_time";
public static final String PREF_SECONDARY_LOCALES = "pref_secondary_locales";
public static final String PREF_SECONDARY_LOCALES_PREFIX = "pref_secondary_locales_";
public static final String PREF_ADD_TO_PERSONAL_DICTIONARY = "pref_add_to_personal_dictionary";
public static final String PREF_NAVBAR_COLOR = "pref_navbar_color";
public static final String PREF_NARROW_KEY_GAPS = "pref_narrow_key_gaps";
public static final String PREF_ENABLED_INPUT_STYLES = "pref_enabled_input_styles";
public static final String PREF_SELECTED_INPUT_STYLE = "pref_selected_input_style";
public static final String PREF_USE_SYSTEM_LOCALES = "pref_use_system_locales";
// This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
// This is being used only for the backward compatibility.
@ -219,6 +220,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
} finally {
mSettingsValuesLock.unlock();
}
if (key.equals(PREF_CUSTOM_INPUT_STYLES)) {
final String additionalSubtypes = readPrefAdditionalSubtypes(prefs, mContext.getResources());
SubtypeSettingsKt.updateAdditionalSubtypes(AdditionalSubtypeUtils.createAdditionalSubtypesArray(additionalSubtypes));
}
}
public void loadSettings(final Context context, final Locale locale,
@ -544,15 +549,27 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID, defValue);
}
// todo: adjust for multiple secondary locales
public static List<Locale> getSecondaryLocales(final SharedPreferences prefs, final String mainLocaleString) {
final Set<String> encodedLocales = prefs.getStringSet(PREF_SECONDARY_LOCALES, new HashSet<>());
for (String loc : encodedLocales) {
String[] locales = loc.split("§");
if (locales.length == 2 && locales[0].equals(mainLocaleString.toLowerCase(Locale.ENGLISH)))
return new ArrayList<Locale>() {{ add(LocaleUtils.constructLocaleFromString(locales[1])); }};
final String localesString = prefs.getString(PREF_SECONDARY_LOCALES_PREFIX + mainLocaleString.toLowerCase(Locale.ROOT), "");
final ArrayList<Locale> locales = new ArrayList<>();
for (String locale : localesString.split(";")) {
if (locale.isEmpty()) continue;
locales.add(LocaleUtils.constructLocaleFromString(locale));
}
return new ArrayList<>();
return locales;
}
public static void setSecondaryLocales(final SharedPreferences prefs, final String mainLocaleString, final List<String> locales) {
if (locales.isEmpty()) {
prefs.edit().putString(PREF_SECONDARY_LOCALES_PREFIX + mainLocaleString.toLowerCase(Locale.ROOT), "").apply();
return;
}
final StringBuilder sb = new StringBuilder();
for (String locale : locales) {
sb.append(";").append(locale);
}
prefs.edit().putString(PREF_SECONDARY_LOCALES_PREFIX + mainLocaleString.toLowerCase(Locale.ROOT), sb.toString()).apply();
}
public static Colors getColors(final Context context, final SharedPreferences prefs) {

View file

@ -29,15 +29,18 @@ import android.provider.Settings.Secure;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.latin.BuildConfig;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.common.FileUtils;
import org.dslul.openboard.inputmethod.latin.utils.ApplicationUtils;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
import org.dslul.openboard.inputmethod.latin.utils.FeedbackUtils;
import org.dslul.openboard.inputmethod.latin.utils.JniUtils;
import org.dslul.openboard.inputmethodcommon.InputMethodSettingsFragment;
import java.util.List;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
@ -83,6 +86,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment {
if (actionBar != null && screenTitle != null) {
actionBar.setTitle(screenTitle);
}
findPreference("screen_languages").setSummary(getEnabledSubtypesLabel());
if (BuildConfig.DEBUG)
askAboutCrashReports();
}
@ -129,11 +133,20 @@ public final class SettingsFragment extends InputMethodSettingsFragment {
return Secure.getInt(activity.getContentResolver(), "user_setup_complete", 0) != 0;
}
private String getEnabledSubtypesLabel() {
final List<InputMethodSubtype> subtypes = SubtypeSettingsKt.getEnabledSubtypes(DeviceProtectedUtils.getSharedPreferences(getActivity()), true);
final StringBuilder sb = new StringBuilder();
for (final InputMethodSubtype subtype : subtypes) {
if (sb.length() > 0)
sb.append(", ");
sb.append(subtype.getDisplayName(getActivity(), getActivity().getPackageName(), getActivity().getApplicationInfo()));
}
return sb.toString();
private void askAboutCrashReports() {
// find crash report files
final File dir = getActivity().getExternalFilesDir(null);
if (dir == null) return;
// final File[] files = dir.listFiles((file, s) -> file.getName().startsWith("crash_report"));
final File[] allFiles = dir.listFiles();
if (allFiles == null) return;
crashReportFiles.clear();

View file

@ -252,7 +252,7 @@ public class SettingsValues {
mClipboardHistoryRetentionTime = Settings.readClipboardHistoryRetentionTime(prefs, res);
mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs);
mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs);
mSecondaryLocales = Settings.getSecondaryLocales(prefs, RichInputMethodManager.getInstance().getCurrentSubtypeLocale().toString());
mSecondaryLocales = Settings.getSecondaryLocales(prefs, SubtypeSettingsKt.getSelectedSubtype(prefs).getLocale());
mColors = Settings.getColors(context, prefs);
mColors.createColorFilters(prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, false));

View file

@ -0,0 +1,221 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Build
import android.view.inputmethod.InputMethodSubtype
import androidx.core.app.LocaleManagerCompat
import androidx.core.content.edit
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
import org.dslul.openboard.inputmethod.latin.BuildConfig
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
import org.xmlpull.v1.XmlPullParser
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
/** @return enabled subtypes. If no subtypes are enabled, but a contextForFallback is provided,
* subtypes for system locales will be returned, or en_US if none found. */
fun getEnabledSubtypes(prefs: SharedPreferences, fallback: Boolean = false): List<InputMethodSubtype> {
require(initialized)
if (prefs.getBoolean(Settings.PREF_USE_SYSTEM_LOCALES, true))
return getDefaultEnabledSubtypes()
return getExplicitlyEnabledSubtypes(fallback)
}
fun getExplicitlyEnabledSubtypes(fallback: Boolean = false): List<InputMethodSubtype> {
require(initialized)
if (fallback && enabledSubtypes.isEmpty())
return getDefaultEnabledSubtypes()
return enabledSubtypes
}
fun getAllAvailableSubtypes(): List<InputMethodSubtype> {
require(initialized)
return resourceSubtypesByLocale.values.flatten() + additionalSubtypes
}
fun addEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
require(initialized)
val subtypeString = subtype.prefString()
val oldSubtypeStrings = prefs.getString(Settings.PREF_ENABLED_INPUT_STYLES, "")!!.split(SUBTYPE_SEPARATOR)
val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(SUBTYPE_SEPARATOR)
prefs.edit { putString(Settings.PREF_ENABLED_INPUT_STYLES, newString) }
if (subtype !in enabledSubtypes) {
enabledSubtypes.add(subtype)
enabledSubtypes.sortBy { it.locale } // for consistent order
}
}
/** returns whether subtype was actually removed, does not remove last subtype */
fun removeEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
require(initialized)
val subtypeString = subtype.prefString()
val oldSubtypeString = prefs.getString(Settings.PREF_ENABLED_INPUT_STYLES, "")!!
val newString = (oldSubtypeString.split(SUBTYPE_SEPARATOR) - subtypeString).joinToString(SUBTYPE_SEPARATOR)
if (newString == oldSubtypeString)
return // already removed
prefs.edit { putString(Settings.PREF_ENABLED_INPUT_STYLES, newString) }
if (subtypeString == prefs.getString(Settings.PREF_SELECTED_INPUT_STYLE, "")) {
// switch subtype if the currently used one has been disabled
val nextSubtype = RichInputMethodManager.getInstance().getNextSubtypeInThisIme(true)
if (subtypeString == nextSubtype?.prefString())
KeyboardSwitcher.getInstance().switchToSubtype(getDefaultEnabledSubtypes().first())
else
KeyboardSwitcher.getInstance().switchToSubtype(nextSubtype)
}
enabledSubtypes.remove(subtype)
}
fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype {
require(initialized)
val subtypeString = prefs.getString(Settings.PREF_SELECTED_INPUT_STYLE, "")!!.split(LOCALE_LAYOUT_SEPARATOR)
val subtype = enabledSubtypes.firstOrNull { subtypeString.first() == it.locale && subtypeString.last() == SubtypeLocaleUtils.getKeyboardLayoutSetName(it) }
?: enabledSubtypes.firstOrNull()
if (subtype == null) {
val defaultSubtypes = getDefaultEnabledSubtypes()
return defaultSubtypes.firstOrNull { subtypeString.first() == it.locale && subtypeString.last() == SubtypeLocaleUtils.getKeyboardLayoutSetName(it) }
?: defaultSubtypes.firstOrNull { subtypeString.first().substringBefore("_") == it.locale.substringBefore("_") && subtypeString.last() == SubtypeLocaleUtils.getKeyboardLayoutSetName(it) }
?: defaultSubtypes.first()
}
return subtype
}
fun setSelectedSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
val subtypeString = subtype.prefString()
if (subtype.locale.isEmpty() || prefs.getString(Settings.PREF_SELECTED_INPUT_STYLE, "") == subtypeString)
return
prefs.edit { putString(Settings.PREF_SELECTED_INPUT_STYLE, subtypeString) }
}
fun isAdditionalSubtype(subtype: InputMethodSubtype): Boolean {
return subtype in additionalSubtypes
}
fun updateAdditionalSubtypes(subtypes: Array<InputMethodSubtype>) {
additionalSubtypes.clear()
additionalSubtypes.addAll(subtypes)
}
fun reloadSystemLocales(context: Context) {
systemLocales.clear()
val localeList = LocaleManagerCompat.getSystemLocales(context)
(0 until localeList.size()).forEach {
val locale = localeList[it]
if (locale != null) systemLocales.add(locale)
}
}
fun getSystemLocales(): List<Locale> {
require(initialized)
return systemLocales
}
fun init(context: Context) {
if (initialized) return
SubtypeLocaleUtils.init(context) // necessary to get the correct getKeyboardLayoutSetName
// necessary to set system locales at start, because for some weird reason (bug?)
// LocaleManagerCompat.getSystemLocales(context) sometimes doesn't return all system locales
reloadSystemLocales(context)
loadResourceSubtypes(context.resources)
loadAdditionalSubtypes(context)
loadEnabledSubtypes(context)
initialized = true
}
private fun getDefaultEnabledSubtypes(): List<InputMethodSubtype> {
val inputMethodSubtypes = systemLocales.mapNotNull { locale ->
val localeString = locale.toString()
val subtypes = resourceSubtypesByLocale[localeString]
?: resourceSubtypesByLocale[localeString.substringBefore("_")] // fall back to language match
subtypes?.firstOrNull() // todo: maybe set default for some languages with multiple resource subtypes?
}
if (inputMethodSubtypes.isEmpty())
// hardcoded fallback for weird cases
return listOf(resourceSubtypesByLocale["en_US"]!!.first())
return inputMethodSubtypes
}
private fun InputMethodSubtype.prefString() =
locale + LOCALE_LAYOUT_SEPARATOR + SubtypeLocaleUtils.getKeyboardLayoutSetName(this)
private fun loadResourceSubtypes(resources: Resources) {
val xml = resources.getXml(R.xml.method)
xml.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
val namespace = "http://schemas.android.com/apk/res/android"
var eventType = xml.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && xml.name == "subtype") {
val icon = xml.getAttributeResourceValue(namespace, "icon", 0)
val label = xml.getAttributeResourceValue(namespace, "label", 0)
val subtypeId = xml.getAttributeIntValue(namespace, "subtypeId", 0)
val locale = xml.getAttributeValue(namespace, "imeSubtypeLocale").intern()
val languageTag = xml.getAttributeValue(namespace, "languageTag")
val imeSubtypeMode = xml.getAttributeValue(namespace, "imeSubtypeMode")
val imeSubtypeExtraValue = xml.getAttributeValue(namespace, "imeSubtypeExtraValue").intern()
val isAsciiCapable = xml.getAttributeBooleanValue(namespace, "isAsciiCapable", false)
val b = InputMethodSubtype.InputMethodSubtypeBuilder()
b.setSubtypeIconResId(icon)
b.setSubtypeNameResId(label)
if (subtypeId != 0)
b.setSubtypeId(subtypeId)
b.setSubtypeLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && languageTag != null)
b.setLanguageTag(languageTag)
b.setSubtypeMode(imeSubtypeMode)
b.setSubtypeExtraValue(imeSubtypeExtraValue)
b.setIsAsciiCapable(isAsciiCapable)
resourceSubtypesByLocale.getOrPut(locale) { ArrayList(2) }.add(b.build())
}
eventType = xml.next()
}
}
private fun loadAdditionalSubtypes(context: Context) {
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
val additionalSubtypeString = Settings.readPrefAdditionalSubtypes(prefs, context.resources)
val subtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(additionalSubtypeString)
additionalSubtypes.addAll(subtypes)
}
// requires loadResourceSubtypes to be called before
private fun loadEnabledSubtypes(context: Context) {
val prefs = DeviceProtectedUtils.getSharedPreferences(context)
val subtypeStrings = prefs.getString(Settings.PREF_ENABLED_INPUT_STYLES, "")!!
.split(SUBTYPE_SEPARATOR).filter { it.isNotEmpty() }.map { it.split(LOCALE_LAYOUT_SEPARATOR) }
for (localeAndLayout in subtypeStrings) {
require(localeAndLayout.size == 2)
val subtypesForLocale = resourceSubtypesByLocale[localeAndLayout.first()]
if (BuildConfig.DEBUG) // should not happen, but should not crash for normal user
require(subtypesForLocale != null)
else if (subtypesForLocale == null)
continue
val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getKeyboardLayoutSetName(it) == localeAndLayout.last() }
?: additionalSubtypes.firstOrNull { it.locale == localeAndLayout.first() && SubtypeLocaleUtils.getKeyboardLayoutSetName(it) == localeAndLayout.last() }
if (BuildConfig.DEBUG) // should not happen, but should not crash for normal user
require(subtype != null)
else if (subtype == null)
continue
enabledSubtypes.add(subtype)
}
}
private var initialized = false
private val enabledSubtypes = mutableListOf<InputMethodSubtype>()
private val resourceSubtypesByLocale = LinkedHashMap<String, MutableList<InputMethodSubtype>>(100)
private val additionalSubtypes = mutableListOf<InputMethodSubtype>()
private val systemLocales = mutableListOf<Locale>()
private const val SUBTYPE_SEPARATOR = ";"
private const val LOCALE_LAYOUT_SEPARATOR = ":"

View file

@ -180,11 +180,14 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
(TextView)findViewById(R.id.setup_step3_bullet), findViewById(R.id.setup_step3),
R.string.setup_step3_title, R.string.setup_step3_instruction,
0 /* finishedInstruction */, R.drawable.ic_setup_step3,
R.string.setup_step3_action);
R.string.setup_step3_action_new);
step3.setAction(new Runnable() {
@Override
public void run() {
invokeSubtypeEnablerOfThisIme();
final Intent intent = new Intent(getApplicationContext(), SettingsActivity.class);
intent.setAction(Intent.ACTION_VIEW);
startActivity(intent);
finish();
}
});
mSetupStepGroup.addStep(step3);
@ -282,19 +285,6 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
mNeedsToAdjustStepNumberToSystemState = true;
}
void invokeSubtypeEnablerOfThisIme() {
final InputMethodInfo imi =
UncachedInputMethodManagerUtils.getInputMethodInfoOf(getPackageName(), mImm);
if (imi == null) {
return;
}
final Intent intent = new Intent();
intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.getId());
startActivity(intent);
}
private int determineSetupStepNumberFromLauncher() {
final int stepNumber = determineSetupStepNumber();
if (stepNumber == STEP_1) {

View file

@ -107,29 +107,35 @@ public final class AdditionalSubtypeUtils {
final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
final ArrayList<InputMethodSubtype> subtypesList = new ArrayList<>(prefSubtypeArray.length);
for (final String prefSubtype : prefSubtypeArray) {
final String[] elems = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
if (elems.length != LENGTH_WITHOUT_EXTRA_VALUE
&& elems.length != LENGTH_WITH_EXTRA_VALUE) {
Log.w(TAG, "Unknown additional subtype specified: " + prefSubtype + " in "
+ prefSubtypes);
continue;
}
final String localeString = elems[INDEX_OF_LOCALE];
final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
// Here we assume that all the additional subtypes have AsciiCapable and EmojiCapable.
// This is actually what the setting dialog for additional subtype is doing.
final InputMethodSubtype subtype = createAsciiEmojiCapableAdditionalSubtype(
localeString, keyboardLayoutSetName);
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed.
continue;
}
subtypesList.add(subtype);
final InputMethodSubtype subtype = createSubtypeFromString(prefSubtype);
if (subtype != null)
subtypesList.add(subtype);
}
return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]);
}
// use string created with getPrefSubtype
public static InputMethodSubtype createSubtypeFromString(final String prefSubtype) {
final String[] elems = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
if (elems.length != LENGTH_WITHOUT_EXTRA_VALUE
&& elems.length != LENGTH_WITH_EXTRA_VALUE) {
Log.w(TAG, "Unknown additional subtype specified: " + prefSubtype);
return null;
}
final String localeString = elems[INDEX_OF_LOCALE];
final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
// Here we assume that all the additional subtypes have AsciiCapable and EmojiCapable.
// This is actually what the setting dialog for additional subtype is doing.
final InputMethodSubtype subtype = createAsciiEmojiCapableAdditionalSubtype(
localeString, keyboardLayoutSetName);
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed.
return null;
}
return subtype;
}
public static String createPrefSubtypes(final InputMethodSubtype[] subtypes) {
if (subtypes == null || subtypes.length == 0) {
return "";

View file

@ -329,6 +329,7 @@ public class DictionaryInfoUtils {
return MAIN_DICT_PREFIX + locale.toLowerCase(Locale.ENGLISH) + ".dict";
}
@Nullable
public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file,
final long offset, final long length) {
try {

View file

@ -0,0 +1,80 @@
package org.dslul.openboard.inputmethod.latin.utils
import android.app.AlertDialog
import android.os.IBinder
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.RelativeSizeSpan
import android.view.WindowManager
import android.view.inputmethod.InputMethodInfo
import android.view.inputmethod.InputMethodSubtype
import org.dslul.openboard.inputmethod.latin.LatinIME
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager
// similar to what showSubtypePicker does in https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/RichInputMethodManager.java
fun showInputMethodPicker(latinIme: LatinIME, richImm: RichInputMethodManager, windowToken: IBinder) {
val pm = latinIme.packageManager
val thisImi = richImm.inputMethodInfoOfThisIme
val currentSubtype = richImm.currentSubtype.rawSubtype
val enabledImis = richImm.inputMethodManager.enabledInputMethodList
.sortedBy { it.hashCode() }.sortedBy { it.loadLabel(pm).toString() } // first label, then hashCode
val enabledSubtypes = mutableListOf<Pair<InputMethodInfo, InputMethodSubtype?>>()
var currentSubtypeIndex = 0
enabledImis.forEach { imi ->
val subtypes = richImm.getEnabledInputMethodSubtypeList(imi, true)
if (subtypes.isEmpty()) {
enabledSubtypes.add(imi to null)
} else {
subtypes.forEach {
if (!it.isAuxiliary) {
enabledSubtypes.add(imi to it)
if (imi == thisImi && it == currentSubtype)
currentSubtypeIndex = enabledSubtypes.lastIndex
}
}
}
}
val items = mutableListOf<SpannableStringBuilder>()
for (imiAndSubtype in enabledSubtypes) {
val (imi, subtype) = imiAndSubtype
val title = SpannableString(subtype?.getDisplayName(latinIme, imi.packageName, imi.serviceInfo.applicationInfo)
?.ifBlank { imi.loadLabel(pm) }
?: imi.loadLabel(pm))
val subtitle = SpannableString(if (subtype == null) "" else "\n${imi.loadLabel(pm)}")
title.setSpan(
RelativeSizeSpan(0.9f), 0, title.length,
Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
subtitle.setSpan(
RelativeSizeSpan(0.85f), 0, subtitle.length,
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
items.add(SpannableStringBuilder().append(title).append(subtitle))
}
val dialog = AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(latinIme))
.setTitle(R.string.select_input_method)
.setSingleChoiceItems(items.toTypedArray(), currentSubtypeIndex) { di, i ->
di.dismiss()
val (imi, subtype) = enabledSubtypes[i]
if (imi == thisImi)
latinIme.switchToSubtype(subtype)
else if (subtype != null)
latinIme.switchInputMethodAndSubtype(imi, subtype)
else
latinIme.switchInputMethod(imi.id)
}
.create()
val window = dialog.window
val layoutParams = window?.attributes
layoutParams?.token = windowToken
layoutParams?.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG
window?.attributes = layoutParams
window?.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
dialog.show()
}

View file

@ -313,14 +313,14 @@ public final class SubtypeLocaleUtils {
return LocaleUtils.constructLocaleFromString(localeString);
}
@Nonnull
@Nullable
public static String getKeyboardLayoutSetDisplayName(
@Nonnull final InputMethodSubtype subtype) {
final String layoutName = getKeyboardLayoutSetName(subtype);
return getKeyboardLayoutSetDisplayName(layoutName);
}
@Nonnull
@Nullable
public static String getKeyboardLayoutSetDisplayName(@Nonnull final String layoutName) {
return sKeyboardLayoutToDisplayNameMap.get(layoutName);
}