Merge branch 'new' into hangul_update

This commit is contained in:
Helium314 2023-09-14 13:43:15 +02:00
commit cf17b4984a
917 changed files with 4740 additions and 12260 deletions

View file

@ -1,5 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Discussions
- name: Dictionary request
url: https://codeberg.org/Helium314/aosp-dictionaries
about: Requests for dictionaries (used for suggestions / autocompletion) will be handled in the linked dictionary repository. You can check whether a dictionary for the language you want already exists there.
- name: Discussion
url: https://github.com/Helium314/OpenBoard/discussions
about: For discussions and feedback about this version of OpenBoard, asking questions or talking about ideas which are not yet actionable (i.e. not suitable for an issue).
- name: Question
url: https://github.com/Helium314/openboard/discussions/new?category=q-a
about: For questions please use the discussions section.

View file

@ -1,13 +0,0 @@
---
name: Language request
about: Use this to request autocompletion support for a missing language, or for issues with existing dictionaries
labels: dictionaries
---
<!--
Please search open and closed issues to avoid duplicates.
Before you suggest adding a language, check if the language is already included in the downloadable dictionaries at https://github.com/Helium314/openboard/tree/new/dictionaries/dict or experimental dictionaries at https://github.com/Helium314/openboard/tree/new/dictionaries/experimental/dict
If you open an issue about an existing dictionary, add information which dictionary you use (included in app, or link)
-->

View file

@ -1,6 +1,6 @@
---
name: Other
about: Anything that does not fit into the other categories
about: Anything that does not fit into the other categories. Please don't use this for questions or discussions.
---
<!--

View file

@ -5,24 +5,22 @@ Might end up on F-Droid...
**This is mostly a development version. On updates there may be changes that reset some settings. Consider all releases as beta quality at best.**
The rough plan for this project:
* Theming
* improve user-defined coloring
* add pre-defined themes
* make automatic theme switch work with the new themes
* Multilingual Typing
* fix some bugs preventing proper usage in some cases (especially concerning key popups)
* possibly has some undiscovered bugs, more testing required
* Dictionaries
* semi-independent in a separate repository: https://codeberg.org/Helium314/aosp-dictionaries
* Internal clean up (xml files, unused resources and classes)
* the current state look rather messy, with many useless / duplicate entries that make changes needlessly tricky
* Make suggestion removal functionality more discoverable
* Fix some bugs along the way
## Features
* Allow loading Glide typing library
* not included in the app, as there is no compatible open source library
* can be extracted from GApps packages (_swypelibs_), or downloaded [here](https://github.com/erkserkserks/openboard/tree/master/app/src/main/jniLibs)
* Multilingual typing
* Load external dictionaries
* get them [here](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/dictionaries), or in the [experimental](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/dictionaries_experimental) section (quality may vary)
* additional dictionaries for emojis or scientific symbols can be used to provide suggestions ("emoji search")
* Adjust keyboard themes (style and colors)
* can follow the system's day/night setting
* Split keyboard
* Number row
* Number pad
* Show all available extra characters on long pressing a key
Once above is done, we can think about renaming and properly releasing the app. First just in this repository, and later on F-Droid.
Important differences / changes to OpenBoard:
## Important differences and changes to OpenBoard
* Debug version can be installed along OpenBoard
* Allow users to add and replace built-in dictionaries
* modified / improved from https://github.com/openboard-team/openboard/pull/569 and https://github.com/openboard-team/openboard/pull/578
@ -41,6 +39,7 @@ Important differences / changes to OpenBoard:
* tested with [Google libraries](https://github.com/erkserkserks/openboard/tree/master/app/src/main/jniLibs) and [others](https://github.com/openboard-team/openboard/issues/3#issuecomment-1200456262) (when building with the [rename](https://github.com/openboard-team/openboard/tree/57d33791d7674e3fe0600eddb72f6b4317b5df00))
* Theming: allow adjusting keyboard colors, https://github.com/openboard-team/openboard/issues/124
* Optionally make the navigation bar follow current theme, https://github.com/Helium314/openboard/issues/4
* Allow defining day/night themes
* Remove suggestions by long pressing on suggestion strip while the more suggestions popup is open, https://github.com/openboard-team/openboard/issues/106
* suggestions get re-added if they are entered again
* Optionally add typed words to system personal dictionary
@ -57,19 +56,29 @@ Important differences / changes to OpenBoard:
* Add number pad
* Overhauled language settings
* Updated translations
* Open dictionary files with the app
* Add more options to the language switch key
Further plan / to do:
* upgrade NDK, https://github.com/openboard-team/openboard/issues/782
* later: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid
* ~multi-lingual typing, https://github.com/openboard-team/openboard/pull/586, https://github.com/openboard-team/openboard/pull/593~
* maybe improve way of merging suggestions from both languages
* test whether it works reasonably well in non-latin scripts
* ~suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660~
* ~improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576~
* ~emoji prediction/search, https://github.com/openboard-team/openboard/pull/749 (better use emoji dictionaries, this is more flexible)~
* ~delete suggestions, https://github.com/openboard-team/openboard/issues/106~
* make functionality more discoverable, e.g. add a button to the _more suggestions_ menu, or replace long-press menu and show more suggestions by scrolling suggestion strip
* add emojis to user history, to be used for next word
## The rough plan/todo before "full" release
* Add/change pre-defined themes
* Internal clean up (xml files, unused resources and classes)
* even after a lot of work here, the current state look rather messy, with many useless and duplicate entries
* work through _todo_s in code
* Make suggestion removal functionality more discoverable
* Better detection when to separate words and when not (e.g. detection of email addresses and URLs)
* Fix some bugs
* especially the spell checker issue https://github.com/Helium314/openboard/issues/55
* "partial" multi-character codepoint deletion with delete gesture (e.g. for emojis), https://github.com/Helium314/openboard/issues/22
Once above is done, we can think about properly releasing the app. First just in this repository, and later on F-Droid.
This would include renaming the app and the package, changing the icon and using a localization tool (most likely weblate).
## Further plan
* more customizable theming
* improved / less bad suggestions in some cases
* add emojis to user history, to be used for next word suggestions
* sliding key input for numpad and emojis (like `?123` and _shift_ sliding input)
* updated suggestion strip, maybe add tools or make the suggestions scroll
-----
# readme for original version of OpenBoard below

View file

@ -14,18 +14,22 @@ android {
buildTypes {
release {
minifyEnabled false
minifyEnabled true
//shrinkResources true todo: test before enabling, a lot of resources are accessed via getIdentifier and possibly not all have been marked with "keep"
debuggable false
jniDebuggable false
renderscriptDebuggable false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/main/proguard.flags'
}
debug {
minifyEnabled false
minifyEnabled true
jniDebuggable false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/main/proguard.flags'
applicationIdSuffix ".debug"
}
buildFeatures {
viewBinding true
}
archivesBaseName = "openboard_" + defaultConfig.versionName
}
@ -59,5 +63,6 @@ dependencies {
implementation 'androidx.preference:preference:1.2.1' // includes appcompat
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.github.skydoves:colorpickerview:2.2.4'
testImplementation 'junit:junit:4.13.2'
}

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2013, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dictionary_pack_client_id" translatable="false">org.dslul.openboard.inputmethod.latin.debug</string>
<!-- <string name="dictionary_pack_settings_activity" translatable="false">org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsActivity</string> -->
<string name="authority" translatable="false">org.dslul.openboard.inputmethod.debug.dictionarypack.aosp</string>
</resources>

View file

@ -160,12 +160,13 @@
</receiver>
<!-- Content providers -->
<!--
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
android:grantUriPermissions="true"
android:exported="false"
android:authorities="@string/authority"
android:multiprocess="false"
android:label="@string/dictionary_provider_name">
</provider>
</provider>-->
</application>
</manifest>

View file

@ -294,8 +294,9 @@ public final class BinaryDictionary extends Dictionary {
}
session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
session.mNativeSuggestOptions.setIsGesture(isGesture);
session.mNativeSuggestOptions.setBlockOffensiveWords(
settingsValuesForSuggestion.mBlockPotentiallyOffensive);
if (isGesture)
session.mNativeSuggestOptions.setIsSpaceAwareGesture(settingsValuesForSuggestion.mSpaceAwareGesture);
session.mNativeSuggestOptions.setBlockOffensiveWords(settingsValuesForSuggestion.mBlockPotentiallyOffensive);
session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
if (inOutWeightOfLangModelVsSpatialModel != null) {
session.mInputOutputWeightOfLangModelVsSpatialModel[0] =

View file

@ -2,6 +2,7 @@ package org.dslul.openboard.inputmethod.accessibility
import android.content.Context
import android.media.AudioManager
import android.os.Build
import android.os.SystemClock
import android.provider.Settings
import android.text.TextUtils
@ -12,7 +13,6 @@ import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.view.inputmethod.EditorInfo
import androidx.core.view.accessibility.AccessibilityEventCompat
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.SuggestedWords
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
@ -127,17 +127,17 @@ class AccessibilityUtils private constructor() {
return
}
// The following is a hack to avoid using the heavy-weight TextToSpeech
// class. Instead, we're just forcing a fake AccessibilityEvent into
// the screen reader to make it speak.
val event = AccessibilityEvent.obtain()
// class. Instead, we're just forcing a fake AccessibilityEvent into
// the screen reader to make it speak.
val event = obtainEvent()
event.packageName = PACKAGE
event.className = CLASS
event.eventTime = SystemClock.uptimeMillis()
event.isEnabled = true
event.text.add(text)
// Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
// announce events.
event.eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT
// announce events.
event.eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
val viewParent = view.parent
if (viewParent == null || viewParent !is ViewGroup) {
Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility")
@ -205,5 +205,21 @@ class AccessibilityUtils private constructor() {
val action = event.action
return action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_HOVER_MOVE
}
fun obtainEvent(eventType: Int): AccessibilityEvent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent(eventType)
} else {
@Suppress("deprecation")
AccessibilityEvent.obtain(eventType)
}
fun obtainEvent(): AccessibilityEvent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent()
} else {
@Suppress("deprecation")
AccessibilityEvent.obtain()
}
}
}

View file

@ -228,7 +228,7 @@ internal class KeyCodeDescriptionMapper private constructor() {
return key.label!!.trim { it <= ' ' }
}
resId = when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> R.string.spoken_description_search
EditorInfo.IME_ACTION_SEARCH -> R.string.label_search_key
EditorInfo.IME_ACTION_GO -> R.string.label_go_key
EditorInfo.IME_ACTION_SEND -> R.string.label_send_key
EditorInfo.IME_ACTION_NEXT -> R.string.label_next_key

View file

@ -73,8 +73,7 @@ open class KeyboardAccessibilityDelegate<KV : KeyboardView?>(protected val mKeyb
* @param text The text to send with the event.
*/
protected fun sendWindowStateChanged(text: String?) {
val stateChange = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
val stateChange = AccessibilityUtils.obtainEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
mKeyboardView!!.onInitializeAccessibilityEvent(stateChange)
stateChange.text.add(text)
stateChange.contentDescription = null

View file

@ -1,10 +1,12 @@
package org.dslul.openboard.inputmethod.accessibility
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityRecord
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityEventCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@ -91,12 +93,12 @@ class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
fun createAccessibilityEvent(key: Key, eventType: Int): AccessibilityEvent {
val virtualViewId = getVirtualViewIdOf(key)
val keyDescription = getKeyDescription(key)
val event = AccessibilityEvent.obtain(eventType)
val event = AccessibilityUtils.obtainEvent(eventType)
event.packageName = mKeyboardView!!.context.packageName
event.className = key.javaClass.name
event.contentDescription = keyDescription
event.isEnabled = true
val record = AccessibilityEventCompat.asRecord(event)
val record: AccessibilityRecord = event
record.setSource(mKeyboardView, virtualViewId)
return event
}
@ -111,16 +113,16 @@ class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
// announcements.
mHoveringNodeId = id
// Invalidate the node info of the key.
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER)
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
}
fun onHoverExitFrom(key: Key) {
mHoveringNodeId = UNDEFINED
// Invalidate the node info of the key to be able to revert the change we have done
// in {@link #onHoverEnterTo(Key)}.
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT)
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT)
}
/**
@ -292,8 +294,8 @@ class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
}
init {
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.Companion.instance
mAccessibilityUtils = AccessibilityUtils.Companion.instance
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.instance
mAccessibilityUtils = AccessibilityUtils.instance
mKeyboardView = keyboardView
mDelegate = delegate
// Since this class is constructed lazily, we might not get a subsequent

View file

@ -24,8 +24,8 @@ object CompatUtils {
}
try {
return targetClass.getMethod(name!!, *parameterTypes)
} catch (e: SecurityException) { // ignore
} catch (e: NoSuchMethodException) {
} catch (_: SecurityException) { // ignore
} catch (_: NoSuchMethodException) {
}
return null
}
@ -36,8 +36,8 @@ object CompatUtils {
}
try {
return targetClass.getField(name!!)
} catch (e: SecurityException) { // ignore
} catch (e: NoSuchFieldException) {
} catch (_: SecurityException) { // ignore
} catch (_: NoSuchFieldException) {
}
return null
}
@ -49,8 +49,8 @@ object CompatUtils {
}
try {
return targetClass.getConstructor(*types)
} catch (e: SecurityException) { // ignore
} catch (e: NoSuchMethodException) {
} catch (_: SecurityException) { // ignore
} catch (_: NoSuchMethodException) {
}
return null
}
@ -153,9 +153,10 @@ object CompatUtils {
}
@Suppress("unchecked_cast")
class ToObjectMethodWrapper<T>(private val mMethod: Method?, private val mDefaultValue: T) {
operator fun invoke(receiver: Any?, vararg args: Any?): T {
return CompatUtils.invoke(receiver, mDefaultValue!!, mMethod, *args) as T
return CompatUtils.invoke(receiver, mDefaultValue, mMethod, *args) as T
}
}

View file

@ -5,7 +5,7 @@ import java.util.*
object EditorInfoCompatUtils {
// Note that EditorInfo.IME_FLAG_FORCE_ASCII has been introduced
// in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
// in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
private val FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField(
EditorInfo::class.java, "IME_FLAG_FORCE_ASCII")
private val OBJ_IME_FLAG_FORCE_ASCII: Int? = CompatUtils.getFieldValue(
@ -13,12 +13,12 @@ object EditorInfoCompatUtils {
private val FIELD_HINT_LOCALES = CompatUtils.getField(
EditorInfo::class.java, "hintLocales")
@kotlin.jvm.JvmStatic
@JvmStatic
fun hasFlagForceAscii(imeOptions: Int): Boolean {
return if (OBJ_IME_FLAG_FORCE_ASCII == null) false else imeOptions and OBJ_IME_FLAG_FORCE_ASCII != 0
}
@kotlin.jvm.JvmStatic
@JvmStatic
fun imeActionName(imeOptions: Int): String {
val actionId = imeOptions and EditorInfo.IME_MASK_ACTION
return when (actionId) {
@ -49,10 +49,10 @@ object EditorInfoCompatUtils {
if (hasFlagForceAscii(imeOptions)) {
flags.append("flagForceAscii|")
}
return if (action != null) flags.toString() + action else flags.toString()
return flags.toString() + action
}
@kotlin.jvm.JvmStatic
@JvmStatic
fun getPrimaryHintLocale(editorInfo: EditorInfo?): Locale? {
if (editorInfo == null) {
return null
@ -61,6 +61,6 @@ object EditorInfoCompatUtils {
?: return null
return if (LocaleListCompatUtils.isEmpty(localeList)) {
null
} else LocaleListCompatUtils.get(localeList, 0)
} else LocaleListCompatUtils[localeList, 0]
}
}

View file

@ -7,7 +7,7 @@ import org.dslul.openboard.inputmethod.compat.CompatUtils.ToBooleanMethodWrapper
object InputConnectionCompatUtils {
private var sInputConnectionType: ClassWrapper? = null
private var sRequestCursorUpdatesMethod: ToBooleanMethodWrapper? = null
val isRequestCursorUpdatesAvailable: Boolean
private val isRequestCursorUpdatesAvailable: Boolean
get() = sRequestCursorUpdatesMethod != null
/**
@ -31,7 +31,7 @@ object InputConnectionCompatUtils {
* as soon as possible to notify the current cursor/anchor position to the input method.
* @return `false` if the request is not handled. Otherwise returns `true`.
*/
@kotlin.jvm.JvmStatic
@JvmStatic
fun requestCursorUpdates(inputConnection: InputConnection,
enableMonitor: Boolean, requestImmediateCallback: Boolean): Boolean {
val cursorUpdateMode = ((if (enableMonitor) CURSOR_UPDATE_MONITOR else 0)

View file

@ -2,28 +2,24 @@ package org.dslul.openboard.inputmethod.compat
import android.os.Build
import android.os.Build.VERSION_CODES
import android.text.TextUtils
import android.util.Log
import android.view.inputmethod.InputMethodSubtype
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype
import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.settings.locale
import java.util.*
object InputMethodSubtypeCompatUtils {
// Note that InputMethodSubtype.getLanguageTag() is expected to be available in Android N+.
private val GET_LANGUAGE_TAG = CompatUtils.getMethod(InputMethodSubtype::class.java, "getLanguageTag")
@kotlin.jvm.JvmStatic
@JvmStatic
fun getLocaleObject(subtype: InputMethodSubtype): Locale { // Locale.forLanguageTag() is available only in Android L and later.
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val languageTag = CompatUtils.invoke(subtype, null, GET_LANGUAGE_TAG) as String?
if (!TextUtils.isEmpty(languageTag)) {
if (!languageTag.isNullOrEmpty()) {
return Locale.forLanguageTag(languageTag)
}
}
return LocaleUtils.constructLocaleFromString(subtype.locale)
return LocaleUtils.constructLocaleFromString(subtype.locale())
}
}

View file

@ -4,42 +4,19 @@ import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils
import android.text.style.SuggestionSpan
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
import org.dslul.openboard.inputmethod.latin.SuggestedWords
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.define.DebugFlags
import java.util.*
object SuggestionSpanUtils {
// Note that SuggestionSpan.FLAG_AUTO_CORRECTION has been introduced
// in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1).
private val FIELD_FLAG_AUTO_CORRECTION = CompatUtils.getField(
SuggestionSpan::class.java, "FLAG_AUTO_CORRECTION")
private val OBJ_FLAG_AUTO_CORRECTION: Int? = CompatUtils.getFieldValue(
null /* receiver */, null /* defaultValue */, FIELD_FLAG_AUTO_CORRECTION) as Int
@kotlin.jvm.JvmStatic
@JvmStatic
@UsedForTesting
fun getTextWithAutoCorrectionIndicatorUnderline(
context: Context?, text: String,locale: Locale?): CharSequence {
if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) {
fun getTextWithAutoCorrectionIndicatorUnderline(context: Context?, text: String, locale: Locale?): CharSequence {
if (text.isEmpty())
return text
}
val spannable: Spannable = SpannableString(text)
val suggestionSpan = SuggestionSpan(context, locale, arrayOf(), OBJ_FLAG_AUTO_CORRECTION, null)
spannable.setSpan(suggestionSpan, 0, text.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE or Spanned.SPAN_COMPOSING)
val suggestionSpan = SuggestionSpan(context, locale, arrayOf(), SuggestionSpan.FLAG_AUTO_CORRECTION, null)
spannable.setSpan(suggestionSpan, 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE or Spanned.SPAN_COMPOSING)
return spannable
}
init {
if (DebugFlags.DEBUG_ENABLED) {
if (OBJ_FLAG_AUTO_CORRECTION == null) {
throw RuntimeException("Field is accidentially null.")
}
}
}
}

View file

@ -1,28 +1,24 @@
package org.dslul.openboard.inputmethod.compat
import android.os.Build
import android.view.textservice.TextInfo
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
object TextInfoCompatUtils {
// Note that TextInfo.getCharSequence() is supposed to be available in API level 21 and later.
private val TEXT_INFO_GET_CHAR_SEQUENCE = CompatUtils.getMethod(TextInfo::class.java, "getCharSequence")
private val TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE = CompatUtils.getConstructor(TextInfo::class.java, CharSequence::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
@get:UsedForTesting
val isCharSequenceSupported: Boolean
get() = TEXT_INFO_GET_CHAR_SEQUENCE != null &&
TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null
get() = TEXT_INFO_GET_CHAR_SEQUENCE != null
@kotlin.jvm.JvmStatic
@JvmStatic
@UsedForTesting
fun newInstance(charSequence: CharSequence, start: Int, end: Int, cookie: Int,
sequenceNumber: Int): TextInfo? {
return if (TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null) {
CompatUtils.newInstance(TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE,
charSequence, start, end, cookie, sequenceNumber) as TextInfo
} else TextInfo(charSequence.subSequence(start, end).toString(), cookie,
sequenceNumber)
sequenceNumber: Int): TextInfo {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
return TextInfo(charSequence, start, end, cookie, sequenceNumber)
return TextInfo(charSequence.subSequence(start, end).toString(), cookie, sequenceNumber)
}
/**
@ -34,11 +30,12 @@ object TextInfoCompatUtils {
* the result of [TextInfo.getText] as fall back. If `textInfo` is `null`,
* returns `null`.
*/
@kotlin.jvm.JvmStatic
@JvmStatic
@UsedForTesting
fun getCharSequenceOrString(textInfo: TextInfo?): CharSequence? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
return textInfo?.charSequence
val defaultValue: CharSequence? = textInfo?.text
return CompatUtils.invoke(textInfo, defaultValue!!,
TEXT_INFO_GET_CHAR_SEQUENCE) as CharSequence
return CompatUtils.invoke(textInfo, defaultValue!!, TEXT_INFO_GET_CHAR_SEQUENCE) as CharSequence
}
}

View file

@ -1,51 +0,0 @@
package org.dslul.openboard.inputmethod.compat
import android.content.Context
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.UserManager
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import java.lang.reflect.Method
/**
* A temporary solution until `UserManagerCompat.isUserUnlocked()` in the support-v4 library
* becomes publicly available.
*/
object UserManagerCompatUtils {
private var METHOD_isUserUnlocked: Method? = null
const val LOCK_STATE_UNKNOWN = 0
const val LOCK_STATE_UNLOCKED = 1
const val LOCK_STATE_LOCKED = 2
/**
* Check if the calling user is running in an "unlocked" state. A user is unlocked only after
* they've entered their credentials (such as a lock pattern or PIN), and credential-encrypted
* private app data storage is available.
* @param context context from which [UserManager] should be obtained.
* @return One of [LockState].
*/
@RequiresApi(VERSION_CODES.M)
@kotlin.jvm.JvmStatic
@LockState
fun getUserLockState(context: Context): Int {
if (METHOD_isUserUnlocked == null) {
return LOCK_STATE_UNKNOWN
}
val userManager = context.getSystemService(UserManager::class.java)
?: return LOCK_STATE_UNKNOWN
val result = CompatUtils.invoke(userManager, null, METHOD_isUserUnlocked) as Boolean
return if (result) LOCK_STATE_UNLOCKED else LOCK_STATE_LOCKED
}
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@IntDef(LOCK_STATE_UNKNOWN, LOCK_STATE_UNLOCKED, LOCK_STATE_LOCKED)
annotation class LockState
init { // We do not try to search the method in Android M and prior.
METHOD_isUserUnlocked = if (Build.VERSION.SDK_INT <= VERSION_CODES.M) {
null
} else {
CompatUtils.getMethod(UserManager::class.java, "isUserUnlocked")
}
}
}

View file

@ -10,8 +10,8 @@ object ViewOutlineProviderCompatUtils {
override fun setInsets(insets: InputMethodService.Insets) {}
}
@kotlin.jvm.JvmStatic
fun setInsetsOutlineProvider(view: View): InsetsUpdater? {
@JvmStatic
fun setInsetsOutlineProvider(view: View): InsetsUpdater {
return if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
EMPTY_INSETS_UPDATER
} else ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view)

View file

@ -2,10 +2,13 @@ package org.dslul.openboard.inputmethod.compat
import android.graphics.Outline
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.view.View
import android.view.ViewOutlineProvider
import androidx.annotation.RequiresApi
import org.dslul.openboard.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
internal object ViewOutlineProviderCompatUtilsLXX {
fun setInsetsOutlineProvider(view: View): InsetsUpdater {
val provider = InsetsOutlineProvider(view)
@ -29,8 +32,7 @@ internal object ViewOutlineProviderCompatUtilsLXX {
return
}
// TODO: Revisit this when floating/resize keyboard is supported.
outline.setRect(
view.left, mLastVisibleTopInsets, view.right, view.bottom)
outline.setRect(view.left, mLastVisibleTopInsets, view.right, view.bottom)
}
companion object {

View file

@ -1,502 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.text.TextUtils
import android.util.Log
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.DisableAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.EnableAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.FinishDeleteAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.ForgetAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.MakeAvailableAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.MarkPreInstalledAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.StartDeleteAction
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.UpdateDataAction
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils
import java.util.*
/**
* Object representing an upgrade from one state to another.
*
* This implementation basically encapsulates a list of Runnable objects. In the future
* it may manage dependencies between them. Concretely, it does not use Runnable because the
* actions need an argument.
*/
/*
The state of a word list follows the following scheme.
| ^
MakeAvailable |
| .------------Forget--------'
V |
STATUS_AVAILABLE <-------------------------.
| |
StartDownloadAction FinishDeleteAction
| |
V |
STATUS_DOWNLOADING EnableAction-- STATUS_DELETING
| | ^
InstallAfterDownloadAction | |
| .---------------' StartDeleteAction
| | |
V V |
STATUS_INSTALLED <--EnableAction-- STATUS_DISABLED
--DisableAction-->
It may also be possible that DisableAction or StartDeleteAction or
DownloadAction run when the file is still downloading. This cancels
the download and returns to STATUS_AVAILABLE.
Also, an UpdateDataAction may apply in any state. It does not affect
the state in any way (nor type, local filename, id or version) but
may update other attributes like description or remote filename.
Forget is an DB maintenance action that removes the entry if it is not installed or disabled.
This happens when the word list information disappeared from the server, or when a new version
is available and we should forget about the old one.
*/
class ActionBatch {
/**
* A piece of update.
*
* Action is basically like a Runnable that takes an argument.
*/
interface Action {
/**
* Execute this action NOW.
* @param context the context to get system services, resources, databases
*/
fun execute(context: Context?)
}
/**
* An action that enables an existing word list.
*/
class EnableAction(clientId: String, wordList: WordListMetadata?) : Action {
private val mClientId: String
// The state to upgrade from. May not be null.
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) {
Log.e(TAG, "EnableAction with a null parameter!")
return
}
DebugLogUtils.l("Enabling word list")
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)!!
val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
if (MetadataDbHelper.STATUS_DISABLED != status
&& MetadataDbHelper.STATUS_DELETING != status) {
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + " : " + status
+ " for an enable action. Cancelling")
return
}
MetadataDbHelper.markEntryAsEnabled(db, mWordList.mId, mWordList.mVersion)
}
companion object {
val TAG = "DictionaryProvider:" + EnableAction::class.java.simpleName
}
init {
DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList)
mClientId = clientId
mWordList = wordList
}
}
/**
* An action that disables a word list.
*/
class DisableAction(clientId: String, wordlist: WordListMetadata?) : Action {
private val mClientId: String
// The word list to disable. May not be null.
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "DisableAction with a null word list!")
return
}
DebugLogUtils.l("Disabling word list : $mWordList")
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)!!
val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
if (MetadataDbHelper.STATUS_INSTALLED == status) { // Disabling an installed word list
MetadataDbHelper.markEntryAsDisabled(db, mWordList.mId, mWordList.mVersion)
} else {
if (MetadataDbHelper.STATUS_DOWNLOADING != status) {
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : "
+ status + " for a disable action. Fall back to marking as available.")
}
// The word list is still downloading. Cancel the download and revert the
// word list status to "available".
}
}
companion object {
val TAG = "DictionaryProvider:" + DisableAction::class.java.simpleName
}
init {
DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
}
}
/**
* An action that makes a word list available.
*/
class MakeAvailableAction(clientId: String, wordlist: WordListMetadata?) : Action {
private val mClientId: String
// The word list to make available. May not be null.
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "MakeAvailableAction with a null word list!")
return
}
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
if (null != MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)) {
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
+ " for a makeavailable action. Marking as available anyway.")
}
DebugLogUtils.l("Making word list available : $mWordList")
// If mLocalFilename is null, then it's a remote file that hasn't been downloaded
// yet, so we set the local filename to the empty string.
val values: ContentValues = MetadataDbHelper.makeContentValues(0,
MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_AVAILABLE,
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
mWordList.mLocalFilename ?: "",
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum,
mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize,
mWordList.mVersion, mWordList.mFormatVersion)
PrivateLog.log("Insert 'available' record for " + mWordList.mDescription
+ " and locale " + mWordList.mLocale)
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values)
}
companion object {
val TAG = "DictionaryProvider:" + MakeAvailableAction::class.java.simpleName
}
init {
DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
}
}
/**
* An action that marks a word list as pre-installed.
*
* This is almost the same as MakeAvailableAction, as it only inserts a line with parameters
* received from outside.
* Unlike MakeAvailableAction, the parameters are not received from a downloaded metadata file
* but from the client directly; it marks a word list as being "installed" and not "available".
* It also explicitly sets the filename to the empty string, so that we don't try to open
* it on our side.
*/
class MarkPreInstalledAction(clientId: String?, wordlist: WordListMetadata?) : Action {
private val mClientId: String?
// The word list to mark pre-installed. May not be null.
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "MarkPreInstalledAction with a null word list!")
return
}
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
if (null != MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)) {
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
+ " for a markpreinstalled action. Marking as preinstalled anyway.")
}
DebugLogUtils.l("Marking word list preinstalled : $mWordList")
// This word list is pre-installed : we don't have its file. We should reset
// the local file name to the empty string so that we don't try to open it
// accidentally. The remote filename may be set by the application if it so wishes.
val values: ContentValues = MetadataDbHelper.makeContentValues(0,
MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED,
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
if (TextUtils.isEmpty(mWordList.mLocalFilename)) "" else mWordList.mLocalFilename,
mWordList.mRemoteFilename, mWordList.mLastUpdate,
mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount,
mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion)
PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription
+ " and locale " + mWordList.mLocale)
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values)
}
companion object {
val TAG = ("DictionaryProvider:"
+ MarkPreInstalledAction::class.java.simpleName)
}
init {
DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
}
}
/**
* An action that updates information about a word list - description, locale etc
*/
class UpdateDataAction(clientId: String, wordlist: WordListMetadata?) : Action {
private val mClientId: String
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "UpdateDataAction with a null word list!")
return
}
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
val oldValues: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)!!
if (null == oldValues) {
Log.e(TAG, "Trying to update data about a non-existing word list. Bailing out.")
return
}
DebugLogUtils.l("Updating data about a word list : $mWordList")
val values: ContentValues = MetadataDbHelper.makeContentValues(
oldValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN),
oldValues.getAsInteger(MetadataDbHelper.TYPE_COLUMN),
oldValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN),
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN),
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum,
mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize,
mWordList.mVersion, mWordList.mFormatVersion)
PrivateLog.log("Updating record for " + mWordList.mDescription
+ " and locale " + mWordList.mLocale)
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion)))
}
companion object {
val TAG = "DictionaryProvider:" + UpdateDataAction::class.java.simpleName
}
init {
DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
}
}
/**
* An action that deletes the metadata about a word list if possible.
*
* This is triggered when a specific word list disappeared from the server, or when a fresher
* word list is available and the old one was not installed.
* If the word list has not been installed, it's possible to delete its associated metadata.
* Otherwise, the settings are retained so that the user can still administrate it.
*/
class ForgetAction(clientId: String, wordlist: WordListMetadata?,
hasNewerVersion: Boolean) : Action {
private val mClientId: String
// The word list to remove. May not be null.
val mWordList: WordListMetadata?
val mHasNewerVersion: Boolean
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "TryRemoveAction with a null word list!")
return
}
DebugLogUtils.l("Trying to remove word list : $mWordList")
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)!!
if (null == values) {
Log.e(TAG, "Trying to update the metadata of a non-existing wordlist. Cancelling.")
return
}
val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
if (mHasNewerVersion && MetadataDbHelper.STATUS_AVAILABLE != status) { // If we have a newer version of this word list, we should be here ONLY if it was
// not installed - else we should be upgrading it.
Log.e(TAG, "Unexpected status for forgetting a word list info : " + status
+ ", removing URL to prevent re-download")
}
if (MetadataDbHelper.STATUS_INSTALLED == status || MetadataDbHelper.STATUS_DISABLED == status || MetadataDbHelper.STATUS_DELETING == status) { // If it is installed or disabled, we need to mark it as deleted so that LatinIME
// will remove it next time it enquires for dictionaries.
// If it is deleting and we don't have a new version, then we have to wait until
// LatinIME actually has deleted it before we can remove its metadata.
// In both cases, remove the URI from the database since it is not supposed to
// be accessible any more.
values.put(MetadataDbHelper.REMOTE_FILENAME_COLUMN, "")
values.put(MetadataDbHelper.STATUS_COLUMN, MetadataDbHelper.STATUS_DELETING)
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion)))
} else { // If it's AVAILABLE or DOWNLOADING or even UNKNOWN, delete the entry.
db.delete(MetadataDbHelper.METADATA_TABLE_NAME,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion)))
}
}
companion object {
val TAG = "DictionaryProvider:" + ForgetAction::class.java.simpleName
}
init {
DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
mHasNewerVersion = hasNewerVersion
}
}
/**
* An action that sets the word list for deletion as soon as possible.
*
* This is triggered when the user requests deletion of a word list. This will mark it as
* deleted in the database, and fire an intent for Android Keyboard to take notice and
* reload its dictionaries right away if it is up. If it is not up now, then it will
* delete the actual file the next time it gets up.
* A file marked as deleted causes the content provider to supply a zero-sized file to
* Android Keyboard, which will overwrite any existing file and provide no words for this
* word list. This is not exactly a "deletion", since there is an actual file which takes up
* a few bytes on the disk, but this allows to override a default dictionary with an empty
* dictionary. This way, there is no need for the user to make a distinction between
* dictionaries installed by default and add-on dictionaries.
*/
class StartDeleteAction(clientId: String, wordlist: WordListMetadata?) : Action {
private val mClientId: String
// The word list to delete. May not be null.
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "StartDeleteAction with a null word list!")
return
}
DebugLogUtils.l("Trying to delete word list : $mWordList")
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)!!
if (null == values) {
Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.")
return
}
val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
if (MetadataDbHelper.STATUS_DISABLED != status) {
Log.e(TAG, "Unexpected status for deleting a word list info : $status")
}
MetadataDbHelper.markEntryAsDeleting(db, mWordList.mId, mWordList.mVersion)
}
companion object {
val TAG = "DictionaryProvider:" + StartDeleteAction::class.java.simpleName
}
init {
DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
}
}
/**
* An action that validates a word list as deleted.
*
* This will restore the word list as available if it still is, or remove the entry if
* it is not any more.
*/
class FinishDeleteAction(clientId: String, wordlist: WordListMetadata?) : Action {
private val mClientId: String
// The word list to delete. May not be null.
val mWordList: WordListMetadata?
override fun execute(context: Context?) {
if (null == mWordList) { // This should never happen
Log.e(TAG, "FinishDeleteAction with a null word list!")
return
}
DebugLogUtils.l("Trying to delete word list : $mWordList")
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, mClientId)
val values: ContentValues = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion)!!
if (null == values) {
Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.")
return
}
val status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
if (MetadataDbHelper.STATUS_DELETING != status) {
Log.e(TAG, "Unexpected status for finish-deleting a word list info : $status")
}
val remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN)
// If there isn't a remote filename any more, then we don't know where to get the file
// from any more, so we remove the entry entirely. As a matter of fact, if the file was
// marked DELETING but disappeared from the metadata on the server, it ended up
// this way.
if (TextUtils.isEmpty(remoteFilename)) {
db.delete(MetadataDbHelper.METADATA_TABLE_NAME,
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
+ MetadataDbHelper.VERSION_COLUMN + " = ?", arrayOf(mWordList.mId, Integer.toString(mWordList.mVersion)))
} else {
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion)
}
}
companion object {
val TAG = "DictionaryProvider:" + FinishDeleteAction::class.java.simpleName
}
init {
DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist)
mClientId = clientId
mWordList = wordlist
}
}
// An action batch consists of an ordered queue of Actions that can execute.
private val mActions: Queue<Action>
fun add(a: Action) {
mActions.add(a)
}
/**
* Append all the actions of another action batch.
* @param that the upgrade to merge into this one.
*/
fun append(that: ActionBatch) {
for (a in that.mActions) {
add(a)
}
}
/**
* Execute this batch.
*
* @param context the context for getting resources, databases, system services.
* @param reporter a Reporter to send errors to.
*/
fun execute(context: Context?, reporter: ProblemReporter?) {
DebugLogUtils.l("Executing a batch of actions")
val remainingActions = mActions
while (!remainingActions.isEmpty()) {
val a = remainingActions.poll()
try {
a.execute(context)
} catch (e: Exception) {
reporter?.report(e)
}
}
}
init {
mActions = LinkedList()
}
}

View file

@ -1,9 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
/**
* Exception thrown when the metadata for the dictionary does not comply to a known format.
*/
class BadFormatException : Exception {
constructor() : super()
constructor(message: String?) : super(message)
}

View file

@ -1,135 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewPropertyAnimator
import android.widget.Button
import android.widget.FrameLayout
import org.dslul.openboard.inputmethod.latin.R
/**
* A view that handles buttons inside it according to a status.
*/
class ButtonSwitcher : FrameLayout {
// One of the above
private var mStatus = NOT_INITIALIZED
private var mAnimateToStatus = NOT_INITIALIZED
private var mInstallButton: Button? = null
private var mCancelButton: Button? = null
private var mDeleteButton: Button? = null
private var mInterfaceState: DictionaryListInterfaceState? = null
private var mOnClickListener: OnClickListener? = null
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context!!, attrs, defStyle)
fun reset(interfaceState: DictionaryListInterfaceState?) {
mStatus = NOT_INITIALIZED
mAnimateToStatus = NOT_INITIALIZED
mInterfaceState = interfaceState
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int,
bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mInstallButton = findViewById<View>(R.id.dict_install_button) as Button
mCancelButton = findViewById<View>(R.id.dict_cancel_button) as Button
mDeleteButton = findViewById<View>(R.id.dict_delete_button) as Button
setInternalOnClickListener(mOnClickListener)
setButtonPositionWithoutAnimation(mStatus)
if (mAnimateToStatus != NOT_INITIALIZED) { // We have been asked to animate before we were ready, so we took a note of it.
// We are now ready: launch the animation.
animateButtonPosition(mStatus, mAnimateToStatus)
mStatus = mAnimateToStatus
mAnimateToStatus = NOT_INITIALIZED
}
}
private fun getButton(status: Int): Button? {
return when (status) {
STATUS_INSTALL -> mInstallButton
STATUS_CANCEL -> mCancelButton
STATUS_DELETE -> mDeleteButton
else -> null
}
}
fun setStatusAndUpdateVisuals(status: Int) {
if (mStatus == NOT_INITIALIZED) {
setButtonPositionWithoutAnimation(status)
mStatus = status
} else {
if (null == mInstallButton) { // We may come here before we have been layout. In this case we don't know our
// size yet so we can't start animations so we need to remember what animation to
// start once layout has gone through.
mAnimateToStatus = status
} else {
animateButtonPosition(mStatus, status)
mStatus = status
}
}
}
private fun setButtonPositionWithoutAnimation(status: Int) { // This may be called by setStatus() before the layout has come yet.
if (null == mInstallButton) return
val width = width
// Set to out of the screen if that's not the currently displayed status
mInstallButton!!.translationX = if (STATUS_INSTALL == status) 0F else width.toFloat()
mCancelButton!!.translationX = if (STATUS_CANCEL == status) 0F else width.toFloat()
mDeleteButton!!.translationX = if (STATUS_DELETE == status) 0F else width.toFloat()
}
// The helper method for {@link AnimatorListenerAdapter}.
fun animateButtonIfStatusIsEqual(newButton: View, newStatus: Int) {
if (newStatus != mStatus) return
animateButton(newButton, ANIMATION_IN)
}
private fun animateButtonPosition(oldStatus: Int, newStatus: Int) {
val oldButton: View? = getButton(oldStatus)
val newButton: View? = getButton(newStatus)
if (null != oldButton && null != newButton) { // Transition between two buttons : animate out, then in
animateButton(oldButton, ANIMATION_OUT).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
animateButtonIfStatusIsEqual(newButton, newStatus)
}
})
} else oldButton?.let { animateButton(it, ANIMATION_OUT) }
?: newButton?.let { animateButton(it, ANIMATION_IN) }
}
fun setInternalOnClickListener(listener: OnClickListener?) {
mOnClickListener = listener
if (null != mInstallButton) { // Already laid out : do it now
mInstallButton!!.setOnClickListener(mOnClickListener)
mCancelButton!!.setOnClickListener(mOnClickListener)
mDeleteButton!!.setOnClickListener(mOnClickListener)
}
}
private fun animateButton(button: View, direction: Int): ViewPropertyAnimator {
val outerX = width.toFloat()
val innerX = button.x - button.translationX
mInterfaceState!!.removeFromCache(parent as View)
if (ANIMATION_IN == direction) {
button.isClickable = true
return button.animate().translationX(0f)
}
button.isClickable = false
return button.animate().translationX(outerX - innerX)
}
companion object {
const val NOT_INITIALIZED = -1
const val STATUS_NO_BUTTON = 0
const val STATUS_INSTALL = 1
const val STATUS_CANCEL = 2
const val STATUS_DELETE = 3
// Animation directions
const val ANIMATION_IN = 1
const val ANIMATION_OUT = 2
}
}

View file

@ -1,23 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.Context
import android.content.SharedPreferences
object CommonPreferences {
private const val COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"
fun getCommonPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0)
}
fun enable(pref: SharedPreferences?, id: String?) {
val editor = pref!!.edit()
editor.putBoolean(id, true)
editor.apply()
}
fun disable(pref: SharedPreferences?, id: String?) {
val editor = pref!!.edit()
editor.putBoolean(id, false)
editor.apply()
}
}

View file

@ -1,64 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.view.View
import java.util.*
/**
* Helper class to maintain the interface state of word list preferences.
*
* This is necessary because the views are created on-demand by calling code. There are many
* situations where views are renewed with little relation with user interaction. For example,
* when scrolling, the view is reused so it doesn't keep its state, which means we need to keep
* it separately. Also whenever the underlying dictionary list undergoes a change (for example,
* update the metadata, or finish downloading) the whole list has to be thrown out and recreated
* in case some dictionaries appeared, disappeared, changed states etc.
*/
class DictionaryListInterfaceState {
internal class State {
var mOpen = false
var mStatus: Int = MetadataDbHelper.Companion.STATUS_UNKNOWN
}
private val mWordlistToState = HashMap<String, State>()
private val mViewCache = ArrayList<View>()
fun isOpen(wordlistId: String?): Boolean {
val state = mWordlistToState[wordlistId] ?: return false
return state.mOpen
}
fun getStatus(wordlistId: String?): Int {
val state = mWordlistToState[wordlistId] ?: return MetadataDbHelper.Companion.STATUS_UNKNOWN
return state.mStatus
}
fun setOpen(wordlistId: String, status: Int) {
val newState: State
val state = mWordlistToState[wordlistId]
newState = state ?: State()
newState.mOpen = true
newState.mStatus = status
mWordlistToState[wordlistId] = newState
}
fun closeAll() {
for (state in mWordlistToState.values) {
state.mOpen = false
}
}
fun findFirstOrphanedView(): View? {
for (v in mViewCache) {
if (null == v.parent) return v
}
return null
}
fun addToCacheAndReturnView(view: View): View {
mViewCache.add(view)
return view
}
fun removeFromCache(view: View?) {
mViewCache.remove(view)
}
}

View file

@ -16,35 +16,14 @@ object DictionaryPackConstants {
/**
* Authority for the ContentProvider protocol.
*/
// TODO: find some way to factorize this string with the one in the resources
// TODO: find some way to factorize this string with the one in the resources
const val AUTHORITY = DICTIONARY_DOMAIN
/**
* The action of the intent for publishing that new dictionary data is available.
*/
// TODO: make this different across different packages. A suggested course of action is
// to use the package name inside this string.
// NOTE: The appended string should be uppercase like all other actions, but it's not for
// historical reasons.
// TODO: make this different across different packages. A suggested course of action is
// to use the package name inside this string.
// NOTE: The appended string should be uppercase like all other actions, but it's not for
// historical reasons.
const val NEW_DICTIONARY_INTENT_ACTION = "$DICTIONARY_DOMAIN.newdict"
/**
* The action of the intent sent by the dictionary pack to ask for a client to make
* itself known. This is used when the settings activity is brought up for a client the
* dictionary pack does not know about.
*/
const val UNKNOWN_DICTIONARY_PROVIDER_CLIENT = (DICTIONARY_DOMAIN
+ ".UNKNOWN_CLIENT")
// In the above intents, the name of the string extra that contains the name of the client
// we want information about.
const val DICTIONARY_PROVIDER_CLIENT_EXTRA = "client"
/**
* The action of the intent to tell the dictionary provider to update now.
*/
const val UPDATE_NOW_INTENT_ACTION = (DICTIONARY_DOMAIN
+ ".UPDATE_NOW")
/**
* The intent action to inform the dictionary provider to initialize the db
* and update now.
*/
const val INIT_AND_UPDATE_NOW_INTENT_ACTION = (DICTIONARY_DOMAIN
+ ".INIT_AND_UPDATE_NOW")
}
}

View file

@ -1,440 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.ContentProvider
import android.content.ContentResolver
import android.content.ContentValues
import android.content.UriMatcher
import android.content.res.AssetFileDescriptor
import android.database.AbstractCursor
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.text.TextUtils
import android.util.Log
import org.dslul.openboard.inputmethod.dictionarypack.ActionBatch.MarkPreInstalledAction
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils
import java.io.FileNotFoundException
import java.util.*
/**
* Provider for dictionaries.
*
* This class is a ContentProvider exposing all available dictionary data as managed by
* the dictionary pack.
*/
@Suppress("deprecation")
class DictionaryProvider : ContentProvider() {
companion object {
private val TAG = DictionaryProvider::class.java.simpleName
const val DEBUG = false
val CONTENT_URI = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + DictionaryPackConstants.AUTHORITY)
private const val QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt"
private const val QUERY_PARAMETER_TRUE = "true"
private const val QUERY_PARAMETER_DELETE_RESULT = "result"
private const val QUERY_PARAMETER_FAILURE = "failure"
const val QUERY_PARAMETER_PROTOCOL_VERSION = "protocol"
private const val NO_MATCH = 0
private const val DICTIONARY_V1_WHOLE_LIST = 1
private const val DICTIONARY_V1_DICT_INFO = 2
private const val DICTIONARY_V2_METADATA = 3
private const val DICTIONARY_V2_WHOLE_LIST = 4
private const val DICTIONARY_V2_DICT_INFO = 5
private const val DICTIONARY_V2_DATAFILE = 6
private val sUriMatcherV1 = UriMatcher(NO_MATCH)
private val sUriMatcherV2 = UriMatcher(NO_MATCH)
// MIME types for dictionary and dictionary list, as required by ContentProvider contract.
const val DICT_LIST_MIME_TYPE = "vnd.android.cursor.item/vnd.google.dictionarylist"
const val DICT_DATAFILE_MIME_TYPE = "vnd.android.cursor.item/vnd.google.dictionary"
const val ID_CATEGORY_SEPARATOR = ":"
private fun matchUri(uri: Uri): Int {
var protocolVersion = 1
val protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION)
if ("2" == protocolVersionArg) protocolVersion = 2
return when (protocolVersion) {
1 -> sUriMatcherV1.match(uri)
2 -> sUriMatcherV2.match(uri)
else -> NO_MATCH
}
}
private fun getClientId(uri: Uri): String? {
var protocolVersion = 1
val protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION)
if ("2" == protocolVersionArg) protocolVersion = 2
return when (protocolVersion) {
1 -> null // In protocol 1, the client ID is always null.
2 -> uri.pathSegments[0]
else -> null
}
}
init {
sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "list", DICTIONARY_V1_WHOLE_LIST)
sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "*", DICTIONARY_V1_DICT_INFO)
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/metadata",
DICTIONARY_V2_METADATA)
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/list", DICTIONARY_V2_WHOLE_LIST)
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/dict/*",
DICTIONARY_V2_DICT_INFO)
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/datafile/*",
DICTIONARY_V2_DATAFILE)
}
}
private class WordListInfo(val mId: String, val mLocale: String, val mRawChecksum: String,
val mMatchLevel: Int)
/**
* A cursor for returning a list of file ids from a List of strings.
*
* This simulates only the necessary methods. It has no error handling to speak of,
* and does not support everything a database does, only a few select necessary methods.
*/
private class ResourcePathCursor(wordLists: Collection<WordListInfo>) : AbstractCursor() {
// The list of word lists served by this provider that match the client request.
val mWordLists: Array<WordListInfo>
override fun getColumnNames(): Array<String> {
return columnNames
}
override fun getCount(): Int {
return mWordLists.size
}
override fun getDouble(column: Int): Double {
return 0.0
}
override fun getFloat(column: Int): Float {
return 0F
}
override fun getInt(column: Int): Int {
return 0
}
override fun getShort(column: Int): Short {
return 0
}
override fun getLong(column: Int): Long {
return 0
}
override fun getString(column: Int): String? {
return when (column) {
0 -> mWordLists[mPos].mId
1 -> mWordLists[mPos].mLocale
2 -> mWordLists[mPos].mRawChecksum
else -> null
}
}
override fun isNull(column: Int): Boolean {
return if (mPos >= mWordLists.size) true else column != 0
}
companion object {
// Column names for the cursor returned by this content provider.
private val columnNames = arrayOf<String>(MetadataDbHelper.WORDLISTID_COLUMN,
MetadataDbHelper.LOCALE_COLUMN, MetadataDbHelper.RAW_CHECKSUM_COLUMN)
}
// Note : the cursor also uses mPos, which is defined in AbstractCursor.
init { // Allocating a 0-size WordListInfo here allows the toArray() method
// to ensure we have a strongly-typed array. It's thrown out. That's
// what the documentation of #toArray says to do in order to get a
// new strongly typed array of the correct size.
mWordLists = wordLists.toTypedArray()
mPos = 0
}
}
override fun onCreate(): Boolean {
return true
}
/**
* Returns the MIME type of the content associated with an Uri
*
* @see android.content.ContentProvider.getType
* @param uri the URI of the content the type of which should be returned.
* @return the MIME type, or null if the URL is not recognized.
*/
override fun getType(uri: Uri): String? {
PrivateLog.log("Asked for type of : $uri")
val match = matchUri(uri)
return when (match) {
NO_MATCH -> null
DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V1_DICT_INFO, DICTIONARY_V2_WHOLE_LIST, DICTIONARY_V2_DICT_INFO -> DICT_LIST_MIME_TYPE
DICTIONARY_V2_DATAFILE -> DICT_DATAFILE_MIME_TYPE
else -> null
}
}
/**
* Query the provider for dictionary files.
*
* This version dispatches the query according to the protocol version found in the
* ?protocol= query parameter. If absent or not well-formed, it defaults to 1.
* @see android.content.ContentProvider.query
* @param uri a content uri (see sUriMatcherV{1,2} at the top of this file for format)
* @param projection ignored. All columns are always returned.
* @param selection ignored.
* @param selectionArgs ignored.
* @param sortOrder ignored. The results are always returned in no particular order.
* @return a cursor matching the uri, or null if the URI was not recognized.
*/
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
DebugLogUtils.l("Uri =", uri)
PrivateLog.log("Query : $uri")
val clientId = getClientId(uri)
val match = matchUri(uri)
return when (match) {
DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V2_WHOLE_LIST -> {
val c: Cursor = MetadataDbHelper.queryDictionaries(context, clientId)
DebugLogUtils.l("List of dictionaries with count", c.count)
PrivateLog.log("Returned a list of " + c.count + " items")
c
}
DICTIONARY_V2_DICT_INFO -> {
// In protocol version 2, we return null if the client is unknown. Otherwise
// we behave exactly like for protocol 1.
if (!MetadataDbHelper.isClientKnown(context, clientId)) return null
val locale = uri.lastPathSegment
val dictFiles = getDictionaryWordListsForLocale(clientId, locale)
// TODO: pass clientId to the following function
if (null != dictFiles && dictFiles.size > 0) {
PrivateLog.log("Returned " + dictFiles.size + " files")
return ResourcePathCursor(dictFiles)
}
PrivateLog.log("No dictionary files for this URL")
ResourcePathCursor(emptyList())
}
DICTIONARY_V1_DICT_INFO -> {
val locale = uri.lastPathSegment
val dictFiles = getDictionaryWordListsForLocale(clientId, locale)
if (null != dictFiles && dictFiles.size > 0) {
PrivateLog.log("Returned " + dictFiles.size + " files")
return ResourcePathCursor(dictFiles)
}
PrivateLog.log("No dictionary files for this URL")
ResourcePathCursor(emptyList())
}
else -> null
}
}
/**
* Helper method to get the wordlist metadata associated with a wordlist ID.
*
* @param clientId the ID of the client
* @param wordlistId the ID of the wordlist for which to get the metadata.
* @return the metadata for this wordlist ID, or null if none could be found.
*/
private fun getWordlistMetadataForWordlistId(clientId: String?,
wordlistId: String?): ContentValues? {
val context = context
if (TextUtils.isEmpty(wordlistId)) return null
val db: SQLiteDatabase = MetadataDbHelper.getDb(context, clientId)
return MetadataDbHelper.getInstalledOrDeletingWordListContentValuesByWordListId(
db, wordlistId)
}
/**
* Opens an asset file for an URI.
*
* Called by [android.content.ContentResolver.openAssetFileDescriptor] or
* [android.content.ContentResolver.openInputStream] from a client requesting a
* dictionary.
* @see android.content.ContentProvider.openAssetFile
* @param uri the URI the file is for.
* @param mode the mode to read the file. MUST be "r" for readonly.
* @return the descriptor, or null if the file is not found or if mode is not equals to "r".
*/
override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor? {
if (null == mode || "r" != mode) return null
val match = matchUri(uri)
if (DICTIONARY_V1_DICT_INFO != match && DICTIONARY_V2_DATAFILE != match) { // Unsupported URI for openAssetFile
Log.w(TAG, "Unsupported URI for openAssetFile : $uri")
return null
}
val wordlistId = uri.lastPathSegment
val clientId = getClientId(uri)
val wordList = getWordlistMetadataForWordlistId(clientId, wordlistId) ?: return null
try {
val status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
if (MetadataDbHelper.STATUS_DELETING == status) { // This will return an empty file (R.raw.empty points at an empty dictionary)
// This is how we "delete" the files. It allows Android Keyboard to fake deleting
// a default dictionary - which is actually in its assets and can't be really
// deleted.
return context!!.resources.openRawResourceFd(
R.raw.empty)
}
val localFilename = wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN)
val f = context!!.getFileStreamPath(localFilename)
val pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY)
return AssetFileDescriptor(pfd, 0, pfd.statSize)
} catch (e: FileNotFoundException) { // No file : fall through and return null
}
return null
}
/**
* Reads the metadata and returns the collection of dictionaries for a given locale.
*
* Word list IDs are expected to be in the form category:manual_id. This method
* will select only one word list for each category: the one with the most specific
* locale matching the locale specified in the URI. The manual id serves only to
* distinguish a word list from another for the purpose of updating, and is arbitrary
* but may not contain a colon.
*
* @param clientId the ID of the client requesting the list
* @param locale the locale for which we want the list, as a String
* @return a collection of ids. It is guaranteed to be non-null, but may be empty.
*/
private fun getDictionaryWordListsForLocale(clientId: String?,
locale: String?): Collection<WordListInfo> {
val context = context
val results: Cursor = MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context,
clientId)
return try {
val dicts = HashMap<String, WordListInfo>()
val idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN)
val localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN)
val localFileNameIndex = results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN)
val rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN)
val statusIndex = results.getColumnIndex(MetadataDbHelper.STATUS_COLUMN)
if (results.moveToFirst()) {
do {
val wordListId = results.getString(idIndex)
if (TextUtils.isEmpty(wordListId)) continue
val wordListIdArray = TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR)
val wordListCategory: String
// This is at the category:manual_id format.
wordListCategory = wordListIdArray[0]
val wordListLocale = results.getString(localeIndex)
val wordListLocalFilename = results.getString(localFileNameIndex)
val wordListRawChecksum = results.getString(rawChecksumIndex)
val wordListStatus = results.getInt(statusIndex)
// Test the requested locale against this wordlist locale. The requested locale
// has to either match exactly or be more specific than the dictionary - a
// dictionary for "en" would match both a request for "en" or for "en_US", but a
// dictionary for "en_GB" would not match a request for "en_US". Thus if all
// three of "en" "en_US" and "en_GB" dictionaries are installed, a request for
// "en_US" would match "en" and "en_US", and a request for "en" only would only
// match the generic "en" dictionary. For more details, see the documentation
// for LocaleUtils#getMatchLevel.
val matchLevel = LocaleUtils.getMatchLevel(wordListLocale, locale)
if (!LocaleUtils.isMatch(matchLevel)) { // The locale of this wordlist does not match the required locale.
// Skip this wordlist and go to the next.
continue
}
if (MetadataDbHelper.STATUS_INSTALLED == wordListStatus) { // If the file does not exist, it has been deleted and the IME should
// already have it. Do not return it. However, this only applies if the
// word list is INSTALLED, for if it is DELETING we should return it always
// so that Android Keyboard can perform the actual deletion.
val f = getContext()!!.getFileStreamPath(wordListLocalFilename)
if (!f.isFile) {
continue
}
}
val currentBestMatch = dicts[wordListCategory]
if (null == currentBestMatch
|| currentBestMatch.mMatchLevel < matchLevel) {
dicts[wordListCategory] = WordListInfo(wordListId, wordListLocale,
wordListRawChecksum, matchLevel)
}
} while (results.moveToNext())
}
Collections.unmodifiableCollection(dicts.values)
} finally {
results.close()
}
}
/**
* Deletes the file pointed by Uri, as returned by openAssetFile.
*
* @param uri the URI the file is for.
* @param selection ignored
* @param selectionArgs ignored
* @return the number of files deleted (0 or 1 in the current implementation)
* @see android.content.ContentProvider.delete
*/
@Throws(UnsupportedOperationException::class)
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val match = matchUri(uri)
if (DICTIONARY_V1_DICT_INFO == match || DICTIONARY_V2_DATAFILE == match) {
return deleteDataFile(uri)
}
return if (DICTIONARY_V2_METADATA == match) {
if (MetadataDbHelper.deleteClient(context, getClientId(uri))) {
1
} else 0
} else 0
// Unsupported URI for delete
}
private fun deleteDataFile(uri: Uri): Int {
val wordlistId = uri.lastPathSegment
val clientId = getClientId(uri)
val wordList = getWordlistMetadataForWordlistId(clientId, wordlistId) ?: return 0
val status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN)
val version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN)
return 0
}
/**
* Insert data into the provider. May be either a metadata source URL or some dictionary info.
*
* @param uri the designated content URI. See sUriMatcherV{1,2} for available URIs.
* @param values the values to insert for this content uri
* @return the URI for the newly inserted item. May be null if arguments don't allow for insert
*/
@Throws(UnsupportedOperationException::class)
override fun insert(uri: Uri, values: ContentValues?): Uri? {
if (null == values) return null // Should never happen but let's be safe
PrivateLog.log("Insert, uri = $uri")
val clientId = getClientId(uri)
when (matchUri(uri)) {
DICTIONARY_V2_METADATA -> // The values should contain a valid client ID and a valid URI for the metadata.
// The client ID may not be null, nor may it be empty because the empty client ID
// is reserved for internal use.
// The metadata URI may not be null, but it may be empty if the client does not
// want the dictionary pack to update the metadata automatically.
MetadataDbHelper.updateClientInfo(context, clientId, values)
DICTIONARY_V2_DICT_INFO -> try {
val newDictionaryMetadata: WordListMetadata = WordListMetadata.createFromContentValues(
MetadataDbHelper.completeWithDefaultValues(values))
MarkPreInstalledAction(clientId, newDictionaryMetadata)
.execute(context)
} catch (e: BadFormatException) {
Log.w(TAG, "Not enough information to insert this dictionary $values", e)
}
DICTIONARY_V1_WHOLE_LIST, DICTIONARY_V1_DICT_INFO -> {
PrivateLog.log("Attempt to insert : $uri")
throw UnsupportedOperationException(
"Insertion in the dictionary is not supported in this version")
}
}
return uri
}
/**
* Updating data is not supported, and will throw an exception.
* @see android.content.ContentProvider.update
* @see android.content.ContentProvider.insert
*/
@Throws(UnsupportedOperationException::class)
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
PrivateLog.log("Attempt to update : $uri")
throw UnsupportedOperationException("Updating dictionary words is not supported")
}
}

View file

@ -1,224 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.app.Activity
import android.content.ContentResolver
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Bundle
import android.preference.Preference
import android.preference.PreferenceFragment
import android.preference.PreferenceGroup
import android.util.Log
import android.view.*
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import java.util.*
/**
* Preference screen.
*/
// keep the fragment for now, as it may contain something useful on dictionary management, but
// suppress warnings
// todo: check this fragment, take what could be useful, and remove the fragment
// same for WordListPreference and WordListMetadata
@Suppress("deprecation")
class DictionarySettingsFragment
/**
* Empty constructor for fragment generation.
*/
: PreferenceFragment() {
private var mLoadingView: View? = null
private var mClientId: String? = null
//private ConnectivityManager mConnectivityManager;
private val mUpdateNowMenu: MenuItem? = null
private var mChangedSettings = false
private val mDictionaryListInterfaceState = DictionaryListInterfaceState()
// never null
private var mCurrentPreferenceMap = TreeMap<String, WordListPreference>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.loading_page, container, true)
mLoadingView = v.findViewById(R.id.loading_container)
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val activity = activity
mClientId = activity.intent.getStringExtra(DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT)
/*mConnectivityManager =
(ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE);*/addPreferencesFromResource(R.xml.dictionary_settings)
refreshInterface()
setHasOptionsMenu(true)
}
override fun onResume() {
super.onResume()
mChangedSettings = false
val activity = activity
val filter = IntentFilter()
object : Thread("onResume") {
override fun run() {
if (!MetadataDbHelper.Companion.isClientKnown(activity, mClientId)) {
Log.i(TAG, "Unknown dictionary pack client: " + mClientId
+ ". Requesting info.")
val unknownClientBroadcast = Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT)
unknownClientBroadcast.putExtra(
DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId)
activity.sendBroadcast(unknownClientBroadcast)
}
}
}.start()
}
override fun onPause() {
super.onPause()
val activity = activity
if (mChangedSettings) {
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
activity.sendBroadcast(newDictBroadcast)
mChangedSettings = false
}
}
private fun findWordListPreference(id: String): WordListPreference? {
val prefScreen: PreferenceGroup? = preferenceScreen
if (null == prefScreen) {
Log.e(TAG, "Could not find the preference group")
return null
}
for (i in prefScreen.preferenceCount - 1 downTo 0) {
val pref = prefScreen.getPreference(i)
if (pref is WordListPreference) {
val wlPref = pref
if (id == wlPref.mWordlistId) {
return wlPref
}
}
}
Log.e(TAG, "Could not find the preference for a word list id $id")
return null
}
fun refreshInterface() {
val activity = activity ?: return
val prefScreen: PreferenceGroup = preferenceScreen
val prefList = createInstalledDictSettingsCollection(mClientId)
activity.runOnUiThread {
// TODO: display this somewhere
// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
removeAnyDictSettings(prefScreen)
var i = 0
for (preference in prefList) {
preference.order = i++
prefScreen.addPreference(preference)
}
}
}
/**
* Creates a WordListPreference list to be added to the screen.
*
* This method only creates the preferences but does not add them.
* Thus, it can be called on another thread.
*
* @param clientId the id of the client for which we want to display the dictionary list
* @return A collection of preferences ready to add to the interface.
*/
private fun createInstalledDictSettingsCollection(
clientId: String?): Collection<Preference> { // This will directly contact the DictionaryProvider and request the list exactly like
// any regular client would do.
// Considering the respective value of the respective constants used here for each path,
// segment, the url generated by this is of the form (assuming "clientId" as a clientId)
// content://org.dslul.openboard.inputmethod.latin.dictionarypack/clientId/list?procotol=2
val contentUri = Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(getString(R.string.authority))
.appendPath(clientId)
.appendPath(DICT_LIST_ID) // Need to use version 2 to get this client's list
.appendQueryParameter(DictionaryProvider.Companion.QUERY_PARAMETER_PROTOCOL_VERSION, "2")
.build()
val activity = activity
val cursor = activity?.contentResolver?.query(contentUri, null, null, null, null)
if (null == cursor) {
val result = ArrayList<Preference>()
result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service))
return result
}
return try {
if (!cursor.moveToFirst()) {
val result = ArrayList<Preference>()
result.add(createErrorMessage(activity, R.string.no_dictionaries_available))
return result
}
val systemLocaleString = Locale.getDefault().toString()
val prefMap = TreeMap<String, WordListPreference>()
val idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN)
val versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN)
val localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN)
val descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN)
val statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN)
val filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN)
do {
val wordlistId = cursor.getString(idIndex)
val version = cursor.getInt(versionIndex)
val localeString = cursor.getString(localeIndex)
val locale = Locale(localeString)
val description = cursor.getString(descriptionIndex)
val status = cursor.getInt(statusIndex)
val matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString)
val matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel)
val filesize = cursor.getInt(filesizeIndex)
// The key is sorted in lexicographic order, according to the match level, then
// the description.
val key = "$matchLevelString.$description.$wordlistId"
val existingPref = prefMap[key]
if (null == existingPref || existingPref.hasPriorityOver(status)) {
val oldPreference = mCurrentPreferenceMap[key]
val pref: WordListPreference
pref = if (null != oldPreference && oldPreference.mVersion == version && oldPreference.hasStatus(status)
&& oldPreference.mLocale == locale) { // If the old preference has all the new attributes, reuse it. Ideally,
// we should reuse the old pref even if its status is different and call
// setStatus here, but setStatus calls Preference#setSummary() which
// needs to be done on the UI thread and we're not on the UI thread
// here. We could do all this work on the UI thread, but in this case
// it's probably lighter to stay on a background thread and throw this
// old preference out.
oldPreference
} else { // Otherwise, discard it and create a new one instead.
// TODO: when the status is different from the old one, we need to
// animate the old one out before animating the new one in.
WordListPreference(activity, mDictionaryListInterfaceState,
mClientId, wordlistId, version, locale, description, status,
filesize)
}
prefMap[key] = pref
}
} while (cursor.moveToNext())
mCurrentPreferenceMap = prefMap
prefMap.values
} finally {
cursor.close()
}
}
companion object {
private val TAG = DictionarySettingsFragment::class.java.simpleName
private const val DICT_LIST_ID = "list"
const val DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT = "clientId"
// private const val MENU_UPDATE_NOW = Menu.FIRST
private fun createErrorMessage(activity: Activity?, messageResource: Int): Preference {
val message = Preference(activity)
message.setTitle(messageResource)
message.isEnabled = false
return message
}
fun removeAnyDictSettings(prefGroup: PreferenceGroup) {
for (i in prefGroup.preferenceCount - 1 downTo 0) {
prefGroup.removePreference(prefGroup.getPreference(i))
}
}
}
}

View file

@ -1,13 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.util.Log
/**
* A very simple problem reporter.
*/
internal class LogProblemReporter(private val TAG: String) : ProblemReporter {
override fun report(e: Exception?) {
Log.e(TAG, "Reporting problem", e)
}
}

View file

@ -1,935 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import android.text.TextUtils
import android.util.Log
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils
import java.io.File
import java.util.*
/**
* Various helper functions for the state database
*/
class MetadataDbHelper private constructor(private val mContext: Context?, private val mClientId: String) : SQLiteOpenHelper(mContext,
METADATA_DATABASE_NAME_STEM + if (TextUtils.isEmpty(mClientId)) "" else ".$mClientId",
null, CURRENT_METADATA_DATABASE_VERSION) {
private fun createClientTable(db: SQLiteDatabase) { // The clients table only exists in the primary db, the one that has an empty client id
if (!TextUtils.isEmpty(mClientId)) return
db.execSQL(METADATA_CREATE_CLIENT_TABLE)
val defaultMetadataUri = mContext!!.getString(R.string.default_metadata_uri)
if (!TextUtils.isEmpty(defaultMetadataUri)) {
val defaultMetadataValues = ContentValues()
defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "")
defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri)
db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues)
}
}
/**
* Create the table and populate it with the resources found inside the apk.
*
* @see SQLiteOpenHelper.onCreate
* @param db the database to create and populate.
*/
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(METADATA_TABLE_CREATE)
createClientTable(db)
}
/**
* Upgrade the database. Upgrade from version 3 is supported.
* Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME.
* Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a
* table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the
* name of the client and contains a table METADATA_TABLE_NAME.
* For schemas, see the above create statements. The schemas have never changed so far.
*
* This method is called by the framework. See [SQLiteOpenHelper.onUpgrade]
* @param db The database we are upgrading
* @param oldVersion The old database version (the one on the disk)
* @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper
*/
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (METADATA_DATABASE_INITIAL_VERSION == oldVersion && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { // Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version
// METADATA_DATABASE_VERSION_WITH_CLIENT_ID
// Only the default database should contain the client table, so we test for mClientId.
if (TextUtils.isEmpty(mClientId)) { // Anyway in version 3 only the default table existed so the emptiness
// test should always be true, but better check to be sure.
createClientTable(db)
}
} else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion
&& CURRENT_METADATA_DATABASE_VERSION >= newVersion) { // Here we drop the client table, so that all clients send us their information again.
// The client table contains the URL to hit to update the available dictionaries list,
// but the info about the dictionaries themselves is stored in the table called
// METADATA_TABLE_NAME and we want to keep it, so we only drop the client table.
db.execSQL("DROP TABLE IF EXISTS $CLIENT_TABLE_NAME")
// Only the default database should contain the client table, so we test for mClientId.
if (TextUtils.isEmpty(mClientId)) {
createClientTable(db)
}
} else { // If we're not in the above case, either we are upgrading from an earlier versionCode
// and we should wipe the database, or we are handling a version we never heard about
// (can only be a bug) so it's safer to wipe the database.
db.execSQL("DROP TABLE IF EXISTS $METADATA_TABLE_NAME")
db.execSQL("DROP TABLE IF EXISTS $CLIENT_TABLE_NAME")
onCreate(db)
}
// A rawChecksum column that did not exist in the previous versions was added that
// corresponds to the md5 checksum of the file after decompression/decryption. This is to
// strengthen the system against corrupted dictionary files.
// The most secure way to upgrade a database is to just test for the column presence, and
// add it if it's not there.
addRawChecksumColumnUnlessPresent(db)
// A retry count column that did not exist in the previous versions was added that
// corresponds to the number of download & installation attempts that have been made
// in order to strengthen the system recovery from corrupted dictionary files.
// The most secure way to upgrade a database is to just test for the column presence, and
// add it if it's not there.
addRetryCountColumnUnlessPresent(db)
}
/**
* Downgrade the database. This drops and recreates the table in all cases.
*/
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { // No matter what the numerical values of oldVersion and newVersion are, we know this
// is a downgrade (newVersion < oldVersion). There is no way to know what the future
// databases will look like, but we know it's extremely likely that it's okay to just
// drop the tables and start from scratch. Hence, we ignore the versions and just wipe
// everything we want to use.
if (oldVersion <= newVersion) {
Log.e(TAG, "onDowngrade database but new version is higher? " + oldVersion + " <= "
+ newVersion)
}
db.execSQL("DROP TABLE IF EXISTS $METADATA_TABLE_NAME")
db.execSQL("DROP TABLE IF EXISTS $CLIENT_TABLE_NAME")
onCreate(db)
}
companion object {
private val TAG = MetadataDbHelper::class.java.simpleName
// This was the initial release version of the database. It should never be
// changed going forward.
private const val METADATA_DATABASE_INITIAL_VERSION = 3
// This is the first released version of the database that implements CLIENTID. It is
// used to identify the versions for upgrades. This should never change going forward.
private const val METADATA_DATABASE_VERSION_WITH_CLIENTID = 6
// The current database version.
// This MUST be increased every time the dictionary pack metadata URL changes.
private const val CURRENT_METADATA_DATABASE_VERSION = 16
private const val NOT_A_DOWNLOAD_ID: Long = -1
// The number of retries allowed when attempting to download a broken dictionary.
const val DICTIONARY_RETRY_THRESHOLD = 2
const val METADATA_TABLE_NAME = "pendingUpdates"
const val CLIENT_TABLE_NAME = "clients"
const val PENDINGID_COLUMN = "pendingid" // Download Manager ID
const val TYPE_COLUMN = "type"
const val STATUS_COLUMN = "status"
const val LOCALE_COLUMN = "locale"
const val WORDLISTID_COLUMN = "id"
const val DESCRIPTION_COLUMN = "description"
const val LOCAL_FILENAME_COLUMN = "filename"
const val REMOTE_FILENAME_COLUMN = "url"
const val DATE_COLUMN = "date"
const val CHECKSUM_COLUMN = "checksum"
const val FILESIZE_COLUMN = "filesize"
const val VERSION_COLUMN = "version"
const val FORMATVERSION_COLUMN = "formatversion"
const val FLAGS_COLUMN = "flags"
const val RAW_CHECKSUM_COLUMN = "rawChecksum"
const val RETRY_COUNT_COLUMN = "remainingRetries"
const val COLUMN_COUNT = 15
private const val CLIENT_CLIENT_ID_COLUMN = "clientid"
private const val CLIENT_METADATA_URI_COLUMN = "uri"
private const val CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid"
private const val CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate"
private const val CLIENT_PENDINGID_COLUMN = "pendingid" // Download Manager ID
const val METADATA_DATABASE_NAME_STEM = "pendingUpdates"
const val METADATA_UPDATE_DESCRIPTION = "metadata"
const val DICTIONARIES_ASSETS_PATH = "dictionaries"
// Statuses, for storing in the STATUS_COLUMN
// IMPORTANT: The following are used as index arrays in ../WordListPreference
// Do not change their values without updating the matched code.
// Unknown status: this should never happen.
const val STATUS_UNKNOWN = 0
// Available: this word list is available, but it is not downloaded (not downloading), because
// it is set not to be used.
const val STATUS_AVAILABLE = 1
// Downloading: this word list is being downloaded.
const val STATUS_DOWNLOADING = 2
// Installed: this word list is installed and usable.
const val STATUS_INSTALLED = 3
// Disabled: this word list is installed, but has been disabled by the user.
const val STATUS_DISABLED = 4
// Deleting: the user marked this word list to be deleted, but it has not been yet because
// Latin IME is not up yet.
const val STATUS_DELETING = 5
// Retry: dictionary got corrupted, so an attempt must be done to download & install it again.
const val STATUS_RETRYING = 6
// Types, for storing in the TYPE_COLUMN
// This is metadata about what is available.
const val TYPE_METADATA = 1
// This is a bulk file. It should replace older files.
const val TYPE_BULK = 2
// This is an incremental update, expected to be small, and meaningless on its own.
const val TYPE_UPDATE = 3
private const val METADATA_TABLE_CREATE = ("CREATE TABLE " + METADATA_TABLE_NAME + " ("
+ PENDINGID_COLUMN + " INTEGER, "
+ TYPE_COLUMN + " INTEGER, "
+ STATUS_COLUMN + " INTEGER, "
+ WORDLISTID_COLUMN + " TEXT, "
+ LOCALE_COLUMN + " TEXT, "
+ DESCRIPTION_COLUMN + " TEXT, "
+ LOCAL_FILENAME_COLUMN + " TEXT, "
+ REMOTE_FILENAME_COLUMN + " TEXT, "
+ DATE_COLUMN + " INTEGER, "
+ CHECKSUM_COLUMN + " TEXT, "
+ FILESIZE_COLUMN + " INTEGER, "
+ VERSION_COLUMN + " INTEGER,"
+ FORMATVERSION_COLUMN + " INTEGER, "
+ FLAGS_COLUMN + " INTEGER, "
+ RAW_CHECKSUM_COLUMN + " TEXT,"
+ RETRY_COUNT_COLUMN + " INTEGER, "
+ "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));")
private const val METADATA_CREATE_CLIENT_TABLE = ("CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " ("
+ CLIENT_CLIENT_ID_COLUMN + " TEXT, "
+ CLIENT_METADATA_URI_COLUMN + " TEXT, "
+ CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, "
+ CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, "
+ CLIENT_PENDINGID_COLUMN + " INTEGER, "
+ FLAGS_COLUMN + " INTEGER, "
+ "PRIMARY KEY (" + CLIENT_CLIENT_ID_COLUMN + "));")
// List of all metadata table columns.
val METADATA_TABLE_COLUMNS = arrayOf(PENDINGID_COLUMN, TYPE_COLUMN,
STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN,
LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN,
FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN,
RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN)
// List of all client table columns.
val CLIENT_TABLE_COLUMNS = arrayOf(CLIENT_CLIENT_ID_COLUMN,
CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN)
// List of public columns returned to clients. Everything that is not in this list is
// private and implementation-dependent.
val DICTIONARIES_LIST_PUBLIC_COLUMNS = arrayOf(STATUS_COLUMN, WORDLISTID_COLUMN,
LOCALE_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN)
// This class exhibits a singleton-like behavior by client ID, so it is getInstance'd
// and has a private c'tor.
private var sInstanceMap: TreeMap<String, MetadataDbHelper>? = null
@Synchronized
fun getInstance(context: Context?,
clientIdOrNull: String?): MetadataDbHelper { // As a backward compatibility feature, null can be passed here to retrieve the "default"
// database. Before multi-client support, the dictionary packed used only one database
// and would not be able to handle several dictionary sets. Passing null here retrieves
// this legacy database. New clients should make sure to always pass a client ID so as
// to avoid conflicts.
val clientId = clientIdOrNull ?: ""
if (null == sInstanceMap) sInstanceMap = TreeMap()
var helper = sInstanceMap!![clientId]
if (null == helper) {
helper = MetadataDbHelper(context, clientId)
sInstanceMap!![clientId] = helper
}
return helper
}
/**
* Get the database itself. This always returns the same object for any client ID. If the
* client ID is null, a default database is returned for backward compatibility. Don't
* pass null for new calls.
*
* @param context the context to create the database from. This is ignored after the first call.
* @param clientId the client id to retrieve the database of. null for default (deprecated)
* @return the database.
*/
fun getDb(context: Context?, clientId: String?): SQLiteDatabase {
return getInstance(context, clientId).writableDatabase
}
private fun addRawChecksumColumnUnlessPresent(db: SQLiteDatabase) {
try {
db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM "
+ METADATA_TABLE_NAME + " LIMIT 0;")
} catch (e: SQLiteException) {
Log.i(TAG, "No $RAW_CHECKSUM_COLUMN column : creating it")
db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN "
+ RAW_CHECKSUM_COLUMN + " TEXT;")
}
}
private fun addRetryCountColumnUnlessPresent(db: SQLiteDatabase) {
try {
db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM "
+ METADATA_TABLE_NAME + " LIMIT 0;")
} catch (e: SQLiteException) {
Log.i(TAG, "No $RETRY_COUNT_COLUMN column : creating it")
db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN "
+ RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";")
}
}
/**
* Given a client ID, returns whether this client exists.
*
* @param context a context to open the database
* @param clientId the client ID to check
* @return true if the client is known, false otherwise
*/
fun isClientKnown(context: Context?, clientId: String?): Boolean { // If the client is known, they'll have a non-null metadata URI. An empty string is
// allowed as a metadata URI, if the client doesn't want any updates to happen.
return null != getMetadataUriAsString(context, clientId)
}
/**
* Returns the metadata URI as a string.
*
* If the client is not known, this will return null. If it is known, it will return
* the URI as a string. Note that the empty string is a valid value.
*
* @param context a context instance to open the database on
* @param clientId the ID of the client we want the metadata URI of
* @return the string representation of the URI
*/
fun getMetadataUriAsString(context: Context?, clientId: String?): String? {
val defaultDb = getDb(context, null)
val cursor = defaultDb.query(CLIENT_TABLE_NAME, arrayOf(CLIENT_METADATA_URI_COLUMN),
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId),
null, null, null, null)
return try {
if (!cursor.moveToFirst()) null else MetadataUriGetter.getUri(context, cursor.getString(0))
} finally {
cursor.close()
}
}
/**
* Update the last metadata update time for all clients using a particular URI.
*
* This method searches for all clients using a particular URI and updates the last
* update time for this client.
* The current time is used as the latest update time. This saved date will be what
* is returned henceforth by [.getLastUpdateDateForClient],
* until this method is called again.
*
* @param context a context instance to open the database on
* @param uri the metadata URI we just downloaded
*/
fun saveLastUpdateTimeOfUri(context: Context?, uri: String) {
PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis())
val values = ContentValues()
values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis())
val defaultDb = getDb(context, null)
val cursor = queryClientIds(context)
try {
if (!cursor.moveToFirst()) return
do {
val clientId = cursor.getString(0)
val metadataUri = getMetadataUriAsString(context, clientId)
if (metadataUri == uri) {
defaultDb.update(CLIENT_TABLE_NAME, values,
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId))
}
} while (cursor.moveToNext())
} finally {
cursor.close()
}
}
/**
* Retrieves the last date at which we updated the metadata for this client.
*
* The returned date is in milliseconds from the EPOCH; this is the same unit as
* returned by [System.currentTimeMillis].
*
* @param context a context instance to open the database on
* @param clientId the client ID to get the latest update date of
* @return the last date at which this client was updated, as a long.
*/
fun getLastUpdateDateForClient(context: Context?, clientId: String?): Long {
val defaultDb = getDb(context, null)
val cursor = defaultDb.query(CLIENT_TABLE_NAME, arrayOf(CLIENT_LAST_UPDATE_DATE_COLUMN),
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId ?: ""),
null, null, null, null)
return try {
if (!cursor.moveToFirst()) 0 else cursor.getLong(0)
// Only one column, return it
} finally {
cursor.close()
}
}
fun getOldestUpdateTime(context: Context?): Long {
val defaultDb = getDb(context, null)
val cursor = defaultDb.query(CLIENT_TABLE_NAME, arrayOf(CLIENT_LAST_UPDATE_DATE_COLUMN),
null, null, null, null, null)
return try {
if (!cursor.moveToFirst()) return 0
val columnIndex = 0 // Only one column queried
// Initialize the earliestTime to the largest possible value.
var earliestTime = Long.MAX_VALUE // Almost 300 million years in the future
do {
val thisTime = cursor.getLong(columnIndex)
earliestTime = Math.min(thisTime, earliestTime)
} while (cursor.moveToNext())
earliestTime
} finally {
cursor.close()
}
}
/**
* Helper method to make content values to write into the database.
* @return content values with all the arguments put with the right column names.
*/
fun makeContentValues(pendingId: Int, type: Int,
status: Int, wordlistId: String?, locale: String?,
description: String?, filename: String?, url: String?, date: Long,
rawChecksum: String?, checksum: String?, retryCount: Int,
filesize: Long, version: Int, formatVersion: Int): ContentValues {
val result = ContentValues(COLUMN_COUNT)
result.put(PENDINGID_COLUMN, pendingId)
result.put(TYPE_COLUMN, type)
result.put(WORDLISTID_COLUMN, wordlistId)
result.put(STATUS_COLUMN, status)
result.put(LOCALE_COLUMN, locale)
result.put(DESCRIPTION_COLUMN, description)
result.put(LOCAL_FILENAME_COLUMN, filename)
result.put(REMOTE_FILENAME_COLUMN, url)
result.put(DATE_COLUMN, date)
result.put(RAW_CHECKSUM_COLUMN, rawChecksum)
result.put(RETRY_COUNT_COLUMN, retryCount)
result.put(CHECKSUM_COLUMN, checksum)
result.put(FILESIZE_COLUMN, filesize)
result.put(VERSION_COLUMN, version)
result.put(FORMATVERSION_COLUMN, formatVersion)
result.put(FLAGS_COLUMN, 0)
return result
}
/**
* Helper method to fill in an incomplete ContentValues with default values.
* A wordlist ID and a locale are required, otherwise BadFormatException is thrown.
* @return the same object that was passed in, completed with default values.
*/
@Throws(BadFormatException::class)
fun completeWithDefaultValues(result: ContentValues): ContentValues {
if (null == result[WORDLISTID_COLUMN] || null == result[LOCALE_COLUMN]) {
throw BadFormatException()
}
// 0 for the pending id, because there is none
if (null == result[PENDINGID_COLUMN]) result.put(PENDINGID_COLUMN, 0)
// This is a binary blob of a dictionary
if (null == result[TYPE_COLUMN]) result.put(TYPE_COLUMN, TYPE_BULK)
// This word list is unknown, but it's present, else we wouldn't be here, so INSTALLED
if (null == result[STATUS_COLUMN]) result.put(STATUS_COLUMN, STATUS_INSTALLED)
// No description unless specified, because we can't guess it
if (null == result[DESCRIPTION_COLUMN]) result.put(DESCRIPTION_COLUMN, "")
// File name - this is an asset, so it works as an already deleted file.
// hence, we need to supply a non-existent file name. Anything will
// do as long as it returns false when tested with File#exist(), and
// the empty string does not, so it's set to "_".
if (null == result[LOCAL_FILENAME_COLUMN]) result.put(LOCAL_FILENAME_COLUMN, "_")
// No remote file name : this can't be downloaded. Unless specified.
if (null == result[REMOTE_FILENAME_COLUMN]) result.put(REMOTE_FILENAME_COLUMN, "")
// 0 for the update date : 1970/1/1. Unless specified.
if (null == result[DATE_COLUMN]) result.put(DATE_COLUMN, 0)
// Raw checksum unknown unless specified
if (null == result[RAW_CHECKSUM_COLUMN]) result.put(RAW_CHECKSUM_COLUMN, "")
// Retry column 0 unless specified
if (null == result[RETRY_COUNT_COLUMN]) result.put(RETRY_COUNT_COLUMN,
DICTIONARY_RETRY_THRESHOLD)
// Checksum unknown unless specified
if (null == result[CHECKSUM_COLUMN]) result.put(CHECKSUM_COLUMN, "")
// No filesize unless specified
if (null == result[FILESIZE_COLUMN]) result.put(FILESIZE_COLUMN, 0)
// Smallest possible version unless specified
if (null == result[VERSION_COLUMN]) result.put(VERSION_COLUMN, 1)
// No flags unless specified
if (null == result[FLAGS_COLUMN]) result.put(FLAGS_COLUMN, 0)
return result
}
/**
* Reads a column in a Cursor as a String and stores it in a ContentValues object.
* @param result the ContentValues object to store the result in.
* @param cursor the Cursor to read the column from.
* @param columnId the column ID to read.
*/
private fun putStringResult(result: ContentValues, cursor: Cursor, columnId: String) {
result.put(columnId, cursor.getString(cursor.getColumnIndex(columnId)))
}
/**
* Reads a column in a Cursor as an int and stores it in a ContentValues object.
* @param result the ContentValues object to store the result in.
* @param cursor the Cursor to read the column from.
* @param columnId the column ID to read.
*/
private fun putIntResult(result: ContentValues, cursor: Cursor, columnId: String) {
result.put(columnId, cursor.getInt(cursor.getColumnIndex(columnId)))
}
private fun getFirstLineAsContentValues(cursor: Cursor): ContentValues? {
val result: ContentValues?
if (cursor.moveToFirst()) {
result = ContentValues(COLUMN_COUNT)
putIntResult(result, cursor, PENDINGID_COLUMN)
putIntResult(result, cursor, TYPE_COLUMN)
putIntResult(result, cursor, STATUS_COLUMN)
putStringResult(result, cursor, WORDLISTID_COLUMN)
putStringResult(result, cursor, LOCALE_COLUMN)
putStringResult(result, cursor, DESCRIPTION_COLUMN)
putStringResult(result, cursor, LOCAL_FILENAME_COLUMN)
putStringResult(result, cursor, REMOTE_FILENAME_COLUMN)
putIntResult(result, cursor, DATE_COLUMN)
putStringResult(result, cursor, RAW_CHECKSUM_COLUMN)
putStringResult(result, cursor, CHECKSUM_COLUMN)
putIntResult(result, cursor, RETRY_COUNT_COLUMN)
putIntResult(result, cursor, FILESIZE_COLUMN)
putIntResult(result, cursor, VERSION_COLUMN)
putIntResult(result, cursor, FORMATVERSION_COLUMN)
putIntResult(result, cursor, FLAGS_COLUMN)
if (cursor.moveToNext()) { // TODO: print the second level of the stack to the log so that we know
// in which code path the error happened
Log.e(TAG, "Several SQL results when we expected only one!")
}
} else {
result = null
}
return result
}
/**
* Gets the info about as specific download, indexed by its DownloadManager ID.
* @param db the database to get the information from.
* @param id the DownloadManager id.
* @return metadata about this download. This returns all columns in the database.
*/
fun getContentValuesByPendingId(db: SQLiteDatabase,
id: Long): ContentValues? {
val cursor = db.query(METADATA_TABLE_NAME,
METADATA_TABLE_COLUMNS,
"$PENDINGID_COLUMN= ?", arrayOf(java.lang.Long.toString(id)),
null, null, null)
?: return null
return try { // There should never be more than one result. If because of some bug there are,
// returning only one result is the right thing to do, because we couldn't handle
// several anyway and we should still handle one.
getFirstLineAsContentValues(cursor)
} finally {
cursor.close()
}
}
/**
* Gets the info about an installed OR deleting word list with a specified id.
*
* Basically, this is the word list that we want to return to Android Keyboard when
* it asks for a specific id.
*
* @param db the database to get the information from.
* @param id the word list ID.
* @return the metadata about this word list.
*/
fun getInstalledOrDeletingWordListContentValuesByWordListId(
db: SQLiteDatabase, id: String?): ContentValues? {
val cursor = db.query(METADATA_TABLE_NAME,
METADATA_TABLE_COLUMNS,
"$WORDLISTID_COLUMN=? AND ($STATUS_COLUMN=? OR $STATUS_COLUMN=?)", arrayOf(id, Integer.toString(STATUS_INSTALLED),
Integer.toString(STATUS_DELETING)),
null, null, null)
?: return null
return try { // There should only be one result, but if there are several, we can't tell which
// is the best, so we just return the first one.
getFirstLineAsContentValues(cursor)
} finally {
cursor.close()
}
}
/**
* Gets the info about a specific word list.
*
* @param db the database to get the information from.
* @param id the word list ID.
* @param version the word list version.
* @return the metadata about this word list.
*/
fun getContentValuesByWordListId(db: SQLiteDatabase,
id: String?, version: Int): ContentValues? {
val cursor = db.query(METADATA_TABLE_NAME,
METADATA_TABLE_COLUMNS,
WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND "
+ FORMATVERSION_COLUMN + "<= ?", arrayOf(id,
Integer.toString(version),
Integer.toString(version)
),
null /* groupBy */,
null /* having */,
"$FORMATVERSION_COLUMN DESC" /* orderBy */)
?: return null
return try { // This is a lookup by primary key, so there can't be more than one result.
getFirstLineAsContentValues(cursor)
} finally {
cursor.close()
}
}
/**
* Gets the info about the latest word list with an id.
*
* @param db the database to get the information from.
* @param id the word list ID.
* @return the metadata about the word list with this id and the latest version number.
*/
fun getContentValuesOfLatestAvailableWordlistById(
db: SQLiteDatabase, id: String): ContentValues? {
val cursor = db.query(METADATA_TABLE_NAME,
METADATA_TABLE_COLUMNS,
"$WORDLISTID_COLUMN= ?", arrayOf(id), null, null, "$VERSION_COLUMN DESC", "1")
?: return null
return try { // Return the first result from the list of results.
getFirstLineAsContentValues(cursor)
} finally {
cursor.close()
}
}
/**
* Gets the current metadata about INSTALLED, AVAILABLE or DELETING dictionaries.
*
* This odd method is tailored to the needs of
* DictionaryProvider#getDictionaryWordListsForContentUri, which needs the word list if
* it is:
* - INSTALLED: this should be returned to LatinIME if the file is still inside the dictionary
* pack, so that it can be copied. If the file is not there, it's been copied already and should
* not be returned, so getDictionaryWordListsForContentUri takes care of this.
* - DELETING: this should be returned to LatinIME so that it can actually delete the file.
* - AVAILABLE: this should not be returned, but should be checked for auto-installation.
*
* @param context the context for getting the database.
* @param clientId the client id for retrieving the database. null for default (deprecated)
* @return a cursor with metadata about usable dictionaries.
*/
fun queryInstalledOrDeletingOrAvailableDictionaryMetadata(
context: Context?, clientId: String?): Cursor { // If clientId is null, we get the defaut DB (see #getInstance() for more about this)
return getDb(context, clientId).query(METADATA_TABLE_NAME,
METADATA_TABLE_COLUMNS,
"$STATUS_COLUMN = ? OR $STATUS_COLUMN = ? OR $STATUS_COLUMN = ?", arrayOf(Integer.toString(STATUS_INSTALLED),
Integer.toString(STATUS_DELETING),
Integer.toString(STATUS_AVAILABLE)),
null, null, LOCALE_COLUMN)
}
/**
* Gets the current metadata about all dictionaries.
*
* This will retrieve the metadata about all dictionaries, including
* older files, or files not yet downloaded.
*
* @param context the context for getting the database.
* @param clientId the client id for retrieving the database. null for default (deprecated)
* @return a cursor with metadata about usable dictionaries.
*/
fun queryCurrentMetadata(context: Context?, clientId: String?): Cursor { // If clientId is null, we get the defaut DB (see #getInstance() for more about this)
return getDb(context, clientId).query(METADATA_TABLE_NAME,
METADATA_TABLE_COLUMNS, null, null, null, null, LOCALE_COLUMN)
}
/**
* Gets the list of all dictionaries known to the dictionary provider, with only public columns.
*
* This will retrieve information about all known dictionaries, and their status. As such,
* it will also return information about dictionaries on the server that have not been
* downloaded yet, but may be requested.
* This only returns public columns. It does not populate internal columns in the returned
* cursor.
* The value returned by this method is intended to be good to be returned directly for a
* request of the list of dictionaries by a client.
*
* @param context the context to read the database from.
* @param clientId the client id for retrieving the database. null for default (deprecated)
* @return a cursor that lists all available dictionaries and their metadata.
*/
fun queryDictionaries(context: Context?, clientId: String?): Cursor { // If clientId is null, we get the defaut DB (see #getInstance() for more about this)
return getDb(context, clientId).query(METADATA_TABLE_NAME,
DICTIONARIES_LIST_PUBLIC_COLUMNS, // Filter out empty locales so as not to return auxiliary data, like a
// data line for downloading metadata:
"$LOCALE_COLUMN != ?", arrayOf(""), // TODO: Reinstate the following code for bulk, then implement partial updates
/* MetadataDbHelper.TYPE_COLUMN + " = ?",
new String[] { Integer.toString(MetadataDbHelper.TYPE_BULK) }, */
null, null, LOCALE_COLUMN)
}
/**
* Deletes all data associated with a client.
*
* @param context the context for opening the database
* @param clientId the ID of the client to delete.
* @return true if the client was successfully deleted, false otherwise.
*/
fun deleteClient(context: Context?, clientId: String?): Boolean { // Remove all metadata associated with this client
val db = getDb(context, clientId)
db.execSQL("DROP TABLE IF EXISTS $METADATA_TABLE_NAME")
db.execSQL(METADATA_TABLE_CREATE)
// Remove this client's entry in the clients table
val defaultDb = getDb(context, "")
return 0 != defaultDb.delete(CLIENT_TABLE_NAME,
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId))
}
/**
* Updates information relative to a specific client.
*
* Updatable information includes the metadata URI and the additional ID column. It may be
* expanded in the future.
* The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must
* be equal to the string passed as an argument for clientId. It may not be empty.
* The passed values must also include a non-null metadata URI in the
* CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the
* CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty.
* If any of the above is not complied with, this function returns without updating data.
*
* @param context the context, to open the database
* @param clientId the ID of the client to update
* @param values the values to update. Must conform to the protocol (see above)
*/
fun updateClientInfo(context: Context?, clientId: String?,
values: ContentValues) { // Sanity check the content values
val valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN)
val valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN)
val valuesMetadataAdditionalId = values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN)
// Empty string is a valid client ID, but external apps may not configure it, so disallow
// both null and empty string.
// Empty string is a valid metadata URI if the client does not want updates, so allow
// empty string but disallow null.
// Empty string is a valid additional ID so allow empty string but disallow null.
if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri || null == valuesMetadataAdditionalId) { // We need all these columns to be filled in
DebugLogUtils.l("Missing parameter for updateClientInfo")
return
}
if (clientId != valuesClientId) { // Mismatch! The client violates the protocol.
DebugLogUtils.l("Received an updateClientInfo request for ", clientId,
" but the values " + "contain a different ID : ", valuesClientId)
return
}
// Default value for a pending ID is NOT_AN_ID
val defaultDb = getDb(context, "")
if (-1L == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
defaultDb.update(CLIENT_TABLE_NAME, values,
"$CLIENT_CLIENT_ID_COLUMN = ?", arrayOf(clientId))
}
}
/**
* Retrieves the list of existing client IDs.
* @param context the context to open the database
* @return a cursor containing only one column, and one client ID per line.
*/
fun queryClientIds(context: Context?): Cursor {
return getDb(context, null).query(CLIENT_TABLE_NAME, arrayOf(CLIENT_CLIENT_ID_COLUMN), null, null, null, null, null)
}
/**
* Marks a downloading entry as having successfully downloaded and being installed.
*
* The metadata database contains information about ongoing processes, typically ongoing
* downloads. This marks such an entry as having finished and having installed successfully,
* so it becomes INSTALLED.
*
* @param db the metadata database.
* @param r content values about the entry to mark as processed.
*/
fun markEntryAsFinishedDownloadingAndInstalled(db: SQLiteDatabase,
r: ContentValues) {
when (r.getAsInteger(TYPE_COLUMN)) {
TYPE_BULK -> {
DebugLogUtils.l("Ended processing a wordlist")
// Updating a bulk word list is a three-step operation:
// - Add the new entry to the table
// - Remove the old entry from the table
// - Erase the old file
// We start by gathering the names of the files we should delete.
val filenames: MutableList<String> = LinkedList()
val c = db.query(METADATA_TABLE_NAME, arrayOf(LOCAL_FILENAME_COLUMN),
LOCALE_COLUMN + " = ? AND " +
WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", arrayOf(r.getAsString(LOCALE_COLUMN),
r.getAsString(WORDLISTID_COLUMN),
Integer.toString(STATUS_INSTALLED)),
null, null, null)
try {
if (c.moveToFirst()) { // There should never be more than one file, but if there are, it's a bug
// and we should remove them all. I think it might happen if the power of
// the phone is suddenly cut during an update.
val filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN)
do {
DebugLogUtils.l("Setting for removal", c.getString(filenameIndex))
filenames.add(c.getString(filenameIndex))
} while (c.moveToNext())
}
} finally {
c.close()
}
r.put(STATUS_COLUMN, STATUS_INSTALLED)
db.beginTransactionNonExclusive()
// Delete all old entries. There should never be any stalled entries, but if
// there are, this deletes them.
db.delete(METADATA_TABLE_NAME,
"$WORDLISTID_COLUMN = ?", arrayOf(r.getAsString(WORDLISTID_COLUMN)))
db.insert(METADATA_TABLE_NAME, null, r)
db.setTransactionSuccessful()
db.endTransaction()
for (filename in filenames) {
try {
val f = File(filename)
f.delete()
} catch (e: SecurityException) { // No permissions to delete. Um. Can't do anything.
} // I don't think anything else can be thrown
}
}
else -> {
}
}
}
/**
* Removes a downloading entry from the database.
*
* This is invoked when a download fails. Either we tried to download, but
* we received a permanent failure and we should remove it, or we got manually
* cancelled and we should leave it at that.
*
* @param db the metadata database.
* @param id the DownloadManager id of the file.
*/
fun deleteDownloadingEntry(db: SQLiteDatabase, id: Long) {
db.delete(METADATA_TABLE_NAME, "$PENDINGID_COLUMN = ? AND $STATUS_COLUMN = ?", arrayOf(java.lang.Long.toString(id), Integer.toString(STATUS_DOWNLOADING)))
}
/**
* Forcefully removes an entry from the database.
*
* This is invoked when a file is broken. The file has been downloaded, but Android
* Keyboard is telling us it could not open it.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
*/
fun deleteEntry(db: SQLiteDatabase, id: String, version: Int) {
db.delete(METADATA_TABLE_NAME, "$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version)))
}
/**
* Internal method that sets the current status of an entry of the database.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
* @param status the status to set the word list to.
* @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID
*/
private fun markEntryAs(db: SQLiteDatabase, id: String?,
version: Int, status: Int, downloadId: Long) {
val values = getContentValuesByWordListId(db, id, version)
values!!.put(STATUS_COLUMN, status)
if (NOT_A_DOWNLOAD_ID != downloadId) {
values.put(PENDINGID_COLUMN, downloadId)
}
db.update(METADATA_TABLE_NAME, values,
"$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version)))
}
/**
* Writes the status column for the wordlist with this id as enabled. Typically this
* means the word list is currently disabled and we want to set its status to INSTALLED.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
*/
fun markEntryAsEnabled(db: SQLiteDatabase, id: String?,
version: Int) {
markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID)
}
/**
* Writes the status column for the wordlist with this id as disabled. Typically this
* means the word list is currently installed and we want to set its status to DISABLED.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
*/
fun markEntryAsDisabled(db: SQLiteDatabase, id: String?,
version: Int) {
markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID)
}
/**
* Writes the status column for the wordlist with this id as available. This happens for
* example when a word list has been deleted but can be downloaded again.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
*/
fun markEntryAsAvailable(db: SQLiteDatabase, id: String?,
version: Int) {
markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID)
}
/**
* Writes the designated word list as downloadable, alongside with its download id.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
* @param downloadId the download id.
*/
fun markEntryAsDownloading(db: SQLiteDatabase, id: String?,
version: Int, downloadId: Long) {
markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId)
}
/**
* Writes the designated word list as deleting.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
*/
fun markEntryAsDeleting(db: SQLiteDatabase, id: String?,
version: Int) {
markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID)
}
/**
* Checks retry counts and marks the word list as retrying if retry is possible.
*
* @param db the metadata database.
* @param id the id of the word list.
* @param version the version of the word list.
* @return `true` if the retry is possible.
*/
fun maybeMarkEntryAsRetrying(db: SQLiteDatabase, id: String?,
version: Int): Boolean {
val values = getContentValuesByWordListId(db, id, version)
val retryCount = values!!.getAsInteger(RETRY_COUNT_COLUMN)
if (retryCount > 1) {
values.put(STATUS_COLUMN, STATUS_RETRYING)
values.put(RETRY_COUNT_COLUMN, retryCount - 1)
db.update(METADATA_TABLE_NAME, values,
"$WORDLISTID_COLUMN = ? AND $VERSION_COLUMN = ?", arrayOf(id, Integer.toString(version)))
return true
}
return false
}
}
}

View file

@ -1,141 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.util.Log
import java.io.IOException
import java.io.InputStreamReader
import java.util.*
/**
* Helper class to easy up manipulation of dictionary pack metadata.
*/
object MetadataHandler {
val TAG = MetadataHandler::class.java.simpleName
// The canonical file name for metadata. This is not the name of a real file on the
// device, but a symbolic name used in the database and in metadata handling. It is never
// tested against, only used for human-readability as the file name for the metadata.
const val METADATA_FILENAME = "metadata.json"
/**
* Reads the data from the cursor and store it in metadata objects.
* @param results the cursor to read data from.
* @return the constructed list of wordlist metadata.
*/
private fun makeMetadataObject(results: Cursor?): List<WordListMetadata> {
val buildingMetadata = ArrayList<WordListMetadata>()
if (null != results && results.moveToFirst()) {
val localeColumn = results.getColumnIndex(MetadataDbHelper.Companion.LOCALE_COLUMN)
val typeColumn = results.getColumnIndex(MetadataDbHelper.Companion.TYPE_COLUMN)
val descriptionColumn = results.getColumnIndex(MetadataDbHelper.Companion.DESCRIPTION_COLUMN)
val idIndex = results.getColumnIndex(MetadataDbHelper.Companion.WORDLISTID_COLUMN)
val updateIndex = results.getColumnIndex(MetadataDbHelper.Companion.DATE_COLUMN)
val fileSizeIndex = results.getColumnIndex(MetadataDbHelper.Companion.FILESIZE_COLUMN)
val rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN)
val checksumIndex = results.getColumnIndex(MetadataDbHelper.Companion.CHECKSUM_COLUMN)
val retryCountIndex = results.getColumnIndex(MetadataDbHelper.Companion.RETRY_COUNT_COLUMN)
val localFilenameIndex = results.getColumnIndex(MetadataDbHelper.Companion.LOCAL_FILENAME_COLUMN)
val remoteFilenameIndex = results.getColumnIndex(MetadataDbHelper.Companion.REMOTE_FILENAME_COLUMN)
val versionIndex = results.getColumnIndex(MetadataDbHelper.Companion.VERSION_COLUMN)
val formatVersionIndex = results.getColumnIndex(MetadataDbHelper.Companion.FORMATVERSION_COLUMN)
do {
buildingMetadata.add(WordListMetadata(results.getString(idIndex),
results.getInt(typeColumn),
results.getString(descriptionColumn),
results.getLong(updateIndex),
results.getLong(fileSizeIndex),
results.getString(rawChecksumIndex),
results.getString(checksumIndex),
results.getInt(retryCountIndex),
results.getString(localFilenameIndex),
results.getString(remoteFilenameIndex),
results.getInt(versionIndex),
results.getInt(formatVersionIndex),
0, results.getString(localeColumn)))
} while (results.moveToNext())
}
return Collections.unmodifiableList(buildingMetadata)
}
/**
* Gets the whole metadata, for installed and not installed dictionaries.
* @param context The context to open files over.
* @param clientId the client id for retrieving the database. null for default (deprecated)
* @return The current metadata.
*/
fun getCurrentMetadata(context: Context?,
clientId: String?): List<WordListMetadata> { // If clientId is null, we get a cursor on the default database (see
// MetadataDbHelper#getInstance() for more on this)
val results: Cursor = MetadataDbHelper.Companion.queryCurrentMetadata(context, clientId)
// If null, we should return makeMetadataObject(null), so we go through.
return try {
makeMetadataObject(results)
} finally {
results.close()
}
}
/**
* Gets the metadata, for a specific dictionary.
*
* @param context The context to open files over.
* @param clientId the client id for retrieving the database. null for default (deprecated).
* @param wordListId the word list ID.
* @param version the word list version.
* @return the current metaData
*/
fun getCurrentMetadataForWordList(context: Context?,
clientId: String?, wordListId: String?, version: Int): WordListMetadata? {
val contentValues: ContentValues = MetadataDbHelper.Companion.getContentValuesByWordListId(
MetadataDbHelper.Companion.getDb(context, clientId), wordListId, version)!!
if (contentValues == null) { // TODO: Figure out why this would happen.
// Check if this happens when the metadata gets updated in the background.
Log.e(TAG, String.format("Unable to find the current metadata for wordlist "
+ "(clientId=%s, wordListId=%s, version=%d) on the database",
clientId, wordListId, version))
return null
}
return WordListMetadata.Companion.createFromContentValues(contentValues)
}
/**
* Read metadata from a stream.
* @param input The stream to read from.
* @return The read metadata.
* @throws IOException if the input stream cannot be read
* @throws BadFormatException if the stream is not in a known format
*/
@Throws(IOException::class, BadFormatException::class)
fun readMetadata(input: InputStreamReader?): List<WordListMetadata?>? {
return MetadataParser.parseMetadata(input)
}
/**
* Finds a single WordListMetadata inside a whole metadata chunk.
*
* Searches through the whole passed metadata for the first WordListMetadata associated
* with the passed ID. If several metadata chunks with the same id are found, it will
* always return the one with the bigger FormatVersion that is less or equal than the
* maximum supported format version (as listed in UpdateHandler).
* This will NEVER return the metadata with a FormatVersion bigger than what is supported,
* even if it is the only word list with this ID.
*
* @param metadata the metadata to search into.
* @param id the word list ID of the metadata to find.
* @return the associated metadata, or null if not found.
*/
fun findWordListById(metadata: List<WordListMetadata>,
id: String): WordListMetadata? {
var bestWordList: WordListMetadata? = null
var bestFormatVersion = Int.MIN_VALUE // To be sure we can't be inadvertently smaller
for (wordList in metadata) {
if (id == wordList.mId && wordList.mFormatVersion > bestFormatVersion) {
bestWordList = wordList
bestFormatVersion = wordList.mFormatVersion
}
}
// If we didn't find any match we'll return null.
return bestWordList
}
}

View file

@ -1,85 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.text.TextUtils
import android.util.JsonReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.*
/**
* Helper class containing functions to parse the dictionary metadata.
*/
object MetadataParser {
// Name of the fields in the JSON-formatted file.
private val ID_FIELD_NAME: String = MetadataDbHelper.Companion.WORDLISTID_COLUMN
private const val LOCALE_FIELD_NAME = "locale"
private val DESCRIPTION_FIELD_NAME: String = MetadataDbHelper.Companion.DESCRIPTION_COLUMN
private const val UPDATE_FIELD_NAME = "update"
private val FILESIZE_FIELD_NAME: String = MetadataDbHelper.Companion.FILESIZE_COLUMN
private val RAW_CHECKSUM_FIELD_NAME: String = MetadataDbHelper.Companion.RAW_CHECKSUM_COLUMN
private val CHECKSUM_FIELD_NAME: String = MetadataDbHelper.Companion.CHECKSUM_COLUMN
private val REMOTE_FILENAME_FIELD_NAME: String = MetadataDbHelper.Companion.REMOTE_FILENAME_COLUMN
private val VERSION_FIELD_NAME: String = MetadataDbHelper.Companion.VERSION_COLUMN
private val FORMATVERSION_FIELD_NAME: String = MetadataDbHelper.Companion.FORMATVERSION_COLUMN
/**
* Parse one JSON-formatted word list metadata.
* @param reader the reader containing the data.
* @return a WordListMetadata object from the parsed data.
* @throws IOException if the underlying reader throws IOException during reading.
*/
@Throws(IOException::class, BadFormatException::class)
private fun parseOneWordList(reader: JsonReader): WordListMetadata {
val arguments = TreeMap<String, String>()
reader.beginObject()
while (reader.hasNext()) {
val name = reader.nextName()
if (!TextUtils.isEmpty(name)) {
arguments[name] = reader.nextString()
}
}
reader.endObject()
if (TextUtils.isEmpty(arguments[ID_FIELD_NAME])
|| TextUtils.isEmpty(arguments[LOCALE_FIELD_NAME])
|| TextUtils.isEmpty(arguments[DESCRIPTION_FIELD_NAME])
|| TextUtils.isEmpty(arguments[UPDATE_FIELD_NAME])
|| TextUtils.isEmpty(arguments[FILESIZE_FIELD_NAME])
|| TextUtils.isEmpty(arguments[CHECKSUM_FIELD_NAME])
|| TextUtils.isEmpty(arguments[REMOTE_FILENAME_FIELD_NAME])
|| TextUtils.isEmpty(arguments[VERSION_FIELD_NAME])
|| TextUtils.isEmpty(arguments[FORMATVERSION_FIELD_NAME])) {
throw BadFormatException(arguments.toString())
}
// TODO: need to find out whether it's bulk or update
// The null argument is the local file name, which is not known at this time and will
// be decided later.
return WordListMetadata(
arguments[ID_FIELD_NAME],
MetadataDbHelper.Companion.TYPE_BULK,
arguments[DESCRIPTION_FIELD_NAME], arguments[UPDATE_FIELD_NAME]!!.toLong(), arguments[FILESIZE_FIELD_NAME]!!.toLong(),
arguments[RAW_CHECKSUM_FIELD_NAME],
arguments[CHECKSUM_FIELD_NAME],
MetadataDbHelper.Companion.DICTIONARY_RETRY_THRESHOLD /* retryCount */,
null,
arguments[REMOTE_FILENAME_FIELD_NAME], arguments[VERSION_FIELD_NAME]!!.toInt(), arguments[FORMATVERSION_FIELD_NAME]!!.toInt(),
0, arguments[LOCALE_FIELD_NAME])
}
/**
* Parses metadata in the JSON format.
* @param input a stream reader expected to contain JSON formatted metadata.
* @return dictionary metadata, as an array of WordListMetadata objects.
* @throws IOException if the underlying reader throws IOException during reading.
* @throws BadFormatException if the data was not in the expected format.
*/
@Throws(IOException::class, BadFormatException::class)
fun parseMetadata(input: InputStreamReader?): List<WordListMetadata> {
val reader = JsonReader(input)
val readInfo = ArrayList<WordListMetadata>()
reader.beginArray()
while (reader.hasNext()) {
val thisMetadata = parseOneWordList(reader)
if (!TextUtils.isEmpty(thisMetadata.mLocale)) readInfo.add(thisMetadata)
}
return Collections.unmodifiableList(readInfo)
}
}

View file

@ -1,12 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.Context
/**
* Helper to get the metadata URI from its base URI.
*/
object MetadataUriGetter {
fun getUri(context: Context?, baseUri: String): String {
return baseUri
}
}

View file

@ -1,69 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import java.text.SimpleDateFormat
import java.util.*
/**
* Class to keep long-term log. This is inactive in production, and is only for debug purposes.
*/
object PrivateLog {
const val DEBUG = false
private const val LOG_DATABASE_NAME = "log"
private const val LOG_TABLE_NAME = "log"
private const val LOG_DATABASE_VERSION = 1
private const val COLUMN_DATE = "date"
private const val COLUMN_EVENT = "event"
private const val LOG_TABLE_CREATE = ("CREATE TABLE " + LOG_TABLE_NAME + " ("
+ COLUMN_DATE + " TEXT,"
+ COLUMN_EVENT + " TEXT);")
val sDateFormat = SimpleDateFormat(
"yyyy/MM/dd HH:mm:ss", Locale.ROOT)
private val sInstance: PrivateLog = PrivateLog
private var sDebugHelper: DebugHelper? = null
@Synchronized
fun getInstance(context: Context?): PrivateLog {
if (!DEBUG) return sInstance
synchronized(PrivateLog::class.java) {
if (sDebugHelper == null) {
sDebugHelper = DebugHelper(context)
}
return sInstance
}
}
fun log(event: String?) {
if (!DEBUG) return
val l = sDebugHelper!!.writableDatabase
DebugHelper.insert(l, event)
}
internal class DebugHelper(context: Context?) : SQLiteOpenHelper(context, LOG_DATABASE_NAME, null, LOG_DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
if (!DEBUG) return
db.execSQL(LOG_TABLE_CREATE)
insert(db, "Created table")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (!DEBUG) return
// Remove all data.
db.execSQL("DROP TABLE IF EXISTS $LOG_TABLE_NAME")
onCreate(db)
insert(db, "Upgrade finished")
}
companion object {
fun insert(db: SQLiteDatabase, event: String?) {
if (!DEBUG) return
val c = ContentValues(2)
c.put(COLUMN_DATE, sDateFormat.format(Date(System.currentTimeMillis())))
c.put(COLUMN_EVENT, event)
db.insert(LOG_TABLE_NAME, null, c)
}
}
}
}

View file

@ -1,8 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
/**
* A simple interface to report problems.
*/
interface ProblemReporter {
fun report(e: Exception?)
}

View file

@ -1,75 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.ContentValues
/**
* The metadata for a single word list.
*
* Instances of this class are always immutable.
*/
class WordListMetadata // In milliseconds
(val mId: String?, // Type, as of MetadataDbHelper#TYPE_*
val mType: Int,
val mDescription: String?, val mLastUpdate: Long, val mFileSize: Long,
val mRawChecksum: String?, val mChecksum: String?, var mRetryCount: Int,
val mLocalFilename: String?, val mRemoteFilename: String?,
// version of this word list
val mVersion: Int, // Version number of the format.
// This implementation of the DictionaryDataService knows how to handle format 1 only.
// This is only for forward compatibility, to be able to upgrade the format without
// breaking old implementations.
val mFormatVersion: Int,
// Always 0 in this version, reserved for future use
val mFlags: Int, // The locale is matched against the locale requested by the client. The matching algorithm
// is a standard locale matching with fallback; it is implemented in
// DictionaryProvider#getDictionaryFileForContentUri.
val mLocale: String?) {
override fun toString(): String {
val sb = StringBuilder(WordListMetadata::class.java.simpleName)
sb.append(" : ").append(mId)
sb.append("\nType : ").append(mType)
sb.append("\nDescription : ").append(mDescription)
sb.append("\nLastUpdate : ").append(mLastUpdate)
sb.append("\nFileSize : ").append(mFileSize)
sb.append("\nRawChecksum : ").append(mRawChecksum)
sb.append("\nChecksum : ").append(mChecksum)
sb.append("\nRetryCount: ").append(mRetryCount)
sb.append("\nLocalFilename : ").append(mLocalFilename)
sb.append("\nRemoteFilename : ").append(mRemoteFilename)
sb.append("\nVersion : ").append(mVersion)
sb.append("\nFormatVersion : ").append(mFormatVersion)
sb.append("\nFlags : ").append(mFlags)
sb.append("\nLocale : ").append(mLocale)
return sb.toString()
}
companion object {
/**
* Create a WordListMetadata from the contents of a ContentValues.
*
* If this lacks any required field, IllegalArgumentException is thrown.
*/
fun createFromContentValues(values: ContentValues): WordListMetadata {
val id = values.getAsString(MetadataDbHelper.WORDLISTID_COLUMN)
val type = values.getAsInteger(MetadataDbHelper.TYPE_COLUMN)
val description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
val lastUpdate = values.getAsLong(MetadataDbHelper.DATE_COLUMN)
val fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN)
val rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN)
val checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN)
val retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN)
val localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN)
val remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN)
val version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN)
val formatVersion = values.getAsInteger(MetadataDbHelper.FORMATVERSION_COLUMN)
val flags = values.getAsInteger(MetadataDbHelper.FLAGS_COLUMN)
val locale = values.getAsString(MetadataDbHelper.LOCALE_COLUMN)
require(!(null == id || null == type || null == description || null == lastUpdate || null == fileSize || null == checksum || null == localFilename || null == remoteFilename || null == version || null == formatVersion || null == flags || null == locale))
return WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum,
checksum, retryCount, localFilename, remoteFilename, version, formatVersion,
flags, locale)
}
}
}

View file

@ -1,198 +0,0 @@
package org.dslul.openboard.inputmethod.dictionarypack
import android.content.Context
import android.preference.Preference
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import org.dslul.openboard.inputmethod.latin.R
import java.util.*
/**
* A preference for one word list.
*
* This preference refers to a single word list, as available in the dictionary
* pack. Upon being pressed, it displays a menu to allow the user to install, disable,
* enable or delete it as appropriate for the current state of the word list.
*/
@Suppress("deprecation")
class WordListPreference(context: Context?,
private val mInterfaceState: DictionaryListInterfaceState, // The id of the client for which this preference is.
private val mClientId: String?,
// Members
// The metadata word list id and version of this word list.
val mWordlistId: String, val mVersion: Int, val mLocale: Locale,
val mDescription: String, status: Int, // The size of the dictionary file
private val mFilesize: Int) : Preference(context, null) {
// The status
private var mStatus = 0
fun setStatus(status: Int) {
if (status == mStatus) return
mStatus = status
summary = getSummary(status)
}
fun hasStatus(status: Int): Boolean {
return status == mStatus
}
public override fun onCreateView(parent: ViewGroup): View {
val orphanedView = mInterfaceState.findFirstOrphanedView()
if (null != orphanedView) return orphanedView // Will be sent to onBindView
val newView = super.onCreateView(parent)
return mInterfaceState.addToCacheAndReturnView(newView)
}
fun hasPriorityOver(otherPrefStatus: Int): Boolean { // Both of these should be one of MetadataDbHelper.STATUS_*
return mStatus > otherPrefStatus
}
private fun getSummary(status: Int): String {
val context = context
return when (status) {
MetadataDbHelper.STATUS_DELETING, MetadataDbHelper.STATUS_AVAILABLE -> context.getString(R.string.dictionary_available)
MetadataDbHelper.STATUS_DOWNLOADING -> context.getString(R.string.dictionary_downloading)
MetadataDbHelper.STATUS_INSTALLED -> context.getString(R.string.dictionary_installed)
MetadataDbHelper.STATUS_DISABLED -> context.getString(R.string.dictionary_disabled)
else -> NO_STATUS_MESSAGE
}
}
private fun disableDict() {
val context = context
val prefs = CommonPreferences.getCommonPreferences(context)
CommonPreferences.disable(prefs, mWordlistId)
if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) {
setStatus(MetadataDbHelper.STATUS_AVAILABLE)
} else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) { // Interface-wise, we should no longer be able to come here. However, this is still
// the right thing to do if we do come here.
setStatus(MetadataDbHelper.STATUS_DISABLED)
} else {
Log.e(TAG, "Unexpected state of the word list for disabling $mStatus")
}
}
private fun enableDict() {
val context = context
val prefs = CommonPreferences.getCommonPreferences(context)
CommonPreferences.enable(prefs, mWordlistId)
if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) {
setStatus(MetadataDbHelper.STATUS_DOWNLOADING)
} else if (MetadataDbHelper.STATUS_DISABLED == mStatus
|| MetadataDbHelper.STATUS_DELETING == mStatus) { // If the status is DELETING, it means Android Keyboard
// has not deleted the word list yet, so we can safely
// turn it to 'installed'. The status DISABLED is still supported internally to
// avoid breaking older installations and all but there should not be a way to
// disable a word list through the interface any more.
setStatus(MetadataDbHelper.STATUS_INSTALLED)
} else {
Log.e(TAG, "Unexpected state of the word list for enabling $mStatus")
}
}
private fun deleteDict() {
val context = context
val prefs = CommonPreferences.getCommonPreferences(context)
CommonPreferences.disable(prefs, mWordlistId)
setStatus(MetadataDbHelper.STATUS_DELETING)
}
override fun onBindView(view: View) {
super.onBindView(view)
(view as ViewGroup).layoutTransition = null
val buttonSwitcher = view.findViewById<View>(
R.id.wordlist_button_switcher) as ButtonSwitcher
// We need to clear the state of the button switcher, because we reuse views; if we didn't
// reset it would animate from whatever its old state was.
buttonSwitcher.reset(mInterfaceState)
if (mInterfaceState.isOpen(mWordlistId)) { // The button is open.
val previousStatus = mInterfaceState.getStatus(mWordlistId)
buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(previousStatus))
if (previousStatus != mStatus) { // We come here if the status has changed since last time. We need to animate
// the transition.
buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus))
mInterfaceState.setOpen(mWordlistId, mStatus)
}
} else { // The button is closed.
buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON)
}
buttonSwitcher.setInternalOnClickListener(View.OnClickListener { onActionButtonClicked() })
view.setOnClickListener { v -> onWordListClicked(v) }
}
fun onWordListClicked(v: View) { // Note : v is the preference view
val listView = v.parent as? ListView ?: return
// Just in case something changed in the framework, test for the concrete class
// Close all first, we'll open back any item that needs to be open.
val wasOpen = mInterfaceState.isOpen(mWordlistId)
mInterfaceState.closeAll()
val indexToOpen = if (wasOpen) { // This button being shown. Take note that we don't want to open any button in the
// loop below.
-1
} else { // This button was not being shown. Open it, and remember the index of this
// child as the one to open in the following loop.
mInterfaceState.setOpen(mWordlistId, mStatus)
listView.indexOfChild(v)
}
val lastDisplayedIndex = listView.lastVisiblePosition - listView.firstVisiblePosition
// The "lastDisplayedIndex" is actually displayed, hence the <=
for (i in 0..lastDisplayedIndex) {
val buttonSwitcher = listView.getChildAt(i)
.findViewById<View>(R.id.wordlist_button_switcher) as ButtonSwitcher
if (i == indexToOpen) {
buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus))
} else {
buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.Companion.STATUS_NO_BUTTON)
}
}
}
fun onActionButtonClicked() {
when (getActionIdFromStatusAndMenuEntry(mStatus)) {
ACTION_ENABLE_DICT -> enableDict()
ACTION_DISABLE_DICT -> disableDict()
ACTION_DELETE_DICT -> deleteDict()
else -> Log.e(TAG, "Unknown menu item pressed")
}
}
companion object {
private val TAG = WordListPreference::class.java.simpleName
// What to display in the "status" field when we receive unknown data as a status from
// the content provider. Empty string sounds sensible.
private const val NO_STATUS_MESSAGE = ""
/// Actions
private const val ACTION_UNKNOWN = 0
private const val ACTION_ENABLE_DICT = 1
private const val ACTION_DISABLE_DICT = 2
private const val ACTION_DELETE_DICT = 3
// The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses
// the values as indices.
private val sStatusActionList = arrayOf(intArrayOf(), intArrayOf(ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_CANCEL, ACTION_DISABLE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_DELETE, ACTION_DELETE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_DELETE, ACTION_DELETE_DICT), intArrayOf(ButtonSwitcher.Companion.STATUS_INSTALL, ACTION_ENABLE_DICT))
fun getButtonSwitcherStatus(status: Int): Int {
if (status >= sStatusActionList.size) {
Log.e(TAG, "Unknown status $status")
return ButtonSwitcher.STATUS_NO_BUTTON
}
return sStatusActionList[status][0]
}
fun getActionIdFromStatusAndMenuEntry(status: Int): Int {
if (status >= sStatusActionList.size) {
Log.e(TAG, "Unknown status $status")
return ACTION_UNKNOWN
}
return sStatusActionList[status][1]
}
}
init {
layoutResource = R.layout.dictionary_line
title = mDescription
setStatus(status)
key = mWordlistId
}
}

View file

@ -17,7 +17,7 @@ interface Combiner {
* @param event the event to combine with the existing state.
* @return the resulting event.
*/
fun processEvent(previousEvents: ArrayList<Event>?, event: Event?): Event?
fun processEvent(previousEvents: ArrayList<Event>?, event: Event?): Event
/**
* Get the feedback that should be shown to the user for the current state of this combiner.

View file

@ -46,13 +46,13 @@ class CombinerChain(initialText: String?) {
* new event. However it may never be null.
*/
fun processEvent(previousEvents: ArrayList<Event>?,
newEvent: Event?): Event? {
newEvent: Event): Event {
val modifiablePreviousEvents = ArrayList(previousEvents!!)
var event = newEvent
for (combiner in mCombiners) { // A combiner can never return more than one event; it can return several
// code points, but they should be encapsulated within one event.
event = combiner.processEvent(modifiablePreviousEvents, event)
if (event!!.isConsumed) { // If the event is consumed, then we don't pass it to subsequent combiners:
if (event.isConsumed) { // If the event is consumed, then we don't pass it to subsequent combiners:
// they should not see it at all.
break
}

View file

@ -188,11 +188,11 @@ class DeadKeyCombiner : Combiner {
// TODO: make this a list of events instead
val mDeadSequence = StringBuilder()
override fun processEvent(previousEvents: ArrayList<Event>?, event: Event?): Event? {
override fun processEvent(previousEvents: ArrayList<Event>?, event: Event?): Event {
if (TextUtils.isEmpty(mDeadSequence)) { // No dead char is currently being tracked: this is the most common case.
if (event!!.isDead) { // The event was a dead key. Start tracking it.
mDeadSequence.appendCodePoint(event.mCodePoint)
return Event.Companion.createConsumedEvent(event)
return Event.createConsumedEvent(event)
}
// Regular keystroke when not keeping track of a dead key. Simply said, there are
// no dead keys at all in the current input, so this combiner has nothing to do and
@ -201,8 +201,7 @@ class DeadKeyCombiner : Combiner {
}
if (Character.isWhitespace(event!!.mCodePoint)
|| event.mCodePoint == mDeadSequence.codePointBefore(mDeadSequence.length)) { // When whitespace or twice the same dead key, we should output the dead sequence as is.
val resultEvent = createEventChainFromSequence(mDeadSequence.toString(),
event)
val resultEvent = createEventChainFromSequence(mDeadSequence.toString(), event)
mDeadSequence.setLength(0)
return resultEvent
}
@ -211,13 +210,13 @@ class DeadKeyCombiner : Combiner {
val trimIndex = mDeadSequence.length - Character.charCount(
mDeadSequence.codePointBefore(mDeadSequence.length))
mDeadSequence.setLength(trimIndex)
return Event.Companion.createConsumedEvent(event)
return Event.createConsumedEvent(event)
}
return event
}
if (event.isDead) {
mDeadSequence.appendCodePoint(event.mCodePoint)
return Event.Companion.createConsumedEvent(event)
return Event.createConsumedEvent(event)
}
// Combine normally.
val sb = StringBuilder()
@ -248,17 +247,16 @@ class DeadKeyCombiner : Combiner {
get() = mDeadSequence
companion object {
private fun createEventChainFromSequence(text: CharSequence,
originalEvent: Event?): Event? {
private fun createEventChainFromSequence(text: CharSequence, originalEvent: Event): Event {
var index = text.length
if (index <= 0) {
return originalEvent
}
var lastEvent: Event? = null
lateinit var lastEvent: Event
do {
val codePoint = Character.codePointBefore(text, index)
lastEvent = Event.Companion.createHardwareKeypressEvent(codePoint,
originalEvent!!.mKeyCode, lastEvent, false /* isKeyRepeat */)
lastEvent = Event.createHardwareKeypressEvent(codePoint,
originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */)
index -= Character.charCount(codePoint)
} while (index > 0)
return lastEvent

View file

@ -22,7 +22,6 @@ import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams;
import org.dslul.openboard.inputmethod.keyboard.internal.KeySpecParser;
import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle;

View file

@ -28,7 +28,6 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import org.dslul.openboard.inputmethod.compat.EditorInfoCompatUtils;
import org.dslul.openboard.inputmethod.compat.UserManagerCompatUtils;
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardBuilder;
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams;
import org.dslul.openboard.inputmethod.keyboard.internal.UniqueKeysCache;
@ -51,6 +50,7 @@ import static org.dslul.openboard.inputmethod.latin.common.Constants.ImeOption.N
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.UserManagerCompat;
/**
* This class represents a set of keyboard layouts. Each of them represents a different keyboard
@ -88,7 +88,6 @@ public final class KeyboardLayoutSet {
private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
new HashMap<>();
@SuppressWarnings("serial")
public static final class KeyboardLayoutSetException extends RuntimeException {
public final KeyboardId mKeyboardId;
@ -166,8 +165,6 @@ public final class KeyboardLayoutSet {
mParams = params;
}
public static final String LOCALE_GEORGIAN = "ka";
@NonNull
public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
final int keyboardLayoutSetElementId;
@ -287,10 +284,7 @@ public final class KeyboardLayoutSet {
// When the device is still unlocked, features like showing the IME setting app need to
// be locked down.
// TODO: Switch to {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library
// when it becomes publicly available.
@UserManagerCompatUtils.LockState final int lockState = UserManagerCompatUtils.getUserLockState(context);
if (lockState == UserManagerCompatUtils.LOCK_STATE_LOCKED) {
if (!UserManagerCompat.isUserUnlocked(context)) {
params.mNoSettingsKey = true;
}
}
@ -363,8 +357,7 @@ public final class KeyboardLayoutSet {
final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
final int xmlId = getXmlId(resources, layoutSetName);
final XmlResourceParser parser = resources.getXml(xmlId);
try {
try (XmlResourceParser parser = resources.getXml(xmlId)) {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
// Bovinate through the XML stupidly searching for TAG_FEATURE, and read
// the script Id from it.
@ -376,8 +369,6 @@ public final class KeyboardLayoutSet {
}
} catch (final IOException | XmlPullParserException e) {
throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
} finally {
parser.close();
}
// If the tag is not found, then the default script is Latin.
return ScriptUtils.SCRIPT_LATIN;
@ -419,8 +410,7 @@ public final class KeyboardLayoutSet {
private void parseKeyboardLayoutSet(final Resources res, final int resId)
throws XmlPullParserException, IOException {
final XmlResourceParser parser = res.getXml(resId);
try {
try (XmlResourceParser parser = res.getXml(resId)) {
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
final int event = parser.next();
if (event == XmlPullParser.START_TAG) {
@ -432,8 +422,6 @@ public final class KeyboardLayoutSet {
}
}
}
} finally {
parser.close();
}
}

View file

@ -129,20 +129,19 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
final Resources res = mThemeContext.getResources();
final int keyboardWidth = ResourceUtils.getKeyboardWidth(res, settingsValues);
final int keyboardHeight = ResourceUtils.getKeyboardHeight(res, settingsValues);
builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
builder.setSubtype(mRichImm.getCurrentSubtype());
builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey);
builder.setNumberRowEnabled(settingsValues.mShowsNumberRow);
builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey());
builder.setEmojiKeyEnabled(settingsValues.mShowsEmojiKey);
builder.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED
&& settingsValues.mIsSplitKeyboardEnabled);
final boolean oneHandedModeEnabled = settingsValues.mOneHandedModeEnabled;
builder.setOneHandedModeEnabled(oneHandedModeEnabled);
mKeyboardLayoutSet = builder.build();
mKeyboardLayoutSet = builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
.setSubtype(mRichImm.getCurrentSubtype())
.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey)
.setNumberRowEnabled(settingsValues.mShowsNumberRow)
.setLanguageSwitchKeyEnabled(settingsValues.isLanguageSwitchKeyEnabled())
.setEmojiKeyEnabled(settingsValues.mShowsEmojiKey)
.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED
&& settingsValues.mIsSplitKeyboardEnabled)
.setOneHandedModeEnabled(oneHandedModeEnabled)
.build();
try {
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState,
oneHandedModeEnabled);
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState, oneHandedModeEnabled);
mKeyboardTextsSet.setLocale(mRichImm.getCurrentSubtypeLocale(), mThemeContext);
} catch (KeyboardLayoutSetException e) {
Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
@ -547,10 +546,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mKeyboardView.closing();
}
updateKeyboardThemeAndContextThemeWrapper(
displayContext, KeyboardTheme.getKeyboardTheme(displayContext /* context */));
mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
R.layout.input_view, null);
updateKeyboardThemeAndContextThemeWrapper(displayContext, KeyboardTheme.getKeyboardTheme(displayContext));
mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(R.layout.input_view, null);
mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
mEmojiPalettesView = mCurrentInputView.findViewById(R.id.emoji_palettes_view);
mClipboardHistoryView = mCurrentInputView.findViewById(R.id.clipboard_history_view);
@ -560,16 +557,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mKeyboardView = mCurrentInputView.findViewById(R.id.keyboard_view);
mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
mKeyboardView.setKeyboardActionListener(mLatinIME);
mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled(
isHardwareAcceleratedDrawingEnabled);
mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
mEmojiPalettesView.setKeyboardActionListener(mLatinIME);
mClipboardHistoryView.setHardwareAcceleratedDrawingEnabled(
isHardwareAcceleratedDrawingEnabled);
mClipboardHistoryView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
mClipboardHistoryView.setKeyboardActionListener(mLatinIME);
// set background color here, otherwise there is a narrow white line between keyboard and suggestion strip
mKeyboardViewWrapper.getBackground().setColorFilter(Settings.getInstance().getCurrent().mColors.backgroundFilter);
return mCurrentInputView;
}

View file

@ -20,7 +20,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import androidx.core.content.ContextCompat;
@ -40,22 +39,17 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
// new themes using the custom colors
public static final String THEME_LIGHT = "light";
public static final String THEME_HOLO_WHITE = "holo_white"; // todo: rename (but useful to have for testing)
public static final String THEME_HOLO_WHITE = "holo_white";
public static final String THEME_DARK = "dark";
public static final String THEME_DARKER = "darker";
public static final String THEME_BLACK = "black";
public static final String THEME_USER = "user";
public static final String THEME_USER_DARK = "user_dark";
public static final String THEME_USER_NIGHT = "user_night";
public static final String[] THEME_VARIANTS = new String[] { THEME_LIGHT, THEME_HOLO_WHITE, THEME_DARK, THEME_DARKER, THEME_BLACK, THEME_USER };
public static final String[] THEME_VARIANTS_DARK = new String[] { THEME_DARK, THEME_DARKER, THEME_BLACK, THEME_USER_DARK };
public static final String[] THEME_VARIANTS_DARK = new String[] { THEME_HOLO_WHITE, THEME_DARK, THEME_DARKER, THEME_BLACK, THEME_USER_NIGHT};
public static final String[] THEME_STYLES = { THEME_STYLE_MATERIAL, THEME_STYLE_HOLO };
private static final String TAG = KeyboardTheme.class.getSimpleName();
static final String KLP_KEYBOARD_THEME_KEY = "pref_keyboard_layout_20110916";
static final String LXX_KEYBOARD_THEME_KEY = "pref_keyboard_theme_20140509";
// These should be aligned with Keyboard.themeId and Keyboard.Case.keyboardTheme
// attributes' values in attrs.xml.
public static final int THEME_ID_HOLO_BASE = 0;
@ -126,44 +120,11 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
return null;
}
/* package private for testing */
static KeyboardTheme getDefaultKeyboardTheme(final SharedPreferences prefs,
final int sdkVersion, final KeyboardTheme[] availableThemeArray) {
final String klpThemeIdString = prefs.getString(KLP_KEYBOARD_THEME_KEY, null);
if (klpThemeIdString != null) {
if (sdkVersion <= VERSION_CODES.KITKAT) {
try {
final int themeId = Integer.parseInt(klpThemeIdString);
final KeyboardTheme theme = searchKeyboardThemeById(themeId,
availableThemeArray);
if (theme != null) {
return theme;
}
Log.w(TAG, "Unknown keyboard theme in KLP preference: " + klpThemeIdString);
} catch (final NumberFormatException e) {
Log.w(TAG, "Illegal keyboard theme in KLP preference: " + klpThemeIdString, e);
}
}
// Remove old preference.
Log.i(TAG, "Remove KLP keyboard theme preference: " + klpThemeIdString);
prefs.edit().remove(KLP_KEYBOARD_THEME_KEY).apply();
}
// TODO: This search algorithm isn't optimal if there are many themes.
for (final KeyboardTheme theme : availableThemeArray) {
if (sdkVersion >= theme.mMinApiVersion) {
return theme;
}
}
return searchKeyboardThemeById(DEFAULT_THEME_ID, availableThemeArray);
}
public static String getKeyboardThemeName(final int themeId) {
final KeyboardTheme theme = searchKeyboardThemeById(themeId, KEYBOARD_THEMES);
return theme.mThemeName;
}
// todo: this actually should be called style now, as the colors are independent
// and selection should be simplified, because really...
public static KeyboardTheme getKeyboardTheme(final Context context) {
final SharedPreferences prefs = DeviceProtectedUtils.getSharedPreferences(context);
final String style = prefs.getString(Settings.PREF_THEME_STYLE, THEME_STYLE_MATERIAL);
@ -177,61 +138,30 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
if (keyboardTheme.mThemeId == matchingId)
return keyboardTheme;
}
return KEYBOARD_THEMES[3]; // base no border as default
return KEYBOARD_THEMES[DEFAULT_THEME_ID];
}
/* package private for testing */
static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs, final int sdkVersion,
final KeyboardTheme[] availableThemeArray) {
final String lxxThemeIdString = prefs.getString(LXX_KEYBOARD_THEME_KEY, null);
if (lxxThemeIdString == null) {
return getDefaultKeyboardTheme(prefs, sdkVersion, availableThemeArray);
}
try {
final int themeId = Integer.parseInt(lxxThemeIdString);
final KeyboardTheme theme = searchKeyboardThemeById(themeId, availableThemeArray);
if (theme != null) {
return theme;
}
Log.w(TAG, "Unknown keyboard theme in LXX preference: " + lxxThemeIdString);
} catch (final NumberFormatException e) {
Log.w(TAG, "Illegal keyboard theme in LXX preference: " + lxxThemeIdString, e);
}
// Remove preference that contains unknown or illegal theme id.
prefs.edit().remove(LXX_KEYBOARD_THEME_KEY).apply();
return getDefaultKeyboardTheme(prefs, sdkVersion, availableThemeArray);
}
public static String getThemeFamily(int themeId) {
if (themeId == THEME_ID_HOLO_BASE) return THEME_STYLE_HOLO;
return THEME_STYLE_MATERIAL;
}
public static boolean getHasKeyBorders(int themeId) {
return themeId != THEME_ID_LXX_BASE; // THEME_ID_LXX_BASE is the only without borders
}
// todo (later): material you, system accent, ...
public static Colors getThemeColors(final String themeColors, final String themeStyle, final Context context, final SharedPreferences prefs) {
final boolean hasBorders = prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, false);
switch (themeColors) {
case THEME_USER:
final int accent = prefs.getInt(Settings.PREF_THEME_USER_COLOR_ACCENT, Color.BLUE);
final int keyBgColor = prefs.getInt(Settings.PREF_THEME_USER_COLOR_KEYS, Color.LTGRAY);
final int keyTextColor = prefs.getInt(Settings.PREF_THEME_USER_COLOR_TEXT, Color.WHITE);
final int hintTextColor = prefs.getInt(Settings.PREF_THEME_USER_COLOR_HINT_TEXT, Color.WHITE);
final int background = prefs.getInt(Settings.PREF_THEME_USER_COLOR_BACKGROUND, Color.DKGRAY);
return Colors.newColors(themeStyle, accent, background, keyBgColor, ColorUtilKt.brightenOrDarken(keyBgColor, true), keyBgColor, keyTextColor, hintTextColor);
case THEME_USER_DARK:
final int accent2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_ACCENT, Color.BLUE);
final int keyBgColor2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_KEYS, Color.LTGRAY);
final int keyTextColor2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_TEXT, Color.WHITE);
final int hintTextColor2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_HINT_TEXT, Color.WHITE);
final int background2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_BACKGROUND, Color.DKGRAY);
return Colors.newColors(themeStyle, accent2, background2, keyBgColor2, ColorUtilKt.brightenOrDarken(keyBgColor2, true), keyBgColor2, keyTextColor2, hintTextColor2);
final int accent = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, false);
final int keyBgColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, false);
final int keyTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, false);
final int hintTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, false);
final int background = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, false);
return new Colors(themeStyle, hasBorders, accent, background, keyBgColor, ColorUtilKt.brightenOrDarken(keyBgColor, true), keyBgColor, keyTextColor, hintTextColor);
case THEME_USER_NIGHT:
final int accent2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, true);
final int keyBgColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, true);
final int keyTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, true);
final int hintTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, true);
final int background2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, true);
return new Colors(themeStyle, hasBorders, accent2, background2, keyBgColor2, ColorUtilKt.brightenOrDarken(keyBgColor2, true), keyBgColor2, keyTextColor2, hintTextColor2);
case THEME_DARK:
return Colors.newColors(
return new Colors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
// colors taken from the drawable
Color.parseColor("#263238"),
@ -242,8 +172,9 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark)
);
case THEME_HOLO_WHITE:
return Colors.newColors(
return new Colors(
themeStyle,
hasBorders,
Color.parseColor("#FFFFFF"),
// colors taken from the drawable
Color.parseColor("#282828"),
@ -254,8 +185,9 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
Color.parseColor("#282828")
);
case THEME_DARKER:
return Colors.newColors(
return new Colors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
ContextCompat.getColor(context, R.color.keyboard_background_lxx_dark_border),
ContextCompat.getColor(context, R.color.key_background_normal_lxx_dark_border),
@ -265,8 +197,9 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark)
);
case THEME_BLACK:
return Colors.newColors(
return new Colors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
ContextCompat.getColor(context, R.color.background_amoled_black),
ContextCompat.getColor(context, R.color.background_amoled_dark),
@ -277,8 +210,9 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
);
case THEME_LIGHT:
default:
return Colors.newColors(
return new Colors(
themeStyle,
hasBorders,
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_light),
ContextCompat.getColor(context, R.color.keyboard_background_lxx_light_border),
ContextCompat.getColor(context, R.color.key_background_normal_lxx_light_border),

View file

@ -39,9 +39,9 @@ import org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPageKeyboardView;
import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams;
import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.common.BackgroundType;
import org.dslul.openboard.inputmethod.latin.common.Colors;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.HoloColors;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView;
import org.dslul.openboard.inputmethod.latin.utils.TypefaceUtils;
@ -141,20 +141,19 @@ public class KeyboardView extends View {
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground).mutate();
mKeyBackground.getPadding(mKeyBackgroundPadding);
final Drawable functionalKeyBackground = keyboardViewAttr.getDrawable(
R.styleable.KeyboardView_functionalKeyBackground);
mFunctionalKeyBackground = (functionalKeyBackground != null) ? functionalKeyBackground.mutate()
: keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground).mutate();
final Drawable spacebarBackground = keyboardViewAttr.getDrawable(
R.styleable.KeyboardView_spacebarBackground);
mSpacebarBackground = (spacebarBackground != null) ? spacebarBackground.mutate()
: keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground).mutate();
if (mColors instanceof HoloColors) // todo: this logic should be in Colors, not here
mActionKeyBackground = mFunctionalKeyBackground;
if (this instanceof EmojiPageKeyboardView || this instanceof MoreSuggestionsView)
mKeyBackground = mColors.getDrawable(BackgroundType.BACKGROUND, keyboardViewAttr);
else if (this instanceof MoreKeysKeyboardView)
mKeyBackground = mColors.getDrawable(BackgroundType.ADJUSTED_BACKGROUND, keyboardViewAttr);
else
mActionKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground).mutate();
mKeyBackground = mColors.getDrawable(BackgroundType.KEY, keyboardViewAttr);
mKeyBackground.getPadding(mKeyBackgroundPadding);
mFunctionalKeyBackground = mColors.getDrawable(BackgroundType.FUNCTIONAL, keyboardViewAttr);
mSpacebarBackground = mColors.getDrawable(BackgroundType.SPACE, keyboardViewAttr);
if (this instanceof MoreKeysKeyboardView)
mActionKeyBackground = mColors.getDrawable(BackgroundType.ACTION_MORE_KEYS, keyboardViewAttr);
else
mActionKeyBackground = mColors.getDrawable(BackgroundType.ACTION, keyboardViewAttr);
mSpacebarIconWidthRatio = keyboardViewAttr.getFloat(
R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f);
@ -179,31 +178,7 @@ public class KeyboardView extends View {
keyAttr.recycle();
mPaint.setAntiAlias(true);
if (this instanceof EmojiPageKeyboardView || this instanceof MoreSuggestionsView) {
mColors.setBackgroundColor(mKeyBackground, Colors.TYPE_BACKGROUND);
} else if (this instanceof MoreKeysKeyboardView) {
mColors.setBackgroundColor(mKeyBackground, Colors.TYPE_ADJUSTED_BACKGROUND);
} else {
mColors.setBackgroundColor(mKeyBackground, Colors.TYPE_KEY);
}
mColors.setBackgroundColor(mActionKeyBackground, Colors.TYPE_ACTION);
mColors.setBackgroundColor(mSpacebarBackground, Colors.TYPE_SPACE);
if (this instanceof MoreKeysKeyboardView)
mColors.setBackgroundColor(mFunctionalKeyBackground, Colors.TYPE_ADJUSTED_BACKGROUND);
else
mColors.setBackgroundColor(mFunctionalKeyBackground, Colors.TYPE_FUNCTIONAL);
if (this.getClass() == MoreKeysKeyboardView.class)
getBackground().setColorFilter(mColors.adjustedBackgroundFilter);
else {
// todo: this should only be applied to specific keyboards, check original version for which one
// and actually this again is something that maybe should be done in Colors
final Drawable keyboardBackground = mColors.getKeyboardBackground();
if (!(this instanceof MoreSuggestionsView) && keyboardBackground != null)
setBackground(keyboardBackground);
else
getBackground().setColorFilter(mColors.backgroundFilter);
}
mColors.setKeyboardBackground(this);
}
@Nullable
@ -623,24 +598,22 @@ public class KeyboardView extends View {
}
private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) {
if (key.isAccentColored() && !(mColors instanceof HoloColors)) { // todo: this logic should not be here
icon.setColorFilter(mColors.actionKeyIconColorFilter);
if (key.isAccentColored()) {
icon.setColorFilter(mColors.getActionKeyIconColorFilter());
} else if (key.isShift() && keyboard != null) {
// todo (idea): replace shift icon with white one and use the normal multiply filters
// this could allow different shift icon with nicer coloring
if (keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
)
icon.setColorFilter(mColors.accent, PorterDuff.Mode.SRC_ATOP); // accent if shifted, needs SRC_ATOP because of underlying drawable
icon.setColorFilter(mColors.getAccent(), PorterDuff.Mode.SRC_ATOP); // todo: switch to accentColorFilter after changing keyboard symbols to white icons
else
icon.setColorFilter(mColors.keyTextFilter); // key text if not shifted
icon.setColorFilter(mColors.getKeyTextFilter()); // key text if not shifted
} else if (key.getBackgroundType() != Key.BACKGROUND_TYPE_NORMAL) {
icon.setColorFilter(mColors.keyTextFilter);
icon.setColorFilter(mColors.getKeyTextFilter());
} else if (this instanceof MoreKeysKeyboardView) {
// set color filter for long press comma key, should not trigger anywhere else
icon.setColorFilter(mColors.keyTextFilter);
icon.setColorFilter(mColors.getKeyTextFilter());
}
}

View file

@ -58,7 +58,6 @@ import org.dslul.openboard.inputmethod.latin.SuggestedWords;
import org.dslul.openboard.inputmethod.latin.common.Colors;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
import org.dslul.openboard.inputmethod.latin.common.HoloColors;
import org.dslul.openboard.inputmethod.latin.settings.DebugSettings;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
@ -165,7 +164,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
// Gesture floating preview text
// TODO: Make this parameter customizable by user via settings.
private int mGestureFloatingPreviewTextLingerTimeout;
private final int mGestureFloatingPreviewTextLingerTimeout;
private final KeyDetector mKeyDetector;
private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
@ -219,10 +218,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f);
final Colors colors = Settings.getInstance().getCurrent().mColors;
if (colors instanceof HoloColors) // todo: this logic should be in Colors
mLanguageOnSpacebarTextColor = colors.keyText;
else
mLanguageOnSpacebarTextColor = colors.keyHintText; //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0);
mLanguageOnSpacebarTextColor = colors.getSpaceBarText(); //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0);
mLanguageOnSpacebarTextShadowRadius = mainKeyboardViewAttr.getFloat(
R.styleable.MainKeyboardView_languageOnSpacebarTextShadowRadius,
LANGUAGE_ON_SPACEBAR_TEXT_SHADOW_RADIUS_DISABLED);
@ -394,7 +390,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
* @param keyboard the keyboard to display in this view
*/
@Override
public void setKeyboard(final Keyboard keyboard) {
public void setKeyboard(@NonNull final Keyboard keyboard) {
// Remove any pending messages, except dismissing preview and key repeat.
mTimerHandler.cancelLongPressTimers();
super.setKeyboard(keyboard);
@ -542,9 +538,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
public void showGestureFloatingPreviewText(@NonNull final SuggestedWords suggestedWords,
final boolean dismissDelayed) {
locatePreviewPlacerView();
final GestureFloatingTextDrawingPreview gestureFloatingTextDrawingPreview =
mGestureFloatingTextDrawingPreview;
gestureFloatingTextDrawingPreview.setSuggestedWords(suggestedWords);
mGestureFloatingTextDrawingPreview.setSuggestedWords(suggestedWords);
if (dismissDelayed) {
mTimerHandler.postDismissGestureFloatingPreviewText(
mGestureFloatingPreviewTextLingerTimeout);
@ -805,8 +799,8 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
}
@Override
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
protected void onDrawKeyTopVisuals(@NonNull final Key key, @NonNull final Canvas canvas,
@NonNull final Paint paint, @NonNull final KeyDrawParams params) {
if (key.altCodeWhileTyping() && key.isEnabled()) {
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
}
@ -920,7 +914,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
// Draw language text with shadow
final float descent = paint.descent();
final float textHeight = -paint.ascent() + descent;
final float baseline = height / 2 + textHeight / 2;
final float baseline = height / 2f + textHeight / 2;
if (mLanguageOnSpacebarTextShadowRadius > 0.0f) {
paint.setShadowLayer(mLanguageOnSpacebarTextShadowRadius, 0, 0,
mLanguageOnSpacebarTextShadowColor);
@ -929,7 +923,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
}
paint.setColor(mLanguageOnSpacebarTextColor);
paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
canvas.drawText(language, width / 2, baseline - descent, paint);
canvas.drawText(language, width / 2f, baseline - descent, paint);
paint.clearShadowLayer();
paint.setTextScaleX(1.0f);
}

View file

@ -26,6 +26,8 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils;
import org.dslul.openboard.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate;
import org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener;
@ -86,8 +88,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
}
@Override
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
protected void onDrawKeyTopVisuals(@NonNull final Key key, @NonNull final Canvas canvas,
@NonNull final Paint paint, @NonNull final KeyDrawParams params) {
if (!key.isSpacer() || !(key instanceof MoreKeysKeyboard.MoreKeyDivider)
|| mDivider == null) {
super.onDrawKeyTopVisuals(key, canvas, paint, params);
@ -103,7 +105,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
}
@Override
public void setKeyboard(final Keyboard keyboard) {
public void setKeyboard(@NonNull final Keyboard keyboard) {
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());

View file

@ -23,6 +23,7 @@ import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodSubtype;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -39,14 +40,17 @@ import org.dslul.openboard.inputmethod.keyboard.internal.PointerTrackerQueue;
import org.dslul.openboard.inputmethod.keyboard.internal.TimerProxy;
import org.dslul.openboard.inputmethod.keyboard.internal.TypingTimeRecorder;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
import java.util.ArrayList;
import java.util.List;
public final class PointerTracker implements PointerTrackerQueue.Element,
BatchInputArbiterListener {
@ -54,7 +58,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
private static final boolean DEBUG_EVENT = false;
private static final boolean DEBUG_MOVE_EVENT = false;
private static final boolean DEBUG_LISTENER = false;
private static boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED || DEBUG_EVENT;
private static final boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED || DEBUG_EVENT;
static final class PointerTrackerParams {
public final boolean mKeySelectionByDraggingFinger;
@ -83,11 +87,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
}
}
private static GestureEnabler sGestureEnabler = new GestureEnabler();
private static final GestureEnabler sGestureEnabler = new GestureEnabler();
// Parameters for pointer handling.
private static PointerTrackerParams sParams;
private static int sPointerStep = (int)(10.0 * Resources.getSystem().getDisplayMetrics().density);
private static final int sPointerStep = (int)(10.0 * Resources.getSystem().getDisplayMetrics().density);
private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams;
private static GestureStrokeDrawingParams sGestureStrokeDrawingParams;
private static boolean sNeedsPhantomSuddenMoveEventHack;
@ -132,8 +136,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
private int mLastY;
private int mStartX;
private int mStartY;
private int mPreviousY;
private long mStartTime;
private boolean mCursorMoved = false;
private boolean mLanguageSlideStarted = false;
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
@ -715,6 +721,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
setPressedKeyGraphics(key, eventTime);
mStartX = x;
mStartY = y;
mPreviousY = y;
mStartTime = System.currentTimeMillis();
}
}
@ -915,12 +922,33 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
private void onMoveEventInternal(final int x, final int y, final long eventTime) {
final Key oldKey = mCurrentKey;
final SettingsValues sv = Settings.getInstance().getCurrent();
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE && Settings.getInstance().getCurrent().mSpaceTrackpadEnabled) {
//Pointer slider
int steps = (x - mStartX) / sPointerStep;
final int longpressTimeout = 2 * Settings.getInstance().getCurrent().mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
if (steps != 0 && mStartTime + longpressTimeout < System.currentTimeMillis()) {
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE) {
int dX = x - mStartX;
int dY = y - mStartY;
// language switch: upwards movement
if (!mCursorMoved && sv.mSpaceLanguageSlide && -dY > abs(dX) && dY / sPointerStep != 0) {
List<InputMethodSubtype> subtypes = RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypeList(false);
if (subtypes.size() > 1) { // only allow if we have more than one subtype
mLanguageSlideStarted = true;
if (abs(y - mPreviousY) / sPointerStep < 4)
// we want large enough steps between switches
return;
// decide next or previous dependent on up or down
InputMethodSubtype current = RichInputMethodManager.getInstance().getCurrentSubtype().getRawSubtype();
int wantedIndex = (subtypes.indexOf(current) + ((y - mPreviousY > 0) ? 1 : -1)) % subtypes.size();
if (wantedIndex < 0) wantedIndex += subtypes.size();
KeyboardSwitcher.getInstance().switchToSubtype(subtypes.get(wantedIndex));
mPreviousY = y;
return;
}
}
// Pointer slider: sideways movement
int steps = dX / sPointerStep;
final int longpressTimeout = 2 * sv.mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
if (sv.mSpaceTrackpadEnabled && !mLanguageSlideStarted && steps != 0 && mStartTime + longpressTimeout < System.currentTimeMillis()) {
mCursorMoved = true;
mStartX += steps * sPointerStep;
sListener.onMovePointer(steps);
@ -928,8 +956,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
return;
}
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE && Settings.getInstance().getCurrent().mDeleteSwipeEnabled) {
//Delete slider
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE && sv.mDeleteSwipeEnabled) {
// Delete slider
int steps = (x - mStartX) / sPointerStep;
if (abs(steps) > 2 || (mCursorMoved && steps != 0)) {
sTimerProxy.cancelKeyTimersOf(this);
@ -1030,8 +1058,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
return;
}
if (mCursorMoved) {
if (mCursorMoved || mLanguageSlideStarted) {
mCursorMoved = false;
mLanguageSlideStarted = false;
return;
}

View file

@ -12,8 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryEntry
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryManager
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.Colors
import org.dslul.openboard.inputmethod.latin.common.HoloColors
import org.dslul.openboard.inputmethod.latin.common.BackgroundType
import org.dslul.openboard.inputmethod.latin.settings.Settings
class ClipboardAdapter(
@ -56,7 +55,7 @@ class ClipboardAdapter(
setOnTouchListener(this@ViewHolder)
setOnLongClickListener(this@ViewHolder)
setBackgroundResource(itemBackgroundId)
Settings.getInstance().current.mColors.setBackgroundColor(background, Colors.TYPE_KEY)
Settings.getInstance().current.mColors.setBackgroundColor(background, BackgroundType.KEY)
}
pinnedIconView = view.findViewById<ImageView>(R.id.clipboard_entry_pinned_icon).apply {
visibility = View.GONE
@ -69,8 +68,7 @@ class ClipboardAdapter(
}
clipboardLayoutParams.setItemProperties(view)
val colors = Settings.getInstance().current.mColors
if (colors is HoloColors) // todo: this logic should be in Colors, not here
pinnedIconView.colorFilter = colors.accentColorFilter
pinnedIconView.colorFilter = colors.clipboardPinFilter
}
fun setContent(historyEntry: ClipboardHistoryEntry?) {

View file

@ -1,12 +1,9 @@
package org.dslul.openboard.inputmethod.keyboard.clipboard
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.dslul.openboard.inputmethod.latin.settings.Settings
class ClipboardHistoryRecyclerView @JvmOverloads constructor(
context: Context,
@ -56,25 +53,4 @@ class ClipboardHistoryRecyclerView @JvmOverloads constructor(
adapter?.registerAdapterDataObserver(adapterDataObserver)
}
class BottomDividerItemDecoration(dividerHeight: Int, dividerColor: Int) : RecyclerView.ItemDecoration() {
private val paint = Paint()
init {
paint.color = dividerColor
paint.strokeWidth = dividerHeight.toFloat()
paint.colorFilter = Settings.getInstance().current.mColors.backgroundFilter
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: State) {
super.onDrawOver(canvas, parent, state)
canvas.drawLine(parent.paddingLeft.toFloat(),
parent.height - paint.strokeWidth / 2,
parent.width.toFloat() - parent.paddingRight.toFloat(),
parent.height - paint.strokeWidth / 2 ,
paint
)
}
}
}

View file

@ -16,7 +16,7 @@ import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryManager
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.Colors
import org.dslul.openboard.inputmethod.latin.common.BackgroundType
import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.settings.Settings
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils
@ -30,7 +30,6 @@ class ClipboardHistoryView @JvmOverloads constructor(
private val clipboardLayoutParams = ClipboardLayoutParams(context.resources)
private val pinIconId: Int
private val dividerColor: Int
private val functionalKeyBackgroundId: Int
private val keyBackgroundId: Int
@ -46,15 +45,10 @@ class ClipboardHistoryView @JvmOverloads constructor(
init {
val clipboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.ClipboardHistoryView, defStyle, R.style.ClipboardHistoryView)
pinIconId = clipboardViewAttr.getResourceId(
R.styleable.ClipboardHistoryView_iconPinnedClip, 0)
dividerColor = clipboardViewAttr.getColor(
R.styleable.ClipboardHistoryView_dividerBackground, 0)
pinIconId = clipboardViewAttr.getResourceId(R.styleable.ClipboardHistoryView_iconPinnedClip, 0)
clipboardViewAttr.recycle()
val keyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardView, defStyle, R.style.KeyboardView)
keyBackgroundId = keyboardViewAttr.getResourceId(
R.styleable.KeyboardView_keyBackground, 0)
val keyboardViewAttr = context.obtainStyledAttributes(attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView)
keyBackgroundId = keyboardViewAttr.getResourceId(R.styleable.KeyboardView_keyBackground, 0)
functionalKeyBackgroundId = keyboardViewAttr.getResourceId(
R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId)
keyboardViewAttr.recycle()
@ -64,8 +58,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val res = context.resources
// The main keyboard expands to the entire this {@link KeyboardView}.
val width = (ResourceUtils.getDefaultKeyboardWidth(res)
+ paddingLeft + paddingRight)
val width = (ResourceUtils.getDefaultKeyboardWidth(res) + paddingLeft + paddingRight)
val height = (ResourceUtils.getDefaultKeyboardHeight(res)
+ res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height)
+ paddingTop + paddingBottom)
@ -74,6 +67,8 @@ class ClipboardHistoryView @JvmOverloads constructor(
override fun onFinishInflate() {
super.onFinishInflate()
val colors = Settings.getInstance().current.mColors
colors.setKeyboardBackground(this)
clipboardAdapter = ClipboardAdapter(clipboardLayoutParams, this).apply {
itemBackgroundId = keyBackgroundId
pinnedIconResId = pinIconId
@ -82,8 +77,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
clipboardRecyclerView = findViewById<ClipboardHistoryRecyclerView>(R.id.clipboard_list).apply {
val colCount = resources.getInteger(R.integer.config_clipboard_keyboard_col_count)
layoutManager = StaggeredGridLayoutManager(colCount, StaggeredGridLayoutManager.VERTICAL)
val dividerHeight = resources.getDimensionPixelSize(R.dimen.config_clipboard_divider_height)
addItemDecoration(ClipboardHistoryRecyclerView.BottomDividerItemDecoration(dividerHeight, dividerColor))
@Suppress("deprecation") // "no cache" should be fine according to warning in https://developer.android.com/reference/android/view/ViewGroup#setPersistentDrawingCache(int)
persistentDrawingCache = PERSISTENT_NO_CACHE
clipboardLayoutParams.setListProperties(this)
placeholderView = this@ClipboardHistoryView.placeholderView
@ -100,21 +94,15 @@ class ClipboardHistoryView @JvmOverloads constructor(
clearKey = findViewById<ImageButton>(R.id.clipboard_clear).apply {
setOnTouchListener(this@ClipboardHistoryView)
setOnClickListener(this@ClipboardHistoryView)
colorFilter = colors.keyTextFilter
}
val colors = Settings.getInstance().current.mColors
clearKey.colorFilter = colors.keyTextFilter
val colorBackground = colors.keyboardBackground
if (colorBackground != null)
background = colorBackground
else
background.colorFilter = colors.backgroundFilter
}
private fun setupAlphabetKey(key: TextView?, label: String, params: KeyDrawParams) {
key?.apply {
text = label
typeface = params.mTypeface
Settings.getInstance().current.mColors.setBackgroundColor(this.background, Colors.TYPE_FUNCTIONAL)
Settings.getInstance().current.mColors.setBackgroundColor(this.background, BackgroundType.FUNCTIONAL)
setTextColor(params.mFunctionalTextColor)
setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize.toFloat())
}
@ -225,8 +213,8 @@ class ClipboardHistoryView @JvmOverloads constructor(
clipboardRecyclerView.smoothScrollToPosition(at)
}
override fun onClipboardHistoryEntriesRemoved(position: Int, count: Int) {
clipboardAdapter.notifyItemRangeRemoved(position, count)
override fun onClipboardHistoryEntriesRemoved(pos: Int, count: Int) {
clipboardAdapter.notifyItemRangeRemoved(pos, count)
}
override fun onClipboardHistoryEntryMoved(from: Int, to: Int) {

View file

@ -166,7 +166,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
* {@inheritDoc}
*/
@Override
public void setKeyboard(final Keyboard keyboard) {
public void setKeyboard(@NonNull final Keyboard keyboard) {
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
mMoreKeysKeyboardCache.clear();
@ -321,22 +321,12 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
private void registerPress(final Key key) {
// Do not trigger key-down effect right now in case this is actually a fling action.
mPendingKeyDown = new Runnable() {
@Override
public void run() {
callListenerOnPressKey(key);
}
};
mPendingKeyDown = () -> callListenerOnPressKey(key);
mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
}
private void registerLongPress(final Key key) {
mPendingLongPress = new Runnable() {
@Override
public void run() {
onLongPressed(key);
}
};
mPendingLongPress = () -> onLongPressed(key);
mHandler.postDelayed(mPendingLongPress, getLongPressTimeout());
}

View file

@ -19,7 +19,6 @@ package org.dslul.openboard.inputmethod.keyboard.emoji;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
@ -47,6 +46,7 @@ import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet;
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype;
import org.dslul.openboard.inputmethod.latin.common.BackgroundType;
import org.dslul.openboard.inputmethod.latin.common.Colors;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
@ -133,7 +133,7 @@ public final class EmojiPalettesView extends LinearLayout
R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor(
R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
mCategoryPageIndicatorBackground = Settings.getInstance().getCurrent().mColors.adjustedBackground; //emojiPalettesViewAttr.getColor(R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0);
mCategoryPageIndicatorBackground = Settings.getInstance().getCurrent().mColors.getAdjustedBackground(); //emojiPalettesViewAttr.getColor(R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0);
emojiPalettesViewAttr.recycle();
mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener();
mEmojiLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
@ -161,9 +161,7 @@ public final class EmojiPalettesView extends LinearLayout
// TODO: Replace background color with its own setting rather than using the
// category page indicator background as a workaround.
iconView.setBackgroundColor(mCategoryPageIndicatorBackground);
// todo: this doesn't get applied for holo, what could cause this?
// very interesting: in onTabChanged it's applied
iconView.setColorFilter(Settings.getInstance().getCurrent().mColors.keyTextFilter);
iconView.setColorFilter(Settings.getInstance().getCurrent().mColors.getKeyTextFilter());
iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId));
iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId));
tspec.setIndicator(iconView);
@ -190,7 +188,7 @@ public final class EmojiPalettesView extends LinearLayout
tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId);
tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId);
tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId);
tabWidget.setBackgroundColor(colors.accent);
tabWidget.setBackgroundColor(colors.getAccent());
}
mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
@ -272,15 +270,11 @@ public final class EmojiPalettesView extends LinearLayout
mEmojiLayoutParams.setKeyProperties(mSpacebar);
mSpacebarIcon = findViewById(R.id.emoji_keyboard_space_icon);
colors.setBackgroundColor(mAlphabetKeyLeft.getBackground(), Colors.TYPE_FUNCTIONAL);
colors.setBackgroundColor(mDeleteKey.getBackground(), Colors.TYPE_FUNCTIONAL);
colors.setBackgroundColor(mSpacebar.getBackground(), Colors.TYPE_SPACE);
final Drawable background = colors.getKeyboardBackground();
if (background != null)
setBackground(background);
else
getBackground().setColorFilter(colors.backgroundFilter);
mEmojiCategoryPageIndicatorView.setColors(colors.accent, colors.adjustedBackground);
colors.setBackgroundColor(mAlphabetKeyLeft.getBackground(), BackgroundType.FUNCTIONAL);
colors.setBackgroundColor(mDeleteKey.getBackground(), BackgroundType.FUNCTIONAL);
colors.setBackgroundColor(mSpacebar.getBackground(), BackgroundType.SPACE);
colors.setKeyboardBackground(this);
mEmojiCategoryPageIndicatorView.setColors(colors.getAccent(), colors.getAdjustedBackground());
}
@Override
@ -302,10 +296,10 @@ public final class EmojiPalettesView extends LinearLayout
}
final Colors colors = Settings.getInstance().getCurrent().mColors;
if (mCurrentTab != null)
mCurrentTab.setColorFilter(colors.keyTextFilter);
mCurrentTab.setColorFilter(colors.getKeyTextFilter());
mCurrentTab = (ImageView) mTabHost.getCurrentTabView();
// mCurrentTab.setColorFilter(colors.accentColorFilter); // todo (later): doesn't work properly, because enabled drawable is blue -> adjust
mCurrentTab.setColorFilter(colors.accent);
// mCurrentTab.setColorFilter(colors.accentColorFilter); not working because of lxx icon color
mCurrentTab.setColorFilter(colors.getAccent());
}
/**

View file

@ -65,10 +65,10 @@ public class GestureFloatingTextDrawingPreview extends AbstractDrawingPreview {
final Colors colors = Settings.getInstance().getCurrent().mColors;
mGesturePreviewTextSize = mainKeyboardViewAttr.getDimensionPixelSize(
R.styleable.MainKeyboardView_gestureFloatingPreviewTextSize, 0);
mGesturePreviewTextColor = colors.keyText; //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_gestureFloatingPreviewTextColor, 0);
mGesturePreviewTextColor = colors.getKeyText(); //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_gestureFloatingPreviewTextColor, 0);
mGesturePreviewTextOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
R.styleable.MainKeyboardView_gestureFloatingPreviewTextOffset, 0);
mGesturePreviewColor = colors.adjustedBackground; //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_gestureFloatingPreviewColor, 0);
mGesturePreviewColor = colors.getAdjustedBackground(); //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_gestureFloatingPreviewColor, 0);
mGesturePreviewHorizontalPadding = mainKeyboardViewAttr.getDimension(
R.styleable.MainKeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
mGesturePreviewVerticalPadding = mainKeyboardViewAttr.getDimension(

View file

@ -52,7 +52,7 @@ final class GestureTrailDrawingParams {
public final int mTrailLingerDuration;
public GestureTrailDrawingParams(final TypedArray mainKeyboardViewAttr) {
mTrailColor = Settings.getInstance().getCurrent().mColors.accent; //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_gestureTrailColor, 0);
mTrailColor = Settings.getInstance().getCurrent().mColors.getAccent(); //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_gestureTrailColor, 0);
mTrailStartWidth = mainKeyboardViewAttr.getDimension(
R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f);
mTrailEndWidth = mainKeyboardViewAttr.getDimension(

View file

@ -118,7 +118,7 @@ public final class KeyPreviewChoreographer {
final boolean hasMoreKeys = (key.getMoreKeys() != null);
keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition);
final Colors colors = Settings.getInstance().getCurrent().mColors;
keyPreviewView.getBackground().setColorFilter(colors.adjustedBackgroundFilter);
keyPreviewView.getBackground().setColorFilter(colors.getAdjustedBackgroundFilter());
// The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset.
final int previewY = key.getY() - previewHeight + key.getHeight() - mParams.mPreviewOffset

View file

@ -128,19 +128,19 @@ public final class KeyVisualAttributes {
R.styleable.Keyboard_Key_keyPreviewTextRatio);
final Colors colors = Settings.getInstance().getCurrent().mColors;
mTextColor = colors.keyText; //keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
mTextColor = colors.getKeyText(); //keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
mTextInactivatedColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
// todo: maybe a separate color?
mFunctionalTextColor = colors.keyText; //keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0);
mHintLetterColor = colors.keyHintText; //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
mHintLabelColor = colors.keyText; //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
mFunctionalTextColor = colors.getKeyText(); //keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0);
mHintLetterColor = colors.getKeyHintText(); //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
mHintLabelColor = colors.getKeyText(); //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
mShiftedLetterHintInactivatedColor = keyAttr.getColor(
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
mShiftedLetterHintActivatedColor = keyAttr.getColor(
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
// todo: maybe a separate color?
mPreviewTextColor = colors.keyText; //keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0);
mPreviewTextColor = colors.getKeyText(); //keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0);
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f);

View file

@ -167,8 +167,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
mParams.mId = id;
final XmlResourceParser parser = mResources.getXml(xmlId);
try {
try (XmlResourceParser parser = mResources.getXml(xmlId)) {
parseKeyboard(parser);
} catch (XmlPullParserException e) {
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
@ -176,8 +175,6 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
} catch (IOException e) {
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
throw new RuntimeException(e.getMessage(), e);
} finally {
parser.close();
}
return this;
}
@ -481,9 +478,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
final int width = (int)keyWidth;
final int height = row.getRowHeight();
final String hintLabel = moreKeySpecs != null ? "\u25E5" : null;
final KeyboardParams params = mParams;
final Key key = new Key(label, code, outputText, hintLabel, moreKeySpecs,
labelFlags, backgroundType, x, y, width, height, params);
labelFlags, backgroundType, x, y, width, height, mParams);
endKey(key);
row.advanceXPos(keyWidth);
}
@ -555,7 +551,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
final TypedArray keyboardAttr = mResources.obtainAttributes(
attr, R.styleable.Keyboard_Include);
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
int keyboardLayout = 0;
final int keyboardLayout;
try {
XmlParseUtils.checkAttributeExists(
keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
@ -578,15 +574,13 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
mResources.getResourceEntryName(keyboardLayout));
}
final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
try {
try (XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout)) {
parseMerge(parserForInclude, row, skip);
} finally {
if (row != null) {
// Restore Row attributes.
row.popRowAttributes();
}
parserForInclude.close();
}
}
@ -716,7 +710,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
&& keyboardThemeMacthed && modeMatched && navigateNextMatched
&& navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched
&& hasShortcutKeyMatched && numberRowEnabledMatched && languageSwitchKeyEnabledMatched
&& hasShortcutKeyMatched && numberRowEnabledMatched && languageSwitchKeyEnabledMatched
&& emojiKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
&& localeCodeMatched && languageCodeMatched && countryCodeMatched
&& splitLayoutMatched && oneHandedModeEnabledMatched;

View file

@ -21,10 +21,14 @@ import android.content.res.Resources;
import android.text.TextUtils;
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale;
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Locale;
// TODO: Make this an immutable class.
@ -39,7 +43,7 @@ public final class KeyboardTextsSet {
private Resources mResources;
private Locale mResourceLocale;
private String mResourcePackageName;
private String[] mTextsTable;
private final ArrayList<String[]> mTextsTables = new ArrayList<>();
public void setLocale(final Locale locale, final Context context) {
final Resources res = context.getResources();
@ -56,11 +60,25 @@ public final class KeyboardTextsSet {
// Null means the current system locale.
mResourceLocale = SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale;
mResourcePackageName = resourcePackageName;
mTextsTable = KeyboardTextsTable.getTextsTable(locale);
mTextsTables.clear();
if (Settings.getInstance().getCurrent().mShowAllMoreKeys) {
mTextsTables.add(KeyboardTextsTable.getTextsTable(new Locale(SubtypeLocaleUtils.NO_LANGUAGE)));
return;
}
mTextsTables.add(KeyboardTextsTable.getTextsTable(locale));
if (locale != RichInputMethodManager.getInstance().getCurrentSubtypeLocale())
return; // emojiCategory calls this several times with "zz" locale
for (final Locale secondaryLocale : Settings.getInstance().getCurrent().mSecondaryLocales) {
mTextsTables.add(KeyboardTextsTable.getTextsTable(secondaryLocale));
}
}
private String getTextInternal(final String name, final int localeIndex) {
return KeyboardTextsTable.getText(name, mTextsTables.get(localeIndex));
}
public String getText(final String name) {
return KeyboardTextsTable.getText(name, mTextsTable);
return getTextInternal(name, 0); // only used for emoji and clipboard keyboards
}
private static int searchTextNameEnd(final String text, final int start) {
@ -77,13 +95,47 @@ public final class KeyboardTextsSet {
}
// TODO: Resolve text reference when creating {@link KeyboardTextsTable} class.
// todo: this style of merging for different locales it not good, but how to do it better?
public String resolveTextReference(final String rawText) {
if (TextUtils.isEmpty(rawText)) {
return null;
}
if (mTextsTables.size() == 1 || !rawText.startsWith("!text/more")) {
// no need for locale-specific stuff, as they are used for moreKeys only
String text = resolveTextReferenceInternal(rawText, 0);
if (text.isEmpty())
return null;
return text;
}
// get for all languages and merge if necessary
// this is considerably slower than the simple version above, but still for all ~60 calls
// when creation a keyboard, that's only a few ms on S4 mini -> should be acceptable
final ArrayList<String> texts = new ArrayList<>(mTextsTables.size());
for (int i = 0; i < mTextsTables.size(); i++) {
final String text = resolveTextReferenceInternal(rawText, i);
if (text.length() == 0)
continue;
texts.add(text);
}
if (texts.isEmpty())
return null;
if (texts.size() == 1)
return texts.get(0);
final LinkedHashSet<String> moreKeys = new LinkedHashSet<>();
for (final String text : texts) {
// no thanks linter, we don't want to create an intermediate list
for (final String c : text.split(",")) {
moreKeys.add(c);
}
}
return String.join(",", moreKeys);
}
public String resolveTextReferenceInternal(final String rawText, final int localeIndex) {
int level = 0;
String text = rawText;
StringBuilder sb;
final int prefixLength = PREFIX_TEXT.length();
do {
level++;
if (level >= MAX_REFERENCE_INDIRECTION) {
@ -91,7 +143,6 @@ public final class KeyboardTextsSet {
" reference indirection: " + text);
}
final int prefixLength = PREFIX_TEXT.length();
final int size = text.length();
if (size < prefixLength) {
break;
@ -104,12 +155,12 @@ public final class KeyboardTextsSet {
if (sb == null) {
sb = new StringBuilder(text.substring(0, pos));
}
pos = expandReference(text, pos, PREFIX_TEXT, sb);
pos = expandReference(text, pos, PREFIX_TEXT, sb, localeIndex);
} else if (text.startsWith(PREFIX_RESOURCE, pos)) {
if (sb == null) {
sb = new StringBuilder(text.substring(0, pos));
}
pos = expandReference(text, pos, PREFIX_RESOURCE, sb);
pos = expandReference(text, pos, PREFIX_RESOURCE, sb, localeIndex);
} else if (c == BACKSLASH) {
if (sb != null) {
// Append both escape character and escaped character.
@ -125,25 +176,27 @@ public final class KeyboardTextsSet {
text = sb.toString();
}
} while (sb != null);
return TextUtils.isEmpty(text) ? null : text;
return text;
}
private int expandReference(final String text, final int pos, final String prefix,
final StringBuilder sb) {
final StringBuilder sb, final int localeIndex) {
final int prefixLength = prefix.length();
final int end = searchTextNameEnd(text, pos + prefixLength);
final String name = text.substring(pos + prefixLength, end);
if (prefix.equals(PREFIX_TEXT)) {
sb.append(getText(name));
sb.append(getTextInternal(name, localeIndex));
} else { // PREFIX_RESOURCE
final String resourcePackageName = mResourcePackageName;
final RunInLocale<String> getTextJob = new RunInLocale<String>() {
@Override
protected String job(final Resources res) {
// this is for identifiers in strings-action-keys.xml (100% sure nothing else?)
final int resId = res.getIdentifier(name, "string", resourcePackageName);
return res.getString(resId);
}
};
// no need to do it in locale, it's just labels
sb.append(getTextJob.runInLocale(mResources, mResourceLocale));
}
return end - 1;

View file

@ -222,7 +222,7 @@ public final class MoreKeySpec {
if (remain != null) {
list.add(remain);
}
return list.toArray(new String[list.size()]);
return list.toArray(new String[0]);
}
@NonNull
@ -247,7 +247,7 @@ public final class MoreKeySpec {
if (out == null) {
return array;
}
return out.toArray(new String[out.size()]);
return out.toArray(new String[0]);
}
public static String[] insertAdditionalMoreKeys(@Nullable final String[] moreKeySpecs,
@ -300,7 +300,7 @@ public final class MoreKeySpec {
if (out == null && moreKeysCount > 0) {
return moreKeys;
} else if (out != null && out.size() > 0) {
return out.toArray(new String[out.size()]);
return out.toArray(new String[0]);
} else {
return null;
}

View file

@ -46,7 +46,7 @@ public final class SlidingKeyInputDrawingPreview extends AbstractDrawingPreview
private final Paint mPaint = new Paint();
public SlidingKeyInputDrawingPreview(final TypedArray mainKeyboardViewAttr) {
final int previewColor = Settings.getInstance().getCurrent().mColors.accent; //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_slidingKeyInputPreviewColor, 0);
final int previewColor = Settings.getInstance().getCurrent().mColors.getAccent(); //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_slidingKeyInputPreviewColor, 0);
final float previewRadius = mainKeyboardViewAttr.getDimension(
R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0) / 2.0f;
final int PERCENTAGE_INT = 100;

View file

@ -1,368 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dslul.openboard.inputmethod.latin;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants;
import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants;
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils;
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo;
import org.dslul.openboard.inputmethod.latin.utils.MetadataFileUriGetter;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Group class for static methods to help with creation and getting of the binary dictionary
* file from the dictionary provider
*/
public final class BinaryDictionaryFileDumper {
private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName();
private static final boolean DEBUG = false;
/**
* The size of the temporary buffer to copy files.
*/
private static final int FILE_READ_BUFFER_SIZE = 8192;
// TODO: make the following data common with the native code
private static final byte[] MAGIC_NUMBER_VERSION_1 =
new byte[] { (byte)0x78, (byte)0xB1, (byte)0x00, (byte)0x00 };
private static final byte[] MAGIC_NUMBER_VERSION_2 =
new byte[] { (byte)0x9B, (byte)0xC1, (byte)0x3A, (byte)0xFE };
private static final boolean SHOULD_VERIFY_MAGIC_NUMBER =
DecoderSpecificConstants.SHOULD_VERIFY_MAGIC_NUMBER;
private static final boolean SHOULD_VERIFY_CHECKSUM =
DecoderSpecificConstants.SHOULD_VERIFY_CHECKSUM;
private static final String[] DICTIONARY_PROJECTION = {"id"};
private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt";
private static final String QUERY_PARAMETER_TRUE = "true";
private static final String QUERY_PARAMETER_DELETE_RESULT = "result";
private static final String QUERY_PARAMETER_SUCCESS = "success";
private static final String QUERY_PARAMETER_FAILURE = "failure";
// Using protocol version 2 to communicate with the dictionary pack
private static final String QUERY_PARAMETER_PROTOCOL = "protocol";
private static final String QUERY_PARAMETER_PROTOCOL_VALUE = "2";
// The path fragment to append after the client ID for dictionary info requests.
private static final String QUERY_PATH_DICT_INFO = "dict";
// The path fragment to append after the client ID for dictionary datafile requests.
private static final String QUERY_PATH_DATAFILE = "datafile";
// The path fragment to append after the client ID for updating the metadata URI.
private static final String QUERY_PATH_METADATA = "metadata";
private static final String INSERT_METADATA_CLIENT_ID_COLUMN = "clientid";
private static final String INSERT_METADATA_METADATA_URI_COLUMN = "uri";
private static final String INSERT_METADATA_METADATA_ADDITIONAL_ID_COLUMN = "additionalid";
// Prevents this class to be accidentally instantiated.
private BinaryDictionaryFileDumper() {
}
/**
* Returns a URI builder pointing to the dictionary pack.
*
* This creates a URI builder able to build a URI pointing to the dictionary
* pack content provider for a specific dictionary id.
*/
public static Uri.Builder getProviderUriBuilder(final String path) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(DictionaryPackConstants.AUTHORITY).appendPath(path);
}
/**
* Gets the content URI builder for a specified type.
*
* Supported types include QUERY_PATH_DICT_INFO, which takes the locale as
* the extraPath argument, and QUERY_PATH_DATAFILE, which needs a wordlist ID
* as the extraPath argument.
*
* @param clientId the clientId to use
* @param contentProviderClient the instance of content provider client
* @param queryPathType the path element encoding the type
* @param extraPath optional extra argument for this type (typically word list id)
* @return a builder that can build the URI for the best supported protocol version
* @throws RemoteException if the client can't be contacted
*/
private static Uri.Builder getContentUriBuilderForType(final String clientId,
final ContentProviderClient contentProviderClient, final String queryPathType,
final String extraPath) throws RemoteException {
// Check whether protocol v2 is supported by building a v2 URI and calling getType()
// on it. If this returns null, v2 is not supported.
final Uri.Builder uriV2Builder = getProviderUriBuilder(clientId);
uriV2Builder.appendPath(queryPathType);
uriV2Builder.appendPath(extraPath);
uriV2Builder.appendQueryParameter(QUERY_PARAMETER_PROTOCOL,
QUERY_PARAMETER_PROTOCOL_VALUE);
if (null != contentProviderClient.getType(uriV2Builder.build())) return uriV2Builder;
// Protocol v2 is not supported, so create and return the protocol v1 uri.
return getProviderUriBuilder(extraPath);
}
/**
* Queries a content provider for the list of word lists for a specific locale
* available to copy into Latin IME.
*/
private static List<WordListInfo> getWordListWordListInfos(final Locale locale,
final Context context, final boolean hasDefaultWordList) {
final String clientId = context.getString(R.string.dictionary_pack_client_id);
final ContentProviderClient client = context.getContentResolver().
acquireContentProviderClient(getProviderUriBuilder("").build());
if (null == client) return Collections.emptyList();
Cursor cursor = null;
try {
final Uri.Builder builder = getContentUriBuilderForType(clientId, client,
QUERY_PATH_DICT_INFO, locale.toString());
if (!hasDefaultWordList) {
builder.appendQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER,
QUERY_PARAMETER_TRUE);
}
final Uri queryUri = builder.build();
final boolean isProtocolV2 = (QUERY_PARAMETER_PROTOCOL_VALUE.equals(
queryUri.getQueryParameter(QUERY_PARAMETER_PROTOCOL)));
cursor = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null);
if (isProtocolV2 && null == cursor) {
reinitializeClientRecordInDictionaryContentProvider(context, client, clientId);
cursor = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null);
}
if (null == cursor) return Collections.emptyList();
if (cursor.getCount() <= 0 || !cursor.moveToFirst()) {
return Collections.emptyList();
}
final ArrayList<WordListInfo> list = new ArrayList<>();
do {
final String wordListId = cursor.getString(0);
final String wordListLocale = cursor.getString(1);
final String wordListRawChecksum = cursor.getString(2);
if (TextUtils.isEmpty(wordListId)) continue;
list.add(new WordListInfo(wordListId, wordListLocale, wordListRawChecksum));
} while (cursor.moveToNext());
return list;
} catch (RemoteException e) {
// The documentation is unclear as to in which cases this may happen, but it probably
// happens when the content provider got suddenly killed because it crashed or because
// the user disabled it through Settings.
Log.e(TAG, "RemoteException: communication with the dictionary pack cut", e);
return Collections.emptyList();
} catch (Exception e) {
// A crash here is dangerous because crashing here would brick any encrypted device -
// we need the keyboard to be up and working to enter the password, so we don't want
// to die no matter what. So let's be as safe as possible.
Log.e(TAG, "Unexpected exception communicating with the dictionary pack", e);
return Collections.emptyList();
} finally {
if (null != cursor) {
cursor.close();
}
client.release();
}
}
/**
* Helper method to encapsulate exception handling.
*/
private static AssetFileDescriptor openAssetFileDescriptor(
final ContentProviderClient providerClient, final Uri uri) {
try {
return providerClient.openAssetFile(uri, "r");
} catch (FileNotFoundException e) {
// I don't want to log the word list URI here for security concerns. The exception
// contains the name of the file, so let's not pass it to Log.e here.
Log.e(TAG, "Could not find a word list from the dictionary provider."
/* intentionally don't pass the exception (see comment above) */);
return null;
} catch (RemoteException e) {
Log.e(TAG, "Can't communicate with the dictionary pack", e);
return null;
}
}
public static boolean reportBrokenFileToDictionaryProvider(
final ContentProviderClient providerClient, final String clientId,
final String wordlistId) {
try {
final Uri.Builder wordListUriBuilder = getContentUriBuilderForType(clientId,
providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */);
wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
QUERY_PARAMETER_FAILURE);
if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) {
Log.e(TAG, "Unable to delete a word list.");
}
} catch (RemoteException e) {
Log.e(TAG, "Communication with the dictionary provider was cut", e);
return false;
}
return true;
}
// Ideally the two following methods should be merged, but AssetFileDescriptor does not
// implement Closeable although it does implement #close(), and Java does not have
// structural typing.
private static void closeAssetFileDescriptorAndReportAnyException(
final AssetFileDescriptor file) {
try {
if (null != file) file.close();
} catch (Exception e) {
Log.e(TAG, "Exception while closing a file", e);
}
}
private static void closeCloseableAndReportAnyException(final Closeable file) {
try {
if (null != file) file.close();
} catch (Exception e) {
Log.e(TAG, "Exception while closing a file", e);
}
}
/**
* Copies the data in an input stream to a target file if the magic number matches.
*
* If the magic number does not match the expected value, this method throws an
* IOException. Other usual conditions for IOException or FileNotFoundException
* also apply.
*
* @param input the stream to be copied.
* @param output an output stream to copy the data to.
*/
public static void checkMagicAndCopyFileTo(final BufferedInputStream input,
final BufferedOutputStream output) throws IOException {
// Check the magic number
final int length = MAGIC_NUMBER_VERSION_2.length;
final byte[] magicNumberBuffer = new byte[length];
final int readMagicNumberSize = input.read(magicNumberBuffer, 0, length);
if (readMagicNumberSize < length) {
throw new IOException("Less bytes to read than the magic number length");
}
if (SHOULD_VERIFY_MAGIC_NUMBER) {
if (!Arrays.equals(MAGIC_NUMBER_VERSION_2, magicNumberBuffer)) {
if (!Arrays.equals(MAGIC_NUMBER_VERSION_1, magicNumberBuffer)) {
throw new IOException("Wrong magic number for downloaded file");
}
}
}
output.write(magicNumberBuffer);
// Actually copy the file
final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) {
output.write(buffer, 0, readBytes);
}
input.close();
}
private static void reinitializeClientRecordInDictionaryContentProvider(final Context context,
final ContentProviderClient client, final String clientId) throws RemoteException {
final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context);
Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : MetadataFileUri = "
+ metadataFileUri);
final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context);
// Tell the content provider to reset all information about this client id
final Uri metadataContentUri = getProviderUriBuilder(clientId)
.appendPath(QUERY_PATH_METADATA)
.appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE)
.build();
client.delete(metadataContentUri, null, null);
// Update the metadata URI
final ContentValues metadataValues = new ContentValues();
metadataValues.put(INSERT_METADATA_CLIENT_ID_COLUMN, clientId);
metadataValues.put(INSERT_METADATA_METADATA_URI_COLUMN, metadataFileUri);
metadataValues.put(INSERT_METADATA_METADATA_ADDITIONAL_ID_COLUMN, metadataAdditionalId);
client.insert(metadataContentUri, metadataValues);
// Update the dictionary list.
final Uri dictionaryContentUriBase = getProviderUriBuilder(clientId)
.appendPath(QUERY_PATH_DICT_INFO)
.appendQueryParameter(QUERY_PARAMETER_PROTOCOL, QUERY_PARAMETER_PROTOCOL_VALUE)
.build();
final ArrayList<DictionaryInfo> dictionaryList =
DictionaryInfoUtils.getCurrentDictionaryFileNameAndVersionInfo(context);
final int length = dictionaryList.size();
for (int i = 0; i < length; ++i) {
final DictionaryInfo info = dictionaryList.get(i);
Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : Insert " + info);
client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId),
info.toContentValues());
}
// Read from metadata file in resources to get the baseline dictionary info.
// This ensures we start with a sane list of available dictionaries.
final int metadataResourceId = context.getResources().getIdentifier("metadata",
"raw", DictionaryInfoUtils.RESOURCE_PACKAGE_NAME);
if (metadataResourceId == 0) {
Log.w(TAG, "Missing metadata.json resource");
return;
}
InputStream inputStream = null;
try {
inputStream = context.getResources().openRawResource(metadataResourceId);
} catch (Exception e) {
Log.w(TAG, "Failed to read metadata.json from resources", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close metadata.json", e);
}
}
}
}
/**
* Initialize a client record with the dictionary content provider.
*
* This merely acquires the content provider and calls
* #reinitializeClientRecordInDictionaryContentProvider.
*
* @param context the context for resources and providers.
* @param clientId the client ID to use.
*/
public static void initializeClientRecordHelper(final Context context, final String clientId) {
try {
final ContentProviderClient client = context.getContentResolver().
acquireContentProviderClient(getProviderUriBuilder("").build());
if (null == client) return;
reinitializeClientRecordInDictionaryContentProvider(context, client, clientId);
} catch (RemoteException e) {
Log.e(TAG, "Cannot contact the dictionary content provider", e);
}
}
}

View file

@ -16,7 +16,7 @@
package org.dslul.openboard.inputmethod.latin;
import static org.dslul.openboard.inputmethod.latin.settings.LanguageFakeSettingsFragmentKt.USER_DICTIONARY_SUFFIX;
import static org.dslul.openboard.inputmethod.latin.settings.LanguageSettingsFragmentKt.USER_DICTIONARY_SUFFIX;
import android.content.Context;
import android.content.SharedPreferences;
@ -69,7 +69,7 @@ final public class BinaryDictionaryGetter {
public static final String ASSETS_DICTIONARY_FOLDER = "dicts";
// The key considered to read the version attribute in a dictionary file.
private static String VERSION_KEY = "version";
private static final String VERSION_KEY = "version";
// Prevents this from being instantiated
private BinaryDictionaryGetter() {}
@ -187,7 +187,7 @@ final public class BinaryDictionaryGetter {
DictionaryInfoUtils.getCategoryFromFileName(wordList.getName());
final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
if (null == currentBestMatch || currentBestMatch.mMatchLevel <= matchLevel) {
// todo: not nice, related to getDictionaryFiles todo
// todo: not nice, related to todo in getDictionaryFiles
// this is so user-added main dict has priority over internal main dict
// actually any user-added dict has priority, but there aren't any other built-in types
if (wordList.getName().endsWith(USER_DICTIONARY_SUFFIX) || currentBestMatch == null)
@ -206,38 +206,6 @@ final public class BinaryDictionaryGetter {
return result;
}
// ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since
// those do not include whitelist entries, the new code with an old version of the dictionary
// would lose whitelist functionality.
private static boolean hackCanUseDictionaryFile(final File file) {
if (!SHOULD_USE_DICT_VERSION) {
return true;
}
try {
// Read the version of the file
final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file);
final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY);
if (null == version) {
// No version in the options : the format is unexpected
return false;
}
// Version 18 is the first one to include the whitelist
// Obviously this is a big ## HACK ##
return Integer.parseInt(version) >= 18;
} catch (java.io.FileNotFoundException e) {
return false;
} catch (java.io.IOException e) {
return false;
} catch (NumberFormatException e) {
return false;
} catch (BufferUnderflowException e) {
return false;
} catch (UnsupportedFormatException e) {
return false;
}
}
/**
* Returns a list of file addresses for a given locale, trying relevant methods in order.
*
@ -253,7 +221,7 @@ final public class BinaryDictionaryGetter {
// todo: the way of using assets and cached lists should be improved, so that the assets file
// doesn't need to be in cached dir just for checking whether it's a good match
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
final Context context, boolean notifyDictionaryPackForUpdates, final boolean weakMatchAcceptable) {
final Context context, final boolean weakMatchAcceptable) {
loadDictionaryFromAssets(locale.toString(), context, weakMatchAcceptable); // will copy dict to cached word lists if not existing
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context, weakMatchAcceptable);
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
@ -264,10 +232,7 @@ final public class BinaryDictionaryGetter {
// cachedWordLists may not be null, see doc for getCachedDictionaryList
for (final File f : cachedWordLists) {
final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
// remove the hack that requires version 18
// danger of getting an old version is rather low, and user-added dictionaries
// will usually not work as they will likely start at version 1
final boolean canUse = f.canRead(); // && hackCanUseDictionaryFile(f);
final boolean canUse = f.canRead();
if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
foundMainDict = true;
}
@ -276,24 +241,16 @@ final public class BinaryDictionaryGetter {
final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath());
if (null != afa) fileList.add(afa);
} else {
Log.e(TAG, "Found a cached dictionary file for " + locale.toString()
+ " but cannot read or use it");
Log.e(TAG, "Found a cached dictionary file for " + locale + " but cannot read or use it");
}
}
if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
final File dict = loadDictionaryFromAssets(locale.toString(), context, weakMatchAcceptable);
final AssetFileAddress fallbackAsset;
if (dict == null) {
// fall back to the old way (maybe remove? will not work if files are compressed)
final int fallbackResId =
DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
fallbackAsset = loadFallbackResource(context, fallbackResId);
} else {
fallbackAsset = AssetFileAddress.makeFromFileName(dict.getPath());
}
if (null != fallbackAsset) {
fileList.add(fallbackAsset);
if (dict != null) {
final AssetFileAddress fallbackAsset = AssetFileAddress.makeFromFileName(dict.getPath());
if (fallbackAsset != null)
fileList.add(fallbackAsset);
}
}

View file

@ -79,6 +79,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
private final Object mLock = new Object();
// library does not deal well with ngram history for auto-capitalized words, so we adjust the ngram
// context to store next word suggestions for such cases
private boolean mTryChangingWords = false;
private String mChangeFrom = "";
private String mChangeTo = "";
public static final Map<String, Class<? extends ExpandableBinaryDictionary>>
DICT_TYPE_TO_CLASS = new HashMap<>();
@ -95,7 +100,13 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// todo: these caches are never even set, as the corresponding functions are not called...
// and even if they were set, one is only written, but never read, and the other one
// is only read and thus empty and useless -> why? seems they are not needed anyway
// is only read and thus empty and useless -> why?
// anyway, we could just set the same cache using the set functions
// but before doing this, check the potential performance gains
// i.e. how long does a "isValidWord" check take -> on S4 mini 300 µs per dict if ok, but
// sometimes it can also be a few ms
// os if the spell checker is enabled, it's definitely reasonable to cache the results
// but this needs to be done internally, as it should be by language
private LruCache<String, Boolean> mValidSpellingWordReadCache;
private LruCache<String, Boolean> mValidSpellingWordWriteCache;
@ -136,13 +147,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
* A group of dictionaries that work together for a single language.
*/
private static class DictionaryGroup {
// TODO: Add null analysis annotations.
// TODO: Run evaluation to determine a reasonable value for these constants. The current
// values are ad-hoc and chosen without any particular care or methodology.
public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f;
public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f;
public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f;
private static final int MAX_CONFIDENCE = 2;
private static final int MIN_CONFIDENCE = 0;
@ -186,26 +190,25 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
}
// TODO: might need some more tuning, maybe more confidence steps
// todo: might need some more tuning, maybe more confidence steps
private void updateWeights() {
mWeightForTypingInLocale = 1f - 0.15f * (MAX_CONFIDENCE - mConfidence);
mWeightForGesturingInLocale = 1f - 0.05f * (MAX_CONFIDENCE - mConfidence);
}
public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
public float mWeightForTypingInLocale = 1f;
public float mWeightForGesturingInLocale = 1f;
public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
new ConcurrentHashMap<>();
public DictionaryGroup() {
this(null /* locale */, null /* mainDict */, null /* account */,
Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */);
this(null /* locale */, null /* mainDict */, null /* account */, Collections.emptyMap() /* subDicts */);
}
public DictionaryGroup(@Nullable final Locale locale,
@Nullable final Dictionary mainDict,
@Nullable final String account,
final Map<String, ExpandableBinaryDictionary> subDicts) {
@NonNull final Map<String, ExpandableBinaryDictionary> subDicts) {
mLocale = locale;
mAccount = account;
// The main dictionary can be asynchronously loaded.
@ -215,13 +218,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
}
private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) {
if (dict != null) {
mSubDictMap.put(dictType, dict);
}
private void setSubDict(@NonNull final String dictType, @NonNull final ExpandableBinaryDictionary dict) {
mSubDictMap.put(dictType, dict);
}
public void setMainDict(final Dictionary mainDict) {
public void setMainDict(@Nullable final Dictionary mainDict) {
// Close old dictionary if exists. Main dictionary can be assigned multiple times.
final Dictionary oldDict = mMainDict;
mMainDict = mainDict;
@ -230,18 +231,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
}
public Dictionary getDict(final String dictType) {
public @Nullable Dictionary getDict(@NonNull final String dictType) {
if (Dictionary.TYPE_MAIN.equals(dictType)) {
return mMainDict;
}
return getSubDict(dictType);
}
public ExpandableBinaryDictionary getSubDict(final String dictType) {
public @Nullable ExpandableBinaryDictionary getSubDict(@NonNull final String dictType) {
return mSubDictMap.get(dictType);
}
public boolean hasDict(final String dictType, @Nullable final String account) {
public boolean hasDict(@NonNull final String dictType, @Nullable final String account) {
if (Dictionary.TYPE_MAIN.equals(dictType)) {
return mMainDict != null;
}
@ -255,7 +256,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
return mSubDictMap.containsKey(dictType);
}
public void closeDict(final String dictType) {
public void closeDict(@NonNull final String dictType) {
final Dictionary dict;
if (Dictionary.TYPE_MAIN.equals(dictType)) {
dict = mMainDict;
@ -480,13 +481,8 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
final List<Locale> locales, final DictionaryInitializationListener listener) {
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
@Override
public void run() {
doReloadUninitializedMainDictionaries(
context, locales, listener, latchForWaitingLoadingMainDictionary);
}
});
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() ->
doReloadUninitializedMainDictionaries(context, locales, listener, latchForWaitingLoadingMainDictionary));
}
void doReloadUninitializedMainDictionaries(final Context context, final List<Locale> locales,
@ -674,7 +670,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
private void addToPersonalDictionaryIfInvalidButInHistory(String suggestion, boolean[] validWordForDictionary) {
// we need one clearly preferred group to assign it to the correct language
int highestGroup = -1;
int highestGroupConfidence = -1;
// require confidence to be MAX_CONFIDENCE, to be sure about language
// since the word is unknown, confidence has already been reduced, but after a first miss
// confidence is actually reduced to MAX_CONFIDENCE if it was larger
int highestGroupConfidence = DictionaryGroup.MAX_CONFIDENCE - 1;
for (int i = 0; i < mDictionaryGroups.size(); i ++) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
if (dictionaryGroup.mConfidence > highestGroupConfidence) {
@ -701,13 +700,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
if (userDict != null && userHistoryDict.isInDictionary(suggestion)) {
if (userDict.isInDictionary(suggestion)) // is this check necessary?
return;
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
@Override
public void run() {
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() ->
UserDictionary.Words.addWord(userDict.mContext, suggestion,
250 /*FREQUENCY_FOR_USER_DICTIONARY_ADDS*/, null, dictionaryGroup.mLocale);
}
});
250 /*FREQUENCY_FOR_USER_DICTIONARY_ADDS*/, null, dictionaryGroup.mLocale));
}
}
@ -747,6 +742,8 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
if (maxFreq == 0 && blockPotentiallyOffensive) {
return;
}
if (mTryChangingWords)
mTryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(mChangeFrom, mChangeTo);
final String secondWord;
if (wasAutoCapitalized) {
// used word with lower-case first letter instead of all lower-case, as auto-capitalize
@ -763,6 +760,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// If however the word is not in the dictionary, or exists as a de-capitalized word
// only, then we consider that was a lower-case word that had been auto-capitalized.
secondWord = decapitalizedWord;
mTryChangingWords = true;
mChangeFrom = word;
mChangeTo = secondWord;
}
} else {
// HACK: We'd like to avoid adding the capitalized form of common words to the User
@ -825,6 +825,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// TODO: Revise the way to fusion suggestion results.
@Override
@SuppressWarnings("unchecked")
@NonNull public SuggestionResults getSuggestionResults(ComposedData composedData,
NgramContext ngramContext, @NonNull final Keyboard keyboard,
SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId,
@ -885,12 +886,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
int sessionId, long proximityInfoHandle, float[] weightOfLangModelVsSpatialModel,
DictionaryGroup dictGroup) {
final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
final float weightForLocale = composedData.mIsBatchMode
? dictGroup.mWeightForGesturingInLocale
: dictGroup.mWeightForTypingInLocale;
for (final String dictType : ALL_DICTIONARY_TYPES) {
final Dictionary dictionary = dictGroup.getDict(dictType);
if (null == dictionary) continue;
final float weightForLocale = composedData.mIsBatchMode
? dictGroup.mWeightForGesturingInLocale
: dictGroup.mWeightForTypingInLocale;
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
dictionary.getSuggestions(composedData, ngramContext,
proximityInfoHandle, settingsValuesForSuggestion, sessionId,

View file

@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit;
* This class automatically creates and releases up to 3 facilitator instances using LRU policy.
*/
public class DictionaryFacilitatorLruCache {
private static final String TAG = "DictionaryFacilitatorLruCache";
private static final String TAG = "DictFacilitatorLruCache";
private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;

View file

@ -16,11 +16,14 @@
package org.dslul.openboard.inputmethod.latin;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import org.dslul.openboard.inputmethod.latin.makedict.DictionaryHeader;
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils;
import java.io.File;
@ -54,7 +57,7 @@ public final class DictionaryFactory {
final LinkedList<Dictionary> dictList = new LinkedList<>();
ArrayList<AssetFileAddress> assetFileList =
BinaryDictionaryGetter.getDictionaryFiles(locale, context, true, false);
BinaryDictionaryGetter.getDictionaryFiles(locale, context, false);
boolean mainFound = false;
for (AssetFileAddress fileAddress : assetFileList) {
@ -64,12 +67,18 @@ public final class DictionaryFactory {
}
}
if (!mainFound) // try again and allow weaker match
assetFileList = BinaryDictionaryGetter.getDictionaryFiles(locale, context, true, true);
assetFileList = BinaryDictionaryGetter.getDictionaryFiles(locale, context, true);
for (final AssetFileAddress f : assetFileList) {
final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(new File(f.mFilename), f.mOffset, f.mLength);
String dictType = Dictionary.TYPE_MAIN;
if (header != null) {
// make sure the suggested words dictionary has the correct type
dictType = header.mIdString.split(":")[0];
}
final ReadOnlyBinaryDictionary readOnlyBinaryDictionary =
new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength,
false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
false /* useFullEditDistance */, locale, dictType);
if (readOnlyBinaryDictionary.isValidDictionary()) {
if(locale.getLanguage().equals("ko")) {
// Use KoreanDictionary for Korean locale
@ -98,38 +107,14 @@ public final class DictionaryFactory {
public static void killDictionary(final Context context, final AssetFileAddress f) {
if (f.pointsToPhysicalFile()) {
f.deleteUnderlyingFile();
// Warn the dictionary provider if the dictionary came from there.
final ContentProviderClient providerClient;
try {
providerClient = context.getContentResolver().acquireContentProviderClient(
BinaryDictionaryFileDumper.getProviderUriBuilder("").build());
} catch (final SecurityException e) {
Log.e(TAG, "No permission to communicate with the dictionary provider", e);
return;
}
if (null == providerClient) {
Log.e(TAG, "Can't establish communication with the dictionary provider");
return;
}
final String wordlistId =
DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName());
// TODO: this is a reasonable last resort, but it is suboptimal.
// The following will remove the entry for this dictionary with the dictionary
// provider. When the metadata is downloaded again, we will try downloading it
// again.
// However, in the practice that will mean the user will find themselves without
// the new dictionary. That's fine for languages where it's included in the APK,
// but for other languages it will leave the user without a dictionary at all until
// the next update, which may be a few days away.
// Ideally, we would trigger a new download right away, and use increasing retry
// delays for this particular id/version combination.
// Then again, this is expected to only ever happen in case of human mistake. If
// the wrong file is on the server, the following is still doing the right thing.
// If it's a file left over from the last version however, it's not great.
BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider(
providerClient,
context.getString(R.string.dictionary_pack_client_id),
wordlistId);
// notify the user
// todo: use an alertDialog to avoid the toast not showing up on Android 13+
// but asyncTask doesn't work because android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
// https://stackoverflow.com/questions/7199014/show-an-alertdialog-from-a-background-thread-with-the-appcontext
final String wordlistId = DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName());
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(context, "dictionary "+wordlistId+" is invalid, deleting", Toast.LENGTH_LONG).show()
);
}
}

View file

@ -95,7 +95,6 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei
}
// If we come here none of the authorities matched the one we searched for.
// We can exit safely.
return;
} else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
&& !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
if (null == mService) {
@ -120,22 +119,6 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei
return;
}
mService.resetSuggestMainDict();
} else if (action.equals(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT)) {
if (null != mService) {
// Careful! This is returning if the service is NOT null. This is because we
// should come here instantiated by the framework in reaction to a broadcast of
// the above action, so we should gave gone through the no-args constructor.
Log.e(TAG, "Called with intent " + action + " but we have a reference to the "
+ "service: this should never happen");
return;
}
// The dictionary provider does not know about some client. We check that it's really
// us that it needs to know about, and if it's the case, we register with the provider.
final String wantedClientId =
intent.getStringExtra(DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA);
final String myClientId = context.getString(R.string.dictionary_pack_client_id);
if (!wantedClientId.equals(myClientId)) return; // Not for us
BinaryDictionaryFileDumper.initializeClientRecordHelper(context, myClientId);
}
}
}

View file

@ -34,10 +34,10 @@ import java.util.List;
* A class for detecting Emoji-Alt physical key.
*/
final class EmojiAltPhysicalKeyDetector {
private static final String TAG = "EmojiAltPhysicalKeyDetector";
private static final String TAG = "EmojiAltPhysKeyDetector";
private static final boolean DEBUG = false;
private List<EmojiHotKeys> mHotKeysList;
private final List<EmojiHotKeys> mHotKeysList;
private static class HotKeySet extends HashSet<Pair<Integer, Integer>> { }
@ -120,7 +120,7 @@ final class EmojiAltPhysicalKeyDetector {
}
public EmojiAltPhysicalKeyDetector(@NonNull final Resources resources) {
mHotKeysList = new ArrayList<EmojiHotKeys>();
mHotKeysList = new ArrayList<>();
final HotKeySet emojiSwitchSet = parseHotKeys(
resources, R.array.keyboard_switcher_emoji);
@ -192,8 +192,8 @@ final class EmojiAltPhysicalKeyDetector {
Log.w(TAG, "Expected 2 integers in " + name + "[" + i + "] : " + values[i]);
}
try {
final Integer keyCode = Integer.parseInt(valuePair[0]);
final Integer metaState = Integer.parseInt(valuePair[1]);
final int keyCode = Integer.parseInt(valuePair[0]);
final int metaState = Integer.parseInt(valuePair[1]);
final Pair<Integer, Integer> key = Pair.create(
keyCode, KeyEvent.normalizeMetaState(metaState));
keySet.add(key);

View file

@ -169,15 +169,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
private static void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) {
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
@Override
public void run() {
lock.lock();
try {
task.run();
} finally {
lock.unlock();
}
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() -> {
lock.lock();
try {
task.run();
} finally {
lock.unlock();
}
});
}
@ -199,12 +196,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
@Override
public void close() {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
closeBinaryDictionary();
}
});
asyncExecuteTaskWithWriteLock(this::closeBinaryDictionary);
}
protected Map<String, String> getHeaderAttributeMap() {
@ -220,12 +212,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
private void removeBinaryDictionary() {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
removeBinaryDictionaryLocked();
}
});
asyncExecuteTaskWithWriteLock(this::removeBinaryDictionaryLocked);
}
void removeBinaryDictionaryLocked() {
@ -248,12 +235,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
public void clear() {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
removeBinaryDictionaryLocked();
createOnMemoryBinaryDictionaryLocked();
}
asyncExecuteTaskWithWriteLock(() -> {
removeBinaryDictionaryLocked();
createOnMemoryBinaryDictionaryLocked();
});
}
@ -261,14 +245,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* Check whether GC is needed and run GC if required.
*/
public void runGCIfRequired(final boolean mindsBlockByGC) {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(mindsBlockByGC);
asyncExecuteTaskWithWriteLock(() -> {
if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(mindsBlockByGC);
});
}
@ -280,17 +261,13 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
private void updateDictionaryWithWriteLock(@NonNull final Runnable updateTask) {
reloadDictionaryIfRequired();
final Runnable task = new Runnable() {
@Override
public void run() {
if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
updateTask.run();
asyncExecuteTaskWithWriteLock(() -> {
if (getBinaryDictionary() == null) {
return;
}
};
asyncExecuteTaskWithWriteLock(task);
runGCIfRequiredLocked(true /* mindsBlockByGC */);
updateTask.run();
});
}
/**
@ -299,13 +276,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
public void addUnigramEntry(final String word, final int frequency,
final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
final boolean isPossiblyOffensive, final int timestamp) {
updateDictionaryWithWriteLock(new Runnable() {
@Override
public void run() {
addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq,
isNotAWord, isPossiblyOffensive, timestamp);
}
});
updateDictionaryWithWriteLock(() -> addUnigramLocked(word, frequency, shortcutTarget,
shortcutFreq, isNotAWord, isPossiblyOffensive, timestamp));
}
protected void addUnigramLocked(final String word, final int frequency,
@ -322,18 +294,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
public void removeUnigramEntryDynamically(final String word) {
reloadDictionaryIfRequired();
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
if (!binaryDictionary.removeUnigramEntry(word)) {
if (DEBUG) {
Log.i(TAG, "Cannot remove unigram entry: " + word);
}
asyncExecuteTaskWithWriteLock(() -> {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
if (!binaryDictionary.removeUnigramEntry(word)) {
if (DEBUG) {
Log.i(TAG, "Cannot remove unigram entry: " + word);
}
}
});
@ -345,15 +314,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
public void addNgramEntry(@NonNull final NgramContext ngramContext, final String word,
final int frequency, final int timestamp) {
reloadDictionaryIfRequired();
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addNgramEntryLocked(ngramContext, word, frequency, timestamp);
asyncExecuteTaskWithWriteLock(() -> {
if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addNgramEntryLocked(ngramContext, word, frequency, timestamp);
});
}
@ -372,19 +338,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
public void updateEntriesForWord(@NonNull final NgramContext ngramContext,
final String word, final boolean isValidWord, final int count, final int timestamp) {
updateDictionaryWithWriteLock(new Runnable() {
@Override
public void run() {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
if (!binaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word,
isValidWord, count, timestamp)) {
if (DEBUG) {
Log.e(TAG, "Cannot update counter. word: " + word
+ " context: " + ngramContext.toString());
}
updateDictionaryWithWriteLock(() -> {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
if (!binaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word,
isValidWord, count, timestamp)) {
if (DEBUG) {
Log.e(TAG, "Cannot update counter. word: " + word
+ " context: " + ngramContext);
}
}
});
@ -410,21 +373,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
@NonNull final ArrayList<WordInputEventForPersonalization> inputEvents,
final UpdateEntriesForInputEventsCallback callback) {
reloadDictionaryIfRequired();
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
try {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
binaryDictionary.updateEntriesForInputEvents(
inputEvents.toArray(
new WordInputEventForPersonalization[inputEvents.size()]));
} finally {
if (callback != null) {
callback.onFinished();
}
asyncExecuteTaskWithWriteLock(() -> {
try {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
binaryDictionary.updateEntriesForInputEvents(inputEvents.toArray(new WordInputEventForPersonalization[0]));
} finally {
if (callback != null) {
callback.onFinished();
}
}
});
@ -601,32 +559,28 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return;
}
final File dictFile = mDictFile;
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
try {
if (!dictFile.exists() || isNeededToRecreate()) {
// If the dictionary file does not exist or contents have been updated,
// generate a new one.
asyncExecuteTaskWithWriteLock(() -> {
try {
if (!dictFile.exists() || isNeededToRecreate()) {
// If the dictionary file does not exist or contents have been updated,
// generate a new one.
createNewDictionaryLocked();
} else if (getBinaryDictionary() == null) {
// Otherwise, load the existing dictionary.
loadBinaryDictionaryLocked();
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary != null && !(isValidDictionaryLocked()
// TODO: remove the check below
&& matchesExpectedBinaryDictFormatVersionForThisType(binaryDictionary.getFormatVersion()))) {
// Binary dictionary or its format version is not valid. Regenerate
// the dictionary file. createNewDictionaryLocked will remove the
// existing files if appropriate.
createNewDictionaryLocked();
} else if (getBinaryDictionary() == null) {
// Otherwise, load the existing dictionary.
loadBinaryDictionaryLocked();
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary != null && !(isValidDictionaryLocked()
// TODO: remove the check below
&& matchesExpectedBinaryDictFormatVersionForThisType(
binaryDictionary.getFormatVersion()))) {
// Binary dictionary or its format version is not valid. Regenerate
// the dictionary file. createNewDictionaryLocked will remove the
// existing files if appropriate.
createNewDictionaryLocked();
}
}
clearNeedsToRecreate();
} finally {
isReloading.set(false);
}
clearNeedsToRecreate();
} finally {
isReloading.set(false);
}
});
}
@ -636,18 +590,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
@Override
public void onFinishInput() {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
binaryDictionary.flushWithGCIfHasUpdated();
} else {
binaryDictionary.flush();
}
asyncExecuteTaskWithWriteLock(() -> {
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
binaryDictionary.flushWithGCIfHasUpdated();
} else {
binaryDictionary.flush();
}
});
}
@ -670,12 +621,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
@UsedForTesting
public void waitAllTasksForTests() {
final CountDownLatch countDownLatch = new CountDownLatch(1);
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
countDownLatch.countDown();
}
});
asyncExecuteTaskWithWriteLock(countDownLatch::countDown);
try {
countDownLatch.await();
} catch (InterruptedException e) {
@ -694,35 +640,31 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
reloadDictionaryIfRequired();
final String tag = TAG;
final String dictName = mDictName;
asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
@Override
public void run() {
Log.d(tag, "Dump dictionary: " + dictName + " for " + mLocale);
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
try {
final DictionaryHeader header = binaryDictionary.getHeader();
Log.d(tag, "Format version: " + binaryDictionary.getFormatVersion());
Log.d(tag, CombinedFormatUtils.formatAttributeMap(
header.mDictionaryOptions.mAttributes));
} catch (final UnsupportedFormatException e) {
Log.d(tag, "Cannot fetch header information.", e);
}
int token = 0;
do {
final BinaryDictionary.GetNextWordPropertyResult result =
binaryDictionary.getNextWordProperty(token);
final WordProperty wordProperty = result.mWordProperty;
if (wordProperty == null) {
Log.d(tag, " dictionary is empty.");
break;
}
Log.d(tag, wordProperty.toString());
token = result.mNextToken;
} while (token != 0);
asyncExecuteTaskWithLock(mLock.readLock(), () -> {
Log.d(tag, "Dump dictionary: " + dictName + " for " + mLocale);
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
try {
final DictionaryHeader header = binaryDictionary.getHeader();
Log.d(tag, "Format version: " + binaryDictionary.getFormatVersion());
Log.d(tag, CombinedFormatUtils.formatAttributeMap(header.mDictionaryOptions.mAttributes));
} catch (final UnsupportedFormatException e) {
Log.d(tag, "Cannot fetch header information.", e);
}
int token = 0;
do {
final BinaryDictionary.GetNextWordPropertyResult result =
binaryDictionary.getNextWordProperty(token);
final WordProperty wordProperty = result.mWordProperty;
if (wordProperty == null) {
Log.d(tag, " dictionary is empty.");
break;
}
Log.d(tag, wordProperty.toString());
token = result.mNextToken;
} while (token != 0);
});
}
@ -733,31 +675,27 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
reloadDictionaryIfRequired();
final AsyncResultHolder<WordProperty[]> result =
new AsyncResultHolder<>("WordPropertiesForSync");
asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
@Override
public void run() {
final ArrayList<WordProperty> wordPropertyList = new ArrayList<>();
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
int token = 0;
do {
// TODO: We need a new API that returns *new* un-synced data.
final BinaryDictionary.GetNextWordPropertyResult nextWordPropertyResult =
binaryDictionary.getNextWordProperty(token);
final WordProperty wordProperty = nextWordPropertyResult.mWordProperty;
if (wordProperty == null) {
break;
}
wordPropertyList.add(wordProperty);
token = nextWordPropertyResult.mNextToken;
} while (token != 0);
result.set(wordPropertyList.toArray(new WordProperty[wordPropertyList.size()]));
asyncExecuteTaskWithLock(mLock.readLock(), () -> {
final ArrayList<WordProperty> wordPropertyList = new ArrayList<>();
final BinaryDictionary binaryDictionary = getBinaryDictionary();
if (binaryDictionary == null) {
return;
}
int token = 0;
do {
// TODO: We need a new API that returns *new* un-synced data.
final BinaryDictionary.GetNextWordPropertyResult nextWordPropertyResult =
binaryDictionary.getNextWordProperty(token);
final WordProperty wordProperty = nextWordPropertyResult.mWordProperty;
if (wordProperty == null) {
break;
}
wordPropertyList.add(wordProperty);
token = nextWordPropertyResult.mNextToken;
} while (token != 0);
result.set(wordPropertyList.toArray(new WordProperty[0]));
});
// TODO: Figure out the best timeout duration for this API.
return result.get(DEFAULT_WORD_PROPERTIES_FOR_SYNC,
TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
return result.get(DEFAULT_WORD_PROPERTIES_FOR_SYNC, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
}
}

View file

@ -16,12 +16,11 @@
package org.dslul.openboard.inputmethod.latin;
import android.os.Build;
import android.text.InputType;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
import androidx.core.view.inputmethod.EditorInfoCompat;
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils;
@ -94,14 +93,10 @@ public final class InputAttributes {
}
// inputClass == InputType.TYPE_CLASS_TEXT
final int variation = inputType & InputType.TYPE_MASK_VARIATION;
final boolean flagNoSuggestions =
0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
final boolean flagMultiLine =
0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE);
final boolean flagAutoCorrect =
0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT);
final boolean flagAutoComplete =
0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
final boolean flagNoSuggestions = 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
final boolean flagMultiLine = 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE);
final boolean flagAutoCorrect = 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT);
final boolean flagAutoComplete = 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
// TODO: Have a helper method in InputTypeUtils
// Make sure that passwords are not displayed in {@link SuggestionStripView}.
@ -140,7 +135,10 @@ public final class InputAttributes {
&& InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != variation;
mNoLearning = flagNoSuggestions || (editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
mNoLearning = flagNoSuggestions || (editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0;
else
mNoLearning = flagNoSuggestions;
}
public boolean isTypeNull() {

View file

@ -2,13 +2,14 @@ package org.dslul.openboard.inputmethod.latin
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton
import org.dslul.openboard.inputmethod.keyboard.KeyboardActionListener
import org.dslul.openboard.inputmethod.latin.common.Colors
import org.dslul.openboard.inputmethod.latin.common.BackgroundType
import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.settings.Settings
@ -57,8 +58,10 @@ class KeyboardWrapperView @JvmOverloads constructor(
val colors = Settings.getInstance().current.mColors
stopOneHandedModeBtn.colorFilter = colors.keyTextFilter
switchOneHandedModeBtn.colorFilter = colors.keyTextFilter
colors.setBackgroundColor(stopOneHandedModeBtn.background, Colors.TYPE_BACKGROUND)
colors.setBackgroundColor(switchOneHandedModeBtn.background, Colors.TYPE_BACKGROUND)
colors.setBackgroundColor(stopOneHandedModeBtn.background, BackgroundType.BACKGROUND)
colors.setBackgroundColor(switchOneHandedModeBtn.background, BackgroundType.BACKGROUND)
setBackgroundColor(Color.WHITE) // otherwise background might be null
colors.setKeyboardBackground(this)
}
@SuppressLint("RtlHardcoded")
@ -120,22 +123,13 @@ class KeyboardWrapperView @JvmOverloads constructor(
buttonsLeft + (spareWidth + switchOneHandedModeBtn.measuredWidth) / 2,
2 * stopOneHandedModeBtn.measuredHeight + switchOneHandedModeBtn.measuredHeight
)
Settings.getInstance().current.mColors.keyboardBackground?.let { background = it }
}
init {
val keyboardAttr = context.obtainStyledAttributes(attrs,
R.styleable.Keyboard, defStyle, R.style.Keyboard)
@SuppressLint("CustomViewStyleable")
val keyboardAttr = context.obtainStyledAttributes(attrs, R.styleable.Keyboard, defStyle, R.style.Keyboard)
iconStopOneHandedModeId = keyboardAttr.getResourceId(R.styleable.Keyboard_iconStopOneHandedMode, 0)
iconSwitchOneHandedModeId = keyboardAttr.getResourceId(R.styleable.Keyboard_iconSwitchOneHandedMode, 0)
keyboardAttr.recycle()
val themeAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardTheme, defStyle, 0)
val keyboardViewStyleId = themeAttr.getResourceId(R.styleable.KeyboardTheme_mainKeyboardViewStyle, 0)
themeAttr.recycle()
val styleAttr = context.obtainStyledAttributes(keyboardViewStyleId, intArrayOf(android.R.attr.background))
setBackgroundResource(styleAttr.getResourceId(0, 0))
styleAttr.recycle()
}
}
}

View file

@ -63,7 +63,6 @@ import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher;
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView;
import org.dslul.openboard.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import org.dslul.openboard.inputmethod.latin.common.Colors;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
@ -160,7 +159,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
private StatsUtilsManager mStatsUtilsManager;
private final StatsUtilsManager mStatsUtilsManager;
// Working variable for {@link #startShowingInputView()} and
// {@link #onEvaluateInputViewShown()}.
private boolean mIsExecutingStartShowingInputView;
@ -213,7 +212,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
final RestartAfterDeviceUnlockReceiver mRestartAfterDeviceUnlockReceiver = new RestartAfterDeviceUnlockReceiver();
private AlertDialog mOptionsDialog; // todo: this is always null -> remove?
private AlertDialog mOptionsDialog;
private final boolean mIsHardwareAcceleratedDrawingEnabled;
@ -754,7 +753,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mInputLogic.mSuggest.setAutoCorrectionThreshold(
settingsValues.mAutoCorrectionThreshold);
}
mInputLogic.mSuggest.setPlausibilityThreshold(settingsValues.mPlausibilityThreshold);
}
/**
@ -1070,8 +1068,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
suggest.setAutoCorrectionThreshold(
currentSettingsValues.mAutoCorrectionThreshold);
}
suggest.setPlausibilityThreshold(currentSettingsValues.mPlausibilityThreshold);
switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
getCurrentRecapitalizeState());
if (needToCallLoadKeyboardLater) {
@ -1419,13 +1415,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public boolean onCustomRequest(final int requestCode) {
if (isShowingOptionDialog()) return false;
switch (requestCode) {
case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
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;
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) {
if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
mOptionsDialog = InputMethodPickerKt.createInputMethodPickerDialog(this, mRichImm, mKeyboardSwitcher.getMainKeyboardView().getWindowToken());
mOptionsDialog.show();
return true;
}
return false;
}
return false;
}
@ -1475,30 +1471,59 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return mOptionsDialog != null && mOptionsDialog.isShowing();
}
// TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
// called when language switch key is pressed (either the keyboard key, or long-press comma)
public void switchToNextSubtype() {
if (shouldSwitchToOtherInputMethods()) {
// 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);
}
}
final boolean switchSubtype = mSettings.getCurrent().mLanguageSwitchKeyToOtherSubtypes;
final boolean switchIme = mSettings.getCurrent().mLanguageSwitchKeyToOtherImes;
// switch IME if wanted and possible
if (switchIme && !switchSubtype && switchInputMethod())
return;
final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
// switch subtype if wanted and possible
if (switchSubtype && !switchIme && hasMoreThanOneSubtype) {
// switch to previous subtype if current one was used, otherwise cycle through list
mSubtypeState.switchSubtype(mRichImm);
return;
}
// language key set to switch both, or language key is not shown on keyboard -> switch both
if (hasMoreThanOneSubtype && mSubtypeState.mCurrentSubtypeHasBeenUsed) {
mSubtypeState.switchSubtype(mRichImm);
return;
}
if (shouldSwitchToOtherInputMethods()) {
final InputMethodSubtype nextSubtype = mRichImm.getNextSubtypeInThisIme(false);
// todo (later): this will switch IME if we are at the end of the list, but ideally we
// want to switch IME only if all internal subtypes are unused
// -> need to store used/unused subtypes in mSubtypeState
if (nextSubtype != null) {
switchToSubtype(nextSubtype);
return;
} else if (switchInputMethod()){
return;
}
}
mSubtypeState.switchSubtype(mRichImm);
}
private boolean switchInputMethod() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
return switchToNextInputMethod(false);
final IBinder token = getWindow().getWindow().getAttributes().token;
return mRichImm.getInputMethodManager().switchToNextInputMethod(token, false);
}
@SuppressWarnings("deprecation")
public boolean shouldSwitchToOtherInputMethods() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
return shouldOfferSwitchingToNextInputMethod();
final IBinder token = getWindow().getWindow().getAttributes().token;
if (token == null) {
return mSettings.getCurrent().mLanguageSwitchKeyToOtherImes;
}
return mRichImm.getInputMethodManager().shouldOfferSwitchingToNextInputMethod(token);
}
public void switchInputMethodAndSubtype(final InputMethodInfo imi, final InputMethodSubtype subtype) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchInputMethod(imi.getId(), subtype);
@ -1974,10 +1999,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void debugDumpStateAndCrashWithException(final String context) {
final SettingsValues settingsValues = mSettings.getCurrent();
final StringBuilder s = new StringBuilder(settingsValues.toString());
s.append("\nAttributes : ").append(settingsValues.mInputAttributes)
.append("\nContext : ").append(context);
throw new RuntimeException(s.toString());
String s = settingsValues.toString() + "\nAttributes : " + settingsValues.mInputAttributes +
"\nContext : " + context;
throw new RuntimeException(s);
}
@Override
@ -1997,32 +2021,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: Dump all settings values
}
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 IBinder token = getWindow().getWindow().getAttributes().token;
if (token == null) {
return mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList;
}
return mRichImm.getInputMethodManager().shouldOfferSwitchingToNextInputMethod(token);
}
public boolean shouldShowLanguageSwitchKey() {
// TODO: Revisit here to reorganize the settings. Probably we can/should use different
// strategy once the implementation of
// {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well.
return mSettings.getCurrent().isLanguageSwitchKeyEnabled();
}
// slightly modified from Simple Keyboard: https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/LatinIME.java
private void setNavigationBarColor() {
final SettingsValues settingsValues = mSettings.getCurrent();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || !settingsValues.mCustomNavBarColor)
return;
final int color = settingsValues.mColors.navBar;
final int color = settingsValues.mColors.getNavBar();
final Window window = getWindow().getWindow();
if (window == null)
return;

View file

@ -119,6 +119,19 @@ public class NgramContext {
mMaxPrevWordCount = maxPrevWordCount;
}
public boolean changeWordIfAfterBeginningOfSentence(final String from, final String to) {
boolean beginning = false;
for (int i = mPrevWordsCount - 1; i >= 0; i--) {
WordInfo info = mPrevWordsInfo[i];
if (beginning && TextUtils.equals(info.mWord, from)) {
mPrevWordsInfo[i] = new WordInfo(to);
return true;
}
beginning = info.mIsBeginningOfSentence;
}
return false;
}
/**
* Create next prevWordsInfo using current prevWordsInfo.
*/
@ -175,8 +188,7 @@ public class NgramContext {
}
}
}
final String[] contextStringArray = prevTermList.toArray(new String[prevTermList.size()]);
return contextStringArray;
return prevTermList.toArray(new String[prevTermList.size()]);
}
public boolean isValid() {
@ -227,7 +239,7 @@ public class NgramContext {
public int hashCode() {
int hashValue = 0;
for (final WordInfo wordInfo : mPrevWordsInfo) {
if (wordInfo == null || !WordInfo.EMPTY_WORD_INFO.equals(wordInfo)) {
if (!WordInfo.EMPTY_WORD_INFO.equals(wordInfo)) {
break;
}
hashValue ^= wordInfo.hashCode();
@ -267,7 +279,7 @@ public class NgramContext {
@Override
public String toString() {
final StringBuffer builder = new StringBuffer();
final StringBuilder builder = new StringBuilder();
for (int i = 0; i < mPrevWordsCount; i++) {
final WordInfo wordInfo = mPrevWordsInfo[i];
builder.append("PrevWord[");

View file

@ -1,651 +0,0 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dslul.openboard.inputmethod.latin;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.UserDictionary;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
import org.dslul.openboard.inputmethod.latin.common.CollectionUtils;
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
import org.dslul.openboard.inputmethod.latin.utils.ExecutorUtils;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class provides the ability to look into the system-wide "Personal dictionary". It loads the
* data once when created and reloads it when notified of changes to {@link UserDictionary}
*
* It can be used directly to validate words or expand shortcuts, and it can be used by instances
* of {@link PersonalLanguageModelHelper} that create language model files for a specific input
* locale.
*
* Note, that the initial dictionary loading happens asynchronously so it is possible (hopefully
* rarely) that {@link #isValidWord} or {@link #expandShortcut} is called before the initial load
* has started.
*
* The caller should explicitly call {@link #close} when the object is no longer needed, in order
* to release any resources and references to this object. A service should create this object in
* {@link android.app.Service#onCreate} and close it in {@link android.app.Service#onDestroy}.
*/
public class PersonalDictionaryLookup implements Closeable {
/**
* To avoid loading too many dictionary entries in memory, we cap them at this number. If
* that number is exceeded, the lowest-frequency items will be dropped. Note, there is no
* explicit cap on the number of locales in every entry.
*/
private static final int MAX_NUM_ENTRIES = 1000;
/**
* The delay (in milliseconds) to impose on reloads. Previously scheduled reloads will be
* cancelled if a new reload is scheduled before the delay expires. Thus, only the last
* reload in the series of frequent reloads will execute.
*
* Note, this value should be low enough to allow the "Add to dictionary" feature in the
* TextView correction (red underline) drop-down menu to work properly in the following case:
*
* 1. User types OOV (out-of-vocabulary) word.
* 2. The OOV is red-underlined.
* 3. User selects "Add to dictionary". The red underline disappears while the OOV is
* in a composing span.
* 4. The user taps space. The red underline should NOT reappear. If this value is very
* high and the user performs the space tap fast enough, the red underline may reappear.
*/
@UsedForTesting
static final int RELOAD_DELAY_MS = 200;
@UsedForTesting
static final Locale ANY_LOCALE = new Locale("");
private final String mTag;
private final ContentResolver mResolver;
private final String mServiceName;
/**
* Interface to implement for classes interested in getting notified of updates.
*/
public interface PersonalDictionaryListener {
void onUpdate();
}
private final Set<PersonalDictionaryListener> mListeners = new HashSet<>();
public void addListener(@NonNull final PersonalDictionaryListener listener) {
mListeners.add(listener);
}
public void removeListener(@NonNull final PersonalDictionaryListener listener) {
mListeners.remove(listener);
}
/**
* Broadcast the update to all the Locale-specific language models.
*/
@UsedForTesting
void notifyListeners() {
for (PersonalDictionaryListener listener : mListeners) {
listener.onUpdate();
}
}
/**
* Content observer for changes to the personal dictionary. It has the following properties:
* 1. It spawns off a reload in another thread, after some delay.
* 2. It cancels previously scheduled reloads, and only executes the latest.
* 3. It may be called multiple times quickly in succession (and is in fact called so
* when the dictionary is edited through its settings UI, when sometimes multiple
* notifications are sent for the edited entry, but also for the entire dictionary).
*/
private class PersonalDictionaryContentObserver extends ContentObserver implements Runnable {
public PersonalDictionaryContentObserver() {
super(null);
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
// Support pre-API16 platforms.
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "onChange() : URI = " + uri);
}
// Cancel (but don't interrupt) any pending reloads (except the initial load).
if (mReloadFuture != null && !mReloadFuture.isCancelled() &&
!mReloadFuture.isDone()) {
// Note, that if already cancelled or done, this will do nothing.
boolean isCancelled = mReloadFuture.cancel(false);
if (DebugFlags.DEBUG_ENABLED) {
if (isCancelled) {
Log.d(mTag, "onChange() : Canceled previous reload request");
} else {
Log.d(mTag, "onChange() : Failed to cancel previous reload request");
}
}
}
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "onChange() : Scheduling reload in " + RELOAD_DELAY_MS + " ms");
}
// Schedule a new reload after RELOAD_DELAY_MS.
mReloadFuture = ExecutorUtils.getBackgroundExecutor(mServiceName)
.schedule(this, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
loadPersonalDictionary();
}
}
private final PersonalDictionaryContentObserver mPersonalDictionaryContentObserver =
new PersonalDictionaryContentObserver();
/**
* Indicates that a load is in progress, so no need for another.
*/
private AtomicBoolean mIsLoading = new AtomicBoolean(false);
/**
* Indicates that this lookup object has been close()d.
*/
private AtomicBoolean mIsClosed = new AtomicBoolean(false);
/**
* We store a map from a dictionary word to the set of locales & raw string(as it appears)
* We then iterate over the set of locales to find a match using LocaleUtils.
*/
private volatile HashMap<String, HashMap<Locale, String>> mDictWords;
/**
* We store a map from a shortcut to a word for each locale.
* Shortcuts that apply to any locale are keyed by {@link #ANY_LOCALE}.
*/
private volatile HashMap<Locale, HashMap<String, String>> mShortcutsPerLocale;
/**
* The last-scheduled reload future. Saved in order to cancel a pending reload if a new one
* is coming.
*/
private volatile ScheduledFuture<?> mReloadFuture;
private volatile List<DictionaryStats> mDictionaryStats;
/**
* @param context the context from which to obtain content resolver
*/
public PersonalDictionaryLookup(
@NonNull final Context context,
@NonNull final String serviceName) {
mTag = serviceName + ".Personal";
Log.i(mTag, "create()");
mServiceName = serviceName;
mDictionaryStats = new ArrayList<DictionaryStats>();
mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, 0));
mDictionaryStats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, 0));
// Obtain a content resolver.
mResolver = context.getContentResolver();
}
public List<DictionaryStats> getDictionaryStats() {
return mDictionaryStats;
}
public void open() {
Log.i(mTag, "open()");
// Schedule the initial load to run immediately. It's possible that the first call to
// isValidWord occurs before the dictionary has actually loaded, so it should not
// assume that the dictionary has been loaded.
loadPersonalDictionary();
// Register the observer to be notified on changes to the personal dictionary and all
// individual items.
//
// If the user is interacting with the Personal Dictionary settings UI, or with the
// "Add to dictionary" drop-down option, duplicate notifications will be sent for the same
// edit: if a new entry is added, there is a notification for the entry itself, and
// separately for the entire dictionary. However, when used programmatically,
// only notifications for the specific edits are sent. Thus, the observer is registered to
// receive every possible notification, and instead has throttling logic to avoid doing too
// many reloads.
mResolver.registerContentObserver(
UserDictionary.Words.CONTENT_URI,
true /* notifyForDescendents */,
mPersonalDictionaryContentObserver);
}
/**
* To be called by the garbage collector in the off chance that the service did not clean up
* properly. Do not rely on this getting called, and make sure close() is called explicitly.
*/
@Override
public void finalize() throws Throwable {
try {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "finalize()");
}
close();
} finally {
super.finalize();
}
}
/**
* Cleans up PersonalDictionaryLookup: shuts down any extra threads and unregisters the observer.
*
* It is safe, but not advised to call this multiple times, and isValidWord would continue to
* work, but no data will be reloaded any longer.
*/
@Override
public void close() {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "close() : Unregistering content observer");
}
if (mIsClosed.compareAndSet(false, true)) {
// Unregister the content observer.
mResolver.unregisterContentObserver(mPersonalDictionaryContentObserver);
}
}
/**
* Returns true if the initial load has been performed.
*
* @return true if the initial load is successful
*/
public boolean isLoaded() {
return mDictWords != null && mShortcutsPerLocale != null;
}
/**
* Returns the set of words defined for the given locale and more general locales.
*
* For example, input locale en_US uses data for en_US, en, and the global dictionary.
*
* Note that this method returns expanded words, not shortcuts. Shortcuts are handled
* by {@link #getShortcutsForLocale}.
*
* @param inputLocale the locale to restrict for
* @return set of words that apply to the given locale.
*/
public Set<String> getWordsForLocale(@NonNull final Locale inputLocale) {
final HashMap<String, HashMap<Locale, String>> dictWords = mDictWords;
if (CollectionUtils.isNullOrEmpty(dictWords)) {
return Collections.emptySet();
}
final Set<String> words = new HashSet<>();
final String inputLocaleString = inputLocale.toString();
for (String word : dictWords.keySet()) {
HashMap<Locale, String> localeStringMap = dictWords.get(word);
if (!CollectionUtils.isNullOrEmpty(localeStringMap)) {
for (Locale wordLocale : localeStringMap.keySet()) {
final String wordLocaleString = wordLocale.toString();
final int match = LocaleUtils.getMatchLevel(wordLocaleString, inputLocaleString);
if (LocaleUtils.isMatch(match)) {
words.add(localeStringMap.get(wordLocale));
}
}
}
}
return words;
}
/**
* Returns the set of shortcuts defined for the given locale and more general locales.
*
* For example, input locale en_US uses data for en_US, en, and the global dictionary.
*
* Note that this method returns shortcut keys, not expanded words. Words are handled
* by {@link #getWordsForLocale}.
*
* @param inputLocale the locale to restrict for
* @return set of shortcuts that apply to the given locale.
*/
public Set<String> getShortcutsForLocale(@NonNull final Locale inputLocale) {
final Map<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
return Collections.emptySet();
}
final Set<String> shortcuts = new HashSet<>();
if (!TextUtils.isEmpty(inputLocale.getCountry())) {
// First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
final Map<String, String> countryShortcuts = shortcutsPerLocale.get(inputLocale);
if (!CollectionUtils.isNullOrEmpty(countryShortcuts)) {
shortcuts.addAll(countryShortcuts.keySet());
}
}
// Next look for the language-specific shortcut: en, fr, etc.
final Locale languageOnlyLocale =
LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
final Map<String, String> languageShortcuts = shortcutsPerLocale.get(languageOnlyLocale);
if (!CollectionUtils.isNullOrEmpty(languageShortcuts)) {
shortcuts.addAll(languageShortcuts.keySet());
}
// If all else fails, look for a global shortcut.
final Map<String, String> globalShortcuts = shortcutsPerLocale.get(ANY_LOCALE);
if (!CollectionUtils.isNullOrEmpty(globalShortcuts)) {
shortcuts.addAll(globalShortcuts.keySet());
}
return shortcuts;
}
/**
* Determines if the given word is a valid word in the given locale based on the dictionary.
* It tries hard to find a match: for example, casing is ignored and if the word is present in a
* more general locale (e.g. en or all locales), and isValidWord is asking for a more specific
* locale (e.g. en_US), it will be considered a match.
*
* @param word the word to match
* @param inputLocale the locale in which to match the word
* @return true iff the word has been matched for this locale in the dictionary.
*/
public boolean isValidWord(@NonNull final String word, @NonNull final Locale inputLocale) {
if (!isLoaded()) {
// This is a corner case in the event the initial load of the dictionary has not
// completed. In that case, we assume the word is not a valid word in the dictionary.
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : Initial load not complete");
}
return false;
}
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : Word [" + word + "] in Locale [" + inputLocale + "]");
}
// Atomically obtain the current copy of mDictWords;
final HashMap<String, HashMap<Locale, String>> dictWords = mDictWords;
// Lowercase the word using the given locale. Note, that dictionary
// words are lowercased using their locale, and theoretically the
// lowercasing between two matching locales may differ. For simplicity
// we ignore that possibility.
final String lowercased = word.toLowerCase(inputLocale);
final HashMap<Locale, String> dictLocales = dictWords.get(lowercased);
if (CollectionUtils.isNullOrEmpty(dictLocales)) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : No entry for word [" + word + "]");
}
return false;
} else {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : Found entry for word [" + word + "]");
}
// Iterate over the locales this word is in.
for (final Locale dictLocale : dictLocales.keySet()) {
final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(),
inputLocale.toString());
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : MatchLevel for DictLocale [" + dictLocale
+ "] and InputLocale [" + inputLocale + "] is " + matchLevel);
}
if (LocaleUtils.isMatch(matchLevel)) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : MatchLevel " + matchLevel + " IS a match");
}
return true;
}
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : MatchLevel " + matchLevel + " is NOT a match");
}
}
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "isValidWord() : False, since none of the locales matched");
}
return false;
}
}
/**
* Expands the given shortcut for the given locale.
*
* @param shortcut the shortcut to expand
* @param inputLocale the locale in which to expand the shortcut
* @return expanded shortcut iff the word is a shortcut in the dictionary.
*/
@Nullable public String expandShortcut(
@NonNull final String shortcut, @NonNull final Locale inputLocale) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "expandShortcut() : Shortcut [" + shortcut + "] for [" + inputLocale + "]");
}
// Atomically obtain the current copy of mShortcuts;
final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = mShortcutsPerLocale;
// Exit as early as possible. Most users don't use shortcuts.
if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "expandShortcut() : User has no shortcuts");
}
return null;
}
if (!TextUtils.isEmpty(inputLocale.getCountry())) {
// First look for the country-specific shortcut: en_US, en_UK, fr_FR, etc.
final String expansionForCountry = expandShortcut(
shortcutsPerLocale, shortcut, inputLocale);
if (!TextUtils.isEmpty(expansionForCountry)) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "expandShortcut() : Country expansion is ["
+ expansionForCountry + "]");
}
return expansionForCountry;
}
}
// Next look for the language-specific shortcut: en, fr, etc.
final Locale languageOnlyLocale =
LocaleUtils.constructLocaleFromString(inputLocale.getLanguage());
final String expansionForLanguage = expandShortcut(
shortcutsPerLocale, shortcut, languageOnlyLocale);
if (!TextUtils.isEmpty(expansionForLanguage)) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "expandShortcut() : Language expansion is ["
+ expansionForLanguage + "]");
}
return expansionForLanguage;
}
// If all else fails, look for a global shortcut.
final String expansionForGlobal = expandShortcut(shortcutsPerLocale, shortcut, ANY_LOCALE);
if (!TextUtils.isEmpty(expansionForGlobal) && DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "expandShortcut() : Global expansion is [" + expansionForGlobal + "]");
}
return expansionForGlobal;
}
@Nullable private String expandShortcut(
@Nullable final HashMap<Locale, HashMap<String, String>> shortcutsPerLocale,
@NonNull final String shortcut,
@NonNull final Locale locale) {
if (CollectionUtils.isNullOrEmpty(shortcutsPerLocale)) {
return null;
}
final HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(locale);
if (CollectionUtils.isNullOrEmpty(localeShortcuts)) {
return null;
}
return localeShortcuts.get(shortcut);
}
/**
* Loads the personal dictionary in the current thread.
*
* Only one reload can happen at a time. If already running, will exit quickly.
*/
private void loadPersonalDictionary() {
// Bail out if already in the process of loading.
if (!mIsLoading.compareAndSet(false, true)) {
Log.i(mTag, "loadPersonalDictionary() : Already Loading (exit)");
return;
}
Log.i(mTag, "loadPersonalDictionary() : Start Loading");
HashMap<String, HashMap<Locale, String>> dictWords = new HashMap<>();
HashMap<Locale, HashMap<String, String>> shortcutsPerLocale = new HashMap<>();
// Load the dictionary. Items are returned in the default sort order (by frequency).
Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI,
null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER);
if (null == cursor || cursor.getCount() < 1) {
Log.i(mTag, "loadPersonalDictionary() : Empty");
} else {
// Iterate over the entries in the personal dictionary. Note, that iteration is in
// descending frequency by default.
while (dictWords.size() < MAX_NUM_ENTRIES && cursor.moveToNext()) {
// If there is no column for locale, skip this entry. An empty
// locale on the other hand will not be skipped.
final int dictLocaleIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
if (dictLocaleIndex < 0) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Entry without LOCALE, skipping");
}
continue;
}
// If there is no column for word, skip this entry.
final int dictWordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD);
if (dictWordIndex < 0) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Entry without WORD, skipping");
}
continue;
}
// If the word is null, skip this entry.
final String rawDictWord = cursor.getString(dictWordIndex);
if (null == rawDictWord) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Null word");
}
continue;
}
// If the locale is null, that's interpreted to mean all locales. Note, the special
// zz locale for an Alphabet (QWERTY) layout will not match any actual language.
String localeString = cursor.getString(dictLocaleIndex);
if (null == localeString) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Null locale for word [" +
rawDictWord + "], assuming all locales");
}
// For purposes of LocaleUtils, an empty locale matches everything.
localeString = "";
}
final Locale dictLocale = LocaleUtils.constructLocaleFromString(localeString);
// Lowercase the word before storing it.
final String dictWord = rawDictWord.toLowerCase(dictLocale);
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Adding word [" + dictWord
+ "] for locale " + dictLocale + "with value" + rawDictWord);
}
// Check if there is an existing entry for this word.
HashMap<Locale, String> dictLocales = dictWords.get(dictWord);
if (CollectionUtils.isNullOrEmpty(dictLocales)) {
// If there is no entry for this word, create one.
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Word [" + dictWord +
"] not seen for other locales, creating new entry");
}
dictLocales = new HashMap<>();
dictWords.put(dictWord, dictLocales);
}
// Append the locale to the list of locales this word is in.
dictLocales.put(dictLocale, rawDictWord);
// If there is no column for a shortcut, we're done.
final int shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT);
if (shortcutIndex < 0) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Entry without SHORTCUT, done");
}
continue;
}
// If the shortcut is null, we're done.
final String shortcut = cursor.getString(shortcutIndex);
if (shortcut == null) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(mTag, "loadPersonalDictionary() : Null shortcut");
}
continue;
}
// Else, save the shortcut.
HashMap<String, String> localeShortcuts = shortcutsPerLocale.get(dictLocale);
if (localeShortcuts == null) {
localeShortcuts = new HashMap<>();
shortcutsPerLocale.put(dictLocale, localeShortcuts);
}
// Map to the raw input, which might be capitalized.
// This lets the user create a shortcut from "gm" to "General Motors".
localeShortcuts.put(shortcut, rawDictWord);
}
}
List<DictionaryStats> stats = new ArrayList<>();
stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER, dictWords.size()));
int numShortcuts = 0;
for (HashMap<String, String> shortcuts : shortcutsPerLocale.values()) {
numShortcuts += shortcuts.size();
}
stats.add(new DictionaryStats(ANY_LOCALE, Dictionary.TYPE_USER_SHORTCUT, numShortcuts));
mDictionaryStats = stats;
// Atomically replace the copy of mDictWords and mShortcuts.
mDictWords = dictWords;
mShortcutsPerLocale = shortcutsPerLocale;
// Allow other calls to loadPersonalDictionary to execute now.
mIsLoading.set(false);
Log.i(mTag, "loadPersonalDictionary() : Loaded " + mDictWords.size()
+ " words and " + numShortcuts + " shortcuts");
notifyListeners();
}
}

View file

@ -699,7 +699,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
* @return a range containing the text surrounding the cursor
*/
public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations,
final int scriptId) {
final int scriptId, final boolean justDeleted) {
mIC = mParent.getCurrentInputConnection();
if (!isConnected()) {
return null;
@ -718,43 +718,24 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return null;
}
// issue:
// type 2 words and space, press delete twice -> remaining word and space before are selected
// now on next key press, the space before the word is removed
// or complete a word by choosing a suggestion than press backspace -> same thing
// what is sometimes happening (depending on app, or maybe input field attributes):
// we just pressed delete, and getTextBeforeCursor gets the correct text,
// but getTextBeforeCursorAndDetectLaggyConnection returns the old word, before the deletion (not sure why)
// -> we try to detect this difference, and then try to fix it
// interestingly, getTextBeforeCursor seems to only get the correct text because it uses
// mCommittedTextBeforeComposingText where the text is cached
// what could be actually going on? we probably need to fetch the text because we want updated styles if any
// we need text before, and text after is always empty or a separator or similar
if (before.length() > 0 && (after.length() == 0 || !isPartOfCompositionForScript(Character.codePointAt(after, 0), spacingAndPunctuations, scriptId))) {
final int lastBeforeCodePoint = Character.codePointBefore(before, before.length());
// check whether before ends with the same codepoint as getTextBeforeCursor
int lastBeforeLength = Character.charCount(lastBeforeCodePoint);
CharSequence codePointBeforeCursor = getTextBeforeCursor(lastBeforeLength, 0);
if (codePointBeforeCursor.length() != 0 && Character.codePointAt(codePointBeforeCursor, 0) != lastBeforeCodePoint) {
// they are different, as is expected from the issue
// now check whether they are the same if the last codepoint of before is removed
final CharSequence beforeWithoutLast = before.subSequence(0, before.length() - lastBeforeLength);
final CharSequence beforeCursor = getTextBeforeCursor(beforeWithoutLast.length(), 0);
if (beforeCursor.length() == beforeWithoutLast.length()) {
boolean same = true;
// CharSequence has undefined equals, so we need to compare characters
for (int i = 0; i < beforeCursor.length(); i++) {
if (beforeCursor.charAt(i) != beforeWithoutLast.charAt(i)) {
same = false;
break;
}
}
if (same) {
before = beforeWithoutLast;
}
}
}
// we need text before, and text after is either empty or a separator or similar
if (justDeleted && before.length() > 0 &&
(after.length() == 0
|| !isPartOfCompositionForScript(Character.codePointAt(after, 0), spacingAndPunctuations, scriptId)
)
) {
// issue:
// type 2 words and space, press delete twice -> remaining word and space before are selected
// now on next key press, the space before the word is removed
// or complete a word by choosing a suggestion, then press backspace -> same thing
// what is sometimes happening (depending on app, or maybe input field attributes):
// we just pressed delete, and getTextBeforeCursor gets the correct text,
// but getTextBeforeCursorAndDetectLaggyConnection returns the old word, before the deletion (not sure why)
// -> we try to detect this difference, and then try to fix it
// interestingly, getTextBeforeCursor seems to only get the correct text because it uses
// mCommittedTextBeforeComposingText, where the text is cached
// what could be actually going on? we probably need to fetch the text, because we want updated styles (if any)
before = fixIncorrectLength(before);
}
// Going backward, find the first breaking point (separator)
@ -794,6 +775,37 @@ public final class RichInputConnection implements PrivateCommandPerformer {
hasUrlSpans);
}
// mostly fixes an issue where the space before the word is selected after deleting a codepoint,
// because the text length is not yet updated in the field (i.e. trying to select "word length"
// before cursor, but the last letter has just been deleted and thus the space before is also selected)
private CharSequence fixIncorrectLength(final CharSequence before) {
// don't use codepoints, just do the simple thing...
int initialCheckLength = Math.min(3, before.length());
// this should have been checked before calling this method, but better be safe
if (initialCheckLength == 0) return before;
final CharSequence lastCharsInBefore = before.subSequence(before.length() - initialCheckLength, before.length());
final CharSequence lastCharsBeforeCursor = getTextBeforeCursor(initialCheckLength, 0);
// if the last 3 chars are equal, we can be relatively sure to not have this bug (can still be e.g. rrrr, which is not detected)
// (we could also check everything though, it's just a little slower)
if (TextUtils.equals(lastCharsInBefore, lastCharsBeforeCursor)) return before;
// delete will hopefully have deleted a codepoint, not only a char
// we want to compare whether the text before the cursor is the same as "before" without
// the last codepoint. if yes, return "before" without the last codepoint
final int lastBeforeCodePoint = Character.codePointBefore(before, before.length());
int lastBeforeLength = Character.charCount(lastBeforeCodePoint);
final CharSequence codePointBeforeCursor = getTextBeforeCursor(lastBeforeLength, 0);
if (codePointBeforeCursor.length() == 0) return before;
// now check whether they are the same if the last codepoint of before is removed
final CharSequence beforeWithoutLast = before.subSequence(0, before.length() - lastBeforeLength);
final CharSequence beforeCursor = getTextBeforeCursor(beforeWithoutLast.length(), 0);
if (beforeCursor.length() != beforeWithoutLast.length()) return before;
if (TextUtils.equals(beforeCursor, beforeWithoutLast))
return beforeWithoutLast;
return before;
}
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
boolean checkTextAfter) {
if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) {

View file

@ -35,11 +35,9 @@ import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static org.dslul.openboard.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
@ -118,7 +116,7 @@ public class RichInputMethodManager {
public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) {
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(true);
final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
final int currentIndex = enabledSubtypes.indexOf(currentSubtype);
if (currentIndex == INDEX_NOT_FOUND) {
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
+ SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
@ -197,38 +195,17 @@ public class RichInputMethodManager {
}
public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
return checkIfSubtypeBelongsToList(subtype,
getEnabledInputMethodSubtypeList(
getInputMethodInfoOfThisIme(),
true /* allowsImplicitlySelectedSubtypes */));
return getEnabledInputMethodSubtypeList(getInputMethodInfoOfThisIme(), true)
.contains(subtype);
}
public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
final InputMethodSubtype subtype) {
public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(final InputMethodSubtype subtype) {
final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(subtype,
getMyEnabledInputMethodSubtypeList(false /* allowsImplicitlySelectedSubtypes */));
final boolean subtypeExplicitlyEnabled = getMyEnabledInputMethodSubtypeList(false)
.contains(subtype);
return subtypeEnabled && !subtypeExplicitlyEnabled;
}
private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
final List<InputMethodSubtype> subtypes) {
return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
}
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);
if (ims.equals(subtype)) {
return index;
}
}
return INDEX_NOT_FOUND;
}
public void onSubtypeChanged(@NonNull final InputMethodSubtype newSubtype) {
updateCurrentSubtype(newSubtype);
updateShortcutIme();
@ -392,38 +369,13 @@ public class RichInputMethodManager {
updateShortcutIme();
}
// todo: remove?
public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
final Locale systemLocale = mContext.getResources().getConfiguration().locale;
final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>();
final InputMethodManager inputMethodManager = getInputMethodManager();
final List<InputMethodInfo> enabledInputMethodInfoList =
inputMethodManager.getEnabledInputMethodList();
for (final InputMethodInfo info : enabledInputMethodInfoList) {
final List<InputMethodSubtype> enabledSubtypes =
inputMethodManager.getEnabledInputMethodSubtypeList(
info, true /* allowsImplicitlySelectedSubtypes */);
if (enabledSubtypes.isEmpty()) {
// An IME with no subtypes is found.
return false;
}
enabledSubtypesOfEnabledImes.addAll(enabledSubtypes);
}
for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) {
if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty()
&& !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
return false;
}
}
return true;
}
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
// todo: is shortcutIme only voice input, or can it be something else?
// if always voice input, rename it and other things like mHasShortcutKey
private void updateShortcutIme() {
if (DEBUG) {
Log.d(TAG, "Update shortcut IME from : "
@ -495,12 +447,6 @@ public class RichInputMethodManager {
}
public boolean isShortcutImeReady() {
if (mShortcutInputMethodInfo == null) {
return false;
}
if (mShortcutSubtype == null) {
return true;
}
return true;
return mShortcutInputMethodInfo != null;
}
}

View file

@ -43,7 +43,7 @@ public class RichInputMethodSubtype {
private static final String TAG = RichInputMethodSubtype.class.getSimpleName();
private static final HashMap<Locale, Locale> sLocaleMap = initializeLocaleMap();
private static final HashMap<Locale, Locale> initializeLocaleMap() {
private static HashMap<Locale, Locale> initializeLocaleMap() {
final HashMap<Locale, Locale> map = new HashMap<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Locale#forLanguageTag is available on API Level 21+.
@ -182,12 +182,18 @@ public class RichInputMethodSubtype {
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
@NonNull
private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
new RichInputMethodSubtype(new InputMethodSubtype(
R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE));
new RichInputMethodSubtype(new InputMethodSubtype.InputMethodSubtypeBuilder()
.setSubtypeNameResId(R.string.subtype_no_language_qwerty)
.setSubtypeIconResId(R.drawable.ic_ime_switcher_dark)
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
.setSubtypeMode(KEYBOARD_MODE)
.setSubtypeExtraValue(EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE)
.setIsAuxiliary(false)
.setOverridesImplicitlyEnabledSubtype(false)
.setSubtypeId(SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE)
.setIsAsciiCapable(true)
.build());
// Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
// Dummy Emoji subtype. See {@link R.xml.method}.
private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
@ -195,13 +201,17 @@ public class RichInputMethodSubtype {
"KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
@NonNull
private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype(
new InputMethodSubtype(
R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE =
new RichInputMethodSubtype(new InputMethodSubtype.InputMethodSubtypeBuilder()
.setSubtypeNameResId(R.string.subtype_emoji)
.setSubtypeIconResId(R.drawable.ic_ime_switcher_dark)
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
.setSubtypeMode(KEYBOARD_MODE)
.setSubtypeExtraValue(EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE)
.setIsAuxiliary(false)
.setOverridesImplicitlyEnabledSubtype(false)
.setSubtypeId(SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE)
.build());
private static RichInputMethodSubtype sNoLanguageSubtype;
private static RichInputMethodSubtype sEmojiSubtype;
@ -210,8 +220,7 @@ public class RichInputMethodSubtype {
RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype;
if (noLanguageSubtype == null) {
final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance()
.findSubtypeByLocaleAndKeyboardLayoutSet(
SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
.findSubtypeByLocaleAndKeyboardLayoutSet(SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
if (rawNoLanguageSubtype != null) {
noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype);
}
@ -221,8 +230,7 @@ public class RichInputMethodSubtype {
return noLanguageSubtype;
}
Log.w(TAG, "Can't find any language with QWERTY subtype");
Log.w(TAG, "No input method subtype found; returning dummy subtype: "
+ DUMMY_NO_LANGUAGE_SUBTYPE);
Log.w(TAG, "No input method subtype found; returning dummy subtype: " + DUMMY_NO_LANGUAGE_SUBTYPE);
return DUMMY_NO_LANGUAGE_SUBTYPE;
}
@ -231,8 +239,7 @@ public class RichInputMethodSubtype {
RichInputMethodSubtype emojiSubtype = sEmojiSubtype;
if (emojiSubtype == null) {
final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance()
.findSubtypeByLocaleAndKeyboardLayoutSet(
SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
.findSubtypeByLocaleAndKeyboardLayoutSet(SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
if (rawEmojiSubtype != null) {
emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
}
@ -242,8 +249,7 @@ public class RichInputMethodSubtype {
return emojiSubtype;
}
Log.w(TAG, "Can't find emoji subtype");
Log.w(TAG, "No input method subtype found; returning dummy subtype: "
+ DUMMY_EMOJI_SUBTYPE);
Log.w(TAG, "No input method subtype found; returning dummy subtype: " + DUMMY_EMOJI_SUBTYPE);
return DUMMY_EMOJI_SUBTYPE;
}
}

View file

@ -26,6 +26,7 @@ import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion;
import org.dslul.openboard.inputmethod.latin.utils.AutoCorrectionUtils;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
@ -86,15 +87,6 @@ public final class Suggest {
mAutoCorrectionThreshold = threshold;
}
/**
* Set the normalized-score threshold for what we consider a "plausible" suggestion, in
* the same dimension as the auto-correction threshold.
* @param threshold the threshold
*/
public void setPlausibilityThreshold(final float threshold) {
mPlausibilityThreshold = threshold;
}
public interface OnGetSuggestedWordsCallback {
void onGetSuggestedWords(final SuggestedWords suggestedWords);
}
@ -306,9 +298,6 @@ public final class Suggest {
final int firstOccurrenceOfTypedWordInSuggestions,
final SuggestedWordInfo typedWordFirstOccurrenceWordInfo
) {
// todo:
// tune the suggestion score thresholds (currently 900k, maybe should depend on autocorrect)
// maybe tune the empty word suggestion min difference (currently 20, seems quite ok)
final String consideredWord = trailingSingleQuotesCount > 0
? typedWordString.substring(0, typedWordString.length() - trailingSingleQuotesCount)
: typedWordString;
@ -322,6 +311,7 @@ public final class Suggest {
// We allow auto-correction if whitelisting is not required or the word is whitelisted,
// or if the word had more than one char and was not suggested.
final boolean allowsToBeAutoCorrected;
final int scoreLimit = Settings.getInstance().getCurrent().mScoreLimitForAutocorrect;
if ((SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION || whitelistedWord != null)
|| (consideredWord.length() > 1 && (sourceDictionaryOfRemovedWord == null)) // more than 1 letter and not in dictionary
) {
@ -331,7 +321,7 @@ public final class Suggest {
putEmptyWordSuggestions.run();
final SuggestedWordInfo first = firstAndTypedWordEmptyInfos.get(0);
final SuggestedWordInfo typed = firstAndTypedWordEmptyInfos.get(1);
if (firstSuggestionInContainer.mScore > 900000) {
if (firstSuggestionInContainer.mScore > scoreLimit) {
allowsToBeAutoCorrected = true; // suggestion has good score, allow
} else if (first == null) {
allowsToBeAutoCorrected = false; // no autocorrect if first suggestion unknown in this context
@ -375,11 +365,7 @@ public final class Suggest {
// list, "will" would always auto-correct to "Will" which is unwanted. Hence, no
// main dict => no auto-correct. Also, it would probably get obnoxious quickly.
// TODO: now that we have personalization, we may want to re-evaluate this decision
|| !dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()
// If the first suggestion is a shortcut we never auto-correct to it, regardless
// of how strong it is (whitelist entries are not KIND_SHORTCUT but KIND_WHITELIST).
// TODO: we may want to have shortcut-only entries auto-correct in the future.
|| suggestionResults.first().isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) {
|| !dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()) {
hasAutoCorrection = false;
} else {
final SuggestedWordInfo firstSuggestion = suggestionResults.first();
@ -400,23 +386,21 @@ public final class Suggest {
// is determined, see #isAllowedByAutoCorrectionWithSpaceFilter.
// TODO: this should not have its own logic here but be handled by the dictionary.
final boolean allowed = isAllowedByAutoCorrectionWithSpaceFilter(firstSuggestion);
if (allowed && typedWordFirstOccurrenceWordInfo != null && typedWordFirstOccurrenceWordInfo.mScore > 900000) {
if (allowed && typedWordFirstOccurrenceWordInfo != null && typedWordFirstOccurrenceWordInfo.mScore > scoreLimit) {
// typed word is valid and has good score
// do not auto-correct if typed word is better match than first suggestion
final SuggestedWordInfo first = firstSuggestionInContainer != null ? firstSuggestionInContainer : firstSuggestion;
final Locale dictLocale = dictionaryFacilitator.getCurrentLocale();
if (first.mScore < 900000) {
if (first.mScore < scoreLimit) {
// don't allow if suggestion has too low score
// todo: maybe lower this to ~600k? 500k will be too aggressive
// or make it depend on autocorrect threshold
return new boolean[]{ true, false };
}
if (first.mSourceDict.mLocale != typedWordFirstOccurrenceWordInfo.mSourceDict.mLocale) {
// dict locale different -> return the better match
return new boolean[]{ true, dictLocale == first.mSourceDict.mLocale };
}
// todo: this may need tuning, especially the score difference thing
// the score difference may need tuning, but so far it seems alright
final int firstWordBonusScore = (first.isKindOf(SuggestedWordInfo.KIND_WHITELIST) ? 20 : 0) // large bonus because it's wanted by dictionary
+ (StringUtils.isLowerCaseAscii(typedWordString) ? 5 : 0) // small bonus because typically only ascii is typed (applies to latin keyboards only)
+ (first.mScore > typedWordFirstOccurrenceWordInfo.mScore ? 5 : 0); // small bonus if score is higher

View file

@ -203,7 +203,7 @@ public class SuggestedWords {
}
@NonNull
public static final SuggestedWords getEmptyInstance() {
public static SuggestedWords getEmptyInstance() {
return SuggestedWords.EMPTY;
}
@ -347,8 +347,9 @@ public class SuggestedWords {
return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
}
public boolean isAprapreateForAutoCorrection() {
return (mKindAndFlags & KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION) != 0;
public boolean isAppropriateForAutoCorrection() {
return (mKindAndFlags & KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION) != 0
|| isKindOf(KIND_SHORTCUT);
}
public void setDebugString(final String str) {

View file

@ -127,7 +127,7 @@ public final class WordComposer {
refreshTypedWordCache();
}
private final void refreshTypedWordCache() {
private void refreshTypedWordCache() {
mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback();
mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length());
}
@ -144,7 +144,7 @@ public final class WordComposer {
return size() == 1;
}
public final boolean isComposingWord() {
public boolean isComposingWord() {
return size() > 0;
}

View file

@ -1,161 +0,0 @@
package org.dslul.openboard.inputmethod.latin.common;
import static org.dslul.openboard.inputmethod.latin.utils.ColorUtilKt.*;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import androidx.core.graphics.BlendModeColorFilterCompat;
import androidx.core.graphics.BlendModeCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme;
// todo: maybe kotlin? would make it much shorter and more readable
public class Colors {
public int navBar;
public final int accent;
public final int background;
public final int keyBackground;
public final int functionalKey;
public final int spaceBar;
public final int keyText;
public final int keyHintText;
public int adjustedBackground;
public int adjustedKeyText;
// todo (later): evaluate which colors, colorFilters and colorStateLists area actually necessary
public ColorFilter backgroundFilter;
public ColorFilter adjustedBackgroundFilter;
public ColorFilter keyBackgroundFilter;
public ColorFilter functionalKeyBackgroundFilter;
public ColorFilter spaceBarFilter;
public ColorFilter keyTextFilter;
public ColorFilter accentColorFilter;
public ColorFilter actionKeyIconColorFilter;
private ColorStateList backgroundStateList;
private ColorStateList keyStateList;
private ColorStateList functionalKeyStateList;
private ColorStateList actionKeyStateList;
private ColorStateList spaceBarStateList;
private ColorStateList adjustedBackgroundStateList;
public static Colors newColors(String themeStyle, int accent, int background, int keyBackground, int functionalKey, int spaceBar, int keyText, int keyHintText) {
if (themeStyle.equals(KeyboardTheme.THEME_STYLE_HOLO))
return new HoloColors(accent, background, keyBackground, functionalKey, spaceBar, keyText, keyHintText);
return new Colors(accent, background, keyBackground, functionalKey, spaceBar, keyText, keyHintText);
}
protected Colors(int _accent, int _background, int _keyBackground, int _functionalKey, int _spaceBar, int _keyText, int _keyHintText) {
accent = _accent;
background = _background;
keyBackground = _keyBackground;
functionalKey = _functionalKey;
spaceBar = _spaceBar;
keyText = _keyText;
keyHintText = _keyHintText;
navBar = background;
}
/** set background colors including state list to the drawable */
// todo: this can be used for setting more complicated filters
// may be necessary for reproducing holo theme (extend Colors and override this in sth like HoloColors?)
public void setBackgroundColor(final Drawable background, final int type) {
final ColorStateList list;
switch (type) {
case TYPE_KEY:
list = keyStateList;
break;
case TYPE_SPACE:
list = spaceBarStateList;
break;
case TYPE_ADJUSTED_BACKGROUND:
list = adjustedBackgroundStateList;
break;
case TYPE_ACTION:
list = actionKeyStateList;
break;
case TYPE_FUNCTIONAL:
list = functionalKeyStateList;
break;
case TYPE_BACKGROUND:
default:
list = backgroundStateList;
}
DrawableCompat.setTintMode(background, PorterDuff.Mode.MULTIPLY);
DrawableCompat.setTintList(background, list);
}
@Nullable
public Drawable getKeyboardBackground() {
return null;
}
public static final int TYPE_BACKGROUND = 0;
public static final int TYPE_KEY = 1;
public static final int TYPE_FUNCTIONAL = 2;
public static final int TYPE_ACTION = 3;
public static final int TYPE_SPACE = 4;
public static final int TYPE_ADJUSTED_BACKGROUND = 5;
public void createColorFilters(final boolean hasKeyBorders) {
final int[][] states = new int[][] {
// are other states used?
// looks like only microphone ("shortcut") key can ever be disabled, but then it's not shown anyway...
// checkable/checked is only used for sticky shift key in old holo theme, but drawables not used in new version
// active is used for action key
// empty is used for emoji and spacers
new int[] { android.R.attr.state_pressed}, // pressed
new int[] { -android.R.attr.state_pressed}, // not pressed
};
// todo (idea): make better use of the states?
// could also use / create StateListDrawables in colors (though that's a style than a color...)
// this would better allow choosing e.g. cornered/rounded drawables for moreKeys or moreSuggestions
backgroundFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(background, BlendModeCompat.MODULATE);
adjustedKeyText = brightenOrDarken(keyText, true);
// color to be used if exact background color would be bad contrast, e.g. more keys popup or no border space bar
if (isDarkColor(background)) {
adjustedBackground = brighten(background);
adjustedBackgroundStateList = new ColorStateList(states, new int[] { brighten(adjustedBackground), adjustedBackground });
} else {
adjustedBackground = darken(background);
adjustedBackgroundStateList = new ColorStateList(states, new int[] { darken(adjustedBackground), adjustedBackground });
}
adjustedBackgroundFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(adjustedBackground, BlendModeCompat.MODULATE);
if (hasKeyBorders) {
keyBackgroundFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(keyBackground, BlendModeCompat.MODULATE);
functionalKeyBackgroundFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(functionalKey, BlendModeCompat.MODULATE);
spaceBarFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(spaceBar, BlendModeCompat.MODULATE);
backgroundStateList = new ColorStateList(states, new int[] { brightenOrDarken(background, true), background });
keyStateList = new ColorStateList(states, new int[] { brightenOrDarken(keyBackground, true), keyBackground });
functionalKeyStateList = new ColorStateList(states, new int[] { brightenOrDarken(functionalKey, true), functionalKey });
actionKeyStateList = new ColorStateList(states, new int[] { brightenOrDarken(accent, true), accent });
spaceBarStateList = new ColorStateList(states, new int[] { brightenOrDarken(spaceBar, true), spaceBar });
} else {
// need to set color to background if key borders are disabled, or there will be ugly keys
keyBackgroundFilter = backgroundFilter;
functionalKeyBackgroundFilter = keyBackgroundFilter;
spaceBarFilter = keyBackgroundFilter;
backgroundStateList = new ColorStateList(states, new int[] { brightenOrDarken(background, true), background });
keyStateList = backgroundStateList;
functionalKeyStateList = backgroundStateList;
actionKeyStateList = new ColorStateList(states, new int[] { brightenOrDarken(accent, true), accent });
spaceBarStateList = adjustedBackgroundStateList;
}
keyTextFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(keyText, BlendModeCompat.SRC_ATOP);
accentColorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(accent, BlendModeCompat.MODULATE);
actionKeyIconColorFilter = isBrightColor(accent) // the white icon may not have enough contrast, and can't be adjusted by the user
? BlendModeColorFilterCompat.createBlendModeColorFilterCompat(Color.DKGRAY, BlendModeCompat.SRC_ATOP)
: null;
}
}

View file

@ -0,0 +1,193 @@
package org.dslul.openboard.inputmethod.latin.common
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.view.View
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.drawable.DrawableCompat
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme.THEME_STYLE_HOLO
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme.THEME_STYLE_MATERIAL
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView
import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView
import org.dslul.openboard.inputmethod.keyboard.clipboard.ClipboardHistoryView
import org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPageKeyboardView
import org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPalettesView
import org.dslul.openboard.inputmethod.latin.KeyboardWrapperView
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripView
import org.dslul.openboard.inputmethod.latin.utils.*
class Colors (
val themeStyle: String,
val hasKeyBorders: Boolean,
val accent: Int,
val background: Int,
val keyBackground: Int,
val functionalKey: Int,
val spaceBar: Int,
val keyText: Int,
val keyHintText: Int
) {
val navBar: Int
val adjustedBackground: Int
val adjustedKeyText: Int
val spaceBarText: Int
// todo (later): evaluate which colors, colorFilters and colorStateLists are actually necessary
// also, ideally the color filters would be private and chosen internally depending on type
val backgroundFilter: ColorFilter
val adjustedBackgroundFilter: ColorFilter
val keyBackgroundFilter: ColorFilter
val functionalKeyBackgroundFilter: ColorFilter
val spaceBarFilter: ColorFilter
val keyTextFilter: ColorFilter
val accentColorFilter: ColorFilter
val actionKeyIconColorFilter: ColorFilter?
val clipboardPinFilter: ColorFilter?
private val backgroundStateList: ColorStateList
private val keyStateList: ColorStateList
private val functionalKeyStateList: ColorStateList
private val actionKeyStateList: ColorStateList
private val spaceBarStateList: ColorStateList
private val adjustedBackgroundStateList: ColorStateList
val keyboardBackground: Drawable?
init {
accentColorFilter = colorFilter(accent)
if (themeStyle == THEME_STYLE_HOLO) {
val darkerBackground = adjustLuminosityAndKeepAlpha(background, -0.2f)
navBar = darkerBackground
keyboardBackground = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(background, darkerBackground))
spaceBarText = keyText
clipboardPinFilter = accentColorFilter
} else {
navBar = background
keyboardBackground = null
spaceBarText = keyHintText
clipboardPinFilter = null
}
// create color filters, todo: maybe better / simplify
val states = arrayOf(intArrayOf(android.R.attr.state_pressed), intArrayOf(-android.R.attr.state_pressed))
fun stateList(pressed: Int, normal: Int) =
ColorStateList(states, intArrayOf(pressed, normal))
// todo (idea): make better use of the states?
// could also use / create StateListDrawables in colors (though that's a style than a color...)
// this would better allow choosing e.g. cornered/rounded drawables for moreKeys or moreSuggestions
backgroundFilter = colorFilter(background)
adjustedKeyText = brightenOrDarken(keyText, true)
// color to be used if exact background color would be bad contrast, e.g. more keys popup or no border space bar
if (isDarkColor(background)) {
adjustedBackground = brighten(background)
adjustedBackgroundStateList = stateList(brighten(adjustedBackground), adjustedBackground)
} else {
adjustedBackground = darken(background)
adjustedBackgroundStateList = stateList(darken(adjustedBackground), adjustedBackground)
}
adjustedBackgroundFilter = colorFilter(adjustedBackground)
if (hasKeyBorders) {
keyBackgroundFilter = colorFilter(keyBackground)
functionalKeyBackgroundFilter = colorFilter(functionalKey)
spaceBarFilter = colorFilter(spaceBar)
backgroundStateList = stateList(brightenOrDarken(background, true), background)
keyStateList = if (themeStyle == THEME_STYLE_HOLO) stateList(keyBackground, keyBackground)
else stateList(brightenOrDarken(keyBackground, true), keyBackground)
functionalKeyStateList = stateList(brightenOrDarken(functionalKey, true), functionalKey)
actionKeyStateList = if (themeStyle == THEME_STYLE_HOLO) functionalKeyStateList
else stateList(brightenOrDarken(accent, true), accent)
spaceBarStateList = if (themeStyle == THEME_STYLE_HOLO) stateList(spaceBar, spaceBar)
else stateList(brightenOrDarken(spaceBar, true), spaceBar)
} else {
// need to set color to background if key borders are disabled, or there will be ugly keys
keyBackgroundFilter = backgroundFilter
functionalKeyBackgroundFilter = keyBackgroundFilter
spaceBarFilter = colorFilter(spaceBar)
backgroundStateList = stateList(brightenOrDarken(background, true), background)
keyStateList = backgroundStateList
functionalKeyStateList = backgroundStateList
actionKeyStateList = if (themeStyle == THEME_STYLE_HOLO) functionalKeyStateList
else stateList(brightenOrDarken(accent, true), accent)
spaceBarStateList = stateList(brightenOrDarken(spaceBar, true), spaceBar)
}
keyTextFilter = colorFilter(keyText, BlendModeCompat.SRC_ATOP)
actionKeyIconColorFilter = when {
themeStyle == THEME_STYLE_HOLO -> keyTextFilter
// the white icon may not have enough contrast, and can't be adjusted by the user
isBrightColor(accent) -> colorFilter(Color.DKGRAY, BlendModeCompat.SRC_ATOP)
else -> null
}
}
/** set background colors including state list to the drawable */
fun setBackgroundColor(background: Drawable, type: BackgroundType) {
val colorStateList = when (type) {
BackgroundType.BACKGROUND -> backgroundStateList
BackgroundType.KEY -> keyStateList
BackgroundType.FUNCTIONAL -> functionalKeyStateList
BackgroundType.ACTION -> actionKeyStateList
BackgroundType.SPACE -> spaceBarStateList
BackgroundType.ADJUSTED_BACKGROUND -> adjustedBackgroundStateList
BackgroundType.SUGGESTION -> if (!hasKeyBorders && themeStyle == THEME_STYLE_MATERIAL)
adjustedBackgroundStateList
else backgroundStateList
BackgroundType.ACTION_MORE_KEYS -> if (themeStyle == THEME_STYLE_HOLO)
adjustedBackgroundStateList
else actionKeyStateList
}
DrawableCompat.setTintMode(background, PorterDuff.Mode.MULTIPLY)
DrawableCompat.setTintList(background, colorStateList)
}
// using !! for the color filter because null is only returned for unsupported modes, which are not used
private fun colorFilter(color: Int, mode: BlendModeCompat = BlendModeCompat.MODULATE): ColorFilter =
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, mode)!!
fun getDrawable(type: BackgroundType, attr: TypedArray): Drawable {
val drawable = when (type) {
BackgroundType.KEY, BackgroundType.ADJUSTED_BACKGROUND, BackgroundType.BACKGROUND,
BackgroundType.SUGGESTION, BackgroundType.ACTION_MORE_KEYS ->
attr.getDrawable(R.styleable.KeyboardView_keyBackground)
BackgroundType.FUNCTIONAL -> attr.getDrawable(R.styleable.KeyboardView_functionalKeyBackground)
BackgroundType.SPACE -> attr.getDrawable(R.styleable.KeyboardView_spacebarBackground)
BackgroundType.ACTION -> {
if (themeStyle == THEME_STYLE_HOLO && hasKeyBorders) // no borders has a very small pressed drawable otherwise
attr.getDrawable(R.styleable.KeyboardView_functionalKeyBackground)
else
attr.getDrawable(R.styleable.KeyboardView_keyBackground)
}
}?.mutate() ?: attr.getDrawable(R.styleable.KeyboardView_keyBackground)?.mutate()!! // keyBackground always exists
setBackgroundColor(drawable, type)
return drawable
}
fun setKeyboardBackground(view: View) {
when (view) {
is MoreSuggestionsView -> view.background.colorFilter = backgroundFilter
is MoreKeysKeyboardView -> view.background.colorFilter = adjustedBackgroundFilter
is SuggestionStripView -> setBackgroundColor(view.background, BackgroundType.SUGGESTION)
is EmojiPageKeyboardView, // to make EmojiPalettesView background visible, which does not scroll
is MainKeyboardView -> view.setBackgroundColor(Color.TRANSPARENT) // otherwise causes issues with wrapper view when using one-handed mode
is KeyboardWrapperView, is EmojiPalettesView, is ClipboardHistoryView -> {
if (keyboardBackground != null) view.background = keyboardBackground
else view.background.colorFilter = backgroundFilter
}
else -> view.background.colorFilter = backgroundFilter
}
}
}
enum class BackgroundType {
BACKGROUND, KEY, FUNCTIONAL, ACTION, ACTION_MORE_KEYS, SPACE, ADJUSTED_BACKGROUND, SUGGESTION
}

View file

@ -101,12 +101,6 @@ public final class Constants {
*/
public static final String EMOJI_CAPABLE = "EmojiCapable";
/**
* The subtype extra value used to indicate that this subtype requires a network
* connection to work.
*/
public static final String REQ_NETWORK_CONNECTIVITY = "requireNetworkConnectivity";
/**
* The subtype extra value used to indicate that the display name of this subtype
* contains a "%s" for printf-like replacement and it should be replaced by

View file

@ -1,26 +0,0 @@
package org.dslul.openboard.inputmethod.latin.common;
import static org.dslul.openboard.inputmethod.latin.utils.ColorUtilKt.adjustLuminosityAndKeepAlpha;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
public class HoloColors extends Colors {
private final Drawable keyboardBackground = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[] { background, adjustLuminosityAndKeepAlpha(background, -0.2f) }
);
protected HoloColors(int _accent, int _background, int _keyBackground, int _functionalKey, int _spaceBar, int _keyText, int _keyHintText) {
super(_accent, _background, _keyBackground, _functionalKey, _spaceBar, _keyText, _keyHintText);
navBar = adjustLuminosityAndKeepAlpha(background, -0.2f);
}
@Override
public Drawable getKeyboardBackground() {
// thanks a lot google for omitting something extremely exotic like a "subtract" color
// filter that could be simply applied on top of a brighter version of keyboard_background_holo
return keyboardBackground;
}
}

View file

@ -35,6 +35,10 @@ public class NativeSuggestOptions {
setBooleanOption(IS_GESTURE, value);
}
public void setIsSpaceAwareGesture(final boolean value) {
setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value);
}
public void setUseFullEditDistance(final boolean value) {
setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
}

View file

@ -1,9 +1,15 @@
package org.dslul.openboard.inputmethod.latin.define
import android.content.SharedPreferences
import org.dslul.openboard.inputmethod.latin.BuildConfig
import org.dslul.openboard.inputmethod.latin.settings.DebugSettings
object DebugFlags {
const val DEBUG_ENABLED = false
@JvmField
var DEBUG_ENABLED = false
@JvmStatic
fun init(prefs: SharedPreferences?) {}
}
fun init(prefs: SharedPreferences) {
DEBUG_ENABLED = BuildConfig.DEBUG && prefs.getBoolean(DebugSettings.PREF_DEBUG_MODE, false)
}
}

View file

@ -54,7 +54,6 @@ import org.dslul.openboard.inputmethod.latin.common.InputPointers;
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion;
import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations;
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
@ -1261,7 +1260,7 @@ public final class InputLogic {
&& settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
final TextRange range = mConnection.getWordRangeAtCursor(
settingsValues.mSpacingAndPunctuations,
currentKeyboardScriptId);
currentKeyboardScriptId, false);
if (range != null) {
return range.mWord.toString();
}
@ -1609,7 +1608,7 @@ public final class InputLogic {
return;
}
final TextRange range = mConnection.getWordRangeAtCursor(
settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId);
settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId, true);
if (null == range) return; // Happens if we don't have an input connection at all
if (range.length() <= 0) {
// Race condition, or touching a word in a non-supported script.
@ -2326,7 +2325,7 @@ public final class InputLogic {
// hence 2; if we aren't, we should just skip whitespace if any, so 1.
mWordComposer.isComposingWord() ? 2 : 1),
keyboard,
new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive),
settingsValues.mSettingsValuesForSuggestion,
settingsValues.mAutoCorrectionEnabledPerUserSettings,
inputStyle, sequenceNumber, callback);
}

View file

@ -18,22 +18,24 @@ package org.dslul.openboard.inputmethod.latin.settings;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet;
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
import org.dslul.openboard.inputmethod.latin.BuildConfig;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.SystemBroadcastReceiver;
import org.dslul.openboard.inputmethod.latin.common.FileUtils;
import org.dslul.openboard.inputmethod.latin.define.JniLibName;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -56,59 +58,47 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_advanced);
final Resources res = getResources();
final Context context = getActivity();
final Context context = requireContext();
// When we are called from the Settings application but we are not already running, some
// singleton and utility classes may not have been initialized. We have to call
// initialization method of these classes here. See {@link LatinIME#onCreate()}.
AudioAndHapticFeedbackManager.init(context);
final SharedPreferences prefs = getSharedPreferences();
if (!Settings.isInternal(prefs)) {
if (!BuildConfig.DEBUG) {
removePreference(Settings.SCREEN_DEBUG);
}
setupKeyLongpressTimeoutSettings();
final Preference bla = findPreference("load_gesture_library");
if (bla != null) {
bla.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// get architecture for telling user which file to use
String abi;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
abi = Build.SUPPORTED_ABIS[0];
} else {
abi = Build.CPU_ABI;
}
// show delete / add dialog
final AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(R.string.load_gesture_library)
.setMessage(context.getString(R.string.load_gesture_library_message, abi))
.setPositiveButton(R.string.load_gesture_library_button_load, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/octet-stream");
startActivityForResult(intent, REQUEST_CODE_GESTURE_LIBRARY);
}
})
.setNegativeButton(android.R.string.cancel, null);
libfile = new File(context.getFilesDir().getAbsolutePath() + File.separator + JniLibName.JNI_LIB_IMPORT_FILE_NAME);
if (libfile.exists())
builder.setNeutralButton(R.string.load_gesture_library_button_delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
libfile.delete();
Runtime.getRuntime().exit(0);
}
});
builder.show();
return true;
final Preference loadGestureLibrary = findPreference("load_gesture_library");
if (loadGestureLibrary != null) {
loadGestureLibrary.setOnPreferenceClickListener(preference -> {
// get architecture for telling user which file to use
String abi;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abi = Build.SUPPORTED_ABIS[0];
} else {
abi = Build.CPU_ABI;
}
// show delete / add dialog
final AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(R.string.load_gesture_library)
.setMessage(context.getString(R.string.load_gesture_library_message, abi))
.setPositiveButton(R.string.load_gesture_library_button_load, (dialogInterface, i) -> {
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/octet-stream");
startActivityForResult(intent, REQUEST_CODE_GESTURE_LIBRARY);
})
.setNegativeButton(android.R.string.cancel, null);
libfile = new File(context.getFilesDir().getAbsolutePath() + File.separator + JniLibName.JNI_LIB_IMPORT_FILE_NAME);
if (libfile.exists())
builder.setNeutralButton(R.string.load_gesture_library_button_delete, (dialogInterface, i) -> {
libfile.delete();
Runtime.getRuntime().exit(0);
});
builder.show();
return true;
});
}
}
@ -118,17 +108,11 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
if (requestCode != REQUEST_CODE_GESTURE_LIBRARY || resultCode != Activity.RESULT_OK || resultData == null) return;
if (resultData.getData() != null && libfile != null) {
try {
FileOutputStream out = new FileOutputStream(libfile);
final InputStream in = getActivity().getContentResolver().openInputStream(resultData.getData());
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.flush();
Runtime.getRuntime().exit(0);
final InputStream in = requireContext().getContentResolver().openInputStream(resultData.getData());
FileUtils.copyStreamToNewFile(in, libfile);
Runtime.getRuntime().exit(0); // exit will restart the app, so library will be loaded
} catch (IOException e) {
// should inform user
// should inform user, but probably the issues will only come when reading the library
}
}
}
@ -165,7 +149,7 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
@Override
public String getValueText(final int value) {
return res.getString(R.string.abbreviation_unit_milliseconds, value);
return res.getString(R.string.abbreviation_unit_milliseconds, Integer.toString(value));
}
@Override
@ -176,7 +160,9 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) {
SystemBroadcastReceiver.toggleAppIcon(getActivity());
SystemBroadcastReceiver.toggleAppIcon(requireContext());
} else if (key.equals(Settings.PREF_SHOW_ALL_MORE_KEYS)) {
KeyboardLayoutSet.onKeyboardThemeChanged();
}
}
}

View file

@ -1,28 +1,9 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dslul.openboard.inputmethod.latin.settings
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.TwoStatePreference
@ -37,39 +18,42 @@ import java.util.*
/**
* "Appearance" settings sub screen.
*/
@Suppress("Deprecation") // yes everything here is deprecated, but only work on this if really necessary
// todo: simplify when removing old themes (or migrating holo to same style as user themes)
// there is a bunch of ugly things in the theme settings, and mostly for historic reasons...
// idea for color selection
// left: which color (background, key, text,...)
// right: color preview (always the correct one, even if determined automatically)
// maybe copy parts from simple keyboard, see e.g. screenshot 4 in https://github.com/SimpleMobileTools/Simple-Keyboard/tree/main/fastlane/metadata/android/en-US/images/phoneScreenshots
// below (for some colors, with indent):
// enable user-defining (most colors, but definitely not background)
// use system accent (for accent and text colors)
// on click: color selector
// maybe copy parts from simple keyboard, see e.g. screenshot 4 in https://github.com/SimpleMobileTools/Simple-Keyboard/tree/main/fastlane/metadata/android/en-US/images/phoneScreenshots
// but full range would be preferable
// use some color picker library? would likely allow nicer tuning
class AppearanceSettingsFragment : SubScreenFragment(), Preference.OnPreferenceChangeListener, OnSharedPreferenceChangeListener {
class AppearanceSettingsFragment : SubScreenFragment() {
private var selectedThemeId = 0
private var needsReload = false
private lateinit var themeFamilyPref: ListPreference
private lateinit var themeVariantPref: ListPreference
private var customThemeVariantNightPref: ListPreference? = null
private lateinit var keyBordersPref: TwoStatePreference
private var dayNightPref: TwoStatePreference? = null
private lateinit var userColorsPref: Preference
private val themeFamilyPref: ListPreference by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_STYLE)!! }
private val themeVariantPref: ListPreference by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT)!! }
private val themeVariantNightPref: ListPreference? by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT_NIGHT) }
private val dayNightPref: TwoStatePreference? by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_DAY_NIGHT) }
private val userColorsPref: Preference by lazy { preferenceScreen.findPreference("theme_select_colors")!! }
private val userColorsPrefNight: Preference? by lazy { preferenceScreen.findPreference("theme_select_colors_night") }
override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)
addPreferencesFromResource(R.xml.prefs_screen_appearance)
val keyboardTheme = KeyboardTheme.getKeyboardTheme(activity)
selectedThemeId = keyboardTheme.mThemeId
removeUnsuitablePreferences()
setupTheme()
setThemeVariantPrefs(sharedPreferences.getString(Settings.PREF_THEME_STYLE, KeyboardTheme.THEME_STYLE_MATERIAL)!!)
setupKeyboardHeight(Settings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE)
}
override fun onPause() {
super.onPause()
if (needsReload)
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
needsReload = false
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
super.onSharedPreferenceChanged(prefs, key)
needsReload = true // may not always be the necessary, but that's ok
}
private fun removeUnsuitablePreferences() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
removePreference(Settings.PREF_THEME_DAY_NIGHT)
removePreference(Settings.PREF_THEME_VARIANT_NIGHT)
@ -81,243 +65,77 @@ class AppearanceSettingsFragment : SubScreenFragment(), Preference.OnPreferenceC
) {
removePreference(Settings.PREF_THEME_DAY_NIGHT)
removePreference(Settings.PREF_THEME_VARIANT_NIGHT)
removePreference("theme_select_colors_night")
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// todo: consider removing the preference, and always set the navbar color
removePreference(Settings.PREF_NAVBAR_COLOR)
}
setupTheme()
val metrics = requireContext().resources.displayMetrics
val widthDp = metrics.widthPixels / metrics.density
val heightDp = metrics.heightPixels / metrics.density
if (!ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED || (min(widthDp, heightDp) < 600 && max(widthDp, heightDp) < 720)) {
removePreference(Settings.PREF_ENABLE_SPLIT_KEYBOARD)
}
setupKeyboardHeight(
Settings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE)
}
override fun onResume() {
super.onResume()
updateThemePreferencesState()
updateAfterPreferenceChanged()
}
override fun onPause() {
super.onPause()
// if (needsReload) // todo: until re-working settings, just always reload
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
needsReload = false
}
override fun onPreferenceChange(preference: Preference, value: Any?): Boolean {
(preference as? ListPreference)?.apply {
summary = entries[entryValues.indexOfFirst { it == value }]
}
saveSelectedThemeId()
return true
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
super.onSharedPreferenceChanged(prefs, key)
updateAfterPreferenceChanged()
}
// doing things on changing, but with the old values is not good, this is at least a little better
private fun updateAfterPreferenceChanged() {
customThemeVariantNightPref?.apply {
if (true) { //KeyboardTheme.getIsCustom(selectedThemeId)) {
// show preference to allow choosing a night theme
// can't hide a preference, at least not without category or maybe some androidx things
// -> just disable it instead (for now...)
isEnabled = sharedPreferences!!.getBoolean(Settings.PREF_THEME_DAY_NIGHT, false)
} else
isEnabled = false
val variant = sharedPreferences!!.getString(Settings.PREF_THEME_VARIANT_NIGHT, KeyboardTheme.THEME_DARKER)
val variants = KeyboardTheme.THEME_VARIANTS_DARK
entries = variants.map {
// todo: this workaround get the same string as for "user" theme, maybe clarify that it's a separate theme
val name = if (it == "user_dark") "theme_name_user" else "theme_name_$it"
val resId = resources.getIdentifier(name, "string", requireContext().packageName)
private fun setThemeVariantPrefs(themeFamily: String) {
themeVariantPref.apply {
entryValues = if (themeFamily == KeyboardTheme.THEME_STYLE_HOLO) KeyboardTheme.THEME_VARIANTS
else KeyboardTheme.THEME_VARIANTS.filterNot { it == KeyboardTheme.THEME_HOLO_WHITE }.toTypedArray()
entries = entryValues.map {
val resId = resources.getIdentifier("theme_name_$it", "string", requireContext().packageName)
if (resId == 0) it else getString(resId)
}.toTypedArray()
entryValues = variants
value = variant
val name = if (variant == "user_dark") "theme_name_user" else "theme_name_$variant"
val resId = resources.getIdentifier(name, "string", requireContext().packageName)
summary = if (resId == 0) variant else getString(resId)
}
userColorsPref.apply {
isEnabled = true //KeyboardTheme.getIsCustom(selectedThemeId)
&& (sharedPreferences!!.getString(Settings.PREF_THEME_VARIANT, KeyboardTheme.THEME_LIGHT) == KeyboardTheme.THEME_USER
|| (sharedPreferences!!.getString(Settings.PREF_THEME_VARIANT_NIGHT, KeyboardTheme.THEME_DARKER) == KeyboardTheme.THEME_USER_DARK
&& sharedPreferences!!.getBoolean(Settings.PREF_THEME_DAY_NIGHT, false)
))
isEnabled = true
}
}
if (value !in entryValues)
value = entryValues.first().toString()
summary = entries[entryValues.indexOfFirst { it == value }]
private fun saveSelectedThemeId(
family: String = themeFamilyPref.value,
variant: String = themeVariantPref.value,
keyBorders: Boolean = keyBordersPref.isChecked
) {
// selectedThemeId = KeyboardTheme.getThemeForParameters(family, variant, keyBorders)
// KeyboardTheme.saveKeyboardThemeId(selectedThemeId, sharedPreferences)
}
private fun updateThemePreferencesState(skipThemeFamily: Boolean = false, skipThemeVariant: Boolean = false) {
val themeFamily = KeyboardTheme.getThemeFamily(selectedThemeId)
val isLegacyFamily = KeyboardTheme.THEME_STYLE_HOLO == themeFamily
if (!skipThemeFamily) {
themeFamilyPref.apply {
value = themeFamily
summary = themeFamily
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
summary = entries[entryValues.indexOfFirst { it == value }]
userColorsPref.isVisible = value == KeyboardTheme.THEME_USER
true
}
}
val variants = KeyboardTheme.THEME_VARIANTS
val variant = sharedPreferences.getString(Settings.PREF_THEME_VARIANT, KeyboardTheme.THEME_LIGHT)
if (!skipThemeVariant) {
themeVariantPref.apply {
entries = if (isLegacyFamily) variants // todo: translatable string for holo, not internal name
else variants.map {
val resId = resources.getIdentifier("theme_name_$it", "string", requireContext().packageName)
if (resId == 0) it else getString(resId)
}.toTypedArray()
entryValues = variants
value = variant ?: variants[0]
summary = if (isLegacyFamily) variant
else {
val resId = resources.getIdentifier("theme_name_$variant", "string", requireContext().packageName)
if (resId == 0) variant else getString(resId)
}
themeVariantNightPref?.apply {
entryValues = if (themeFamily == KeyboardTheme.THEME_STYLE_HOLO) KeyboardTheme.THEME_VARIANTS_DARK
else KeyboardTheme.THEME_VARIANTS_DARK.filterNot { it == KeyboardTheme.THEME_HOLO_WHITE }.toTypedArray()
entries = entryValues.map {
val resId = resources.getIdentifier("theme_name_$it", "string", requireContext().packageName)
if (resId == 0) it else getString(resId)
}.toTypedArray()
if (value !in entryValues)
value = entryValues.first().toString()
summary = entries[entryValues.indexOfFirst { it == value }]
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
summary = entries[entryValues.indexOfFirst { it == value }]
userColorsPrefNight?.isVisible = value == KeyboardTheme.THEME_USER_NIGHT
true
}
}
keyBordersPref.apply {
isEnabled = !isLegacyFamily
isChecked = isLegacyFamily || KeyboardTheme.getHasKeyBorders(selectedThemeId)
}
dayNightPref?.apply {
isEnabled = !isLegacyFamily
isChecked = !isLegacyFamily && /*KeyboardTheme.getIsCustom(selectedThemeId) &&*/ sharedPreferences!!.getBoolean(Settings.PREF_THEME_DAY_NIGHT, false)
}
}
private fun setupTheme() {
themeFamilyPref = preferenceScreen.findPreference(Settings.PREF_THEME_STYLE)!!
themeFamilyPref.apply {
entries = KeyboardTheme.THEME_STYLES
entryValues = KeyboardTheme.THEME_STYLES
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
summary = entries[entryValues.indexOfFirst { it == value }]
saveSelectedThemeId(family = value as String)
updateThemePreferencesState(skipThemeFamily = true)
setThemeVariantPrefs(value.toString())
true
}
summary = entries[entryValues.indexOfFirst { it == value }]
}
// todo: remove!
themeVariantPref = preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT)!!
themeVariantPref.apply {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
summary = entries[entryValues.indexOfFirst { it == value }]
if (themeFamilyPref.value == KeyboardTheme.THEME_STYLE_MATERIAL) {
// not so nice workaround, could be removed in the necessary re-work: new value seems
// to be stored only after this method call, but we update the summary and user-defined color enablement in here -> store it now
if (value == sharedPreferences!!.getString(Settings.PREF_THEME_VARIANT, KeyboardTheme.THEME_LIGHT))
return@OnPreferenceChangeListener true // avoid infinite loop
sharedPreferences!!.edit { putString(Settings.PREF_THEME_VARIANT, value as String) }
summary = entries[entryValues.indexOfFirst { it == value }]
needsReload = true
}
saveSelectedThemeId(variant = value as String)
updateThemePreferencesState(skipThemeFamily = true, skipThemeVariant = true)
true
}
}
keyBordersPref = preferenceScreen.findPreference(Settings.PREF_THEME_KEY_BORDERS)!!
keyBordersPref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
saveSelectedThemeId(keyBorders = value as Boolean)
updateThemePreferencesState(skipThemeFamily = true)
true
}
dayNightPref = preferenceScreen.findPreference(Settings.PREF_THEME_DAY_NIGHT)!!
dayNightPref?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
updateThemePreferencesState(skipThemeFamily = true)
themeVariantNightPref?.isVisible = value as Boolean
userColorsPrefNight?.isVisible = value && themeVariantNightPref?.value == KeyboardTheme.THEME_USER_NIGHT
true
}
customThemeVariantNightPref = preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT_NIGHT) as? ListPreference
customThemeVariantNightPref?.apply {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
// not so nice workaround, could be removed in the necessary re-work: new value seems
// to be stored only after this method call, but we update the summary and user-defined color enablement in here -> store it now
if (value == sharedPreferences!!.getString(Settings.PREF_THEME_VARIANT_NIGHT, KeyboardTheme.THEME_DARK))
return@OnPreferenceChangeListener true // avoid infinite loop
sharedPreferences!!.edit { putString(Settings.PREF_THEME_VARIANT_NIGHT, value as String) }
summary = entries[entryValues.indexOfFirst { it == value }]
needsReload = true
true
}
}
userColorsPref = preferenceScreen.findPreference(Settings.PREF_THEME_USER)!!
userColorsPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
if (sharedPreferences.getBoolean(Settings.PREF_THEME_DAY_NIGHT, false) && sharedPreferences.getString(Settings.PREF_THEME_VARIANT, KeyboardTheme.THEME_LIGHT) == KeyboardTheme.THEME_USER)
AlertDialog.Builder(requireContext())
.setMessage(R.string.day_or_night_colors)
.setPositiveButton(R.string.day_or_night_night) { _, _ -> adjustColors(true)}
.setNegativeButton(R.string.day_or_night_day) { _, _ -> adjustColors(false)}
.show()
else if (sharedPreferences.getBoolean(Settings.PREF_THEME_DAY_NIGHT, false)) // only night theme custom
adjustColors(true)
else // customize day theme
adjustColors(false)
true
}
preferenceScreen.findPreference<Preference>(Settings.PREF_NARROW_KEY_GAPS)?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
needsReload = true
true
}
}
private fun adjustColors(dark: Boolean) {
val items = listOf(R.string.select_color_background, R.string.select_color_key, R.string.select_color_key_hint, R.string.select_color_accent, R.string.select_color_key_background)
.map { requireContext().getString(it) }
val itemsArray = if (keyBordersPref.isChecked) items.toTypedArray()
else items.subList(0, 4).toTypedArray()
AlertDialog.Builder(requireContext())
.setPositiveButton(android.R.string.ok, null)
.setTitle(R.string.select_color_to_adjust)
.setItems(itemsArray) { _, i ->
val (pref, default) =
if (dark)
when (i) {
0 -> Settings.PREF_THEME_USER_DARK_COLOR_BACKGROUND to Color.DKGRAY
1 -> Settings.PREF_THEME_USER_DARK_COLOR_TEXT to Color.WHITE
2 -> Settings.PREF_THEME_USER_DARK_COLOR_HINT_TEXT to Color.WHITE
3 -> Settings.PREF_THEME_USER_DARK_COLOR_ACCENT to Color.BLUE
else -> Settings.PREF_THEME_USER_DARK_COLOR_KEYS to Color.LTGRAY
}
else
when (i) {
0 -> Settings.PREF_THEME_USER_COLOR_BACKGROUND to Color.DKGRAY
1 -> Settings.PREF_THEME_USER_COLOR_TEXT to Color.WHITE
2 -> Settings.PREF_THEME_USER_COLOR_HINT_TEXT to Color.WHITE
3 -> Settings.PREF_THEME_USER_COLOR_ACCENT to Color.BLUE
else -> Settings.PREF_THEME_USER_COLOR_KEYS to Color.LTGRAY
}
val d = ColorPickerDialog(requireContext(), items[i], sharedPreferences, pref, default) { needsReload = true}
d.show()
}
.show()
themeVariantNightPref?.isVisible = dayNightPref?.isChecked == true
userColorsPref.isVisible = themeVariantPref.value == KeyboardTheme.THEME_USER
userColorsPrefNight?.isVisible = dayNightPref?.isChecked == true && themeVariantNightPref?.value == KeyboardTheme.THEME_USER_NIGHT
}
private fun setupKeyboardHeight(prefKey: String, defaultValue: Float) {
@ -345,11 +163,7 @@ class AppearanceSettingsFragment : SubScreenFragment(), Preference.OnPreferenceC
})
}
override fun onDisplayPreferenceDialog(preference: Preference) {
super.onDisplayPreferenceDialog(preference)
}
companion object {
private const val PERCENTAGE_FLOAT = 100.0f
}
}
}

View file

@ -1,114 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// adapted from https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/settings/ColorDialogPreference.java
package org.dslul.openboard.inputmethod.latin.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.view.View;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import org.dslul.openboard.inputmethod.latin.R;
public class ColorPickerDialog extends AlertDialog implements SeekBar.OnSeekBarChangeListener {
protected ColorPickerDialog(final Context context, final String title, final SharedPreferences prefs,
final String colorPref, final int defaultColor, Runnable onChanged) {
super(context);
setTitle(title);
View view = getLayoutInflater().inflate(R.layout.color_dialog, null);
mSeekBarRed = (SeekBar)view.findViewById(R.id.seek_bar_dialog_bar_red);
mSeekBarRed.setMax(255);
mSeekBarRed.setOnSeekBarChangeListener(this);
mSeekBarRed.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
mSeekBarRed.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
mSeekBarGreen = (SeekBar)view.findViewById(R.id.seek_bar_dialog_bar_green);
mSeekBarGreen.setMax(255);
mSeekBarGreen.setOnSeekBarChangeListener(this);
mSeekBarGreen.getThumb().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_IN);
mSeekBarGreen.getProgressDrawable().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_IN);
mSeekBarBlue = (SeekBar)view.findViewById(R.id.seek_bar_dialog_bar_blue);
mSeekBarBlue.setMax(255);
mSeekBarBlue.setOnSeekBarChangeListener(this);
mSeekBarBlue.getThumb().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
mSeekBarBlue.getProgressDrawable().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
mValueView = (TextView)view.findViewById(R.id.seek_bar_dialog_value);
setView(view);
// init with correct values
// using onShowListener?
setOnShowListener(dialogInterface -> {
int color = prefs.getInt(colorPref, defaultColor);
mSeekBarRed.setProgress(Color.red(color));
mSeekBarGreen.setProgress(Color.green(color));
mSeekBarBlue.setProgress(Color.blue(color));
setHeaderText(color);
});
// set on ok and on cancel listeners
setButton(BUTTON_NEGATIVE, context.getText(android.R.string.cancel), (dialogInterface, i) -> dismiss());
setButton(BUTTON_POSITIVE, context.getText(android.R.string.ok), (dialogInterface, i) -> {
final int value = Color.rgb(
mSeekBarRed.getProgress(),
mSeekBarGreen.getProgress(),
mSeekBarBlue.getProgress());
prefs.edit().putInt(colorPref, value).apply();
onChanged.run();
dismiss();
});
}
private final TextView mValueView;
private final SeekBar mSeekBarRed;
private final SeekBar mSeekBarGreen;
private final SeekBar mSeekBarBlue;
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {
int color = Color.rgb(
mSeekBarRed.getProgress(),
mSeekBarGreen.getProgress(),
mSeekBarBlue.getProgress());
setHeaderText(color);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
private void setHeaderText(int color) {
mValueView.setText(getValueText(color));
boolean bright = Color.red(color) + Color.green(color) + Color.blue(color) > 128 * 3;
mValueView.setTextColor(bright ? Color.BLACK : Color.WHITE);
mValueView.setBackgroundColor(color);
}
private String getValueText(final int value) {
String temp = Integer.toHexString(value);
for (; temp.length() < 8; temp = "0" + temp);
return temp.substring(2).toUpperCase();
}
}

View file

@ -0,0 +1,166 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.app.Activity
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.view.forEachIndexed
import androidx.fragment.app.Fragment
import com.skydoves.colorpickerview.ColorPickerDialog
import com.skydoves.colorpickerview.flag.BubbleFlag
import com.skydoves.colorpickerview.flag.FlagMode
import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager
import org.dslul.openboard.inputmethod.latin.databinding.ColorSettingBinding
import org.dslul.openboard.inputmethod.latin.databinding.ColorSettingsBinding
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
import org.dslul.openboard.inputmethod.latin.utils.ExecutorUtils
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils
open class ColorsSettingsFragment : Fragment(R.layout.color_settings) {
private val binding by viewBinding(ColorSettingsBinding::bind)
open val isNight = false
open val titleResId = R.string.select_user_colors
private val prefs by lazy { DeviceProtectedUtils.getSharedPreferences(requireContext()) }
private val colorPrefs = listOf(
Settings.PREF_COLOR_BACKGROUND_SUFFIX,
Settings.PREF_COLOR_KEYS_SUFFIX,
Settings.PREF_COLOR_TEXT_SUFFIX,
Settings.PREF_COLOR_HINT_TEXT_SUFFIX,
Settings.PREF_COLOR_ACCENT_SUFFIX,
)
override fun onResume() {
super.onResume()
val activity: Activity? = activity
if (activity is AppCompatActivity) {
val actionBar = activity.supportActionBar ?: return
actionBar.setTitle(titleResId)
}
if (isNight != ResourceUtils.isNight(requireContext().resources)) {
// reload to get the right configuration
prefs.edit { putBoolean(Settings.PREF_FORCE_OPPOSITE_THEME, true) }
reloadKeyboard(false)
}
}
override fun onPause() {
super.onPause()
prefs.edit { putBoolean(Settings.PREF_FORCE_OPPOSITE_THEME, false) }
if (isNight != ResourceUtils.isNight(requireContext().resources))
// reload again so the correct configuration is applied
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val colorPrefNames = listOf(
R.string.select_color_background,
R.string.select_color_key_background,
R.string.select_color_key,
R.string.select_color_key_hint,
R.string.select_color_accent,
).map { requireContext().getString(it) }
val prefPrefix = if (isNight) Settings.PREF_THEME_USER_COLOR_NIGHT_PREFIX else Settings.PREF_THEME_USER_COLOR_PREFIX
colorPrefs.forEachIndexed { index, colorPref ->
val csb = ColorSettingBinding.inflate(layoutInflater, binding.colorSettingsContainer, true)
csb.colorSwitch.isChecked = !prefs.getBoolean(prefPrefix + colorPref + Settings.PREF_AUTO_USER_COLOR_SUFFIX, true)
csb.colorPreview.setColorFilter(Settings.readUserColor(prefs, requireContext(), colorPrefs[index], isNight))
csb.colorText.text = colorPrefNames[index]
if (!csb.colorSwitch.isChecked) {
csb.colorSummary.setText(R.string.auto_user_color)
}
val switchListener = CompoundButton.OnCheckedChangeListener { _, b ->
val hidden = RichInputMethodManager.getInstance().inputMethodManager.hideSoftInputFromWindow(binding.dummyText.windowToken, 0)
prefs.edit { putBoolean(prefPrefix + colorPref + Settings.PREF_AUTO_USER_COLOR_SUFFIX, !b) }
if (b) csb.colorSummary.text = ""
else csb.colorSummary.setText(R.string.auto_user_color)
reloadKeyboard(hidden)
updateColorPreviews()
}
csb.colorSwitch.setOnCheckedChangeListener(switchListener)
val clickListener = View.OnClickListener {
val hidden = RichInputMethodManager.getInstance().inputMethodManager.hideSoftInputFromWindow(binding.dummyText.windowToken, 0)
val b = ColorPickerDialog.Builder(requireContext())
.setTitle(colorPrefNames[index])
// todo: later alphy bar should be activated, but currently setting alpha leads to glitches,
// e.g. when setting alpha on key text it's not applied for key icons, but for emojis
.attachAlphaSlideBar(false)
.setPositiveButton(android.R.string.ok, ColorEnvelopeListener { envelope, _ ->
prefs.edit { putInt(prefPrefix + colorPrefs[index], envelope.color) }
if (!csb.colorSwitch.isChecked) {
prefs.edit { putBoolean(prefPrefix + colorPref + Settings.PREF_AUTO_USER_COLOR_SUFFIX, false) }
csb.colorSwitch.setOnCheckedChangeListener(null)
csb.colorSwitch.isChecked = true
csb.colorSummary.text = ""
csb.colorSwitch.setOnCheckedChangeListener(switchListener)
reloadKeyboard(hidden)
updateColorPreviews()
return@ColorEnvelopeListener
}
reloadKeyboard(hidden)
updateColorPreviews()
})
.setNegativeButton(android.R.string.cancel) { _, _ ->
if (hidden)
RichInputMethodManager.getInstance().inputMethodManager.showSoftInput(binding.dummyText, 0)
}
val initialColor = if (prefs.contains(prefPrefix + colorPref))
prefs.getInt(prefPrefix + colorPref, Color.GRAY)
else
Settings.readUserColor(prefs, requireContext(), colorPrefs[index], isNight)
b.colorPickerView.setInitialColor(initialColor)
// set better color drawable? neither the white circle nor the plus is nice
b.colorPickerView.setSelectorDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_plus))
b.colorPickerView.flagView = BubbleFlag(requireContext()).apply { flagMode = FlagMode.ALWAYS }
b.show()
}
csb.colorTextContainer.setOnClickListener(clickListener)
csb.colorPreview.setOnClickListener(clickListener)
}
}
private fun updateColorPreviews() {
binding.colorSettingsContainer.forEachIndexed { index, view ->
val color = Settings.readUserColor(prefs, requireContext(), colorPrefs[index], isNight)
view.findViewById<ImageView>(R.id.color_preview)?.setColorFilter(color)
}
}
private fun reloadKeyboard(show: Boolean) {
// todo: any way to make some kind of "light update" to keyboard?
// only reloading main keyboard view is necessary...
// or get an actual (live) preview instead of the full keyboard?
// or accelerate keyboard inflate, a big here issue is emojiCategory creating many keyboards
// KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
// if (!show) return
// Thread.sleep(100) // some pause is necessary to avoid visual glitches
// RichInputMethodManager.getInstance().inputMethodManager.showSoftInput(binding.dummyText, 0)
// return
// todo: fix slowdowns and sometimes showing glitches with above, then move away from executor
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute {
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
if (!show) return@execute
// for some reason showing again does not work when running with executor
// but when running without it's noticeably slow, and sometimes produces glitches
Thread.sleep(100)
RichInputMethodManager.getInstance().inputMethodManager.showSoftInput(binding.dummyText, 0)
}
}
}
class ColorsNightSettingsFragment : ColorsSettingsFragment() {
override val isNight = true
override val titleResId = R.string.select_user_colors_night
}

View file

@ -19,8 +19,6 @@ package org.dslul.openboard.inputmethod.latin.settings;
import static org.dslul.openboard.inputmethod.latin.permissions.PermissionsManager.get;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -68,8 +66,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_correction);
final Context context = getActivity();
final PackageManager pm = context.getPackageManager();
final PackageManager pm = requireContext().getPackageManager();
final Preference editPersonalDictionary =
findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
@ -79,7 +76,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
if (ri == null) {
overwriteUserDictionaryPreference(editPersonalDictionary);
}
mLookupContactsPreference = (SwitchPreferenceCompat) findPreference(AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY);
mLookupContactsPreference = findPreference(AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY);
refreshEnabledSettings();
}
@ -91,7 +88,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
&& !PermissionsUtil.checkAllPermissionsGranted(
getActivity() /* context */, Manifest.permission.READ_CONTACTS)
) {
get(getActivity() /* context */).requestPermissions(this /* PermissionsResultCallback */,
get(requireContext()).requestPermissions(this /* PermissionsResultCallback */,
getActivity() /* activity */, Manifest.permission.READ_CONTACTS);
}
refreshEnabledSettings();
@ -113,15 +110,14 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
}
private void refreshEnabledSettings() {
setPreferenceEnabled(Settings.PREF_AUTO_CORRECTION_CONFIDENCE,
Settings.readAutoCorrectEnabled(getSharedPreferences(), getResources()));
setPreferenceEnabled(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, getSharedPreferences().getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true));
setPreferenceVisible(Settings.PREF_AUTO_CORRECTION_CONFIDENCE,
Settings.readAutoCorrectEnabled(getSharedPreferences()));
setPreferenceVisible(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, getSharedPreferences().getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true));
turnOffLookupContactsIfNoPermission();
}
private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
final Activity activity = getActivity();
final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(requireActivity());
if (null == localeList) {
// The locale list is null if and only if the user dictionary service is
// not present or disabled. In this case we need to remove the preference.

View file

@ -29,8 +29,6 @@ public final class DebugSettings {
public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch";
public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS =
"pref_has_custom_key_preview_animation_params";
public static final String PREF_RESIZE_KEYBOARD = "pref_resize_keyboard";
public static final String PREF_KEYBOARD_HEIGHT_SCALE = "pref_keyboard_height_scale";
public static final String PREF_KEY_PREVIEW_DISMISS_DURATION =
"pref_key_preview_dismiss_duration";
public static final String PREF_KEY_PREVIEW_DISMISS_END_X_SCALE =

View file

@ -23,6 +23,7 @@ import android.content.res.Resources;
import android.os.Bundle;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.TwoStatePreference;
@ -57,8 +58,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI);
}
final PreferenceGroup dictDumpPreferenceGroup =
(PreferenceGroup)findPreference(PREF_KEY_DUMP_DICTS);
final PreferenceGroup dictDumpPreferenceGroup = findPreference(PREF_KEY_DUMP_DICTS);
for (final String dictName : DictionaryFacilitatorImpl.DICT_TYPE_TO_CLASS.keySet()) {
final Preference pref = new DictDumpPreference(getActivity(), dictName);
pref.setOnPreferenceClickListener(this);
@ -81,11 +81,9 @@ public final class DebugSettingsFragment extends SubScreenFragment
defaultKeyPreviewDismissEndScale);
setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
defaultKeyPreviewDismissEndScale);
setupKeyboardHeight(
DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE);
mServiceNeedsRestart = false;
mDebugMode = (TwoStatePreference) findPreference(DebugSettings.PREF_DEBUG_MODE);
mDebugMode = findPreference(DebugSettings.PREF_DEBUG_MODE);
updateDebugMode();
}
@ -101,15 +99,14 @@ public final class DebugSettingsFragment extends SubScreenFragment
}
@Override
public boolean onPreferenceClick(final Preference pref) {
final Context context = getActivity();
public boolean onPreferenceClick(@NonNull final Preference pref) {
if (pref instanceof DictDumpPreference) {
final DictDumpPreference dictDumpPref = (DictDumpPreference)pref;
final String dictName = dictDumpPref.mDictName;
final Intent intent = new Intent(
DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName);
context.sendBroadcast(intent);
pref.getContext().sendBroadcast(intent);
return true;
}
return true;
@ -119,7 +116,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
public void onStop() {
super.onStop();
if (mServiceNeedsRestart) {
Process.killProcess(Process.myPid());
Runtime.getRuntime().exit(0);
}
}
@ -129,11 +126,8 @@ public final class DebugSettingsFragment extends SubScreenFragment
mDebugMode.setChecked(prefs.getBoolean(DebugSettings.PREF_DEBUG_MODE, false));
updateDebugMode();
mServiceNeedsRestart = true;
return;
}
if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
} else if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
mServiceNeedsRestart = true;
return;
}
}
@ -153,7 +147,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
private void setupKeyPreviewAnimationScale(final String prefKey, final float defaultValue) {
final SharedPreferences prefs = getSharedPreferences();
final Resources res = getResources();
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
final SeekBarDialogPreference pref = findPreference(prefKey);
if (pref == null) {
return;
}
@ -205,7 +199,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
private void setupKeyPreviewAnimationDuration(final String prefKey, final int defaultValue) {
final SharedPreferences prefs = getSharedPreferences();
final Resources res = getResources();
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
final SeekBarDialogPreference pref = findPreference(prefKey);
if (pref == null) {
return;
}
@ -232,7 +226,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
@Override
public String getValueText(final int value) {
return res.getString(R.string.abbreviation_unit_milliseconds, value);
return res.getString(R.string.abbreviation_unit_milliseconds, Integer.toString(value));
}
@Override
@ -240,49 +234,4 @@ public final class DebugSettingsFragment extends SubScreenFragment
});
}
private void setupKeyboardHeight(final String prefKey, final float defaultValue) {
final SharedPreferences prefs = getSharedPreferences();
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
if (pref == null) {
return;
}
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
private static final float PERCENTAGE_FLOAT = 100.0f;
private float getValueFromPercentage(final int percentage) {
return percentage / PERCENTAGE_FLOAT;
}
private int getPercentageFromValue(final float floatValue) {
return (int)(floatValue * PERCENTAGE_FLOAT);
}
@Override
public void writeValue(final int value, final String key) {
prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
}
@Override
public void writeDefaultValue(final String key) {
prefs.edit().remove(key).apply();
}
@Override
public int readValue(final String key) {
return getPercentageFromValue(Settings.readKeyboardHeight(prefs, defaultValue));
}
@Override
public int readDefaultValue(final String key) {
return getPercentageFromValue(defaultValue);
}
@Override
public String getValueText(final int value) {
return String.format(Locale.ROOT, "%d%%", value);
}
@Override
public void feedbackValue(final int value) {}
});
}
}

View file

@ -0,0 +1,46 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
// taken from StreetComplete, ViewBinder.kt
inline fun <reified T : ViewBinding> Fragment.viewBinding(
noinline viewBinder: (View) -> T,
rootViewId: Int? = null
) = FragmentViewBindingPropertyDelegate(this, viewBinder, rootViewId)
class FragmentViewBindingPropertyDelegate<T : ViewBinding>(
private val fragment: Fragment,
private val viewBinder: (View) -> T,
private val rootViewId: Int? = null
) : ReadOnlyProperty<Fragment, T>, LifecycleEventObserver {
private var binding: T? = null
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
binding = null
source.lifecycle.removeObserver(this)
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
if (binding == null) {
val rootView = if (rootViewId != null) {
thisRef.requireView().findViewById<ViewGroup>(rootViewId)!!.getChildAt(0)
} else {
thisRef.requireView()
}
binding = viewBinder(rootView)
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
}
return binding!!
}
}

Some files were not shown because too many files have changed in this diff Show more