mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-17 23:42:55 +00:00
Merge branch 'new' into hangul_update
This commit is contained in:
commit
cf17b4984a
917 changed files with 4740 additions and 12260 deletions
|
@ -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] =
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package org.dslul.openboard.inputmethod.dictionarypack
|
||||
|
||||
/**
|
||||
* A simple interface to report problems.
|
||||
*/
|
||||
interface ProblemReporter {
|
||||
fun report(e: Exception?)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!!
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.dslul.openboard.inputmethod.latin.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
|
@ -34,5 +36,22 @@ public final class GestureSettingsFragment extends SubScreenFragment {
|
|||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
addPreferencesFromResource(R.xml.prefs_screen_gesture);
|
||||
refreshSettingsEnablement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||
refreshSettingsEnablement();
|
||||
}
|
||||
|
||||
private void refreshSettingsEnablement() {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
setPreferenceVisible(Settings.PREF_GESTURE_PREVIEW_TRAIL,
|
||||
Settings.readGestureInputEnabled(prefs, res));
|
||||
setPreferenceVisible(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT,
|
||||
Settings.readGestureInputEnabled(prefs, res));
|
||||
setPreferenceVisible(Settings.PREF_GESTURE_SPACE_AWARE,
|
||||
Settings.readGestureInputEnabled(prefs, res));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ class LanguageFilterListFakePreference(searchField: EditText, recyclerView: Recy
|
|||
private val adapter = LanguageAdapter(emptyList(), recyclerView.context)
|
||||
private val sortedSubtypes = mutableListOf<MutableList<SubtypeInfo>>()
|
||||
|
||||
fun setSettingsFragment(newFragment: LanguageFakeSettingsFragment?) {
|
||||
fun setSettingsFragment(newFragment: LanguageSettingsFragment?) {
|
||||
adapter.fragment = newFragment
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ class LanguageAdapter(list: List<MutableList<SubtypeInfo>> = listOf(), context:
|
|||
RecyclerView.Adapter<LanguageAdapter.ViewHolder>() {
|
||||
var onlySystemLocales = false
|
||||
private val prefs = DeviceProtectedUtils.getSharedPreferences(context)
|
||||
var fragment: LanguageFakeSettingsFragment? = null
|
||||
var fragment: LanguageSettingsFragment? = null
|
||||
|
||||
var list: List<MutableList<SubtypeInfo>> = list
|
||||
set(value) {
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.dslul.openboard.inputmethod.latin.settings
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -23,10 +22,10 @@ import java.util.*
|
|||
class LanguageSettingsDialog(
|
||||
context: Context,
|
||||
private val infos: MutableList<SubtypeInfo>,
|
||||
private val fragment: LanguageFakeSettingsFragment?,
|
||||
private val fragment: LanguageSettingsFragment?,
|
||||
private val onlySystemLocales: Boolean,
|
||||
private val onSubtypesChanged: () -> Unit
|
||||
) : AlertDialog(context), LanguageFakeSettingsFragment.Listener {
|
||||
) : AlertDialog(context), LanguageSettingsFragment.Listener {
|
||||
private val prefs = DeviceProtectedUtils.getSharedPreferences(context)!!
|
||||
private val view = LayoutInflater.from(context).inflate(R.layout.locale_settings_dialog, null)
|
||||
private val mainLocaleString = infos.first().subtype.locale()
|
||||
|
@ -179,7 +178,7 @@ class LanguageSettingsDialog(
|
|||
private fun fillDictionariesView(dictionariesView: LinearLayout) {
|
||||
dictionariesView.findViewById<ImageView>(R.id.add_dictionary).setOnClickListener {
|
||||
val link = "<a href='$DICTIONARY_URL'>" + context.getString(R.string.dictionary_link_text) + "</a>"
|
||||
val message = Html.fromHtml(context.getString(R.string.add_dictionary, link))
|
||||
val message = SpannableStringUtils.fromHtml(context.getString(R.string.add_dictionary, link))
|
||||
val dialog = Builder(context)
|
||||
.setTitle(R.string.add_new_dictionary_title)
|
||||
.setMessage(message)
|
||||
|
|
|
@ -24,7 +24,7 @@ import java.util.*
|
|||
|
||||
// not a SettingsFragment, because with androidx.preferences it's very complicated or
|
||||
// impossible to have the languages RecyclerView scrollable (this way it works nicely out of the box)
|
||||
class LanguageFakeSettingsFragment : Fragment(R.layout.language_fake_settings) {
|
||||
class LanguageSettingsFragment : Fragment(R.layout.language_settings) {
|
||||
private val sortedSubtypes = LinkedHashMap<String, MutableList<SubtypeInfo>>()
|
||||
private val enabledSubtypes = mutableListOf<InputMethodSubtype>()
|
||||
private val systemLocales = mutableListOf<Locale>()
|
||||
|
@ -81,12 +81,11 @@ class LanguageFakeSettingsFragment : Fragment(R.layout.language_fake_settings) {
|
|||
sortedSubtypes.clear()
|
||||
// list of all subtypes, any subtype added to sortedSubtypes will be removed to avoid duplicates
|
||||
val allSubtypes = getAllAvailableSubtypes().toMutableList()
|
||||
// maybe make use of the map used by SubtypeSettings for performance reasons?
|
||||
// todo: re-write this, it's hard to understand
|
||||
// also consider that more _ZZ languages might be added
|
||||
fun List<Locale>.sortedAddToSubtypesAndRemoveFromAllSubtypes() {
|
||||
val subtypesToAdd = mutableListOf<SubtypeInfo>()
|
||||
forEach { locale ->
|
||||
// this could be rather slow with looping multiple times over all ~100 subtypes,
|
||||
// but usually there aren't many locales to be checked, and usually the first loop already finds a match
|
||||
val localeString = locale.toString()
|
||||
val iterator = allSubtypes.iterator()
|
||||
var added = false
|
||||
|
@ -112,8 +111,6 @@ class LanguageFakeSettingsFragment : Fragment(R.layout.language_fake_settings) {
|
|||
}
|
||||
}
|
||||
// special treatment for the known languages with _ZZ types
|
||||
// todo: later: make it a bit less weird... and probably faster
|
||||
// consider that more _ZZ languages might be added
|
||||
if (!added && (locale.language == "sr" || locale.language == "hi")) {
|
||||
val languageString = locale.language
|
||||
val iter = allSubtypes.iterator()
|
|
@ -47,14 +47,12 @@ public class LocalSettingsConstants {
|
|||
DebugSettings.PREF_DEBUG_MODE,
|
||||
DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH,
|
||||
DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS,
|
||||
DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE,
|
||||
DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION,
|
||||
DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE,
|
||||
DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
|
||||
DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
|
||||
DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE,
|
||||
DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE,
|
||||
DebugSettings.PREF_RESIZE_KEYBOARD,
|
||||
DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI,
|
||||
DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW
|
||||
};
|
||||
|
|
|
@ -86,16 +86,16 @@ public final class PreferencesSettingsFragment extends SubScreenFragment {
|
|||
private void refreshEnablingsOfKeypressSoundAndVibrationAndHistRetentionSettings() {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS,
|
||||
setPreferenceVisible(Settings.PREF_VIBRATION_DURATION_SETTINGS,
|
||||
Settings.readVibrationEnabled(prefs, res));
|
||||
setPreferenceEnabled(Settings.PREF_KEYPRESS_SOUND_VOLUME,
|
||||
setPreferenceVisible(Settings.PREF_KEYPRESS_SOUND_VOLUME,
|
||||
Settings.readKeypressSoundEnabled(prefs, res));
|
||||
setPreferenceEnabled(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME,
|
||||
setPreferenceVisible(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME,
|
||||
Settings.readClipboardHistoryEnabled(prefs));
|
||||
}
|
||||
|
||||
private void setupKeypressVibrationDurationSettings() {
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
|
||||
final SeekBarDialogPreference pref = findPreference(
|
||||
Settings.PREF_VIBRATION_DURATION_SETTINGS);
|
||||
if (pref == null) {
|
||||
return;
|
||||
|
@ -133,20 +133,20 @@ public final class PreferencesSettingsFragment extends SubScreenFragment {
|
|||
if (value < 0) {
|
||||
return res.getString(R.string.settings_system_default);
|
||||
}
|
||||
return res.getString(R.string.abbreviation_unit_milliseconds, value);
|
||||
return res.getString(R.string.abbreviation_unit_milliseconds, Integer.toString(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupKeypressSoundVolumeSettings() {
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
|
||||
final SeekBarDialogPreference pref = findPreference(
|
||||
Settings.PREF_KEYPRESS_SOUND_VOLUME);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
final AudioManager am = (AudioManager)getActivity().getSystemService(Context.AUDIO_SERVICE);
|
||||
final AudioManager am = (AudioManager) requireContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
|
||||
private static final float PERCENTAGE_FLOAT = 100.0f;
|
||||
|
||||
|
@ -197,7 +197,7 @@ public final class PreferencesSettingsFragment extends SubScreenFragment {
|
|||
private void setupHistoryRetentionTimeSettings() {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
|
||||
final SeekBarDialogPreference pref = findPreference(
|
||||
Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME);
|
||||
if (pref == null) {
|
||||
return;
|
||||
|
@ -228,7 +228,7 @@ public final class PreferencesSettingsFragment extends SubScreenFragment {
|
|||
if (value <= 0) {
|
||||
return res.getString(R.string.settings_no_limit);
|
||||
}
|
||||
return res.getString(R.string.abbreviation_unit_minutes, value);
|
||||
return res.getString(R.string.abbreviation_unit_minutes, Integer.toString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
/*
|
||||
* 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.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
|
||||
/**
|
||||
* Radio Button preference
|
||||
*/
|
||||
public class RadioButtonPreference extends Preference {
|
||||
interface OnRadioButtonClickedListener {
|
||||
/**
|
||||
* Called when this preference needs to be saved its state.
|
||||
*
|
||||
* @param preference This preference.
|
||||
*/
|
||||
void onRadioButtonClicked(RadioButtonPreference preference);
|
||||
}
|
||||
|
||||
private boolean mIsSelected;
|
||||
private RadioButton mRadioButton;
|
||||
private OnRadioButtonClickedListener mListener;
|
||||
private final View.OnClickListener mClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
callListenerOnRadioButtonClicked();
|
||||
}
|
||||
};
|
||||
|
||||
public RadioButtonPreference(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public RadioButtonPreference(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.preferenceStyle);
|
||||
}
|
||||
|
||||
public RadioButtonPreference(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setWidgetLayoutResource(R.layout.radio_button_preference_widget);
|
||||
}
|
||||
|
||||
public void setOnRadioButtonClickedListener(final OnRadioButtonClickedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
void callListenerOnRadioButtonClicked() {
|
||||
if (mListener != null) {
|
||||
mListener.onRadioButtonClicked(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final View view = holder.itemView;
|
||||
mRadioButton = view.findViewById(R.id.radio_button);
|
||||
mRadioButton.setChecked(mIsSelected);
|
||||
mRadioButton.setOnClickListener(mClickListener);
|
||||
view.setOnClickListener(mClickListener);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return mIsSelected;
|
||||
}
|
||||
|
||||
public void setSelected(final boolean selected) {
|
||||
if (selected == mIsSelected) {
|
||||
return;
|
||||
}
|
||||
mIsSelected = selected;
|
||||
if (mRadioButton != null) {
|
||||
mRadioButton.setChecked(selected);
|
||||
}
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
|
@ -123,7 +123,6 @@ public final class SeekBarDialogPreference extends Preference
|
|||
final int value = getClippedValueFromProgress(mSeekBar.getProgress());
|
||||
setSummary(mValueProxy.getValueText(value));
|
||||
mValueProxy.writeValue(value, key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,16 @@ import android.content.SharedPreferences;
|
|||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import android.util.TypedValue;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.Gravity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme;
|
||||
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
|
||||
|
@ -37,6 +41,7 @@ import org.dslul.openboard.inputmethod.latin.common.Colors;
|
|||
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.ColorUtilKt;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.JniUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
||||
|
@ -66,79 +71,54 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static final String PREF_THEME_VARIANT_NIGHT = "theme_variant_night";
|
||||
public static final String PREF_THEME_KEY_BORDERS = "theme_key_borders";
|
||||
public static final String PREF_THEME_DAY_NIGHT = "theme_auto_day_night";
|
||||
public static final String PREF_THEME_USER = "theme_select_colors";
|
||||
public static final String PREF_THEME_USER_COLOR_TEXT = "theme_color_text";
|
||||
public static final String PREF_THEME_USER_COLOR_HINT_TEXT = "theme_color_hint_text";
|
||||
public static final String PREF_THEME_USER_COLOR_BACKGROUND = "theme_color_background";
|
||||
public static final String PREF_THEME_USER_COLOR_KEYS = "theme_color_keys";
|
||||
public static final String PREF_THEME_USER_COLOR_ACCENT = "theme_color_accent";
|
||||
public static final String PREF_THEME_USER_DARK_COLOR_TEXT = "theme_dark_color_text";
|
||||
public static final String PREF_THEME_USER_DARK_COLOR_HINT_TEXT = "theme_dark_color_hint_text";
|
||||
public static final String PREF_THEME_USER_DARK_COLOR_BACKGROUND = "theme_dark_color_background";
|
||||
public static final String PREF_THEME_USER_DARK_COLOR_KEYS = "theme_dark_color_keys";
|
||||
public static final String PREF_THEME_USER_DARK_COLOR_ACCENT = "theme_dark_color_accent";
|
||||
// PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead.
|
||||
public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode";
|
||||
public static final String PREF_THEME_USER_COLOR_PREFIX = "theme_color_";
|
||||
public static final String PREF_THEME_USER_COLOR_NIGHT_PREFIX = "theme_dark_color_";
|
||||
public static final String PREF_COLOR_KEYS_SUFFIX = "keys";
|
||||
public static final String PREF_COLOR_ACCENT_SUFFIX = "accent";
|
||||
public static final String PREF_COLOR_TEXT_SUFFIX = "text";
|
||||
public static final String PREF_COLOR_HINT_TEXT_SUFFIX = "hint_text";
|
||||
public static final String PREF_COLOR_BACKGROUND_SUFFIX = "background";
|
||||
public static final String PREF_AUTO_USER_COLOR_SUFFIX = "_auto";
|
||||
public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key";
|
||||
public static final String PREF_CLIPBOARD_CLIPBOARD_KEY = "pref_clipboard_clipboard_key";
|
||||
public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
|
||||
public static final String PREF_ADD_DICTIONARY = "add_dictionary";
|
||||
public static final String PREF_AUTO_CORRECTION = "pref_key_auto_correction";
|
||||
public static final String PREF_AUTO_CORRECTION_CONFIDENCE = "pref_key_auto_correction_confidence";
|
||||
// PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead.
|
||||
public static final String PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE = "show_suggestions_setting";
|
||||
public static final String PREF_SHOW_SUGGESTIONS = "show_suggestions";
|
||||
public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts";
|
||||
public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD =
|
||||
"pref_key_use_double_space_period";
|
||||
public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE =
|
||||
"pref_key_block_potentially_offensive";
|
||||
public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS =
|
||||
Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT;
|
||||
public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD = "pref_key_use_double_space_period";
|
||||
public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = "pref_key_block_potentially_offensive";
|
||||
public static final boolean SHOULD_SHOW_LXX_SUGGESTION_UI =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
|
||||
public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
|
||||
"pref_show_language_switch_key";
|
||||
public static final String PREF_SHOW_EMOJI_KEY =
|
||||
"pref_show_emoji_key";
|
||||
public static final String PREF_SHOW_CLIPBOARD_KEY =
|
||||
"pref_show_clipboard_key";
|
||||
public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
|
||||
"pref_include_other_imes_in_language_switch_list";
|
||||
public static final String PREF_LANGUAGE_SWITCH_KEY = "pref_language_switch_key";
|
||||
public static final String PREF_SHOW_EMOJI_KEY = "pref_show_emoji_key";
|
||||
public static final String PREF_SHOW_CLIPBOARD_KEY = "pref_show_clipboard_key";
|
||||
public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
|
||||
public static final String PREF_ENABLE_SPLIT_KEYBOARD = "pref_split_keyboard";
|
||||
public static final String PREF_KEYBOARD_HEIGHT_SCALE = "pref_keyboard_height_scale";
|
||||
public static final String PREF_SPACE_TRACKPAD = "pref_space_trackpad";
|
||||
public static final String PREF_DELETE_SWIPE = "pref_delete_swipe";
|
||||
public static final String PREF_AUTOSPACE_AFTER_PUNCTUATION = "pref_autospace_after_punctuation";
|
||||
public static final String PREF_ALWAYS_INCOGNITO_MODE =
|
||||
"pref_always_incognito_mode";
|
||||
public static final String PREF_ALWAYS_INCOGNITO_MODE = "pref_always_incognito_mode";
|
||||
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
|
||||
public static final String PREF_GESTURE_INPUT = "gesture_input";
|
||||
public static final String PREF_VIBRATION_DURATION_SETTINGS =
|
||||
"pref_vibration_duration_settings";
|
||||
public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings";
|
||||
public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume";
|
||||
public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout";
|
||||
public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY =
|
||||
"pref_enable_emoji_alt_physical_key";
|
||||
public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY = "pref_enable_emoji_alt_physical_key";
|
||||
public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
|
||||
public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
|
||||
"pref_gesture_floating_preview_text";
|
||||
public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = "pref_gesture_floating_preview_text";
|
||||
public static final String PREF_GESTURE_SPACE_AWARE = "pref_gesture_space_aware";
|
||||
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
|
||||
|
||||
public static final String PREF_ONE_HANDED_MODE = "pref_one_handed_mode_enabled";
|
||||
public static final String PREF_ONE_HANDED_GRAVITY = "pref_one_handed_mode_gravity";
|
||||
|
||||
public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal";
|
||||
|
||||
public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging";
|
||||
|
||||
public static final String PREF_SHOW_NUMBER_ROW =
|
||||
"pref_show_number_row";
|
||||
public static final String PREF_SHOW_NUMBER_ROW = "pref_show_number_row";
|
||||
|
||||
public static final String PREF_SHOW_HINTS = "pref_show_hints";
|
||||
|
||||
public static final String PREF_SPACE_TO_CHANGE_LANG = "prefs_long_press_keyboard_to_change_lang";
|
||||
public static final String PREF_SPACE_LANGUAGE_SLIDE = "pref_space_language_slide";
|
||||
|
||||
public static final String PREF_ENABLE_CLIPBOARD_HISTORY = "pref_enable_clipboard_history";
|
||||
public static final String PREF_CLIPBOARD_HISTORY_RETENTION_TIME = "pref_clipboard_history_retention_time";
|
||||
|
@ -150,13 +130,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static final String PREF_ENABLED_INPUT_STYLES = "pref_enabled_input_styles";
|
||||
public static final String PREF_SELECTED_INPUT_STYLE = "pref_selected_input_style";
|
||||
public static final String PREF_USE_SYSTEM_LOCALES = "pref_use_system_locales";
|
||||
public static final String PREF_SHOW_ALL_MORE_KEYS = "pref_show_all_more_keys";
|
||||
|
||||
public static final String PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG = "pref_dont_show_missing_dict_dialog";
|
||||
|
||||
// This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
|
||||
// This is being used only for the backward compatibility.
|
||||
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
|
||||
"pref_suppress_language_switch_key";
|
||||
|
||||
private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN =
|
||||
"pref_last_used_personalization_token";
|
||||
|
@ -171,6 +148,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id";
|
||||
public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = "last_shown_emoji_category_page_id";
|
||||
|
||||
// used as a workaround against keyboard not showing edited theme in ColorsSettingsFragment
|
||||
public static final String PREF_FORCE_OPPOSITE_THEME = "force_opposite_theme";
|
||||
|
||||
private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f;
|
||||
private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1;
|
||||
|
||||
|
@ -249,10 +229,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
return mSettingsValues;
|
||||
}
|
||||
|
||||
public boolean isInternal() {
|
||||
return mSettingsValues.mIsInternal;
|
||||
}
|
||||
|
||||
public static int readScreenMetrics(final Resources res) {
|
||||
return res.getInteger(R.integer.config_screen_metrics);
|
||||
}
|
||||
|
@ -260,8 +236,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
// Accessed from the settings interface, hence public
|
||||
public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
return prefs.getBoolean(PREF_SOUND_ON,
|
||||
res.getBoolean(R.bool.config_default_sound_enabled));
|
||||
return prefs.getBoolean(PREF_SOUND_ON, res.getBoolean(R.bool.config_default_sound_enabled));
|
||||
}
|
||||
|
||||
public static boolean readVibrationEnabled(final SharedPreferences prefs,
|
||||
|
@ -271,8 +246,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
res.getBoolean(R.bool.config_default_vibration_enabled));
|
||||
}
|
||||
|
||||
public static boolean readAutoCorrectEnabled(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
public static boolean readAutoCorrectEnabled(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_AUTO_CORRECTION, true);
|
||||
}
|
||||
|
||||
|
@ -282,10 +256,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
res.getString(R.string.auto_correction_threshold_mode_index_modest));
|
||||
}
|
||||
|
||||
public static float readPlausibilityThreshold(final Resources res) {
|
||||
return Float.parseFloat(res.getString(R.string.plausibility_threshold));
|
||||
}
|
||||
|
||||
public static boolean readBlockPotentiallyOffensive(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
return prefs.getBoolean(PREF_BLOCK_POTENTIALLY_OFFENSIVE,
|
||||
|
@ -408,10 +378,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static int readDefaultClipboardHistoryRetentionTime(final Resources res) {
|
||||
return res.getInteger(R.integer.config_clipboard_history_retention_time);
|
||||
}
|
||||
|
||||
public static boolean readShowsNumberRow(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_SHOW_NUMBER_ROW, false);
|
||||
}
|
||||
|
||||
public static float readKeyboardHeight(final SharedPreferences prefs,
|
||||
final float defaultValue) {
|
||||
|
@ -474,10 +440,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
&& conf.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES;
|
||||
}
|
||||
|
||||
public static boolean isInternal(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_KEY_IS_INTERNAL, false);
|
||||
}
|
||||
|
||||
public void writeLastUsedPersonalizationToken(byte[] token) {
|
||||
if (token == null) {
|
||||
mPrefs.edit().remove(PREF_LAST_USED_PERSONALIZATION_TOKEN).apply();
|
||||
|
@ -573,9 +535,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
}
|
||||
|
||||
public static Colors getColorsForCurrentTheme(final Context context, final SharedPreferences prefs) {
|
||||
// todo: night mode can be unspecified -> maybe need to adjust for correct behavior on some devices?
|
||||
final boolean isNight = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||
final String themeColors = (isNight && prefs.getBoolean(Settings.PREF_THEME_DAY_NIGHT, false))
|
||||
boolean isNight = ResourceUtils.isNight(context.getResources());
|
||||
if (prefs.getBoolean(PREF_FORCE_OPPOSITE_THEME, false)) isNight = !isNight;
|
||||
final String themeColors = (isNight && prefs.getBoolean(PREF_THEME_DAY_NIGHT, context.getResources().getBoolean(R.bool.day_night_default)))
|
||||
? prefs.getString(Settings.PREF_THEME_VARIANT_NIGHT, KeyboardTheme.THEME_DARKER)
|
||||
: prefs.getString(Settings.PREF_THEME_VARIANT, KeyboardTheme.THEME_LIGHT);
|
||||
final String themeStyle = prefs.getString(Settings.PREF_THEME_STYLE, KeyboardTheme.THEME_STYLE_MATERIAL);
|
||||
|
@ -583,4 +545,58 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
return KeyboardTheme.getThemeColors(themeColors, themeStyle, context, prefs);
|
||||
}
|
||||
|
||||
public static int readUserColor(final SharedPreferences prefs, final Context context, final String colorName, final boolean isNight) {
|
||||
final String pref = getColorPref(colorName, isNight);
|
||||
if (prefs.getBoolean(pref + PREF_AUTO_USER_COLOR_SUFFIX, true)) {
|
||||
return determineAutoColor(prefs, context, colorName, isNight);
|
||||
}
|
||||
if (prefs.contains(pref))
|
||||
return prefs.getInt(pref, Color.GRAY);
|
||||
else return determineAutoColor(prefs, context, colorName, isNight);
|
||||
}
|
||||
|
||||
public static String getColorPref(final String color, final boolean isNight) {
|
||||
return (isNight ? PREF_THEME_USER_COLOR_NIGHT_PREFIX : PREF_THEME_USER_COLOR_PREFIX) + color;
|
||||
}
|
||||
|
||||
private static int determineAutoColor(final SharedPreferences prefs, final Context context, final String color, final boolean isNight) {
|
||||
switch (color) {
|
||||
case PREF_COLOR_ACCENT_SUFFIX:
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
// try determining accent color on Android 10 & 11, accent is not available in resources
|
||||
// todo: test whether this actually works
|
||||
final Context wrapper = new ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault);
|
||||
final TypedValue value = new TypedValue();
|
||||
if (wrapper.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true))
|
||||
return value.data;
|
||||
}
|
||||
return ContextCompat.getColor(getDayNightContext(context, isNight), R.color.accent);
|
||||
case PREF_COLOR_TEXT_SUFFIX:
|
||||
// base it on background color, and not key, because it's also used for suggestions
|
||||
if (ColorUtilKt.isBrightColor(readUserColor(prefs, context, PREF_COLOR_BACKGROUND_SUFFIX, isNight))) return Color.BLACK;
|
||||
else return Color.WHITE;
|
||||
case PREF_COLOR_HINT_TEXT_SUFFIX:
|
||||
if (ColorUtilKt.isBrightColor(readUserColor(prefs, context, PREF_COLOR_KEYS_SUFFIX, isNight))) return Color.DKGRAY;
|
||||
else return Color.LTGRAY;
|
||||
case PREF_COLOR_KEYS_SUFFIX:
|
||||
return ColorUtilKt.brightenOrDarken(readUserColor(prefs, context, PREF_COLOR_BACKGROUND_SUFFIX, isNight), isNight);
|
||||
case PREF_COLOR_BACKGROUND_SUFFIX:
|
||||
default:
|
||||
return ContextCompat.getColor(getDayNightContext(context, isNight), R.color.keyboard_background);
|
||||
}
|
||||
}
|
||||
|
||||
public static Context getDayNightContext(final Context context, final boolean wantNight) {
|
||||
final boolean isNight = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||
if (isNight == wantNight)
|
||||
return context;
|
||||
final Configuration config = new Configuration(context.getResources().getConfiguration());
|
||||
final int night = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
final int uiModeWithNightBitsZero = config.uiMode - night;
|
||||
config.uiMode = uiModeWithNightBitsZero + (wantNight ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO);
|
||||
final ContextThemeWrapper wrapper = new ContextThemeWrapper(context, R.style.platformActivityTheme);
|
||||
wrapper.applyOverrideConfiguration(config);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -54,9 +54,16 @@ public final class SettingsActivity extends AppCompatActivity
|
|||
new NewDictionaryAdder(this, null).addDictionary(i.getData(), null);
|
||||
setIntent(new Intent()); // avoid opening again
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
if (getSupportFragmentManager().getFragments().isEmpty())
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue