mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-18 16:03:12 +00:00
Merge branch 'new' into hangul_update
This commit is contained in:
commit
cf17b4984a
917 changed files with 4740 additions and 12260 deletions
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,5 +1,11 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Discussions
|
- name: Dictionary request
|
||||||
|
url: https://codeberg.org/Helium314/aosp-dictionaries
|
||||||
|
about: Requests for dictionaries (used for suggestions / autocompletion) will be handled in the linked dictionary repository. You can check whether a dictionary for the language you want already exists there.
|
||||||
|
- name: Discussion
|
||||||
url: https://github.com/Helium314/OpenBoard/discussions
|
url: https://github.com/Helium314/OpenBoard/discussions
|
||||||
about: For discussions and feedback about this version of OpenBoard, asking questions or talking about ideas which are not yet actionable (i.e. not suitable for an issue).
|
about: For discussions and feedback about this version of OpenBoard, asking questions or talking about ideas which are not yet actionable (i.e. not suitable for an issue).
|
||||||
|
- name: Question
|
||||||
|
url: https://github.com/Helium314/openboard/discussions/new?category=q-a
|
||||||
|
about: For questions please use the discussions section.
|
||||||
|
|
13
.github/ISSUE_TEMPLATE/new-language-request.md
vendored
13
.github/ISSUE_TEMPLATE/new-language-request.md
vendored
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
name: Language request
|
|
||||||
about: Use this to request autocompletion support for a missing language, or for issues with existing dictionaries
|
|
||||||
labels: dictionaries
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Please search open and closed issues to avoid duplicates.
|
|
||||||
|
|
||||||
Before you suggest adding a language, check if the language is already included in the downloadable dictionaries at https://github.com/Helium314/openboard/tree/new/dictionaries/dict or experimental dictionaries at https://github.com/Helium314/openboard/tree/new/dictionaries/experimental/dict
|
|
||||||
|
|
||||||
If you open an issue about an existing dictionary, add information which dictionary you use (included in app, or link)
|
|
||||||
-->
|
|
2
.github/ISSUE_TEMPLATE/other.md
vendored
2
.github/ISSUE_TEMPLATE/other.md
vendored
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: Other
|
name: Other
|
||||||
about: Anything that does not fit into the other categories
|
about: Anything that does not fit into the other categories. Please don't use this for questions or discussions.
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
67
README.md
67
README.md
|
@ -5,24 +5,22 @@ Might end up on F-Droid...
|
||||||
|
|
||||||
**This is mostly a development version. On updates there may be changes that reset some settings. Consider all releases as beta quality at best.**
|
**This is mostly a development version. On updates there may be changes that reset some settings. Consider all releases as beta quality at best.**
|
||||||
|
|
||||||
The rough plan for this project:
|
## Features
|
||||||
* Theming
|
* Allow loading Glide typing library
|
||||||
* improve user-defined coloring
|
* not included in the app, as there is no compatible open source library
|
||||||
* add pre-defined themes
|
* can be extracted from GApps packages (_swypelibs_), or downloaded [here](https://github.com/erkserkserks/openboard/tree/master/app/src/main/jniLibs)
|
||||||
* make automatic theme switch work with the new themes
|
* Multilingual typing
|
||||||
* Multilingual Typing
|
* Load external dictionaries
|
||||||
* fix some bugs preventing proper usage in some cases (especially concerning key popups)
|
* get them [here](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/dictionaries), or in the [experimental](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/dictionaries_experimental) section (quality may vary)
|
||||||
* possibly has some undiscovered bugs, more testing required
|
* additional dictionaries for emojis or scientific symbols can be used to provide suggestions ("emoji search")
|
||||||
* Dictionaries
|
* Adjust keyboard themes (style and colors)
|
||||||
* semi-independent in a separate repository: https://codeberg.org/Helium314/aosp-dictionaries
|
* can follow the system's day/night setting
|
||||||
* Internal clean up (xml files, unused resources and classes)
|
* Split keyboard
|
||||||
* the current state look rather messy, with many useless / duplicate entries that make changes needlessly tricky
|
* Number row
|
||||||
* Make suggestion removal functionality more discoverable
|
* Number pad
|
||||||
* Fix some bugs along the way
|
* Show all available extra characters on long pressing a key
|
||||||
|
|
||||||
Once above is done, we can think about renaming and properly releasing the app. First just in this repository, and later on F-Droid.
|
## Important differences and changes to OpenBoard
|
||||||
|
|
||||||
Important differences / changes to OpenBoard:
|
|
||||||
* Debug version can be installed along OpenBoard
|
* Debug version can be installed along OpenBoard
|
||||||
* Allow users to add and replace built-in dictionaries
|
* Allow users to add and replace built-in dictionaries
|
||||||
* modified / improved from https://github.com/openboard-team/openboard/pull/569 and https://github.com/openboard-team/openboard/pull/578
|
* modified / improved from https://github.com/openboard-team/openboard/pull/569 and https://github.com/openboard-team/openboard/pull/578
|
||||||
|
@ -41,6 +39,7 @@ Important differences / changes to OpenBoard:
|
||||||
* tested with [Google libraries](https://github.com/erkserkserks/openboard/tree/master/app/src/main/jniLibs) and [others](https://github.com/openboard-team/openboard/issues/3#issuecomment-1200456262) (when building with the [rename](https://github.com/openboard-team/openboard/tree/57d33791d7674e3fe0600eddb72f6b4317b5df00))
|
* tested with [Google libraries](https://github.com/erkserkserks/openboard/tree/master/app/src/main/jniLibs) and [others](https://github.com/openboard-team/openboard/issues/3#issuecomment-1200456262) (when building with the [rename](https://github.com/openboard-team/openboard/tree/57d33791d7674e3fe0600eddb72f6b4317b5df00))
|
||||||
* Theming: allow adjusting keyboard colors, https://github.com/openboard-team/openboard/issues/124
|
* Theming: allow adjusting keyboard colors, https://github.com/openboard-team/openboard/issues/124
|
||||||
* Optionally make the navigation bar follow current theme, https://github.com/Helium314/openboard/issues/4
|
* Optionally make the navigation bar follow current theme, https://github.com/Helium314/openboard/issues/4
|
||||||
|
* Allow defining day/night themes
|
||||||
* Remove suggestions by long pressing on suggestion strip while the more suggestions popup is open, https://github.com/openboard-team/openboard/issues/106
|
* Remove suggestions by long pressing on suggestion strip while the more suggestions popup is open, https://github.com/openboard-team/openboard/issues/106
|
||||||
* suggestions get re-added if they are entered again
|
* suggestions get re-added if they are entered again
|
||||||
* Optionally add typed words to system personal dictionary
|
* Optionally add typed words to system personal dictionary
|
||||||
|
@ -57,19 +56,29 @@ Important differences / changes to OpenBoard:
|
||||||
* Add number pad
|
* Add number pad
|
||||||
* Overhauled language settings
|
* Overhauled language settings
|
||||||
* Updated translations
|
* Updated translations
|
||||||
|
* Open dictionary files with the app
|
||||||
|
* Add more options to the language switch key
|
||||||
|
|
||||||
Further plan / to do:
|
## The rough plan/todo before "full" release
|
||||||
* upgrade NDK, https://github.com/openboard-team/openboard/issues/782
|
* Add/change pre-defined themes
|
||||||
* later: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid
|
* Internal clean up (xml files, unused resources and classes)
|
||||||
* ~multi-lingual typing, https://github.com/openboard-team/openboard/pull/586, https://github.com/openboard-team/openboard/pull/593~
|
* even after a lot of work here, the current state look rather messy, with many useless and duplicate entries
|
||||||
* maybe improve way of merging suggestions from both languages
|
* work through _todo_s in code
|
||||||
* test whether it works reasonably well in non-latin scripts
|
* Make suggestion removal functionality more discoverable
|
||||||
* ~suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660~
|
* Better detection when to separate words and when not (e.g. detection of email addresses and URLs)
|
||||||
* ~improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576~
|
* Fix some bugs
|
||||||
* ~emoji prediction/search, https://github.com/openboard-team/openboard/pull/749 (better use emoji dictionaries, this is more flexible)~
|
* especially the spell checker issue https://github.com/Helium314/openboard/issues/55
|
||||||
* ~delete suggestions, https://github.com/openboard-team/openboard/issues/106~
|
* "partial" multi-character codepoint deletion with delete gesture (e.g. for emojis), https://github.com/Helium314/openboard/issues/22
|
||||||
* make functionality more discoverable, e.g. add a button to the _more suggestions_ menu, or replace long-press menu and show more suggestions by scrolling suggestion strip
|
|
||||||
* add emojis to user history, to be used for next word
|
Once above is done, we can think about properly releasing the app. First just in this repository, and later on F-Droid.
|
||||||
|
This would include renaming the app and the package, changing the icon and using a localization tool (most likely weblate).
|
||||||
|
|
||||||
|
## Further plan
|
||||||
|
* more customizable theming
|
||||||
|
* improved / less bad suggestions in some cases
|
||||||
|
* add emojis to user history, to be used for next word suggestions
|
||||||
|
* sliding key input for numpad and emojis (like `?123` and _shift_ sliding input)
|
||||||
|
* updated suggestion strip, maybe add tools or make the suggestions scroll
|
||||||
|
|
||||||
-----
|
-----
|
||||||
# readme for original version of OpenBoard below
|
# readme for original version of OpenBoard below
|
||||||
|
|
|
@ -14,18 +14,22 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
//shrinkResources true todo: test before enabling, a lot of resources are accessed via getIdentifier and possibly not all have been marked with "keep"
|
||||||
debuggable false
|
debuggable false
|
||||||
jniDebuggable false
|
jniDebuggable false
|
||||||
renderscriptDebuggable false
|
renderscriptDebuggable false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/main/proguard.flags'
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
jniDebuggable false
|
jniDebuggable false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/main/proguard.flags'
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
archivesBaseName = "openboard_" + defaultConfig.versionName
|
archivesBaseName = "openboard_" + defaultConfig.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,5 +63,6 @@ dependencies {
|
||||||
implementation 'androidx.preference:preference:1.2.1' // includes appcompat
|
implementation 'androidx.preference:preference:1.2.1' // includes appcompat
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'com.github.skydoves:colorpickerview:2.2.4'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
/*
|
|
||||||
**
|
|
||||||
** Copyright 2013, The Android Open Source Project
|
|
||||||
**
|
|
||||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
** you may not use this file except in compliance with the License.
|
|
||||||
** You may obtain a copy of the License at
|
|
||||||
**
|
|
||||||
** http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
**
|
|
||||||
** Unless required by applicable law or agreed to in writing, software
|
|
||||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
** See the License for the specific language governing permissions and
|
|
||||||
** limitations under the License.
|
|
||||||
*/
|
|
||||||
-->
|
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
|
||||||
<string name="dictionary_pack_client_id" translatable="false">org.dslul.openboard.inputmethod.latin.debug</string>
|
|
||||||
<!-- <string name="dictionary_pack_settings_activity" translatable="false">org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsActivity</string> -->
|
|
||||||
<string name="authority" translatable="false">org.dslul.openboard.inputmethod.debug.dictionarypack.aosp</string>
|
|
||||||
</resources>
|
|
|
@ -160,12 +160,13 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- Content providers -->
|
<!-- Content providers -->
|
||||||
|
<!--
|
||||||
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
|
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:authorities="@string/authority"
|
android:authorities="@string/authority"
|
||||||
android:multiprocess="false"
|
android:multiprocess="false"
|
||||||
android:label="@string/dictionary_provider_name">
|
android:label="@string/dictionary_provider_name">
|
||||||
</provider>
|
</provider>-->
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -294,8 +294,9 @@ public final class BinaryDictionary extends Dictionary {
|
||||||
}
|
}
|
||||||
session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
|
session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
|
||||||
session.mNativeSuggestOptions.setIsGesture(isGesture);
|
session.mNativeSuggestOptions.setIsGesture(isGesture);
|
||||||
session.mNativeSuggestOptions.setBlockOffensiveWords(
|
if (isGesture)
|
||||||
settingsValuesForSuggestion.mBlockPotentiallyOffensive);
|
session.mNativeSuggestOptions.setIsSpaceAwareGesture(settingsValuesForSuggestion.mSpaceAwareGesture);
|
||||||
|
session.mNativeSuggestOptions.setBlockOffensiveWords(settingsValuesForSuggestion.mBlockPotentiallyOffensive);
|
||||||
session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
|
session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
|
||||||
if (inOutWeightOfLangModelVsSpatialModel != null) {
|
if (inOutWeightOfLangModelVsSpatialModel != null) {
|
||||||
session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
|
session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.dslul.openboard.inputmethod.accessibility
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
@ -12,7 +13,6 @@ import android.view.ViewGroup
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import androidx.core.view.accessibility.AccessibilityEventCompat
|
|
||||||
import org.dslul.openboard.inputmethod.latin.R
|
import org.dslul.openboard.inputmethod.latin.R
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
|
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
|
||||||
|
@ -127,17 +127,17 @@ class AccessibilityUtils private constructor() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// The following is a hack to avoid using the heavy-weight TextToSpeech
|
// The following is a hack to avoid using the heavy-weight TextToSpeech
|
||||||
// class. Instead, we're just forcing a fake AccessibilityEvent into
|
// class. Instead, we're just forcing a fake AccessibilityEvent into
|
||||||
// the screen reader to make it speak.
|
// the screen reader to make it speak.
|
||||||
val event = AccessibilityEvent.obtain()
|
val event = obtainEvent()
|
||||||
event.packageName = PACKAGE
|
event.packageName = PACKAGE
|
||||||
event.className = CLASS
|
event.className = CLASS
|
||||||
event.eventTime = SystemClock.uptimeMillis()
|
event.eventTime = SystemClock.uptimeMillis()
|
||||||
event.isEnabled = true
|
event.isEnabled = true
|
||||||
event.text.add(text)
|
event.text.add(text)
|
||||||
// Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
|
// Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
|
||||||
// announce events.
|
// announce events.
|
||||||
event.eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT
|
event.eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
|
||||||
val viewParent = view.parent
|
val viewParent = view.parent
|
||||||
if (viewParent == null || viewParent !is ViewGroup) {
|
if (viewParent == null || viewParent !is ViewGroup) {
|
||||||
Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility")
|
Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility")
|
||||||
|
@ -205,5 +205,21 @@ class AccessibilityUtils private constructor() {
|
||||||
val action = event.action
|
val action = event.action
|
||||||
return action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_HOVER_MOVE
|
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 <= ' ' }
|
return key.label!!.trim { it <= ' ' }
|
||||||
}
|
}
|
||||||
resId = when (actionId) {
|
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_GO -> R.string.label_go_key
|
||||||
EditorInfo.IME_ACTION_SEND -> R.string.label_send_key
|
EditorInfo.IME_ACTION_SEND -> R.string.label_send_key
|
||||||
EditorInfo.IME_ACTION_NEXT -> R.string.label_next_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.
|
* @param text The text to send with the event.
|
||||||
*/
|
*/
|
||||||
protected fun sendWindowStateChanged(text: String?) {
|
protected fun sendWindowStateChanged(text: String?) {
|
||||||
val stateChange = AccessibilityEvent.obtain(
|
val stateChange = AccessibilityUtils.obtainEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
||||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
|
||||||
mKeyboardView!!.onInitializeAccessibilityEvent(stateChange)
|
mKeyboardView!!.onInitializeAccessibilityEvent(stateChange)
|
||||||
stateChange.text.add(text)
|
stateChange.text.add(text)
|
||||||
stateChange.contentDescription = null
|
stateChange.contentDescription = null
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package org.dslul.openboard.inputmethod.accessibility
|
package org.dslul.openboard.inputmethod.accessibility
|
||||||
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.view.accessibility.AccessibilityRecord
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.accessibility.AccessibilityEventCompat
|
import androidx.core.view.accessibility.AccessibilityEventCompat
|
||||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||||
|
@ -91,12 +93,12 @@ class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
|
||||||
fun createAccessibilityEvent(key: Key, eventType: Int): AccessibilityEvent {
|
fun createAccessibilityEvent(key: Key, eventType: Int): AccessibilityEvent {
|
||||||
val virtualViewId = getVirtualViewIdOf(key)
|
val virtualViewId = getVirtualViewIdOf(key)
|
||||||
val keyDescription = getKeyDescription(key)
|
val keyDescription = getKeyDescription(key)
|
||||||
val event = AccessibilityEvent.obtain(eventType)
|
val event = AccessibilityUtils.obtainEvent(eventType)
|
||||||
event.packageName = mKeyboardView!!.context.packageName
|
event.packageName = mKeyboardView!!.context.packageName
|
||||||
event.className = key.javaClass.name
|
event.className = key.javaClass.name
|
||||||
event.contentDescription = keyDescription
|
event.contentDescription = keyDescription
|
||||||
event.isEnabled = true
|
event.isEnabled = true
|
||||||
val record = AccessibilityEventCompat.asRecord(event)
|
val record: AccessibilityRecord = event
|
||||||
record.setSource(mKeyboardView, virtualViewId)
|
record.setSource(mKeyboardView, virtualViewId)
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
@ -111,16 +113,16 @@ class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
|
||||||
// announcements.
|
// announcements.
|
||||||
mHoveringNodeId = id
|
mHoveringNodeId = id
|
||||||
// Invalidate the node info of the key.
|
// Invalidate the node info of the key.
|
||||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
|
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
|
||||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER)
|
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onHoverExitFrom(key: Key) {
|
fun onHoverExitFrom(key: Key) {
|
||||||
mHoveringNodeId = UNDEFINED
|
mHoveringNodeId = UNDEFINED
|
||||||
// Invalidate the node info of the key to be able to revert the change we have done
|
// Invalidate the node info of the key to be able to revert the change we have done
|
||||||
// in {@link #onHoverEnterTo(Key)}.
|
// in {@link #onHoverEnterTo(Key)}.
|
||||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
|
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
|
||||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT)
|
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -292,8 +294,8 @@ class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.Companion.instance
|
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.instance
|
||||||
mAccessibilityUtils = AccessibilityUtils.Companion.instance
|
mAccessibilityUtils = AccessibilityUtils.instance
|
||||||
mKeyboardView = keyboardView
|
mKeyboardView = keyboardView
|
||||||
mDelegate = delegate
|
mDelegate = delegate
|
||||||
// Since this class is constructed lazily, we might not get a subsequent
|
// Since this class is constructed lazily, we might not get a subsequent
|
||||||
|
|
|
@ -24,8 +24,8 @@ object CompatUtils {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return targetClass.getMethod(name!!, *parameterTypes)
|
return targetClass.getMethod(name!!, *parameterTypes)
|
||||||
} catch (e: SecurityException) { // ignore
|
} catch (_: SecurityException) { // ignore
|
||||||
} catch (e: NoSuchMethodException) {
|
} catch (_: NoSuchMethodException) {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ object CompatUtils {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return targetClass.getField(name!!)
|
return targetClass.getField(name!!)
|
||||||
} catch (e: SecurityException) { // ignore
|
} catch (_: SecurityException) { // ignore
|
||||||
} catch (e: NoSuchFieldException) {
|
} catch (_: NoSuchFieldException) {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -49,8 +49,8 @@ object CompatUtils {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return targetClass.getConstructor(*types)
|
return targetClass.getConstructor(*types)
|
||||||
} catch (e: SecurityException) { // ignore
|
} catch (_: SecurityException) { // ignore
|
||||||
} catch (e: NoSuchMethodException) {
|
} catch (_: NoSuchMethodException) {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -153,9 +153,10 @@ object CompatUtils {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
class ToObjectMethodWrapper<T>(private val mMethod: Method?, private val mDefaultValue: T) {
|
class ToObjectMethodWrapper<T>(private val mMethod: Method?, private val mDefaultValue: T) {
|
||||||
operator fun invoke(receiver: Any?, vararg args: Any?): 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 {
|
object EditorInfoCompatUtils {
|
||||||
// Note that EditorInfo.IME_FLAG_FORCE_ASCII has been introduced
|
// 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(
|
private val FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField(
|
||||||
EditorInfo::class.java, "IME_FLAG_FORCE_ASCII")
|
EditorInfo::class.java, "IME_FLAG_FORCE_ASCII")
|
||||||
private val OBJ_IME_FLAG_FORCE_ASCII: Int? = CompatUtils.getFieldValue(
|
private val OBJ_IME_FLAG_FORCE_ASCII: Int? = CompatUtils.getFieldValue(
|
||||||
|
@ -13,12 +13,12 @@ object EditorInfoCompatUtils {
|
||||||
private val FIELD_HINT_LOCALES = CompatUtils.getField(
|
private val FIELD_HINT_LOCALES = CompatUtils.getField(
|
||||||
EditorInfo::class.java, "hintLocales")
|
EditorInfo::class.java, "hintLocales")
|
||||||
|
|
||||||
@kotlin.jvm.JvmStatic
|
@JvmStatic
|
||||||
fun hasFlagForceAscii(imeOptions: Int): Boolean {
|
fun hasFlagForceAscii(imeOptions: Int): Boolean {
|
||||||
return if (OBJ_IME_FLAG_FORCE_ASCII == null) false else imeOptions and OBJ_IME_FLAG_FORCE_ASCII != 0
|
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 {
|
fun imeActionName(imeOptions: Int): String {
|
||||||
val actionId = imeOptions and EditorInfo.IME_MASK_ACTION
|
val actionId = imeOptions and EditorInfo.IME_MASK_ACTION
|
||||||
return when (actionId) {
|
return when (actionId) {
|
||||||
|
@ -49,10 +49,10 @@ object EditorInfoCompatUtils {
|
||||||
if (hasFlagForceAscii(imeOptions)) {
|
if (hasFlagForceAscii(imeOptions)) {
|
||||||
flags.append("flagForceAscii|")
|
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? {
|
fun getPrimaryHintLocale(editorInfo: EditorInfo?): Locale? {
|
||||||
if (editorInfo == null) {
|
if (editorInfo == null) {
|
||||||
return null
|
return null
|
||||||
|
@ -61,6 +61,6 @@ object EditorInfoCompatUtils {
|
||||||
?: return null
|
?: return null
|
||||||
return if (LocaleListCompatUtils.isEmpty(localeList)) {
|
return if (LocaleListCompatUtils.isEmpty(localeList)) {
|
||||||
null
|
null
|
||||||
} else LocaleListCompatUtils.get(localeList, 0)
|
} else LocaleListCompatUtils[localeList, 0]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,7 +7,7 @@ import org.dslul.openboard.inputmethod.compat.CompatUtils.ToBooleanMethodWrapper
|
||||||
object InputConnectionCompatUtils {
|
object InputConnectionCompatUtils {
|
||||||
private var sInputConnectionType: ClassWrapper? = null
|
private var sInputConnectionType: ClassWrapper? = null
|
||||||
private var sRequestCursorUpdatesMethod: ToBooleanMethodWrapper? = null
|
private var sRequestCursorUpdatesMethod: ToBooleanMethodWrapper? = null
|
||||||
val isRequestCursorUpdatesAvailable: Boolean
|
private val isRequestCursorUpdatesAvailable: Boolean
|
||||||
get() = sRequestCursorUpdatesMethod != null
|
get() = sRequestCursorUpdatesMethod != null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,7 @@ object InputConnectionCompatUtils {
|
||||||
* as soon as possible to notify the current cursor/anchor position to the input method.
|
* 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`.
|
* @return `false` if the request is not handled. Otherwise returns `true`.
|
||||||
*/
|
*/
|
||||||
@kotlin.jvm.JvmStatic
|
@JvmStatic
|
||||||
fun requestCursorUpdates(inputConnection: InputConnection,
|
fun requestCursorUpdates(inputConnection: InputConnection,
|
||||||
enableMonitor: Boolean, requestImmediateCallback: Boolean): Boolean {
|
enableMonitor: Boolean, requestImmediateCallback: Boolean): Boolean {
|
||||||
val cursorUpdateMode = ((if (enableMonitor) CURSOR_UPDATE_MONITOR else 0)
|
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
|
||||||
import android.os.Build.VERSION_CODES
|
import android.os.Build.VERSION_CODES
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.inputmethod.InputMethodSubtype
|
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.common.LocaleUtils
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.locale
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object InputMethodSubtypeCompatUtils {
|
object InputMethodSubtypeCompatUtils {
|
||||||
// Note that InputMethodSubtype.getLanguageTag() is expected to be available in Android N+.
|
// Note that InputMethodSubtype.getLanguageTag() is expected to be available in Android N+.
|
||||||
private val GET_LANGUAGE_TAG = CompatUtils.getMethod(InputMethodSubtype::class.java, "getLanguageTag")
|
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.
|
fun getLocaleObject(subtype: InputMethodSubtype): Locale { // Locale.forLanguageTag() is available only in Android L and later.
|
||||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
||||||
val languageTag = CompatUtils.invoke(subtype, null, GET_LANGUAGE_TAG) as String?
|
val languageTag = CompatUtils.invoke(subtype, null, GET_LANGUAGE_TAG) as String?
|
||||||
if (!TextUtils.isEmpty(languageTag)) {
|
if (!languageTag.isNullOrEmpty()) {
|
||||||
return Locale.forLanguageTag(languageTag)
|
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.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.TextUtils
|
|
||||||
import android.text.style.SuggestionSpan
|
import android.text.style.SuggestionSpan
|
||||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
|
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.*
|
import java.util.*
|
||||||
|
|
||||||
object SuggestionSpanUtils {
|
object SuggestionSpanUtils {
|
||||||
// Note that SuggestionSpan.FLAG_AUTO_CORRECTION has been introduced
|
@JvmStatic
|
||||||
// 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
|
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
fun getTextWithAutoCorrectionIndicatorUnderline(
|
fun getTextWithAutoCorrectionIndicatorUnderline(context: Context?, text: String, locale: Locale?): CharSequence {
|
||||||
context: Context?, text: String,locale: Locale?): CharSequence {
|
if (text.isEmpty())
|
||||||
if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) {
|
|
||||||
return text
|
return text
|
||||||
}
|
|
||||||
val spannable: Spannable = SpannableString(text)
|
val spannable: Spannable = SpannableString(text)
|
||||||
val suggestionSpan = SuggestionSpan(context, locale, arrayOf(), OBJ_FLAG_AUTO_CORRECTION, null)
|
val suggestionSpan = SuggestionSpan(context, locale, arrayOf(), SuggestionSpan.FLAG_AUTO_CORRECTION, null)
|
||||||
spannable.setSpan(suggestionSpan, 0, text.length,
|
spannable.setSpan(suggestionSpan, 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE or Spanned.SPAN_COMPOSING)
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE or Spanned.SPAN_COMPOSING)
|
|
||||||
return spannable
|
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
|
package org.dslul.openboard.inputmethod.compat
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.view.textservice.TextInfo
|
import android.view.textservice.TextInfo
|
||||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting
|
||||||
|
|
||||||
object TextInfoCompatUtils {
|
object TextInfoCompatUtils {
|
||||||
// Note that TextInfo.getCharSequence() is supposed to be available in API level 21 and later.
|
// 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_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
|
@get:UsedForTesting
|
||||||
val isCharSequenceSupported: Boolean
|
val isCharSequenceSupported: Boolean
|
||||||
get() = TEXT_INFO_GET_CHAR_SEQUENCE != null &&
|
get() = TEXT_INFO_GET_CHAR_SEQUENCE != null
|
||||||
TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null
|
|
||||||
|
|
||||||
@kotlin.jvm.JvmStatic
|
@JvmStatic
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
fun newInstance(charSequence: CharSequence, start: Int, end: Int, cookie: Int,
|
fun newInstance(charSequence: CharSequence, start: Int, end: Int, cookie: Int,
|
||||||
sequenceNumber: Int): TextInfo? {
|
sequenceNumber: Int): TextInfo {
|
||||||
return if (TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||||
CompatUtils.newInstance(TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE,
|
return TextInfo(charSequence, start, end, cookie, sequenceNumber)
|
||||||
charSequence, start, end, cookie, sequenceNumber) as TextInfo
|
return TextInfo(charSequence.subSequence(start, end).toString(), cookie, sequenceNumber)
|
||||||
} else 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`,
|
* the result of [TextInfo.getText] as fall back. If `textInfo` is `null`,
|
||||||
* returns `null`.
|
* returns `null`.
|
||||||
*/
|
*/
|
||||||
@kotlin.jvm.JvmStatic
|
@JvmStatic
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
fun getCharSequenceOrString(textInfo: TextInfo?): CharSequence? {
|
fun getCharSequenceOrString(textInfo: TextInfo?): CharSequence? {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
return textInfo?.charSequence
|
||||||
val defaultValue: CharSequence? = textInfo?.text
|
val defaultValue: CharSequence? = textInfo?.text
|
||||||
return CompatUtils.invoke(textInfo, defaultValue!!,
|
return CompatUtils.invoke(textInfo, defaultValue!!, TEXT_INFO_GET_CHAR_SEQUENCE) as CharSequence
|
||||||
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) {}
|
override fun setInsets(insets: InputMethodService.Insets) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlin.jvm.JvmStatic
|
@JvmStatic
|
||||||
fun setInsetsOutlineProvider(view: View): InsetsUpdater? {
|
fun setInsetsOutlineProvider(view: View): InsetsUpdater {
|
||||||
return if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
|
return if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
|
||||||
EMPTY_INSETS_UPDATER
|
EMPTY_INSETS_UPDATER
|
||||||
} else ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view)
|
} else ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view)
|
||||||
|
|
|
@ -2,10 +2,13 @@ package org.dslul.openboard.inputmethod.compat
|
||||||
|
|
||||||
import android.graphics.Outline
|
import android.graphics.Outline
|
||||||
import android.inputmethodservice.InputMethodService
|
import android.inputmethodservice.InputMethodService
|
||||||
|
import android.os.Build
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewOutlineProvider
|
import android.view.ViewOutlineProvider
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import org.dslul.openboard.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater
|
import org.dslul.openboard.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
internal object ViewOutlineProviderCompatUtilsLXX {
|
internal object ViewOutlineProviderCompatUtilsLXX {
|
||||||
fun setInsetsOutlineProvider(view: View): InsetsUpdater {
|
fun setInsetsOutlineProvider(view: View): InsetsUpdater {
|
||||||
val provider = InsetsOutlineProvider(view)
|
val provider = InsetsOutlineProvider(view)
|
||||||
|
@ -29,8 +32,7 @@ internal object ViewOutlineProviderCompatUtilsLXX {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: Revisit this when floating/resize keyboard is supported.
|
// TODO: Revisit this when floating/resize keyboard is supported.
|
||||||
outline.setRect(
|
outline.setRect(view.left, mLastVisibleTopInsets, view.right, view.bottom)
|
||||||
view.left, mLastVisibleTopInsets, view.right, view.bottom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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.
|
* 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
|
const val AUTHORITY = DICTIONARY_DOMAIN
|
||||||
/**
|
/**
|
||||||
* The action of the intent for publishing that new dictionary data is available.
|
* 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
|
// TODO: make this different across different packages. A suggested course of action is
|
||||||
// to use the package name inside this string.
|
// to use the package name inside this string.
|
||||||
// NOTE: The appended string should be uppercase like all other actions, but it's not for
|
// NOTE: The appended string should be uppercase like all other actions, but it's not for
|
||||||
// historical reasons.
|
// historical reasons.
|
||||||
const val NEW_DICTIONARY_INTENT_ACTION = "$DICTIONARY_DOMAIN.newdict"
|
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.
|
* @param event the event to combine with the existing state.
|
||||||
* @return the resulting event.
|
* @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.
|
* 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.
|
* new event. However it may never be null.
|
||||||
*/
|
*/
|
||||||
fun processEvent(previousEvents: ArrayList<Event>?,
|
fun processEvent(previousEvents: ArrayList<Event>?,
|
||||||
newEvent: Event?): Event? {
|
newEvent: Event): Event {
|
||||||
val modifiablePreviousEvents = ArrayList(previousEvents!!)
|
val modifiablePreviousEvents = ArrayList(previousEvents!!)
|
||||||
var event = newEvent
|
var event = newEvent
|
||||||
for (combiner in mCombiners) { // A combiner can never return more than one event; it can return several
|
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.
|
// code points, but they should be encapsulated within one event.
|
||||||
event = combiner.processEvent(modifiablePreviousEvents, 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.
|
// they should not see it at all.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,11 +188,11 @@ class DeadKeyCombiner : Combiner {
|
||||||
// TODO: make this a list of events instead
|
// TODO: make this a list of events instead
|
||||||
val mDeadSequence = StringBuilder()
|
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 (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.
|
if (event!!.isDead) { // The event was a dead key. Start tracking it.
|
||||||
mDeadSequence.appendCodePoint(event.mCodePoint)
|
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
|
// 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
|
// 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)
|
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.
|
|| 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(),
|
val resultEvent = createEventChainFromSequence(mDeadSequence.toString(), event)
|
||||||
event)
|
|
||||||
mDeadSequence.setLength(0)
|
mDeadSequence.setLength(0)
|
||||||
return resultEvent
|
return resultEvent
|
||||||
}
|
}
|
||||||
|
@ -211,13 +210,13 @@ class DeadKeyCombiner : Combiner {
|
||||||
val trimIndex = mDeadSequence.length - Character.charCount(
|
val trimIndex = mDeadSequence.length - Character.charCount(
|
||||||
mDeadSequence.codePointBefore(mDeadSequence.length))
|
mDeadSequence.codePointBefore(mDeadSequence.length))
|
||||||
mDeadSequence.setLength(trimIndex)
|
mDeadSequence.setLength(trimIndex)
|
||||||
return Event.Companion.createConsumedEvent(event)
|
return Event.createConsumedEvent(event)
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
if (event.isDead) {
|
if (event.isDead) {
|
||||||
mDeadSequence.appendCodePoint(event.mCodePoint)
|
mDeadSequence.appendCodePoint(event.mCodePoint)
|
||||||
return Event.Companion.createConsumedEvent(event)
|
return Event.createConsumedEvent(event)
|
||||||
}
|
}
|
||||||
// Combine normally.
|
// Combine normally.
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
@ -248,17 +247,16 @@ class DeadKeyCombiner : Combiner {
|
||||||
get() = mDeadSequence
|
get() = mDeadSequence
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun createEventChainFromSequence(text: CharSequence,
|
private fun createEventChainFromSequence(text: CharSequence, originalEvent: Event): Event {
|
||||||
originalEvent: Event?): Event? {
|
|
||||||
var index = text.length
|
var index = text.length
|
||||||
if (index <= 0) {
|
if (index <= 0) {
|
||||||
return originalEvent
|
return originalEvent
|
||||||
}
|
}
|
||||||
var lastEvent: Event? = null
|
lateinit var lastEvent: Event
|
||||||
do {
|
do {
|
||||||
val codePoint = Character.codePointBefore(text, index)
|
val codePoint = Character.codePointBefore(text, index)
|
||||||
lastEvent = Event.Companion.createHardwareKeypressEvent(codePoint,
|
lastEvent = Event.createHardwareKeypressEvent(codePoint,
|
||||||
originalEvent!!.mKeyCode, lastEvent, false /* isKeyRepeat */)
|
originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */)
|
||||||
index -= Character.charCount(codePoint)
|
index -= Character.charCount(codePoint)
|
||||||
} while (index > 0)
|
} while (index > 0)
|
||||||
return lastEvent
|
return lastEvent
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams;
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeySpecParser;
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeySpecParser;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle;
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle;
|
||||||
|
|
|
@ -28,7 +28,6 @@ import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodSubtype;
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.compat.EditorInfoCompatUtils;
|
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.KeyboardBuilder;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams;
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.UniqueKeysCache;
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.UserManagerCompat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a set of keyboard layouts. Each of them represents a different keyboard
|
* 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 =
|
private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
|
||||||
new HashMap<>();
|
new HashMap<>();
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
public static final class KeyboardLayoutSetException extends RuntimeException {
|
public static final class KeyboardLayoutSetException extends RuntimeException {
|
||||||
public final KeyboardId mKeyboardId;
|
public final KeyboardId mKeyboardId;
|
||||||
|
|
||||||
|
@ -166,8 +165,6 @@ public final class KeyboardLayoutSet {
|
||||||
mParams = params;
|
mParams = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String LOCALE_GEORGIAN = "ka";
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
|
public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
|
||||||
final int keyboardLayoutSetElementId;
|
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
|
// When the device is still unlocked, features like showing the IME setting app need to
|
||||||
// be locked down.
|
// be locked down.
|
||||||
// TODO: Switch to {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library
|
if (!UserManagerCompat.isUserUnlocked(context)) {
|
||||||
// when it becomes publicly available.
|
|
||||||
@UserManagerCompatUtils.LockState final int lockState = UserManagerCompatUtils.getUserLockState(context);
|
|
||||||
if (lockState == UserManagerCompatUtils.LOCK_STATE_LOCKED) {
|
|
||||||
params.mNoSettingsKey = true;
|
params.mNoSettingsKey = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,8 +357,7 @@ public final class KeyboardLayoutSet {
|
||||||
final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
|
final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
|
||||||
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
|
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
|
||||||
final int xmlId = getXmlId(resources, layoutSetName);
|
final int xmlId = getXmlId(resources, layoutSetName);
|
||||||
final XmlResourceParser parser = resources.getXml(xmlId);
|
try (XmlResourceParser parser = resources.getXml(xmlId)) {
|
||||||
try {
|
|
||||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||||
// Bovinate through the XML stupidly searching for TAG_FEATURE, and read
|
// Bovinate through the XML stupidly searching for TAG_FEATURE, and read
|
||||||
// the script Id from it.
|
// the script Id from it.
|
||||||
|
@ -376,8 +369,6 @@ public final class KeyboardLayoutSet {
|
||||||
}
|
}
|
||||||
} catch (final IOException | XmlPullParserException e) {
|
} catch (final IOException | XmlPullParserException e) {
|
||||||
throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
|
throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
|
||||||
} finally {
|
|
||||||
parser.close();
|
|
||||||
}
|
}
|
||||||
// If the tag is not found, then the default script is Latin.
|
// If the tag is not found, then the default script is Latin.
|
||||||
return ScriptUtils.SCRIPT_LATIN;
|
return ScriptUtils.SCRIPT_LATIN;
|
||||||
|
@ -419,8 +410,7 @@ public final class KeyboardLayoutSet {
|
||||||
|
|
||||||
private void parseKeyboardLayoutSet(final Resources res, final int resId)
|
private void parseKeyboardLayoutSet(final Resources res, final int resId)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
final XmlResourceParser parser = res.getXml(resId);
|
try (XmlResourceParser parser = res.getXml(resId)) {
|
||||||
try {
|
|
||||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||||
final int event = parser.next();
|
final int event = parser.next();
|
||||||
if (event == XmlPullParser.START_TAG) {
|
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 Resources res = mThemeContext.getResources();
|
||||||
final int keyboardWidth = ResourceUtils.getKeyboardWidth(res, settingsValues);
|
final int keyboardWidth = ResourceUtils.getKeyboardWidth(res, settingsValues);
|
||||||
final int keyboardHeight = ResourceUtils.getKeyboardHeight(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;
|
final boolean oneHandedModeEnabled = settingsValues.mOneHandedModeEnabled;
|
||||||
builder.setOneHandedModeEnabled(oneHandedModeEnabled);
|
mKeyboardLayoutSet = builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
|
||||||
mKeyboardLayoutSet = builder.build();
|
.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 {
|
try {
|
||||||
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState,
|
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState, oneHandedModeEnabled);
|
||||||
oneHandedModeEnabled);
|
|
||||||
mKeyboardTextsSet.setLocale(mRichImm.getCurrentSubtypeLocale(), mThemeContext);
|
mKeyboardTextsSet.setLocale(mRichImm.getCurrentSubtypeLocale(), mThemeContext);
|
||||||
} catch (KeyboardLayoutSetException e) {
|
} catch (KeyboardLayoutSetException e) {
|
||||||
Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
|
Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
|
||||||
|
@ -547,10 +546,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
||||||
mKeyboardView.closing();
|
mKeyboardView.closing();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateKeyboardThemeAndContextThemeWrapper(
|
updateKeyboardThemeAndContextThemeWrapper(displayContext, KeyboardTheme.getKeyboardTheme(displayContext));
|
||||||
displayContext, KeyboardTheme.getKeyboardTheme(displayContext /* context */));
|
mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(R.layout.input_view, null);
|
||||||
mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
|
|
||||||
R.layout.input_view, null);
|
|
||||||
mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
|
mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
|
||||||
mEmojiPalettesView = mCurrentInputView.findViewById(R.id.emoji_palettes_view);
|
mEmojiPalettesView = mCurrentInputView.findViewById(R.id.emoji_palettes_view);
|
||||||
mClipboardHistoryView = mCurrentInputView.findViewById(R.id.clipboard_history_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 = mCurrentInputView.findViewById(R.id.keyboard_view);
|
||||||
mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
|
mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
|
||||||
mKeyboardView.setKeyboardActionListener(mLatinIME);
|
mKeyboardView.setKeyboardActionListener(mLatinIME);
|
||||||
mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled(
|
mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
|
||||||
isHardwareAcceleratedDrawingEnabled);
|
|
||||||
mEmojiPalettesView.setKeyboardActionListener(mLatinIME);
|
mEmojiPalettesView.setKeyboardActionListener(mLatinIME);
|
||||||
mClipboardHistoryView.setHardwareAcceleratedDrawingEnabled(
|
mClipboardHistoryView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
|
||||||
isHardwareAcceleratedDrawingEnabled);
|
|
||||||
mClipboardHistoryView.setKeyboardActionListener(mLatinIME);
|
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;
|
return mCurrentInputView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
@ -40,22 +39,17 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
|
||||||
|
|
||||||
// new themes using the custom colors
|
// new themes using the custom colors
|
||||||
public static final String THEME_LIGHT = "light";
|
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_DARK = "dark";
|
||||||
public static final String THEME_DARKER = "darker";
|
public static final String THEME_DARKER = "darker";
|
||||||
public static final String THEME_BLACK = "black";
|
public static final String THEME_BLACK = "black";
|
||||||
public static final String THEME_USER = "user";
|
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 = 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 };
|
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
|
// These should be aligned with Keyboard.themeId and Keyboard.Case.keyboardTheme
|
||||||
// attributes' values in attrs.xml.
|
// attributes' values in attrs.xml.
|
||||||
public static final int THEME_ID_HOLO_BASE = 0;
|
public static final int THEME_ID_HOLO_BASE = 0;
|
||||||
|
@ -126,44 +120,11 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
|
||||||
return null;
|
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) {
|
public static String getKeyboardThemeName(final int themeId) {
|
||||||
final KeyboardTheme theme = searchKeyboardThemeById(themeId, KEYBOARD_THEMES);
|
final KeyboardTheme theme = searchKeyboardThemeById(themeId, KEYBOARD_THEMES);
|
||||||
return theme.mThemeName;
|
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) {
|
public static KeyboardTheme getKeyboardTheme(final Context context) {
|
||||||
final SharedPreferences prefs = DeviceProtectedUtils.getSharedPreferences(context);
|
final SharedPreferences prefs = DeviceProtectedUtils.getSharedPreferences(context);
|
||||||
final String style = prefs.getString(Settings.PREF_THEME_STYLE, THEME_STYLE_MATERIAL);
|
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)
|
if (keyboardTheme.mThemeId == matchingId)
|
||||||
return keyboardTheme;
|
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) {
|
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) {
|
switch (themeColors) {
|
||||||
case THEME_USER:
|
case THEME_USER:
|
||||||
final int accent = prefs.getInt(Settings.PREF_THEME_USER_COLOR_ACCENT, Color.BLUE);
|
final int accent = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, false);
|
||||||
final int keyBgColor = prefs.getInt(Settings.PREF_THEME_USER_COLOR_KEYS, Color.LTGRAY);
|
final int keyBgColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, false);
|
||||||
final int keyTextColor = prefs.getInt(Settings.PREF_THEME_USER_COLOR_TEXT, Color.WHITE);
|
final int keyTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, false);
|
||||||
final int hintTextColor = prefs.getInt(Settings.PREF_THEME_USER_COLOR_HINT_TEXT, Color.WHITE);
|
final int hintTextColor = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, false);
|
||||||
final int background = prefs.getInt(Settings.PREF_THEME_USER_COLOR_BACKGROUND, Color.DKGRAY);
|
final int background = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, false);
|
||||||
return Colors.newColors(themeStyle, accent, background, keyBgColor, ColorUtilKt.brightenOrDarken(keyBgColor, true), keyBgColor, keyTextColor, hintTextColor);
|
return new Colors(themeStyle, hasBorders, accent, background, keyBgColor, ColorUtilKt.brightenOrDarken(keyBgColor, true), keyBgColor, keyTextColor, hintTextColor);
|
||||||
case THEME_USER_DARK:
|
case THEME_USER_NIGHT:
|
||||||
final int accent2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_ACCENT, Color.BLUE);
|
final int accent2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_ACCENT_SUFFIX, true);
|
||||||
final int keyBgColor2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_KEYS, Color.LTGRAY);
|
final int keyBgColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_KEYS_SUFFIX, true);
|
||||||
final int keyTextColor2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_TEXT, Color.WHITE);
|
final int keyTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_TEXT_SUFFIX, true);
|
||||||
final int hintTextColor2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_HINT_TEXT, Color.WHITE);
|
final int hintTextColor2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_HINT_TEXT_SUFFIX, true);
|
||||||
final int background2 = prefs.getInt(Settings.PREF_THEME_USER_DARK_COLOR_BACKGROUND, Color.DKGRAY);
|
final int background2 = Settings.readUserColor(prefs, context, Settings.PREF_COLOR_BACKGROUND_SUFFIX, true);
|
||||||
return Colors.newColors(themeStyle, accent2, background2, keyBgColor2, ColorUtilKt.brightenOrDarken(keyBgColor2, true), keyBgColor2, keyTextColor2, hintTextColor2);
|
return new Colors(themeStyle, hasBorders, accent2, background2, keyBgColor2, ColorUtilKt.brightenOrDarken(keyBgColor2, true), keyBgColor2, keyTextColor2, hintTextColor2);
|
||||||
case THEME_DARK:
|
case THEME_DARK:
|
||||||
return Colors.newColors(
|
return new Colors(
|
||||||
themeStyle,
|
themeStyle,
|
||||||
|
hasBorders,
|
||||||
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
|
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
|
||||||
// colors taken from the drawable
|
// colors taken from the drawable
|
||||||
Color.parseColor("#263238"),
|
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)
|
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark)
|
||||||
);
|
);
|
||||||
case THEME_HOLO_WHITE:
|
case THEME_HOLO_WHITE:
|
||||||
return Colors.newColors(
|
return new Colors(
|
||||||
themeStyle,
|
themeStyle,
|
||||||
|
hasBorders,
|
||||||
Color.parseColor("#FFFFFF"),
|
Color.parseColor("#FFFFFF"),
|
||||||
// colors taken from the drawable
|
// colors taken from the drawable
|
||||||
Color.parseColor("#282828"),
|
Color.parseColor("#282828"),
|
||||||
|
@ -254,8 +185,9 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
|
||||||
Color.parseColor("#282828")
|
Color.parseColor("#282828")
|
||||||
);
|
);
|
||||||
case THEME_DARKER:
|
case THEME_DARKER:
|
||||||
return Colors.newColors(
|
return new Colors(
|
||||||
themeStyle,
|
themeStyle,
|
||||||
|
hasBorders,
|
||||||
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
|
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.keyboard_background_lxx_dark_border),
|
||||||
ContextCompat.getColor(context, R.color.key_background_normal_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)
|
ContextCompat.getColor(context, R.color.key_hint_letter_color_lxx_dark)
|
||||||
);
|
);
|
||||||
case THEME_BLACK:
|
case THEME_BLACK:
|
||||||
return Colors.newColors(
|
return new Colors(
|
||||||
themeStyle,
|
themeStyle,
|
||||||
|
hasBorders,
|
||||||
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_dark),
|
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_black),
|
||||||
ContextCompat.getColor(context, R.color.background_amoled_dark),
|
ContextCompat.getColor(context, R.color.background_amoled_dark),
|
||||||
|
@ -277,8 +210,9 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> {
|
||||||
);
|
);
|
||||||
case THEME_LIGHT:
|
case THEME_LIGHT:
|
||||||
default:
|
default:
|
||||||
return Colors.newColors(
|
return new Colors(
|
||||||
themeStyle,
|
themeStyle,
|
||||||
|
hasBorders,
|
||||||
ContextCompat.getColor(context, R.color.gesture_trail_color_lxx_light),
|
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.keyboard_background_lxx_light_border),
|
||||||
ContextCompat.getColor(context, R.color.key_background_normal_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.KeyDrawParams;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes;
|
import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes;
|
||||||
import org.dslul.openboard.inputmethod.latin.R;
|
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.Colors;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
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.settings.Settings;
|
||||||
import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView;
|
import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.TypefaceUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.TypefaceUtils;
|
||||||
|
@ -141,20 +141,19 @@ public class KeyboardView extends View {
|
||||||
|
|
||||||
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
|
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
|
||||||
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
|
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
|
||||||
mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground).mutate();
|
if (this instanceof EmojiPageKeyboardView || this instanceof MoreSuggestionsView)
|
||||||
mKeyBackground.getPadding(mKeyBackgroundPadding);
|
mKeyBackground = mColors.getDrawable(BackgroundType.BACKGROUND, keyboardViewAttr);
|
||||||
final Drawable functionalKeyBackground = keyboardViewAttr.getDrawable(
|
else if (this instanceof MoreKeysKeyboardView)
|
||||||
R.styleable.KeyboardView_functionalKeyBackground);
|
mKeyBackground = mColors.getDrawable(BackgroundType.ADJUSTED_BACKGROUND, keyboardViewAttr);
|
||||||
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;
|
|
||||||
else
|
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(
|
mSpacebarIconWidthRatio = keyboardViewAttr.getFloat(
|
||||||
R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f);
|
R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f);
|
||||||
|
@ -179,31 +178,7 @@ public class KeyboardView extends View {
|
||||||
keyAttr.recycle();
|
keyAttr.recycle();
|
||||||
|
|
||||||
mPaint.setAntiAlias(true);
|
mPaint.setAntiAlias(true);
|
||||||
|
mColors.setKeyboardBackground(this);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -623,24 +598,22 @@ public class KeyboardView extends View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) {
|
private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) {
|
||||||
if (key.isAccentColored() && !(mColors instanceof HoloColors)) { // todo: this logic should not be here
|
if (key.isAccentColored()) {
|
||||||
icon.setColorFilter(mColors.actionKeyIconColorFilter);
|
icon.setColorFilter(mColors.getActionKeyIconColorFilter());
|
||||||
} else if (key.isShift() && keyboard != null) {
|
} 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
|
if (keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED
|
||||||
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED
|
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED
|
||||||
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED
|
||||||
|| keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_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
|
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) {
|
} else if (key.getBackgroundType() != Key.BACKGROUND_TYPE_NORMAL) {
|
||||||
icon.setColorFilter(mColors.keyTextFilter);
|
icon.setColorFilter(mColors.getKeyTextFilter());
|
||||||
} else if (this instanceof MoreKeysKeyboardView) {
|
} else if (this instanceof MoreKeysKeyboardView) {
|
||||||
// set color filter for long press comma key, should not trigger anywhere else
|
// 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.Colors;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
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.DebugSettings;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
||||||
|
@ -165,7 +164,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
|
|
||||||
// Gesture floating preview text
|
// Gesture floating preview text
|
||||||
// TODO: Make this parameter customizable by user via settings.
|
// TODO: Make this parameter customizable by user via settings.
|
||||||
private int mGestureFloatingPreviewTextLingerTimeout;
|
private final int mGestureFloatingPreviewTextLingerTimeout;
|
||||||
|
|
||||||
private final KeyDetector mKeyDetector;
|
private final KeyDetector mKeyDetector;
|
||||||
private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
|
private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
|
||||||
|
@ -219,10 +218,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
|
mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
|
||||||
R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f);
|
R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f);
|
||||||
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
||||||
if (colors instanceof HoloColors) // todo: this logic should be in Colors
|
mLanguageOnSpacebarTextColor = colors.getSpaceBarText(); //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0);
|
||||||
mLanguageOnSpacebarTextColor = colors.keyText;
|
|
||||||
else
|
|
||||||
mLanguageOnSpacebarTextColor = colors.keyHintText; //mainKeyboardViewAttr.getColor(R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0);
|
|
||||||
mLanguageOnSpacebarTextShadowRadius = mainKeyboardViewAttr.getFloat(
|
mLanguageOnSpacebarTextShadowRadius = mainKeyboardViewAttr.getFloat(
|
||||||
R.styleable.MainKeyboardView_languageOnSpacebarTextShadowRadius,
|
R.styleable.MainKeyboardView_languageOnSpacebarTextShadowRadius,
|
||||||
LANGUAGE_ON_SPACEBAR_TEXT_SHADOW_RADIUS_DISABLED);
|
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
|
* @param keyboard the keyboard to display in this view
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setKeyboard(final Keyboard keyboard) {
|
public void setKeyboard(@NonNull final Keyboard keyboard) {
|
||||||
// Remove any pending messages, except dismissing preview and key repeat.
|
// Remove any pending messages, except dismissing preview and key repeat.
|
||||||
mTimerHandler.cancelLongPressTimers();
|
mTimerHandler.cancelLongPressTimers();
|
||||||
super.setKeyboard(keyboard);
|
super.setKeyboard(keyboard);
|
||||||
|
@ -542,9 +538,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
public void showGestureFloatingPreviewText(@NonNull final SuggestedWords suggestedWords,
|
public void showGestureFloatingPreviewText(@NonNull final SuggestedWords suggestedWords,
|
||||||
final boolean dismissDelayed) {
|
final boolean dismissDelayed) {
|
||||||
locatePreviewPlacerView();
|
locatePreviewPlacerView();
|
||||||
final GestureFloatingTextDrawingPreview gestureFloatingTextDrawingPreview =
|
mGestureFloatingTextDrawingPreview.setSuggestedWords(suggestedWords);
|
||||||
mGestureFloatingTextDrawingPreview;
|
|
||||||
gestureFloatingTextDrawingPreview.setSuggestedWords(suggestedWords);
|
|
||||||
if (dismissDelayed) {
|
if (dismissDelayed) {
|
||||||
mTimerHandler.postDismissGestureFloatingPreviewText(
|
mTimerHandler.postDismissGestureFloatingPreviewText(
|
||||||
mGestureFloatingPreviewTextLingerTimeout);
|
mGestureFloatingPreviewTextLingerTimeout);
|
||||||
|
@ -805,8 +799,8 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
|
protected void onDrawKeyTopVisuals(@NonNull final Key key, @NonNull final Canvas canvas,
|
||||||
final KeyDrawParams params) {
|
@NonNull final Paint paint, @NonNull final KeyDrawParams params) {
|
||||||
if (key.altCodeWhileTyping() && key.isEnabled()) {
|
if (key.altCodeWhileTyping() && key.isEnabled()) {
|
||||||
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
|
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
|
||||||
}
|
}
|
||||||
|
@ -920,7 +914,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
// Draw language text with shadow
|
// Draw language text with shadow
|
||||||
final float descent = paint.descent();
|
final float descent = paint.descent();
|
||||||
final float textHeight = -paint.ascent() + 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) {
|
if (mLanguageOnSpacebarTextShadowRadius > 0.0f) {
|
||||||
paint.setShadowLayer(mLanguageOnSpacebarTextShadowRadius, 0, 0,
|
paint.setShadowLayer(mLanguageOnSpacebarTextShadowRadius, 0, 0,
|
||||||
mLanguageOnSpacebarTextShadowColor);
|
mLanguageOnSpacebarTextShadowColor);
|
||||||
|
@ -929,7 +923,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
}
|
}
|
||||||
paint.setColor(mLanguageOnSpacebarTextColor);
|
paint.setColor(mLanguageOnSpacebarTextColor);
|
||||||
paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
|
paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
|
||||||
canvas.drawText(language, width / 2, baseline - descent, paint);
|
canvas.drawText(language, width / 2f, baseline - descent, paint);
|
||||||
paint.clearShadowLayer();
|
paint.clearShadowLayer();
|
||||||
paint.setTextScaleX(1.0f);
|
paint.setTextScaleX(1.0f);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,8 @@ import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils;
|
import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils;
|
||||||
import org.dslul.openboard.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate;
|
import org.dslul.openboard.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener;
|
import org.dslul.openboard.inputmethod.keyboard.emoji.OnKeyEventListener;
|
||||||
|
@ -86,8 +88,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
|
protected void onDrawKeyTopVisuals(@NonNull final Key key, @NonNull final Canvas canvas,
|
||||||
final KeyDrawParams params) {
|
@NonNull final Paint paint, @NonNull final KeyDrawParams params) {
|
||||||
if (!key.isSpacer() || !(key instanceof MoreKeysKeyboard.MoreKeyDivider)
|
if (!key.isSpacer() || !(key instanceof MoreKeysKeyboard.MoreKeyDivider)
|
||||||
|| mDivider == null) {
|
|| mDivider == null) {
|
||||||
super.onDrawKeyTopVisuals(key, canvas, paint, params);
|
super.onDrawKeyTopVisuals(key, canvas, paint, params);
|
||||||
|
@ -103,7 +105,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setKeyboard(final Keyboard keyboard) {
|
public void setKeyboard(@NonNull final Keyboard keyboard) {
|
||||||
super.setKeyboard(keyboard);
|
super.setKeyboard(keyboard);
|
||||||
mKeyDetector.setKeyboard(
|
mKeyDetector.setKeyboard(
|
||||||
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
|
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.content.res.TypedArray;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.TimerProxy;
|
||||||
import org.dslul.openboard.inputmethod.keyboard.internal.TypingTimeRecorder;
|
import org.dslul.openboard.inputmethod.keyboard.internal.TypingTimeRecorder;
|
||||||
import org.dslul.openboard.inputmethod.latin.R;
|
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.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
|
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
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 org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public final class PointerTracker implements PointerTrackerQueue.Element,
|
public final class PointerTracker implements PointerTrackerQueue.Element,
|
||||||
BatchInputArbiterListener {
|
BatchInputArbiterListener {
|
||||||
|
@ -54,7 +58,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
||||||
private static final boolean DEBUG_EVENT = false;
|
private static final boolean DEBUG_EVENT = false;
|
||||||
private static final boolean DEBUG_MOVE_EVENT = false;
|
private static final boolean DEBUG_MOVE_EVENT = false;
|
||||||
private static final boolean DEBUG_LISTENER = 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 {
|
static final class PointerTrackerParams {
|
||||||
public final boolean mKeySelectionByDraggingFinger;
|
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.
|
// Parameters for pointer handling.
|
||||||
private static PointerTrackerParams sParams;
|
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 GestureStrokeRecognitionParams sGestureStrokeRecognitionParams;
|
||||||
private static GestureStrokeDrawingParams sGestureStrokeDrawingParams;
|
private static GestureStrokeDrawingParams sGestureStrokeDrawingParams;
|
||||||
private static boolean sNeedsPhantomSuddenMoveEventHack;
|
private static boolean sNeedsPhantomSuddenMoveEventHack;
|
||||||
|
@ -132,8 +136,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
||||||
private int mLastY;
|
private int mLastY;
|
||||||
private int mStartX;
|
private int mStartX;
|
||||||
private int mStartY;
|
private int mStartY;
|
||||||
|
private int mPreviousY;
|
||||||
private long mStartTime;
|
private long mStartTime;
|
||||||
private boolean mCursorMoved = false;
|
private boolean mCursorMoved = false;
|
||||||
|
private boolean mLanguageSlideStarted = false;
|
||||||
|
|
||||||
// true if keyboard layout has been changed.
|
// true if keyboard layout has been changed.
|
||||||
private boolean mKeyboardLayoutHasBeenChanged;
|
private boolean mKeyboardLayoutHasBeenChanged;
|
||||||
|
@ -715,6 +721,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
||||||
setPressedKeyGraphics(key, eventTime);
|
setPressedKeyGraphics(key, eventTime);
|
||||||
mStartX = x;
|
mStartX = x;
|
||||||
mStartY = y;
|
mStartY = y;
|
||||||
|
mPreviousY = y;
|
||||||
mStartTime = System.currentTimeMillis();
|
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) {
|
private void onMoveEventInternal(final int x, final int y, final long eventTime) {
|
||||||
final Key oldKey = mCurrentKey;
|
final Key oldKey = mCurrentKey;
|
||||||
|
final SettingsValues sv = Settings.getInstance().getCurrent();
|
||||||
|
|
||||||
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE && Settings.getInstance().getCurrent().mSpaceTrackpadEnabled) {
|
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE) {
|
||||||
//Pointer slider
|
int dX = x - mStartX;
|
||||||
int steps = (x - mStartX) / sPointerStep;
|
int dY = y - mStartY;
|
||||||
final int longpressTimeout = 2 * Settings.getInstance().getCurrent().mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
|
// language switch: upwards movement
|
||||||
if (steps != 0 && mStartTime + longpressTimeout < System.currentTimeMillis()) {
|
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;
|
mCursorMoved = true;
|
||||||
mStartX += steps * sPointerStep;
|
mStartX += steps * sPointerStep;
|
||||||
sListener.onMovePointer(steps);
|
sListener.onMovePointer(steps);
|
||||||
|
@ -928,8 +956,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE && Settings.getInstance().getCurrent().mDeleteSwipeEnabled) {
|
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE && sv.mDeleteSwipeEnabled) {
|
||||||
//Delete slider
|
// Delete slider
|
||||||
int steps = (x - mStartX) / sPointerStep;
|
int steps = (x - mStartX) / sPointerStep;
|
||||||
if (abs(steps) > 2 || (mCursorMoved && steps != 0)) {
|
if (abs(steps) > 2 || (mCursorMoved && steps != 0)) {
|
||||||
sTimerProxy.cancelKeyTimersOf(this);
|
sTimerProxy.cancelKeyTimersOf(this);
|
||||||
|
@ -1030,8 +1058,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mCursorMoved) {
|
if (mCursorMoved || mLanguageSlideStarted) {
|
||||||
mCursorMoved = false;
|
mCursorMoved = false;
|
||||||
|
mLanguageSlideStarted = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryEntry
|
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryEntry
|
||||||
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryManager
|
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryManager
|
||||||
import org.dslul.openboard.inputmethod.latin.R
|
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.HoloColors
|
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||||
|
|
||||||
class ClipboardAdapter(
|
class ClipboardAdapter(
|
||||||
|
@ -56,7 +55,7 @@ class ClipboardAdapter(
|
||||||
setOnTouchListener(this@ViewHolder)
|
setOnTouchListener(this@ViewHolder)
|
||||||
setOnLongClickListener(this@ViewHolder)
|
setOnLongClickListener(this@ViewHolder)
|
||||||
setBackgroundResource(itemBackgroundId)
|
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 {
|
pinnedIconView = view.findViewById<ImageView>(R.id.clipboard_entry_pinned_icon).apply {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
@ -69,8 +68,7 @@ class ClipboardAdapter(
|
||||||
}
|
}
|
||||||
clipboardLayoutParams.setItemProperties(view)
|
clipboardLayoutParams.setItemProperties(view)
|
||||||
val colors = Settings.getInstance().current.mColors
|
val colors = Settings.getInstance().current.mColors
|
||||||
if (colors is HoloColors) // todo: this logic should be in Colors, not here
|
pinnedIconView.colorFilter = colors.clipboardPinFilter
|
||||||
pinnedIconView.colorFilter = colors.accentColorFilter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setContent(historyEntry: ClipboardHistoryEntry?) {
|
fun setContent(historyEntry: ClipboardHistoryEntry?) {
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
package org.dslul.openboard.inputmethod.keyboard.clipboard
|
package org.dslul.openboard.inputmethod.keyboard.clipboard
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
|
||||||
|
|
||||||
class ClipboardHistoryRecyclerView @JvmOverloads constructor(
|
class ClipboardHistoryRecyclerView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -56,25 +53,4 @@ class ClipboardHistoryRecyclerView @JvmOverloads constructor(
|
||||||
adapter?.registerAdapterDataObserver(adapterDataObserver)
|
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.keyboard.internal.KeyboardIconsSet
|
||||||
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryManager
|
import org.dslul.openboard.inputmethod.latin.ClipboardHistoryManager
|
||||||
import org.dslul.openboard.inputmethod.latin.R
|
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.common.Constants
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils
|
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils
|
||||||
|
@ -30,7 +30,6 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
|
|
||||||
private val clipboardLayoutParams = ClipboardLayoutParams(context.resources)
|
private val clipboardLayoutParams = ClipboardLayoutParams(context.resources)
|
||||||
private val pinIconId: Int
|
private val pinIconId: Int
|
||||||
private val dividerColor: Int
|
|
||||||
private val functionalKeyBackgroundId: Int
|
private val functionalKeyBackgroundId: Int
|
||||||
private val keyBackgroundId: Int
|
private val keyBackgroundId: Int
|
||||||
|
|
||||||
|
@ -46,15 +45,10 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
init {
|
init {
|
||||||
val clipboardViewAttr = context.obtainStyledAttributes(attrs,
|
val clipboardViewAttr = context.obtainStyledAttributes(attrs,
|
||||||
R.styleable.ClipboardHistoryView, defStyle, R.style.ClipboardHistoryView)
|
R.styleable.ClipboardHistoryView, defStyle, R.style.ClipboardHistoryView)
|
||||||
pinIconId = clipboardViewAttr.getResourceId(
|
pinIconId = clipboardViewAttr.getResourceId(R.styleable.ClipboardHistoryView_iconPinnedClip, 0)
|
||||||
R.styleable.ClipboardHistoryView_iconPinnedClip, 0)
|
|
||||||
dividerColor = clipboardViewAttr.getColor(
|
|
||||||
R.styleable.ClipboardHistoryView_dividerBackground, 0)
|
|
||||||
clipboardViewAttr.recycle()
|
clipboardViewAttr.recycle()
|
||||||
val keyboardViewAttr = context.obtainStyledAttributes(attrs,
|
val keyboardViewAttr = context.obtainStyledAttributes(attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView)
|
||||||
R.styleable.KeyboardView, defStyle, R.style.KeyboardView)
|
keyBackgroundId = keyboardViewAttr.getResourceId(R.styleable.KeyboardView_keyBackground, 0)
|
||||||
keyBackgroundId = keyboardViewAttr.getResourceId(
|
|
||||||
R.styleable.KeyboardView_keyBackground, 0)
|
|
||||||
functionalKeyBackgroundId = keyboardViewAttr.getResourceId(
|
functionalKeyBackgroundId = keyboardViewAttr.getResourceId(
|
||||||
R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId)
|
R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId)
|
||||||
keyboardViewAttr.recycle()
|
keyboardViewAttr.recycle()
|
||||||
|
@ -64,8 +58,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
val res = context.resources
|
val res = context.resources
|
||||||
// The main keyboard expands to the entire this {@link KeyboardView}.
|
// The main keyboard expands to the entire this {@link KeyboardView}.
|
||||||
val width = (ResourceUtils.getDefaultKeyboardWidth(res)
|
val width = (ResourceUtils.getDefaultKeyboardWidth(res) + paddingLeft + paddingRight)
|
||||||
+ paddingLeft + paddingRight)
|
|
||||||
val height = (ResourceUtils.getDefaultKeyboardHeight(res)
|
val height = (ResourceUtils.getDefaultKeyboardHeight(res)
|
||||||
+ res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height)
|
+ res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height)
|
||||||
+ paddingTop + paddingBottom)
|
+ paddingTop + paddingBottom)
|
||||||
|
@ -74,6 +67,8 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
|
|
||||||
override fun onFinishInflate() {
|
override fun onFinishInflate() {
|
||||||
super.onFinishInflate()
|
super.onFinishInflate()
|
||||||
|
val colors = Settings.getInstance().current.mColors
|
||||||
|
colors.setKeyboardBackground(this)
|
||||||
clipboardAdapter = ClipboardAdapter(clipboardLayoutParams, this).apply {
|
clipboardAdapter = ClipboardAdapter(clipboardLayoutParams, this).apply {
|
||||||
itemBackgroundId = keyBackgroundId
|
itemBackgroundId = keyBackgroundId
|
||||||
pinnedIconResId = pinIconId
|
pinnedIconResId = pinIconId
|
||||||
|
@ -82,8 +77,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
clipboardRecyclerView = findViewById<ClipboardHistoryRecyclerView>(R.id.clipboard_list).apply {
|
clipboardRecyclerView = findViewById<ClipboardHistoryRecyclerView>(R.id.clipboard_list).apply {
|
||||||
val colCount = resources.getInteger(R.integer.config_clipboard_keyboard_col_count)
|
val colCount = resources.getInteger(R.integer.config_clipboard_keyboard_col_count)
|
||||||
layoutManager = StaggeredGridLayoutManager(colCount, StaggeredGridLayoutManager.VERTICAL)
|
layoutManager = StaggeredGridLayoutManager(colCount, StaggeredGridLayoutManager.VERTICAL)
|
||||||
val dividerHeight = resources.getDimensionPixelSize(R.dimen.config_clipboard_divider_height)
|
@Suppress("deprecation") // "no cache" should be fine according to warning in https://developer.android.com/reference/android/view/ViewGroup#setPersistentDrawingCache(int)
|
||||||
addItemDecoration(ClipboardHistoryRecyclerView.BottomDividerItemDecoration(dividerHeight, dividerColor))
|
|
||||||
persistentDrawingCache = PERSISTENT_NO_CACHE
|
persistentDrawingCache = PERSISTENT_NO_CACHE
|
||||||
clipboardLayoutParams.setListProperties(this)
|
clipboardLayoutParams.setListProperties(this)
|
||||||
placeholderView = this@ClipboardHistoryView.placeholderView
|
placeholderView = this@ClipboardHistoryView.placeholderView
|
||||||
|
@ -100,21 +94,15 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
clearKey = findViewById<ImageButton>(R.id.clipboard_clear).apply {
|
clearKey = findViewById<ImageButton>(R.id.clipboard_clear).apply {
|
||||||
setOnTouchListener(this@ClipboardHistoryView)
|
setOnTouchListener(this@ClipboardHistoryView)
|
||||||
setOnClickListener(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) {
|
private fun setupAlphabetKey(key: TextView?, label: String, params: KeyDrawParams) {
|
||||||
key?.apply {
|
key?.apply {
|
||||||
text = label
|
text = label
|
||||||
typeface = params.mTypeface
|
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)
|
setTextColor(params.mFunctionalTextColor)
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize.toFloat())
|
setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize.toFloat())
|
||||||
}
|
}
|
||||||
|
@ -225,8 +213,8 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
||||||
clipboardRecyclerView.smoothScrollToPosition(at)
|
clipboardRecyclerView.smoothScrollToPosition(at)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClipboardHistoryEntriesRemoved(position: Int, count: Int) {
|
override fun onClipboardHistoryEntriesRemoved(pos: Int, count: Int) {
|
||||||
clipboardAdapter.notifyItemRangeRemoved(position, count)
|
clipboardAdapter.notifyItemRangeRemoved(pos, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClipboardHistoryEntryMoved(from: Int, to: Int) {
|
override fun onClipboardHistoryEntryMoved(from: Int, to: Int) {
|
||||||
|
|
|
@ -166,7 +166,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setKeyboard(final Keyboard keyboard) {
|
public void setKeyboard(@NonNull final Keyboard keyboard) {
|
||||||
super.setKeyboard(keyboard);
|
super.setKeyboard(keyboard);
|
||||||
mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
|
mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
|
||||||
mMoreKeysKeyboardCache.clear();
|
mMoreKeysKeyboardCache.clear();
|
||||||
|
@ -321,22 +321,12 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
|
||||||
|
|
||||||
private void registerPress(final Key key) {
|
private void registerPress(final Key key) {
|
||||||
// Do not trigger key-down effect right now in case this is actually a fling action.
|
// Do not trigger key-down effect right now in case this is actually a fling action.
|
||||||
mPendingKeyDown = new Runnable() {
|
mPendingKeyDown = () -> callListenerOnPressKey(key);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
callListenerOnPressKey(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
|
mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerLongPress(final Key key) {
|
private void registerLongPress(final Key key) {
|
||||||
mPendingLongPress = new Runnable() {
|
mPendingLongPress = () -> onLongPressed(key);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
onLongPressed(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mHandler.postDelayed(mPendingLongPress, getLongPressTimeout());
|
mHandler.postDelayed(mPendingLongPress, getLongPressTimeout());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.dslul.openboard.inputmethod.keyboard.emoji;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
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.AudioAndHapticFeedbackManager;
|
||||||
import org.dslul.openboard.inputmethod.latin.R;
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype;
|
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.Colors;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||||
|
@ -133,7 +133,7 @@ public final class EmojiPalettesView extends LinearLayout
|
||||||
R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
|
R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
|
||||||
mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor(
|
mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor(
|
||||||
R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
|
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();
|
emojiPalettesViewAttr.recycle();
|
||||||
mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener();
|
mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener();
|
||||||
mEmojiLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
|
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
|
// TODO: Replace background color with its own setting rather than using the
|
||||||
// category page indicator background as a workaround.
|
// category page indicator background as a workaround.
|
||||||
iconView.setBackgroundColor(mCategoryPageIndicatorBackground);
|
iconView.setBackgroundColor(mCategoryPageIndicatorBackground);
|
||||||
// todo: this doesn't get applied for holo, what could cause this?
|
iconView.setColorFilter(Settings.getInstance().getCurrent().mColors.getKeyTextFilter());
|
||||||
// very interesting: in onTabChanged it's applied
|
|
||||||
iconView.setColorFilter(Settings.getInstance().getCurrent().mColors.keyTextFilter);
|
|
||||||
iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId));
|
iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId));
|
||||||
iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId));
|
iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId));
|
||||||
tspec.setIndicator(iconView);
|
tspec.setIndicator(iconView);
|
||||||
|
@ -190,7 +188,7 @@ public final class EmojiPalettesView extends LinearLayout
|
||||||
tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId);
|
tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId);
|
||||||
tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId);
|
tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId);
|
||||||
tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId);
|
tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId);
|
||||||
tabWidget.setBackgroundColor(colors.accent);
|
tabWidget.setBackgroundColor(colors.getAccent());
|
||||||
}
|
}
|
||||||
|
|
||||||
mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
|
mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
|
||||||
|
@ -272,15 +270,11 @@ public final class EmojiPalettesView extends LinearLayout
|
||||||
mEmojiLayoutParams.setKeyProperties(mSpacebar);
|
mEmojiLayoutParams.setKeyProperties(mSpacebar);
|
||||||
mSpacebarIcon = findViewById(R.id.emoji_keyboard_space_icon);
|
mSpacebarIcon = findViewById(R.id.emoji_keyboard_space_icon);
|
||||||
|
|
||||||
colors.setBackgroundColor(mAlphabetKeyLeft.getBackground(), Colors.TYPE_FUNCTIONAL);
|
colors.setBackgroundColor(mAlphabetKeyLeft.getBackground(), BackgroundType.FUNCTIONAL);
|
||||||
colors.setBackgroundColor(mDeleteKey.getBackground(), Colors.TYPE_FUNCTIONAL);
|
colors.setBackgroundColor(mDeleteKey.getBackground(), BackgroundType.FUNCTIONAL);
|
||||||
colors.setBackgroundColor(mSpacebar.getBackground(), Colors.TYPE_SPACE);
|
colors.setBackgroundColor(mSpacebar.getBackground(), BackgroundType.SPACE);
|
||||||
final Drawable background = colors.getKeyboardBackground();
|
colors.setKeyboardBackground(this);
|
||||||
if (background != null)
|
mEmojiCategoryPageIndicatorView.setColors(colors.getAccent(), colors.getAdjustedBackground());
|
||||||
setBackground(background);
|
|
||||||
else
|
|
||||||
getBackground().setColorFilter(colors.backgroundFilter);
|
|
||||||
mEmojiCategoryPageIndicatorView.setColors(colors.accent, colors.adjustedBackground);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -302,10 +296,10 @@ public final class EmojiPalettesView extends LinearLayout
|
||||||
}
|
}
|
||||||
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
||||||
if (mCurrentTab != null)
|
if (mCurrentTab != null)
|
||||||
mCurrentTab.setColorFilter(colors.keyTextFilter);
|
mCurrentTab.setColorFilter(colors.getKeyTextFilter());
|
||||||
mCurrentTab = (ImageView) mTabHost.getCurrentTabView();
|
mCurrentTab = (ImageView) mTabHost.getCurrentTabView();
|
||||||
// mCurrentTab.setColorFilter(colors.accentColorFilter); // todo (later): doesn't work properly, because enabled drawable is blue -> adjust
|
// mCurrentTab.setColorFilter(colors.accentColorFilter); not working because of lxx icon color
|
||||||
mCurrentTab.setColorFilter(colors.accent);
|
mCurrentTab.setColorFilter(colors.getAccent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -65,10 +65,10 @@ public class GestureFloatingTextDrawingPreview extends AbstractDrawingPreview {
|
||||||
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
||||||
mGesturePreviewTextSize = mainKeyboardViewAttr.getDimensionPixelSize(
|
mGesturePreviewTextSize = mainKeyboardViewAttr.getDimensionPixelSize(
|
||||||
R.styleable.MainKeyboardView_gestureFloatingPreviewTextSize, 0);
|
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(
|
mGesturePreviewTextOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
|
||||||
R.styleable.MainKeyboardView_gestureFloatingPreviewTextOffset, 0);
|
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(
|
mGesturePreviewHorizontalPadding = mainKeyboardViewAttr.getDimension(
|
||||||
R.styleable.MainKeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
|
R.styleable.MainKeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
|
||||||
mGesturePreviewVerticalPadding = mainKeyboardViewAttr.getDimension(
|
mGesturePreviewVerticalPadding = mainKeyboardViewAttr.getDimension(
|
||||||
|
|
|
@ -52,7 +52,7 @@ final class GestureTrailDrawingParams {
|
||||||
public final int mTrailLingerDuration;
|
public final int mTrailLingerDuration;
|
||||||
|
|
||||||
public GestureTrailDrawingParams(final TypedArray mainKeyboardViewAttr) {
|
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(
|
mTrailStartWidth = mainKeyboardViewAttr.getDimension(
|
||||||
R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f);
|
R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f);
|
||||||
mTrailEndWidth = mainKeyboardViewAttr.getDimension(
|
mTrailEndWidth = mainKeyboardViewAttr.getDimension(
|
||||||
|
|
|
@ -118,7 +118,7 @@ public final class KeyPreviewChoreographer {
|
||||||
final boolean hasMoreKeys = (key.getMoreKeys() != null);
|
final boolean hasMoreKeys = (key.getMoreKeys() != null);
|
||||||
keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition);
|
keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition);
|
||||||
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
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
|
// The key preview is placed vertically above the top edge of the parent key with an
|
||||||
// arbitrary offset.
|
// arbitrary offset.
|
||||||
final int previewY = key.getY() - previewHeight + key.getHeight() - mParams.mPreviewOffset
|
final int previewY = key.getY() - previewHeight + key.getHeight() - mParams.mPreviewOffset
|
||||||
|
|
|
@ -128,19 +128,19 @@ public final class KeyVisualAttributes {
|
||||||
R.styleable.Keyboard_Key_keyPreviewTextRatio);
|
R.styleable.Keyboard_Key_keyPreviewTextRatio);
|
||||||
|
|
||||||
final Colors colors = Settings.getInstance().getCurrent().mColors;
|
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);
|
mTextInactivatedColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
|
||||||
mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
|
mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
|
||||||
// todo: maybe a separate color?
|
// todo: maybe a separate color?
|
||||||
mFunctionalTextColor = colors.keyText; //keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0);
|
mFunctionalTextColor = colors.getKeyText(); //keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0);
|
||||||
mHintLetterColor = colors.keyHintText; //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
|
mHintLetterColor = colors.getKeyHintText(); //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
|
||||||
mHintLabelColor = colors.keyText; //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
|
mHintLabelColor = colors.getKeyText(); //keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
|
||||||
mShiftedLetterHintInactivatedColor = keyAttr.getColor(
|
mShiftedLetterHintInactivatedColor = keyAttr.getColor(
|
||||||
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
|
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
|
||||||
mShiftedLetterHintActivatedColor = keyAttr.getColor(
|
mShiftedLetterHintActivatedColor = keyAttr.getColor(
|
||||||
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
|
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
|
||||||
// todo: maybe a separate color?
|
// 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,
|
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,
|
||||||
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f);
|
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) {
|
public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
|
||||||
mParams.mId = id;
|
mParams.mId = id;
|
||||||
final XmlResourceParser parser = mResources.getXml(xmlId);
|
try (XmlResourceParser parser = mResources.getXml(xmlId)) {
|
||||||
try {
|
|
||||||
parseKeyboard(parser);
|
parseKeyboard(parser);
|
||||||
} catch (XmlPullParserException e) {
|
} catch (XmlPullParserException e) {
|
||||||
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
|
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
|
||||||
|
@ -176,8 +175,6 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
|
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
|
||||||
throw new RuntimeException(e.getMessage(), e);
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
} finally {
|
|
||||||
parser.close();
|
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -481,9 +478,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
|
||||||
final int width = (int)keyWidth;
|
final int width = (int)keyWidth;
|
||||||
final int height = row.getRowHeight();
|
final int height = row.getRowHeight();
|
||||||
final String hintLabel = moreKeySpecs != null ? "\u25E5" : null;
|
final String hintLabel = moreKeySpecs != null ? "\u25E5" : null;
|
||||||
final KeyboardParams params = mParams;
|
|
||||||
final Key key = new Key(label, code, outputText, hintLabel, moreKeySpecs,
|
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);
|
endKey(key);
|
||||||
row.advanceXPos(keyWidth);
|
row.advanceXPos(keyWidth);
|
||||||
}
|
}
|
||||||
|
@ -555,7 +551,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
|
||||||
final TypedArray keyboardAttr = mResources.obtainAttributes(
|
final TypedArray keyboardAttr = mResources.obtainAttributes(
|
||||||
attr, R.styleable.Keyboard_Include);
|
attr, R.styleable.Keyboard_Include);
|
||||||
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
|
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
|
||||||
int keyboardLayout = 0;
|
final int keyboardLayout;
|
||||||
try {
|
try {
|
||||||
XmlParseUtils.checkAttributeExists(
|
XmlParseUtils.checkAttributeExists(
|
||||||
keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
|
keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
|
||||||
|
@ -578,15 +574,13 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
|
||||||
startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
|
startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
|
||||||
mResources.getResourceEntryName(keyboardLayout));
|
mResources.getResourceEntryName(keyboardLayout));
|
||||||
}
|
}
|
||||||
final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
|
try (XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout)) {
|
||||||
try {
|
|
||||||
parseMerge(parserForInclude, row, skip);
|
parseMerge(parserForInclude, row, skip);
|
||||||
} finally {
|
} finally {
|
||||||
if (row != null) {
|
if (row != null) {
|
||||||
// Restore Row attributes.
|
// Restore Row attributes.
|
||||||
row.popRowAttributes();
|
row.popRowAttributes();
|
||||||
}
|
}
|
||||||
parserForInclude.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,7 +710,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
|
||||||
final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
|
final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
|
||||||
&& keyboardThemeMacthed && modeMatched && navigateNextMatched
|
&& keyboardThemeMacthed && modeMatched && navigateNextMatched
|
||||||
&& navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched
|
&& navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched
|
||||||
&& hasShortcutKeyMatched && numberRowEnabledMatched && languageSwitchKeyEnabledMatched
|
&& hasShortcutKeyMatched && numberRowEnabledMatched && languageSwitchKeyEnabledMatched
|
||||||
&& emojiKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
|
&& emojiKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
|
||||||
&& localeCodeMatched && languageCodeMatched && countryCodeMatched
|
&& localeCodeMatched && languageCodeMatched && countryCodeMatched
|
||||||
&& splitLayoutMatched && oneHandedModeEnabledMatched;
|
&& splitLayoutMatched && oneHandedModeEnabledMatched;
|
||||||
|
|
|
@ -21,10 +21,14 @@ import android.content.res.Resources;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
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.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.RunInLocale;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
// TODO: Make this an immutable class.
|
// TODO: Make this an immutable class.
|
||||||
|
@ -39,7 +43,7 @@ public final class KeyboardTextsSet {
|
||||||
private Resources mResources;
|
private Resources mResources;
|
||||||
private Locale mResourceLocale;
|
private Locale mResourceLocale;
|
||||||
private String mResourcePackageName;
|
private String mResourcePackageName;
|
||||||
private String[] mTextsTable;
|
private final ArrayList<String[]> mTextsTables = new ArrayList<>();
|
||||||
|
|
||||||
public void setLocale(final Locale locale, final Context context) {
|
public void setLocale(final Locale locale, final Context context) {
|
||||||
final Resources res = context.getResources();
|
final Resources res = context.getResources();
|
||||||
|
@ -56,11 +60,25 @@ public final class KeyboardTextsSet {
|
||||||
// Null means the current system locale.
|
// Null means the current system locale.
|
||||||
mResourceLocale = SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale;
|
mResourceLocale = SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale;
|
||||||
mResourcePackageName = resourcePackageName;
|
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) {
|
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) {
|
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: 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) {
|
public String resolveTextReference(final String rawText) {
|
||||||
if (TextUtils.isEmpty(rawText)) {
|
if (TextUtils.isEmpty(rawText)) {
|
||||||
return null;
|
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;
|
int level = 0;
|
||||||
String text = rawText;
|
String text = rawText;
|
||||||
StringBuilder sb;
|
StringBuilder sb;
|
||||||
|
final int prefixLength = PREFIX_TEXT.length();
|
||||||
do {
|
do {
|
||||||
level++;
|
level++;
|
||||||
if (level >= MAX_REFERENCE_INDIRECTION) {
|
if (level >= MAX_REFERENCE_INDIRECTION) {
|
||||||
|
@ -91,7 +143,6 @@ public final class KeyboardTextsSet {
|
||||||
" reference indirection: " + text);
|
" reference indirection: " + text);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int prefixLength = PREFIX_TEXT.length();
|
|
||||||
final int size = text.length();
|
final int size = text.length();
|
||||||
if (size < prefixLength) {
|
if (size < prefixLength) {
|
||||||
break;
|
break;
|
||||||
|
@ -104,12 +155,12 @@ public final class KeyboardTextsSet {
|
||||||
if (sb == null) {
|
if (sb == null) {
|
||||||
sb = new StringBuilder(text.substring(0, pos));
|
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)) {
|
} else if (text.startsWith(PREFIX_RESOURCE, pos)) {
|
||||||
if (sb == null) {
|
if (sb == null) {
|
||||||
sb = new StringBuilder(text.substring(0, pos));
|
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) {
|
} else if (c == BACKSLASH) {
|
||||||
if (sb != null) {
|
if (sb != null) {
|
||||||
// Append both escape character and escaped character.
|
// Append both escape character and escaped character.
|
||||||
|
@ -125,25 +176,27 @@ public final class KeyboardTextsSet {
|
||||||
text = sb.toString();
|
text = sb.toString();
|
||||||
}
|
}
|
||||||
} while (sb != null);
|
} while (sb != null);
|
||||||
return TextUtils.isEmpty(text) ? null : text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int expandReference(final String text, final int pos, final String prefix,
|
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 prefixLength = prefix.length();
|
||||||
final int end = searchTextNameEnd(text, pos + prefixLength);
|
final int end = searchTextNameEnd(text, pos + prefixLength);
|
||||||
final String name = text.substring(pos + prefixLength, end);
|
final String name = text.substring(pos + prefixLength, end);
|
||||||
if (prefix.equals(PREFIX_TEXT)) {
|
if (prefix.equals(PREFIX_TEXT)) {
|
||||||
sb.append(getText(name));
|
sb.append(getTextInternal(name, localeIndex));
|
||||||
} else { // PREFIX_RESOURCE
|
} else { // PREFIX_RESOURCE
|
||||||
final String resourcePackageName = mResourcePackageName;
|
final String resourcePackageName = mResourcePackageName;
|
||||||
final RunInLocale<String> getTextJob = new RunInLocale<String>() {
|
final RunInLocale<String> getTextJob = new RunInLocale<String>() {
|
||||||
@Override
|
@Override
|
||||||
protected String job(final Resources res) {
|
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);
|
final int resId = res.getIdentifier(name, "string", resourcePackageName);
|
||||||
return res.getString(resId);
|
return res.getString(resId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// no need to do it in locale, it's just labels
|
||||||
sb.append(getTextJob.runInLocale(mResources, mResourceLocale));
|
sb.append(getTextJob.runInLocale(mResources, mResourceLocale));
|
||||||
}
|
}
|
||||||
return end - 1;
|
return end - 1;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -222,7 +222,7 @@ public final class MoreKeySpec {
|
||||||
if (remain != null) {
|
if (remain != null) {
|
||||||
list.add(remain);
|
list.add(remain);
|
||||||
}
|
}
|
||||||
return list.toArray(new String[list.size()]);
|
return list.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -247,7 +247,7 @@ public final class MoreKeySpec {
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
return out.toArray(new String[out.size()]);
|
return out.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] insertAdditionalMoreKeys(@Nullable final String[] moreKeySpecs,
|
public static String[] insertAdditionalMoreKeys(@Nullable final String[] moreKeySpecs,
|
||||||
|
@ -300,7 +300,7 @@ public final class MoreKeySpec {
|
||||||
if (out == null && moreKeysCount > 0) {
|
if (out == null && moreKeysCount > 0) {
|
||||||
return moreKeys;
|
return moreKeys;
|
||||||
} else if (out != null && out.size() > 0) {
|
} else if (out != null && out.size() > 0) {
|
||||||
return out.toArray(new String[out.size()]);
|
return out.toArray(new String[0]);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ public final class SlidingKeyInputDrawingPreview extends AbstractDrawingPreview
|
||||||
private final Paint mPaint = new Paint();
|
private final Paint mPaint = new Paint();
|
||||||
|
|
||||||
public SlidingKeyInputDrawingPreview(final TypedArray mainKeyboardViewAttr) {
|
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(
|
final float previewRadius = mainKeyboardViewAttr.getDimension(
|
||||||
R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0) / 2.0f;
|
R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0) / 2.0f;
|
||||||
final int PERCENTAGE_INT = 100;
|
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;
|
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.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -69,7 +69,7 @@ final public class BinaryDictionaryGetter {
|
||||||
public static final String ASSETS_DICTIONARY_FOLDER = "dicts";
|
public static final String ASSETS_DICTIONARY_FOLDER = "dicts";
|
||||||
|
|
||||||
// The key considered to read the version attribute in a dictionary file.
|
// 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
|
// Prevents this from being instantiated
|
||||||
private BinaryDictionaryGetter() {}
|
private BinaryDictionaryGetter() {}
|
||||||
|
@ -187,7 +187,7 @@ final public class BinaryDictionaryGetter {
|
||||||
DictionaryInfoUtils.getCategoryFromFileName(wordList.getName());
|
DictionaryInfoUtils.getCategoryFromFileName(wordList.getName());
|
||||||
final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
|
final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
|
||||||
if (null == currentBestMatch || currentBestMatch.mMatchLevel <= matchLevel) {
|
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
|
// 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
|
// 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)
|
if (wordList.getName().endsWith(USER_DICTIONARY_SUFFIX) || currentBestMatch == null)
|
||||||
|
@ -206,38 +206,6 @@ final public class BinaryDictionaryGetter {
|
||||||
return result;
|
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.
|
* 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
|
// 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
|
// 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,
|
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
|
loadDictionaryFromAssets(locale.toString(), context, weakMatchAcceptable); // will copy dict to cached word lists if not existing
|
||||||
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context, weakMatchAcceptable);
|
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context, weakMatchAcceptable);
|
||||||
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
|
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
|
||||||
|
@ -264,10 +232,7 @@ final public class BinaryDictionaryGetter {
|
||||||
// cachedWordLists may not be null, see doc for getCachedDictionaryList
|
// cachedWordLists may not be null, see doc for getCachedDictionaryList
|
||||||
for (final File f : cachedWordLists) {
|
for (final File f : cachedWordLists) {
|
||||||
final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
|
final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
|
||||||
// remove the hack that requires version 18
|
final boolean canUse = f.canRead();
|
||||||
// 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);
|
|
||||||
if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
|
if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
|
||||||
foundMainDict = true;
|
foundMainDict = true;
|
||||||
}
|
}
|
||||||
|
@ -276,24 +241,16 @@ final public class BinaryDictionaryGetter {
|
||||||
final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath());
|
final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath());
|
||||||
if (null != afa) fileList.add(afa);
|
if (null != afa) fileList.add(afa);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Found a cached dictionary file for " + locale.toString()
|
Log.e(TAG, "Found a cached dictionary file for " + locale + " but cannot read or use it");
|
||||||
+ " but cannot read or use it");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
|
if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
|
||||||
final File dict = loadDictionaryFromAssets(locale.toString(), context, weakMatchAcceptable);
|
final File dict = loadDictionaryFromAssets(locale.toString(), context, weakMatchAcceptable);
|
||||||
final AssetFileAddress fallbackAsset;
|
if (dict != null) {
|
||||||
if (dict == null) {
|
final AssetFileAddress fallbackAsset = AssetFileAddress.makeFromFileName(dict.getPath());
|
||||||
// fall back to the old way (maybe remove? will not work if files are compressed)
|
if (fallbackAsset != null)
|
||||||
final int fallbackResId =
|
fileList.add(fallbackAsset);
|
||||||
DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
|
|
||||||
fallbackAsset = loadFallbackResource(context, fallbackResId);
|
|
||||||
} else {
|
|
||||||
fallbackAsset = AssetFileAddress.makeFromFileName(dict.getPath());
|
|
||||||
}
|
|
||||||
if (null != fallbackAsset) {
|
|
||||||
fileList.add(fallbackAsset);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
|
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
|
||||||
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
|
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
|
||||||
private final Object mLock = new Object();
|
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>>
|
public static final Map<String, Class<? extends ExpandableBinaryDictionary>>
|
||||||
DICT_TYPE_TO_CLASS = new HashMap<>();
|
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...
|
// 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
|
// 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> mValidSpellingWordReadCache;
|
||||||
private LruCache<String, Boolean> mValidSpellingWordWriteCache;
|
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.
|
* A group of dictionaries that work together for a single language.
|
||||||
*/
|
*/
|
||||||
private static class DictionaryGroup {
|
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 MAX_CONFIDENCE = 2;
|
||||||
private static final int MIN_CONFIDENCE = 0;
|
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() {
|
private void updateWeights() {
|
||||||
mWeightForTypingInLocale = 1f - 0.15f * (MAX_CONFIDENCE - mConfidence);
|
mWeightForTypingInLocale = 1f - 0.15f * (MAX_CONFIDENCE - mConfidence);
|
||||||
mWeightForGesturingInLocale = 1f - 0.05f * (MAX_CONFIDENCE - mConfidence);
|
mWeightForGesturingInLocale = 1f - 0.05f * (MAX_CONFIDENCE - mConfidence);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
|
public float mWeightForTypingInLocale = 1f;
|
||||||
public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
|
public float mWeightForGesturingInLocale = 1f;
|
||||||
public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
|
public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public DictionaryGroup() {
|
public DictionaryGroup() {
|
||||||
this(null /* locale */, null /* mainDict */, null /* account */,
|
this(null /* locale */, null /* mainDict */, null /* account */, Collections.emptyMap() /* subDicts */);
|
||||||
Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DictionaryGroup(@Nullable final Locale locale,
|
public DictionaryGroup(@Nullable final Locale locale,
|
||||||
@Nullable final Dictionary mainDict,
|
@Nullable final Dictionary mainDict,
|
||||||
@Nullable final String account,
|
@Nullable final String account,
|
||||||
final Map<String, ExpandableBinaryDictionary> subDicts) {
|
@NonNull final Map<String, ExpandableBinaryDictionary> subDicts) {
|
||||||
mLocale = locale;
|
mLocale = locale;
|
||||||
mAccount = account;
|
mAccount = account;
|
||||||
// The main dictionary can be asynchronously loaded.
|
// 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) {
|
private void setSubDict(@NonNull final String dictType, @NonNull final ExpandableBinaryDictionary dict) {
|
||||||
if (dict != null) {
|
mSubDictMap.put(dictType, 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.
|
// Close old dictionary if exists. Main dictionary can be assigned multiple times.
|
||||||
final Dictionary oldDict = mMainDict;
|
final Dictionary oldDict = mMainDict;
|
||||||
mMainDict = mainDict;
|
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)) {
|
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
||||||
return mMainDict;
|
return mMainDict;
|
||||||
}
|
}
|
||||||
return getSubDict(dictType);
|
return getSubDict(dictType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExpandableBinaryDictionary getSubDict(final String dictType) {
|
public @Nullable ExpandableBinaryDictionary getSubDict(@NonNull final String dictType) {
|
||||||
return mSubDictMap.get(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)) {
|
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
||||||
return mMainDict != null;
|
return mMainDict != null;
|
||||||
}
|
}
|
||||||
|
@ -255,7 +256,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
return mSubDictMap.containsKey(dictType);
|
return mSubDictMap.containsKey(dictType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeDict(final String dictType) {
|
public void closeDict(@NonNull final String dictType) {
|
||||||
final Dictionary dict;
|
final Dictionary dict;
|
||||||
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
||||||
dict = mMainDict;
|
dict = mMainDict;
|
||||||
|
@ -480,13 +481,8 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
final List<Locale> locales, final DictionaryInitializationListener listener) {
|
final List<Locale> locales, final DictionaryInitializationListener listener) {
|
||||||
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
|
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
|
||||||
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
|
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
|
||||||
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
|
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() ->
|
||||||
@Override
|
doReloadUninitializedMainDictionaries(context, locales, listener, latchForWaitingLoadingMainDictionary));
|
||||||
public void run() {
|
|
||||||
doReloadUninitializedMainDictionaries(
|
|
||||||
context, locales, listener, latchForWaitingLoadingMainDictionary);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void doReloadUninitializedMainDictionaries(final Context context, final List<Locale> locales,
|
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) {
|
private void addToPersonalDictionaryIfInvalidButInHistory(String suggestion, boolean[] validWordForDictionary) {
|
||||||
// we need one clearly preferred group to assign it to the correct language
|
// we need one clearly preferred group to assign it to the correct language
|
||||||
int highestGroup = -1;
|
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 ++) {
|
for (int i = 0; i < mDictionaryGroups.size(); i ++) {
|
||||||
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
|
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
|
||||||
if (dictionaryGroup.mConfidence > highestGroupConfidence) {
|
if (dictionaryGroup.mConfidence > highestGroupConfidence) {
|
||||||
|
@ -701,13 +700,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
if (userDict != null && userHistoryDict.isInDictionary(suggestion)) {
|
if (userDict != null && userHistoryDict.isInDictionary(suggestion)) {
|
||||||
if (userDict.isInDictionary(suggestion)) // is this check necessary?
|
if (userDict.isInDictionary(suggestion)) // is this check necessary?
|
||||||
return;
|
return;
|
||||||
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
|
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() ->
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
UserDictionary.Words.addWord(userDict.mContext, suggestion,
|
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) {
|
if (maxFreq == 0 && blockPotentiallyOffensive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mTryChangingWords)
|
||||||
|
mTryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(mChangeFrom, mChangeTo);
|
||||||
final String secondWord;
|
final String secondWord;
|
||||||
if (wasAutoCapitalized) {
|
if (wasAutoCapitalized) {
|
||||||
// used word with lower-case first letter instead of all lower-case, as auto-capitalize
|
// 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
|
// 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.
|
// only, then we consider that was a lower-case word that had been auto-capitalized.
|
||||||
secondWord = decapitalizedWord;
|
secondWord = decapitalizedWord;
|
||||||
|
mTryChangingWords = true;
|
||||||
|
mChangeFrom = word;
|
||||||
|
mChangeTo = secondWord;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// HACK: We'd like to avoid adding the capitalized form of common words to the User
|
// 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.
|
// TODO: Revise the way to fusion suggestion results.
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@NonNull public SuggestionResults getSuggestionResults(ComposedData composedData,
|
@NonNull public SuggestionResults getSuggestionResults(ComposedData composedData,
|
||||||
NgramContext ngramContext, @NonNull final Keyboard keyboard,
|
NgramContext ngramContext, @NonNull final Keyboard keyboard,
|
||||||
SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId,
|
SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId,
|
||||||
|
@ -885,12 +886,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
int sessionId, long proximityInfoHandle, float[] weightOfLangModelVsSpatialModel,
|
int sessionId, long proximityInfoHandle, float[] weightOfLangModelVsSpatialModel,
|
||||||
DictionaryGroup dictGroup) {
|
DictionaryGroup dictGroup) {
|
||||||
final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
|
final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
|
||||||
|
final float weightForLocale = composedData.mIsBatchMode
|
||||||
|
? dictGroup.mWeightForGesturingInLocale
|
||||||
|
: dictGroup.mWeightForTypingInLocale;
|
||||||
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
||||||
final Dictionary dictionary = dictGroup.getDict(dictType);
|
final Dictionary dictionary = dictGroup.getDict(dictType);
|
||||||
if (null == dictionary) continue;
|
if (null == dictionary) continue;
|
||||||
final float weightForLocale = composedData.mIsBatchMode
|
|
||||||
? dictGroup.mWeightForGesturingInLocale
|
|
||||||
: dictGroup.mWeightForTypingInLocale;
|
|
||||||
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
|
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
|
||||||
dictionary.getSuggestions(composedData, ngramContext,
|
dictionary.getSuggestions(composedData, ngramContext,
|
||||||
proximityInfoHandle, settingsValuesForSuggestion, sessionId,
|
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.
|
* This class automatically creates and releases up to 3 facilitator instances using LRU policy.
|
||||||
*/
|
*/
|
||||||
public class DictionaryFacilitatorLruCache {
|
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 WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
|
||||||
private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
|
private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,14 @@
|
||||||
|
|
||||||
package org.dslul.openboard.inputmethod.latin;
|
package org.dslul.openboard.inputmethod.latin;
|
||||||
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.AssetFileDescriptor;
|
import android.content.res.AssetFileDescriptor;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
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 org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -54,7 +57,7 @@ public final class DictionaryFactory {
|
||||||
|
|
||||||
final LinkedList<Dictionary> dictList = new LinkedList<>();
|
final LinkedList<Dictionary> dictList = new LinkedList<>();
|
||||||
ArrayList<AssetFileAddress> assetFileList =
|
ArrayList<AssetFileAddress> assetFileList =
|
||||||
BinaryDictionaryGetter.getDictionaryFiles(locale, context, true, false);
|
BinaryDictionaryGetter.getDictionaryFiles(locale, context, false);
|
||||||
|
|
||||||
boolean mainFound = false;
|
boolean mainFound = false;
|
||||||
for (AssetFileAddress fileAddress : assetFileList) {
|
for (AssetFileAddress fileAddress : assetFileList) {
|
||||||
|
@ -64,12 +67,18 @@ public final class DictionaryFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!mainFound) // try again and allow weaker match
|
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) {
|
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 =
|
final ReadOnlyBinaryDictionary readOnlyBinaryDictionary =
|
||||||
new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength,
|
new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength,
|
||||||
false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
|
false /* useFullEditDistance */, locale, dictType);
|
||||||
if (readOnlyBinaryDictionary.isValidDictionary()) {
|
if (readOnlyBinaryDictionary.isValidDictionary()) {
|
||||||
if(locale.getLanguage().equals("ko")) {
|
if(locale.getLanguage().equals("ko")) {
|
||||||
// Use KoreanDictionary for Korean locale
|
// Use KoreanDictionary for Korean locale
|
||||||
|
@ -98,38 +107,14 @@ public final class DictionaryFactory {
|
||||||
public static void killDictionary(final Context context, final AssetFileAddress f) {
|
public static void killDictionary(final Context context, final AssetFileAddress f) {
|
||||||
if (f.pointsToPhysicalFile()) {
|
if (f.pointsToPhysicalFile()) {
|
||||||
f.deleteUnderlyingFile();
|
f.deleteUnderlyingFile();
|
||||||
// Warn the dictionary provider if the dictionary came from there.
|
// notify the user
|
||||||
final ContentProviderClient providerClient;
|
// todo: use an alertDialog to avoid the toast not showing up on Android 13+
|
||||||
try {
|
// but asyncTask doesn't work because android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
|
||||||
providerClient = context.getContentResolver().acquireContentProviderClient(
|
// https://stackoverflow.com/questions/7199014/show-an-alertdialog-from-a-background-thread-with-the-appcontext
|
||||||
BinaryDictionaryFileDumper.getProviderUriBuilder("").build());
|
final String wordlistId = DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName());
|
||||||
} catch (final SecurityException e) {
|
new Handler(Looper.getMainLooper()).post(() ->
|
||||||
Log.e(TAG, "No permission to communicate with the dictionary provider", e);
|
Toast.makeText(context, "dictionary "+wordlistId+" is invalid, deleting", Toast.LENGTH_LONG).show()
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,6 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei
|
||||||
}
|
}
|
||||||
// If we come here none of the authorities matched the one we searched for.
|
// If we come here none of the authorities matched the one we searched for.
|
||||||
// We can exit safely.
|
// We can exit safely.
|
||||||
return;
|
|
||||||
} else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
|
} else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
&& !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
|
&& !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
|
||||||
if (null == mService) {
|
if (null == mService) {
|
||||||
|
@ -120,22 +119,6 @@ public final class DictionaryPackInstallBroadcastReceiver extends BroadcastRecei
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mService.resetSuggestMainDict();
|
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.
|
* A class for detecting Emoji-Alt physical key.
|
||||||
*/
|
*/
|
||||||
final class EmojiAltPhysicalKeyDetector {
|
final class EmojiAltPhysicalKeyDetector {
|
||||||
private static final String TAG = "EmojiAltPhysicalKeyDetector";
|
private static final String TAG = "EmojiAltPhysKeyDetector";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
private List<EmojiHotKeys> mHotKeysList;
|
private final List<EmojiHotKeys> mHotKeysList;
|
||||||
|
|
||||||
private static class HotKeySet extends HashSet<Pair<Integer, Integer>> { }
|
private static class HotKeySet extends HashSet<Pair<Integer, Integer>> { }
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ final class EmojiAltPhysicalKeyDetector {
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmojiAltPhysicalKeyDetector(@NonNull final Resources resources) {
|
public EmojiAltPhysicalKeyDetector(@NonNull final Resources resources) {
|
||||||
mHotKeysList = new ArrayList<EmojiHotKeys>();
|
mHotKeysList = new ArrayList<>();
|
||||||
|
|
||||||
final HotKeySet emojiSwitchSet = parseHotKeys(
|
final HotKeySet emojiSwitchSet = parseHotKeys(
|
||||||
resources, R.array.keyboard_switcher_emoji);
|
resources, R.array.keyboard_switcher_emoji);
|
||||||
|
@ -192,8 +192,8 @@ final class EmojiAltPhysicalKeyDetector {
|
||||||
Log.w(TAG, "Expected 2 integers in " + name + "[" + i + "] : " + values[i]);
|
Log.w(TAG, "Expected 2 integers in " + name + "[" + i + "] : " + values[i]);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final Integer keyCode = Integer.parseInt(valuePair[0]);
|
final int keyCode = Integer.parseInt(valuePair[0]);
|
||||||
final Integer metaState = Integer.parseInt(valuePair[1]);
|
final int metaState = Integer.parseInt(valuePair[1]);
|
||||||
final Pair<Integer, Integer> key = Pair.create(
|
final Pair<Integer, Integer> key = Pair.create(
|
||||||
keyCode, KeyEvent.normalizeMetaState(metaState));
|
keyCode, KeyEvent.normalizeMetaState(metaState));
|
||||||
keySet.add(key);
|
keySet.add(key);
|
||||||
|
|
|
@ -169,15 +169,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) {
|
private static void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) {
|
||||||
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
|
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() -> {
|
||||||
@Override
|
lock.lock();
|
||||||
public void run() {
|
try {
|
||||||
lock.lock();
|
task.run();
|
||||||
try {
|
} finally {
|
||||||
task.run();
|
lock.unlock();
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -199,12 +196,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(this::closeBinaryDictionary);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
closeBinaryDictionary();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Map<String, String> getHeaderAttributeMap() {
|
protected Map<String, String> getHeaderAttributeMap() {
|
||||||
|
@ -220,12 +212,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeBinaryDictionary() {
|
private void removeBinaryDictionary() {
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(this::removeBinaryDictionaryLocked);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
removeBinaryDictionaryLocked();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeBinaryDictionaryLocked() {
|
void removeBinaryDictionaryLocked() {
|
||||||
|
@ -248,12 +235,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
removeBinaryDictionaryLocked();
|
||||||
public void run() {
|
createOnMemoryBinaryDictionaryLocked();
|
||||||
removeBinaryDictionaryLocked();
|
|
||||||
createOnMemoryBinaryDictionaryLocked();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,14 +245,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
* Check whether GC is needed and run GC if required.
|
* Check whether GC is needed and run GC if required.
|
||||||
*/
|
*/
|
||||||
public void runGCIfRequired(final boolean mindsBlockByGC) {
|
public void runGCIfRequired(final boolean mindsBlockByGC) {
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
if (getBinaryDictionary() == null) {
|
||||||
public void run() {
|
return;
|
||||||
if (getBinaryDictionary() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runGCIfRequiredLocked(mindsBlockByGC);
|
|
||||||
}
|
}
|
||||||
|
runGCIfRequiredLocked(mindsBlockByGC);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,17 +261,13 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
|
|
||||||
private void updateDictionaryWithWriteLock(@NonNull final Runnable updateTask) {
|
private void updateDictionaryWithWriteLock(@NonNull final Runnable updateTask) {
|
||||||
reloadDictionaryIfRequired();
|
reloadDictionaryIfRequired();
|
||||||
final Runnable task = new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
if (getBinaryDictionary() == null) {
|
||||||
public void run() {
|
return;
|
||||||
if (getBinaryDictionary() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
|
||||||
updateTask.run();
|
|
||||||
}
|
}
|
||||||
};
|
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
||||||
asyncExecuteTaskWithWriteLock(task);
|
updateTask.run();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -299,13 +276,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
public void addUnigramEntry(final String word, final int frequency,
|
public void addUnigramEntry(final String word, final int frequency,
|
||||||
final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
|
final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
|
||||||
final boolean isPossiblyOffensive, final int timestamp) {
|
final boolean isPossiblyOffensive, final int timestamp) {
|
||||||
updateDictionaryWithWriteLock(new Runnable() {
|
updateDictionaryWithWriteLock(() -> addUnigramLocked(word, frequency, shortcutTarget,
|
||||||
@Override
|
shortcutFreq, isNotAWord, isPossiblyOffensive, timestamp));
|
||||||
public void run() {
|
|
||||||
addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq,
|
|
||||||
isNotAWord, isPossiblyOffensive, timestamp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addUnigramLocked(final String word, final int frequency,
|
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) {
|
public void removeUnigramEntryDynamically(final String word) {
|
||||||
reloadDictionaryIfRequired();
|
reloadDictionaryIfRequired();
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
||||||
public void run() {
|
if (binaryDictionary == null) {
|
||||||
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
return;
|
||||||
if (binaryDictionary == null) {
|
}
|
||||||
return;
|
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
||||||
}
|
if (!binaryDictionary.removeUnigramEntry(word)) {
|
||||||
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
if (DEBUG) {
|
||||||
if (!binaryDictionary.removeUnigramEntry(word)) {
|
Log.i(TAG, "Cannot remove unigram entry: " + 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,
|
public void addNgramEntry(@NonNull final NgramContext ngramContext, final String word,
|
||||||
final int frequency, final int timestamp) {
|
final int frequency, final int timestamp) {
|
||||||
reloadDictionaryIfRequired();
|
reloadDictionaryIfRequired();
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
if (getBinaryDictionary() == null) {
|
||||||
public void run() {
|
return;
|
||||||
if (getBinaryDictionary() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
|
||||||
addNgramEntryLocked(ngramContext, word, frequency, timestamp);
|
|
||||||
}
|
}
|
||||||
|
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,
|
public void updateEntriesForWord(@NonNull final NgramContext ngramContext,
|
||||||
final String word, final boolean isValidWord, final int count, final int timestamp) {
|
final String word, final boolean isValidWord, final int count, final int timestamp) {
|
||||||
updateDictionaryWithWriteLock(new Runnable() {
|
updateDictionaryWithWriteLock(() -> {
|
||||||
@Override
|
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
||||||
public void run() {
|
if (binaryDictionary == null) {
|
||||||
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
return;
|
||||||
if (binaryDictionary == null) {
|
}
|
||||||
return;
|
if (!binaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word,
|
||||||
}
|
isValidWord, count, timestamp)) {
|
||||||
if (!binaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word,
|
if (DEBUG) {
|
||||||
isValidWord, count, timestamp)) {
|
Log.e(TAG, "Cannot update counter. word: " + word
|
||||||
if (DEBUG) {
|
+ " context: " + ngramContext);
|
||||||
Log.e(TAG, "Cannot update counter. word: " + word
|
|
||||||
+ " context: " + ngramContext.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -410,21 +373,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
@NonNull final ArrayList<WordInputEventForPersonalization> inputEvents,
|
@NonNull final ArrayList<WordInputEventForPersonalization> inputEvents,
|
||||||
final UpdateEntriesForInputEventsCallback callback) {
|
final UpdateEntriesForInputEventsCallback callback) {
|
||||||
reloadDictionaryIfRequired();
|
reloadDictionaryIfRequired();
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
||||||
try {
|
if (binaryDictionary == null) {
|
||||||
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
return;
|
||||||
if (binaryDictionary == null) {
|
}
|
||||||
return;
|
binaryDictionary.updateEntriesForInputEvents(inputEvents.toArray(new WordInputEventForPersonalization[0]));
|
||||||
}
|
} finally {
|
||||||
binaryDictionary.updateEntriesForInputEvents(
|
if (callback != null) {
|
||||||
inputEvents.toArray(
|
callback.onFinished();
|
||||||
new WordInputEventForPersonalization[inputEvents.size()]));
|
|
||||||
} finally {
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onFinished();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -601,32 +559,28 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final File dictFile = mDictFile;
|
final File dictFile = mDictFile;
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
if (!dictFile.exists() || isNeededToRecreate()) {
|
||||||
try {
|
// If the dictionary file does not exist or contents have been updated,
|
||||||
if (!dictFile.exists() || isNeededToRecreate()) {
|
// generate a new one.
|
||||||
// If the dictionary file does not exist or contents have been updated,
|
createNewDictionaryLocked();
|
||||||
// generate a new one.
|
} 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();
|
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
|
@Override
|
||||||
public void onFinishInput() {
|
public void onFinishInput() {
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(() -> {
|
||||||
@Override
|
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
||||||
public void run() {
|
if (binaryDictionary == null) {
|
||||||
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
return;
|
||||||
if (binaryDictionary == null) {
|
}
|
||||||
return;
|
if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
|
||||||
}
|
binaryDictionary.flushWithGCIfHasUpdated();
|
||||||
if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
|
} else {
|
||||||
binaryDictionary.flushWithGCIfHasUpdated();
|
binaryDictionary.flush();
|
||||||
} else {
|
|
||||||
binaryDictionary.flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -670,12 +621,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
public void waitAllTasksForTests() {
|
public void waitAllTasksForTests() {
|
||||||
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
asyncExecuteTaskWithWriteLock(new Runnable() {
|
asyncExecuteTaskWithWriteLock(countDownLatch::countDown);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
countDownLatch.await();
|
countDownLatch.await();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
@ -694,35 +640,31 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
|
||||||
reloadDictionaryIfRequired();
|
reloadDictionaryIfRequired();
|
||||||
final String tag = TAG;
|
final String tag = TAG;
|
||||||
final String dictName = mDictName;
|
final String dictName = mDictName;
|
||||||
asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
|
asyncExecuteTaskWithLock(mLock.readLock(), () -> {
|
||||||
@Override
|
Log.d(tag, "Dump dictionary: " + dictName + " for " + mLocale);
|
||||||
public void run() {
|
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
||||||
Log.d(tag, "Dump dictionary: " + dictName + " for " + mLocale);
|
if (binaryDictionary == null) {
|
||||||
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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();
|
reloadDictionaryIfRequired();
|
||||||
final AsyncResultHolder<WordProperty[]> result =
|
final AsyncResultHolder<WordProperty[]> result =
|
||||||
new AsyncResultHolder<>("WordPropertiesForSync");
|
new AsyncResultHolder<>("WordPropertiesForSync");
|
||||||
asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
|
asyncExecuteTaskWithLock(mLock.readLock(), () -> {
|
||||||
@Override
|
final ArrayList<WordProperty> wordPropertyList = new ArrayList<>();
|
||||||
public void run() {
|
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
||||||
final ArrayList<WordProperty> wordPropertyList = new ArrayList<>();
|
if (binaryDictionary == null) {
|
||||||
final BinaryDictionary binaryDictionary = getBinaryDictionary();
|
return;
|
||||||
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()]));
|
|
||||||
}
|
}
|
||||||
|
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.
|
// TODO: Figure out the best timeout duration for this API.
|
||||||
return result.get(DEFAULT_WORD_PROPERTIES_FOR_SYNC,
|
return result.get(DEFAULT_WORD_PROPERTIES_FOR_SYNC, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
|
||||||
TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,11 @@
|
||||||
|
|
||||||
package org.dslul.openboard.inputmethod.latin;
|
package org.dslul.openboard.inputmethod.latin;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.inputmethod.EditorInfo;
|
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.common.StringUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils;
|
||||||
|
|
||||||
|
@ -94,14 +93,10 @@ public final class InputAttributes {
|
||||||
}
|
}
|
||||||
// inputClass == InputType.TYPE_CLASS_TEXT
|
// inputClass == InputType.TYPE_CLASS_TEXT
|
||||||
final int variation = inputType & InputType.TYPE_MASK_VARIATION;
|
final int variation = inputType & InputType.TYPE_MASK_VARIATION;
|
||||||
final boolean flagNoSuggestions =
|
final boolean flagNoSuggestions = 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
final boolean flagMultiLine = 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||||
final boolean flagMultiLine =
|
final boolean flagAutoCorrect = 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT);
|
||||||
0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
final boolean flagAutoComplete = 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
|
||||||
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
|
// TODO: Have a helper method in InputTypeUtils
|
||||||
// Make sure that passwords are not displayed in {@link SuggestionStripView}.
|
// 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;
|
&& 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() {
|
public boolean isTypeNull() {
|
||||||
|
|
|
@ -2,13 +2,14 @@ package org.dslul.openboard.inputmethod.latin
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardActionListener
|
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.common.Constants
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||||
|
|
||||||
|
@ -57,8 +58,10 @@ class KeyboardWrapperView @JvmOverloads constructor(
|
||||||
val colors = Settings.getInstance().current.mColors
|
val colors = Settings.getInstance().current.mColors
|
||||||
stopOneHandedModeBtn.colorFilter = colors.keyTextFilter
|
stopOneHandedModeBtn.colorFilter = colors.keyTextFilter
|
||||||
switchOneHandedModeBtn.colorFilter = colors.keyTextFilter
|
switchOneHandedModeBtn.colorFilter = colors.keyTextFilter
|
||||||
colors.setBackgroundColor(stopOneHandedModeBtn.background, Colors.TYPE_BACKGROUND)
|
colors.setBackgroundColor(stopOneHandedModeBtn.background, BackgroundType.BACKGROUND)
|
||||||
colors.setBackgroundColor(switchOneHandedModeBtn.background, Colors.TYPE_BACKGROUND)
|
colors.setBackgroundColor(switchOneHandedModeBtn.background, BackgroundType.BACKGROUND)
|
||||||
|
setBackgroundColor(Color.WHITE) // otherwise background might be null
|
||||||
|
colors.setKeyboardBackground(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
@SuppressLint("RtlHardcoded")
|
||||||
|
@ -120,22 +123,13 @@ class KeyboardWrapperView @JvmOverloads constructor(
|
||||||
buttonsLeft + (spareWidth + switchOneHandedModeBtn.measuredWidth) / 2,
|
buttonsLeft + (spareWidth + switchOneHandedModeBtn.measuredWidth) / 2,
|
||||||
2 * stopOneHandedModeBtn.measuredHeight + switchOneHandedModeBtn.measuredHeight
|
2 * stopOneHandedModeBtn.measuredHeight + switchOneHandedModeBtn.measuredHeight
|
||||||
)
|
)
|
||||||
Settings.getInstance().current.mColors.keyboardBackground?.let { background = it }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val keyboardAttr = context.obtainStyledAttributes(attrs,
|
@SuppressLint("CustomViewStyleable")
|
||||||
R.styleable.Keyboard, defStyle, R.style.Keyboard)
|
val keyboardAttr = context.obtainStyledAttributes(attrs, R.styleable.Keyboard, defStyle, R.style.Keyboard)
|
||||||
iconStopOneHandedModeId = keyboardAttr.getResourceId(R.styleable.Keyboard_iconStopOneHandedMode, 0)
|
iconStopOneHandedModeId = keyboardAttr.getResourceId(R.styleable.Keyboard_iconStopOneHandedMode, 0)
|
||||||
iconSwitchOneHandedModeId = keyboardAttr.getResourceId(R.styleable.Keyboard_iconSwitchOneHandedMode, 0)
|
iconSwitchOneHandedModeId = keyboardAttr.getResourceId(R.styleable.Keyboard_iconSwitchOneHandedMode, 0)
|
||||||
keyboardAttr.recycle()
|
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.keyboard.MainKeyboardView;
|
||||||
import org.dslul.openboard.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
|
import org.dslul.openboard.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
|
||||||
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
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.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
|
import org.dslul.openboard.inputmethod.latin.common.InputPointers;
|
||||||
|
@ -160,7 +159,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
@UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
|
@UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
|
||||||
private final SubtypeState mSubtypeState = new SubtypeState();
|
private final SubtypeState mSubtypeState = new SubtypeState();
|
||||||
private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
|
private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
|
||||||
private StatsUtilsManager mStatsUtilsManager;
|
private final StatsUtilsManager mStatsUtilsManager;
|
||||||
// Working variable for {@link #startShowingInputView()} and
|
// Working variable for {@link #startShowingInputView()} and
|
||||||
// {@link #onEvaluateInputViewShown()}.
|
// {@link #onEvaluateInputViewShown()}.
|
||||||
private boolean mIsExecutingStartShowingInputView;
|
private boolean mIsExecutingStartShowingInputView;
|
||||||
|
@ -213,7 +212,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
}
|
}
|
||||||
final RestartAfterDeviceUnlockReceiver mRestartAfterDeviceUnlockReceiver = new RestartAfterDeviceUnlockReceiver();
|
final RestartAfterDeviceUnlockReceiver mRestartAfterDeviceUnlockReceiver = new RestartAfterDeviceUnlockReceiver();
|
||||||
|
|
||||||
private AlertDialog mOptionsDialog; // todo: this is always null -> remove?
|
private AlertDialog mOptionsDialog;
|
||||||
|
|
||||||
private final boolean mIsHardwareAcceleratedDrawingEnabled;
|
private final boolean mIsHardwareAcceleratedDrawingEnabled;
|
||||||
|
|
||||||
|
@ -754,7 +753,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
mInputLogic.mSuggest.setAutoCorrectionThreshold(
|
mInputLogic.mSuggest.setAutoCorrectionThreshold(
|
||||||
settingsValues.mAutoCorrectionThreshold);
|
settingsValues.mAutoCorrectionThreshold);
|
||||||
}
|
}
|
||||||
mInputLogic.mSuggest.setPlausibilityThreshold(settingsValues.mPlausibilityThreshold);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1070,8 +1068,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
suggest.setAutoCorrectionThreshold(
|
suggest.setAutoCorrectionThreshold(
|
||||||
currentSettingsValues.mAutoCorrectionThreshold);
|
currentSettingsValues.mAutoCorrectionThreshold);
|
||||||
}
|
}
|
||||||
suggest.setPlausibilityThreshold(currentSettingsValues.mPlausibilityThreshold);
|
|
||||||
|
|
||||||
switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
|
switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
|
||||||
getCurrentRecapitalizeState());
|
getCurrentRecapitalizeState());
|
||||||
if (needToCallLoadKeyboardLater) {
|
if (needToCallLoadKeyboardLater) {
|
||||||
|
@ -1419,13 +1415,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
@Override
|
@Override
|
||||||
public boolean onCustomRequest(final int requestCode) {
|
public boolean onCustomRequest(final int requestCode) {
|
||||||
if (isShowingOptionDialog()) return false;
|
if (isShowingOptionDialog()) return false;
|
||||||
switch (requestCode) {
|
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) {
|
||||||
case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
|
if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
|
||||||
if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
|
mOptionsDialog = InputMethodPickerKt.createInputMethodPickerDialog(this, mRichImm, mKeyboardSwitcher.getMainKeyboardView().getWindowToken());
|
||||||
InputMethodPickerKt.showInputMethodPicker(this, mRichImm, mKeyboardSwitcher.getMainKeyboardView().getWindowToken());
|
mOptionsDialog.show();
|
||||||
return true; // todo: don't show and return if dialog already shown? but how can this happen?
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1475,30 +1471,59 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
return mOptionsDialog != null && mOptionsDialog.isShowing();
|
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() {
|
public void switchToNextSubtype() {
|
||||||
if (shouldSwitchToOtherInputMethods()) {
|
final boolean switchSubtype = mSettings.getCurrent().mLanguageSwitchKeyToOtherSubtypes;
|
||||||
// todo: this is the old behavior, is this actually wanted?
|
final boolean switchIme = mSettings.getCurrent().mLanguageSwitchKeyToOtherImes;
|
||||||
// maybe make the language switch key more configurable
|
|
||||||
boolean moreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
|
// switch IME if wanted and possible
|
||||||
final InputMethodSubtype nextSubtype = mRichImm.getNextSubtypeInThisIme(moreThanOneSubtype);
|
if (switchIme && !switchSubtype && switchInputMethod())
|
||||||
if (nextSubtype != null) {
|
return;
|
||||||
switchToSubtype(nextSubtype);
|
final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
|
||||||
} else {
|
// switch subtype if wanted and possible
|
||||||
// we are at end of the internal subtype list, switch to next input method
|
if (switchSubtype && !switchIme && hasMoreThanOneSubtype) {
|
||||||
// (for now) don't care about which input method and subtype exactly, let the system choose
|
// switch to previous subtype if current one was used, otherwise cycle through list
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
mSubtypeState.switchSubtype(mRichImm);
|
||||||
switchToNextInputMethod(false);
|
|
||||||
} else {
|
|
||||||
final IBinder token = getWindow().getWindow().getAttributes().token;
|
|
||||||
mRichImm.getInputMethodManager().switchToNextInputMethod(token, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
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);
|
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) {
|
public void switchInputMethodAndSubtype(final InputMethodInfo imi, final InputMethodSubtype subtype) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
switchInputMethod(imi.getId(), subtype);
|
switchInputMethod(imi.getId(), subtype);
|
||||||
|
@ -1974,10 +1999,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
|
|
||||||
public void debugDumpStateAndCrashWithException(final String context) {
|
public void debugDumpStateAndCrashWithException(final String context) {
|
||||||
final SettingsValues settingsValues = mSettings.getCurrent();
|
final SettingsValues settingsValues = mSettings.getCurrent();
|
||||||
final StringBuilder s = new StringBuilder(settingsValues.toString());
|
String s = settingsValues.toString() + "\nAttributes : " + settingsValues.mInputAttributes +
|
||||||
s.append("\nAttributes : ").append(settingsValues.mInputAttributes)
|
"\nContext : " + context;
|
||||||
.append("\nContext : ").append(context);
|
throw new RuntimeException(s);
|
||||||
throw new RuntimeException(s.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1997,32 +2021,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
// TODO: Dump all settings values
|
// 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
|
// 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() {
|
private void setNavigationBarColor() {
|
||||||
final SettingsValues settingsValues = mSettings.getCurrent();
|
final SettingsValues settingsValues = mSettings.getCurrent();
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || !settingsValues.mCustomNavBarColor)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || !settingsValues.mCustomNavBarColor)
|
||||||
return;
|
return;
|
||||||
final int color = settingsValues.mColors.navBar;
|
final int color = settingsValues.mColors.getNavBar();
|
||||||
final Window window = getWindow().getWindow();
|
final Window window = getWindow().getWindow();
|
||||||
if (window == null)
|
if (window == null)
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -119,6 +119,19 @@ public class NgramContext {
|
||||||
mMaxPrevWordCount = maxPrevWordCount;
|
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.
|
* Create next prevWordsInfo using current prevWordsInfo.
|
||||||
*/
|
*/
|
||||||
|
@ -175,8 +188,7 @@ public class NgramContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final String[] contextStringArray = prevTermList.toArray(new String[prevTermList.size()]);
|
return prevTermList.toArray(new String[prevTermList.size()]);
|
||||||
return contextStringArray;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValid() {
|
public boolean isValid() {
|
||||||
|
@ -227,7 +239,7 @@ public class NgramContext {
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hashValue = 0;
|
int hashValue = 0;
|
||||||
for (final WordInfo wordInfo : mPrevWordsInfo) {
|
for (final WordInfo wordInfo : mPrevWordsInfo) {
|
||||||
if (wordInfo == null || !WordInfo.EMPTY_WORD_INFO.equals(wordInfo)) {
|
if (!WordInfo.EMPTY_WORD_INFO.equals(wordInfo)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hashValue ^= wordInfo.hashCode();
|
hashValue ^= wordInfo.hashCode();
|
||||||
|
@ -267,7 +279,7 @@ public class NgramContext {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
final StringBuffer builder = new StringBuffer();
|
final StringBuilder builder = new StringBuilder();
|
||||||
for (int i = 0; i < mPrevWordsCount; i++) {
|
for (int i = 0; i < mPrevWordsCount; i++) {
|
||||||
final WordInfo wordInfo = mPrevWordsInfo[i];
|
final WordInfo wordInfo = mPrevWordsInfo[i];
|
||||||
builder.append("PrevWord[");
|
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
|
* @return a range containing the text surrounding the cursor
|
||||||
*/
|
*/
|
||||||
public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations,
|
public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations,
|
||||||
final int scriptId) {
|
final int scriptId, final boolean justDeleted) {
|
||||||
mIC = mParent.getCurrentInputConnection();
|
mIC = mParent.getCurrentInputConnection();
|
||||||
if (!isConnected()) {
|
if (!isConnected()) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -718,43 +718,24 @@ public final class RichInputConnection implements PrivateCommandPerformer {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue:
|
// we need text before, and text after is either empty or a separator or similar
|
||||||
// type 2 words and space, press delete twice -> remaining word and space before are selected
|
if (justDeleted && before.length() > 0 &&
|
||||||
// now on next key press, the space before the word is removed
|
(after.length() == 0
|
||||||
// or complete a word by choosing a suggestion than press backspace -> same thing
|
|| !isPartOfCompositionForScript(Character.codePointAt(after, 0), spacingAndPunctuations, scriptId)
|
||||||
// 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)
|
// issue:
|
||||||
// -> we try to detect this difference, and then try to fix it
|
// type 2 words and space, press delete twice -> remaining word and space before are selected
|
||||||
// interestingly, getTextBeforeCursor seems to only get the correct text because it uses
|
// now on next key press, the space before the word is removed
|
||||||
// mCommittedTextBeforeComposingText where the text is cached
|
// or complete a word by choosing a suggestion, then press backspace -> same thing
|
||||||
// what could be actually going on? we probably need to fetch the text because we want updated styles if any
|
// what is sometimes happening (depending on app, or maybe input field attributes):
|
||||||
|
// we just pressed delete, and getTextBeforeCursor gets the correct text,
|
||||||
// we need text before, and text after is always empty or a separator or similar
|
// but getTextBeforeCursorAndDetectLaggyConnection returns the old word, before the deletion (not sure why)
|
||||||
if (before.length() > 0 && (after.length() == 0 || !isPartOfCompositionForScript(Character.codePointAt(after, 0), spacingAndPunctuations, scriptId))) {
|
// -> we try to detect this difference, and then try to fix it
|
||||||
final int lastBeforeCodePoint = Character.codePointBefore(before, before.length());
|
// interestingly, getTextBeforeCursor seems to only get the correct text because it uses
|
||||||
// check whether before ends with the same codepoint as getTextBeforeCursor
|
// mCommittedTextBeforeComposingText, where the text is cached
|
||||||
int lastBeforeLength = Character.charCount(lastBeforeCodePoint);
|
// what could be actually going on? we probably need to fetch the text, because we want updated styles (if any)
|
||||||
CharSequence codePointBeforeCursor = getTextBeforeCursor(lastBeforeLength, 0);
|
before = fixIncorrectLength(before);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Going backward, find the first breaking point (separator)
|
// Going backward, find the first breaking point (separator)
|
||||||
|
@ -794,6 +775,37 @@ public final class RichInputConnection implements PrivateCommandPerformer {
|
||||||
hasUrlSpans);
|
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,
|
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
|
||||||
boolean checkTextAfter) {
|
boolean checkTextAfter) {
|
||||||
if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
|
if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
|
||||||
|
|
|
@ -35,11 +35,9 @@ import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.dslul.openboard.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
|
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) {
|
public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) {
|
||||||
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
|
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
|
||||||
final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(true);
|
final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(true);
|
||||||
final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
|
final int currentIndex = enabledSubtypes.indexOf(currentSubtype);
|
||||||
if (currentIndex == INDEX_NOT_FOUND) {
|
if (currentIndex == INDEX_NOT_FOUND) {
|
||||||
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
|
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
|
||||||
+ SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
|
+ SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
|
||||||
|
@ -197,38 +195,17 @@ public class RichInputMethodManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
|
public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
|
||||||
return checkIfSubtypeBelongsToList(subtype,
|
return getEnabledInputMethodSubtypeList(getInputMethodInfoOfThisIme(), true)
|
||||||
getEnabledInputMethodSubtypeList(
|
.contains(subtype);
|
||||||
getInputMethodInfoOfThisIme(),
|
|
||||||
true /* allowsImplicitlySelectedSubtypes */));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
|
public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(final InputMethodSubtype subtype) {
|
||||||
final InputMethodSubtype subtype) {
|
|
||||||
final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
|
final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
|
||||||
final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(subtype,
|
final boolean subtypeExplicitlyEnabled = getMyEnabledInputMethodSubtypeList(false)
|
||||||
getMyEnabledInputMethodSubtypeList(false /* allowsImplicitlySelectedSubtypes */));
|
.contains(subtype);
|
||||||
return subtypeEnabled && !subtypeExplicitlyEnabled;
|
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) {
|
public void onSubtypeChanged(@NonNull final InputMethodSubtype newSubtype) {
|
||||||
updateCurrentSubtype(newSubtype);
|
updateCurrentSubtype(newSubtype);
|
||||||
updateShortcutIme();
|
updateShortcutIme();
|
||||||
|
@ -392,38 +369,13 @@ public class RichInputMethodManager {
|
||||||
updateShortcutIme();
|
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) {
|
private void updateCurrentSubtype(final InputMethodSubtype subtype) {
|
||||||
SubtypeSettingsKt.setSelectedSubtype(DeviceProtectedUtils.getSharedPreferences(mContext), subtype);
|
SubtypeSettingsKt.setSelectedSubtype(DeviceProtectedUtils.getSharedPreferences(mContext), subtype);
|
||||||
mCurrentRichInputMethodSubtype = RichInputMethodSubtype.getRichInputMethodSubtype(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() {
|
private void updateShortcutIme() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Update shortcut IME from : "
|
Log.d(TAG, "Update shortcut IME from : "
|
||||||
|
@ -495,12 +447,6 @@ public class RichInputMethodManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isShortcutImeReady() {
|
public boolean isShortcutImeReady() {
|
||||||
if (mShortcutInputMethodInfo == null) {
|
return mShortcutInputMethodInfo != null;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (mShortcutSubtype == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class RichInputMethodSubtype {
|
||||||
private static final String TAG = RichInputMethodSubtype.class.getSimpleName();
|
private static final String TAG = RichInputMethodSubtype.class.getSimpleName();
|
||||||
|
|
||||||
private static final HashMap<Locale, Locale> sLocaleMap = initializeLocaleMap();
|
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<>();
|
final HashMap<Locale, Locale> map = new HashMap<>();
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// Locale#forLanguageTag is available on API Level 21+.
|
// Locale#forLanguageTag is available on API Level 21+.
|
||||||
|
@ -182,12 +182,18 @@ public class RichInputMethodSubtype {
|
||||||
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
|
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
|
||||||
@NonNull
|
@NonNull
|
||||||
private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
|
private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
|
||||||
new RichInputMethodSubtype(new InputMethodSubtype(
|
new RichInputMethodSubtype(new InputMethodSubtype.InputMethodSubtypeBuilder()
|
||||||
R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
|
.setSubtypeNameResId(R.string.subtype_no_language_qwerty)
|
||||||
SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
|
.setSubtypeIconResId(R.drawable.ic_ime_switcher_dark)
|
||||||
EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
|
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
|
||||||
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
|
.setSubtypeMode(KEYBOARD_MODE)
|
||||||
SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE));
|
.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}.
|
// 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}.
|
// Dummy Emoji subtype. See {@link R.xml.method}.
|
||||||
private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
|
private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
|
||||||
|
@ -195,13 +201,17 @@ public class RichInputMethodSubtype {
|
||||||
"KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
|
"KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
|
||||||
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
|
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
|
||||||
@NonNull
|
@NonNull
|
||||||
private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype(
|
private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE =
|
||||||
new InputMethodSubtype(
|
new RichInputMethodSubtype(new InputMethodSubtype.InputMethodSubtypeBuilder()
|
||||||
R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
|
.setSubtypeNameResId(R.string.subtype_emoji)
|
||||||
SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
|
.setSubtypeIconResId(R.drawable.ic_ime_switcher_dark)
|
||||||
EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
|
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
|
||||||
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
|
.setSubtypeMode(KEYBOARD_MODE)
|
||||||
SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
|
.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 sNoLanguageSubtype;
|
||||||
private static RichInputMethodSubtype sEmojiSubtype;
|
private static RichInputMethodSubtype sEmojiSubtype;
|
||||||
|
|
||||||
|
@ -210,8 +220,7 @@ public class RichInputMethodSubtype {
|
||||||
RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype;
|
RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype;
|
||||||
if (noLanguageSubtype == null) {
|
if (noLanguageSubtype == null) {
|
||||||
final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance()
|
final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance()
|
||||||
.findSubtypeByLocaleAndKeyboardLayoutSet(
|
.findSubtypeByLocaleAndKeyboardLayoutSet(SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
|
||||||
SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
|
|
||||||
if (rawNoLanguageSubtype != null) {
|
if (rawNoLanguageSubtype != null) {
|
||||||
noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype);
|
noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype);
|
||||||
}
|
}
|
||||||
|
@ -221,8 +230,7 @@ public class RichInputMethodSubtype {
|
||||||
return noLanguageSubtype;
|
return noLanguageSubtype;
|
||||||
}
|
}
|
||||||
Log.w(TAG, "Can't find any language with QWERTY subtype");
|
Log.w(TAG, "Can't find any language with QWERTY subtype");
|
||||||
Log.w(TAG, "No input method subtype found; returning dummy subtype: "
|
Log.w(TAG, "No input method subtype found; returning dummy subtype: " + DUMMY_NO_LANGUAGE_SUBTYPE);
|
||||||
+ DUMMY_NO_LANGUAGE_SUBTYPE);
|
|
||||||
return DUMMY_NO_LANGUAGE_SUBTYPE;
|
return DUMMY_NO_LANGUAGE_SUBTYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,8 +239,7 @@ public class RichInputMethodSubtype {
|
||||||
RichInputMethodSubtype emojiSubtype = sEmojiSubtype;
|
RichInputMethodSubtype emojiSubtype = sEmojiSubtype;
|
||||||
if (emojiSubtype == null) {
|
if (emojiSubtype == null) {
|
||||||
final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance()
|
final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance()
|
||||||
.findSubtypeByLocaleAndKeyboardLayoutSet(
|
.findSubtypeByLocaleAndKeyboardLayoutSet(SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
|
||||||
SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
|
|
||||||
if (rawEmojiSubtype != null) {
|
if (rawEmojiSubtype != null) {
|
||||||
emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
|
emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
|
||||||
}
|
}
|
||||||
|
@ -242,8 +249,7 @@ public class RichInputMethodSubtype {
|
||||||
return emojiSubtype;
|
return emojiSubtype;
|
||||||
}
|
}
|
||||||
Log.w(TAG, "Can't find emoji subtype");
|
Log.w(TAG, "Can't find emoji subtype");
|
||||||
Log.w(TAG, "No input method subtype found; returning dummy subtype: "
|
Log.w(TAG, "No input method subtype found; returning dummy subtype: " + DUMMY_EMOJI_SUBTYPE);
|
||||||
+ DUMMY_EMOJI_SUBTYPE);
|
|
||||||
return 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.InputPointers;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
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.settings.SettingsValuesForSuggestion;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.AutoCorrectionUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.AutoCorrectionUtils;
|
||||||
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
|
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
|
||||||
|
@ -86,15 +87,6 @@ public final class Suggest {
|
||||||
mAutoCorrectionThreshold = threshold;
|
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 {
|
public interface OnGetSuggestedWordsCallback {
|
||||||
void onGetSuggestedWords(final SuggestedWords suggestedWords);
|
void onGetSuggestedWords(final SuggestedWords suggestedWords);
|
||||||
}
|
}
|
||||||
|
@ -306,9 +298,6 @@ public final class Suggest {
|
||||||
final int firstOccurrenceOfTypedWordInSuggestions,
|
final int firstOccurrenceOfTypedWordInSuggestions,
|
||||||
final SuggestedWordInfo typedWordFirstOccurrenceWordInfo
|
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
|
final String consideredWord = trailingSingleQuotesCount > 0
|
||||||
? typedWordString.substring(0, typedWordString.length() - trailingSingleQuotesCount)
|
? typedWordString.substring(0, typedWordString.length() - trailingSingleQuotesCount)
|
||||||
: typedWordString;
|
: typedWordString;
|
||||||
|
@ -322,6 +311,7 @@ public final class Suggest {
|
||||||
// We allow auto-correction if whitelisting is not required or the word is whitelisted,
|
// 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.
|
// or if the word had more than one char and was not suggested.
|
||||||
final boolean allowsToBeAutoCorrected;
|
final boolean allowsToBeAutoCorrected;
|
||||||
|
final int scoreLimit = Settings.getInstance().getCurrent().mScoreLimitForAutocorrect;
|
||||||
if ((SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION || whitelistedWord != null)
|
if ((SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION || whitelistedWord != null)
|
||||||
|| (consideredWord.length() > 1 && (sourceDictionaryOfRemovedWord == null)) // more than 1 letter and not in dictionary
|
|| (consideredWord.length() > 1 && (sourceDictionaryOfRemovedWord == null)) // more than 1 letter and not in dictionary
|
||||||
) {
|
) {
|
||||||
|
@ -331,7 +321,7 @@ public final class Suggest {
|
||||||
putEmptyWordSuggestions.run();
|
putEmptyWordSuggestions.run();
|
||||||
final SuggestedWordInfo first = firstAndTypedWordEmptyInfos.get(0);
|
final SuggestedWordInfo first = firstAndTypedWordEmptyInfos.get(0);
|
||||||
final SuggestedWordInfo typed = firstAndTypedWordEmptyInfos.get(1);
|
final SuggestedWordInfo typed = firstAndTypedWordEmptyInfos.get(1);
|
||||||
if (firstSuggestionInContainer.mScore > 900000) {
|
if (firstSuggestionInContainer.mScore > scoreLimit) {
|
||||||
allowsToBeAutoCorrected = true; // suggestion has good score, allow
|
allowsToBeAutoCorrected = true; // suggestion has good score, allow
|
||||||
} else if (first == null) {
|
} else if (first == null) {
|
||||||
allowsToBeAutoCorrected = false; // no autocorrect if first suggestion unknown in this context
|
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
|
// 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.
|
// 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
|
// TODO: now that we have personalization, we may want to re-evaluate this decision
|
||||||
|| !dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()
|
|| !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)) {
|
|
||||||
hasAutoCorrection = false;
|
hasAutoCorrection = false;
|
||||||
} else {
|
} else {
|
||||||
final SuggestedWordInfo firstSuggestion = suggestionResults.first();
|
final SuggestedWordInfo firstSuggestion = suggestionResults.first();
|
||||||
|
@ -400,23 +386,21 @@ public final class Suggest {
|
||||||
// is determined, see #isAllowedByAutoCorrectionWithSpaceFilter.
|
// is determined, see #isAllowedByAutoCorrectionWithSpaceFilter.
|
||||||
// TODO: this should not have its own logic here but be handled by the dictionary.
|
// TODO: this should not have its own logic here but be handled by the dictionary.
|
||||||
final boolean allowed = isAllowedByAutoCorrectionWithSpaceFilter(firstSuggestion);
|
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
|
// typed word is valid and has good score
|
||||||
// do not auto-correct if typed word is better match than first suggestion
|
// do not auto-correct if typed word is better match than first suggestion
|
||||||
final SuggestedWordInfo first = firstSuggestionInContainer != null ? firstSuggestionInContainer : firstSuggestion;
|
final SuggestedWordInfo first = firstSuggestionInContainer != null ? firstSuggestionInContainer : firstSuggestion;
|
||||||
final Locale dictLocale = dictionaryFacilitator.getCurrentLocale();
|
final Locale dictLocale = dictionaryFacilitator.getCurrentLocale();
|
||||||
|
|
||||||
if (first.mScore < 900000) {
|
if (first.mScore < scoreLimit) {
|
||||||
// don't allow if suggestion has too low score
|
// 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 };
|
return new boolean[]{ true, false };
|
||||||
}
|
}
|
||||||
if (first.mSourceDict.mLocale != typedWordFirstOccurrenceWordInfo.mSourceDict.mLocale) {
|
if (first.mSourceDict.mLocale != typedWordFirstOccurrenceWordInfo.mSourceDict.mLocale) {
|
||||||
// dict locale different -> return the better match
|
// dict locale different -> return the better match
|
||||||
return new boolean[]{ true, dictLocale == first.mSourceDict.mLocale };
|
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
|
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)
|
+ (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
|
+ (first.mScore > typedWordFirstOccurrenceWordInfo.mScore ? 5 : 0); // small bonus if score is higher
|
||||||
|
|
|
@ -203,7 +203,7 @@ public class SuggestedWords {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static final SuggestedWords getEmptyInstance() {
|
public static SuggestedWords getEmptyInstance() {
|
||||||
return SuggestedWords.EMPTY;
|
return SuggestedWords.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,8 +347,9 @@ public class SuggestedWords {
|
||||||
return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
|
return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAprapreateForAutoCorrection() {
|
public boolean isAppropriateForAutoCorrection() {
|
||||||
return (mKindAndFlags & KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION) != 0;
|
return (mKindAndFlags & KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION) != 0
|
||||||
|
|| isKindOf(KIND_SHORTCUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDebugString(final String str) {
|
public void setDebugString(final String str) {
|
||||||
|
|
|
@ -127,7 +127,7 @@ public final class WordComposer {
|
||||||
refreshTypedWordCache();
|
refreshTypedWordCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final void refreshTypedWordCache() {
|
private void refreshTypedWordCache() {
|
||||||
mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback();
|
mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback();
|
||||||
mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length());
|
mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length());
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,7 @@ public final class WordComposer {
|
||||||
return size() == 1;
|
return size() == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean isComposingWord() {
|
public boolean isComposingWord() {
|
||||||
return size() > 0;
|
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";
|
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
|
* 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
|
* 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);
|
setBooleanOption(IS_GESTURE, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setIsSpaceAwareGesture(final boolean value) {
|
||||||
|
setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value);
|
||||||
|
}
|
||||||
|
|
||||||
public void setUseFullEditDistance(final boolean value) {
|
public void setUseFullEditDistance(final boolean value) {
|
||||||
setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
|
setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
package org.dslul.openboard.inputmethod.latin.define
|
package org.dslul.openboard.inputmethod.latin.define
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import org.dslul.openboard.inputmethod.latin.BuildConfig
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.DebugSettings
|
||||||
|
|
||||||
object DebugFlags {
|
object DebugFlags {
|
||||||
const val DEBUG_ENABLED = false
|
@JvmField
|
||||||
|
var DEBUG_ENABLED = false
|
||||||
|
|
||||||
@JvmStatic
|
@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.common.StringUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
|
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.settings.SpacingAndPunctuations;
|
||||||
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
import org.dslul.openboard.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
|
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
|
||||||
|
@ -1261,7 +1260,7 @@ public final class InputLogic {
|
||||||
&& settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
|
&& settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
|
||||||
final TextRange range = mConnection.getWordRangeAtCursor(
|
final TextRange range = mConnection.getWordRangeAtCursor(
|
||||||
settingsValues.mSpacingAndPunctuations,
|
settingsValues.mSpacingAndPunctuations,
|
||||||
currentKeyboardScriptId);
|
currentKeyboardScriptId, false);
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
return range.mWord.toString();
|
return range.mWord.toString();
|
||||||
}
|
}
|
||||||
|
@ -1609,7 +1608,7 @@ public final class InputLogic {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final TextRange range = mConnection.getWordRangeAtCursor(
|
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 (null == range) return; // Happens if we don't have an input connection at all
|
||||||
if (range.length() <= 0) {
|
if (range.length() <= 0) {
|
||||||
// Race condition, or touching a word in a non-supported script.
|
// 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.
|
// hence 2; if we aren't, we should just skip whitespace if any, so 1.
|
||||||
mWordComposer.isComposingWord() ? 2 : 1),
|
mWordComposer.isComposingWord() ? 2 : 1),
|
||||||
keyboard,
|
keyboard,
|
||||||
new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive),
|
settingsValues.mSettingsValuesForSuggestion,
|
||||||
settingsValues.mAutoCorrectionEnabledPerUserSettings,
|
settingsValues.mAutoCorrectionEnabledPerUserSettings,
|
||||||
inputStyle, sequenceNumber, callback);
|
inputStyle, sequenceNumber, callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,22 +18,24 @@ package org.dslul.openboard.inputmethod.latin.settings;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet;
|
||||||
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
|
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.R;
|
||||||
import org.dslul.openboard.inputmethod.latin.SystemBroadcastReceiver;
|
import org.dslul.openboard.inputmethod.latin.SystemBroadcastReceiver;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.FileUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.define.JniLibName;
|
import org.dslul.openboard.inputmethod.latin.define.JniLibName;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
@ -56,59 +58,47 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
|
||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
addPreferencesFromResource(R.xml.prefs_screen_advanced);
|
addPreferencesFromResource(R.xml.prefs_screen_advanced);
|
||||||
|
|
||||||
final Resources res = getResources();
|
final Context context = requireContext();
|
||||||
final Context context = getActivity();
|
|
||||||
|
|
||||||
// When we are called from the Settings application but we are not already running, some
|
// 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
|
// singleton and utility classes may not have been initialized. We have to call
|
||||||
// initialization method of these classes here. See {@link LatinIME#onCreate()}.
|
// initialization method of these classes here. See {@link LatinIME#onCreate()}.
|
||||||
AudioAndHapticFeedbackManager.init(context);
|
AudioAndHapticFeedbackManager.init(context);
|
||||||
|
|
||||||
final SharedPreferences prefs = getSharedPreferences();
|
if (!BuildConfig.DEBUG) {
|
||||||
|
|
||||||
if (!Settings.isInternal(prefs)) {
|
|
||||||
removePreference(Settings.SCREEN_DEBUG);
|
removePreference(Settings.SCREEN_DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupKeyLongpressTimeoutSettings();
|
setupKeyLongpressTimeoutSettings();
|
||||||
final Preference bla = findPreference("load_gesture_library");
|
final Preference loadGestureLibrary = findPreference("load_gesture_library");
|
||||||
if (bla != null) {
|
if (loadGestureLibrary != null) {
|
||||||
bla.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
loadGestureLibrary.setOnPreferenceClickListener(preference -> {
|
||||||
@Override
|
// get architecture for telling user which file to use
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
String abi;
|
||||||
// get architecture for telling user which file to use
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
String abi;
|
abi = Build.SUPPORTED_ABIS[0];
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
} else {
|
||||||
abi = Build.SUPPORTED_ABIS[0];
|
abi = Build.CPU_ABI;
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
// 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 (requestCode != REQUEST_CODE_GESTURE_LIBRARY || resultCode != Activity.RESULT_OK || resultData == null) return;
|
||||||
if (resultData.getData() != null && libfile != null) {
|
if (resultData.getData() != null && libfile != null) {
|
||||||
try {
|
try {
|
||||||
FileOutputStream out = new FileOutputStream(libfile);
|
final InputStream in = requireContext().getContentResolver().openInputStream(resultData.getData());
|
||||||
final InputStream in = getActivity().getContentResolver().openInputStream(resultData.getData());
|
FileUtils.copyStreamToNewFile(in, libfile);
|
||||||
byte[] buf = new byte[1024];
|
Runtime.getRuntime().exit(0); // exit will restart the app, so library will be loaded
|
||||||
int len;
|
|
||||||
while ((len = in.read(buf)) > 0) {
|
|
||||||
out.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
out.flush();
|
|
||||||
Runtime.getRuntime().exit(0);
|
|
||||||
} catch (IOException e) {
|
} 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
|
@Override
|
||||||
public String getValueText(final int value) {
|
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
|
@Override
|
||||||
|
@ -176,7 +160,9 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||||
if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) {
|
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
|
package org.dslul.openboard.inputmethod.latin.settings
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.TwoStatePreference
|
import androidx.preference.TwoStatePreference
|
||||||
|
@ -37,39 +18,42 @@ import java.util.*
|
||||||
/**
|
/**
|
||||||
* "Appearance" settings sub screen.
|
* "Appearance" settings sub screen.
|
||||||
*/
|
*/
|
||||||
@Suppress("Deprecation") // yes everything here is deprecated, but only work on this if really necessary
|
class AppearanceSettingsFragment : SubScreenFragment() {
|
||||||
// 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 {
|
|
||||||
|
|
||||||
private var selectedThemeId = 0
|
|
||||||
private var needsReload = false
|
private var needsReload = false
|
||||||
|
|
||||||
private lateinit var themeFamilyPref: ListPreference
|
private val themeFamilyPref: ListPreference by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_STYLE)!! }
|
||||||
private lateinit var themeVariantPref: ListPreference
|
private val themeVariantPref: ListPreference by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT)!! }
|
||||||
private var customThemeVariantNightPref: ListPreference? = null
|
private val themeVariantNightPref: ListPreference? by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT_NIGHT) }
|
||||||
private lateinit var keyBordersPref: TwoStatePreference
|
private val dayNightPref: TwoStatePreference? by lazy { preferenceScreen.findPreference(Settings.PREF_THEME_DAY_NIGHT) }
|
||||||
private var dayNightPref: TwoStatePreference? = null
|
private val userColorsPref: Preference by lazy { preferenceScreen.findPreference("theme_select_colors")!! }
|
||||||
private lateinit var userColorsPref: Preference
|
private val userColorsPrefNight: Preference? by lazy { preferenceScreen.findPreference("theme_select_colors_night") }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(icicle: Bundle?) {
|
override fun onCreate(icicle: Bundle?) {
|
||||||
super.onCreate(icicle)
|
super.onCreate(icicle)
|
||||||
addPreferencesFromResource(R.xml.prefs_screen_appearance)
|
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) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
removePreference(Settings.PREF_THEME_DAY_NIGHT)
|
removePreference(Settings.PREF_THEME_DAY_NIGHT)
|
||||||
removePreference(Settings.PREF_THEME_VARIANT_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_DAY_NIGHT)
|
||||||
removePreference(Settings.PREF_THEME_VARIANT_NIGHT)
|
removePreference(Settings.PREF_THEME_VARIANT_NIGHT)
|
||||||
|
removePreference("theme_select_colors_night")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// todo: consider removing the preference, and always set the navbar color
|
// todo: consider removing the preference, and always set the navbar color
|
||||||
removePreference(Settings.PREF_NAVBAR_COLOR)
|
removePreference(Settings.PREF_NAVBAR_COLOR)
|
||||||
}
|
}
|
||||||
setupTheme()
|
|
||||||
|
|
||||||
|
|
||||||
val metrics = requireContext().resources.displayMetrics
|
val metrics = requireContext().resources.displayMetrics
|
||||||
val widthDp = metrics.widthPixels / metrics.density
|
val widthDp = metrics.widthPixels / metrics.density
|
||||||
val heightDp = metrics.heightPixels / metrics.density
|
val heightDp = metrics.heightPixels / metrics.density
|
||||||
if (!ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED || (min(widthDp, heightDp) < 600 && max(widthDp, heightDp) < 720)) {
|
if (!ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED || (min(widthDp, heightDp) < 600 && max(widthDp, heightDp) < 720)) {
|
||||||
removePreference(Settings.PREF_ENABLE_SPLIT_KEYBOARD)
|
removePreference(Settings.PREF_ENABLE_SPLIT_KEYBOARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupKeyboardHeight(
|
|
||||||
Settings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
private fun setThemeVariantPrefs(themeFamily: String) {
|
||||||
super.onResume()
|
themeVariantPref.apply {
|
||||||
updateThemePreferencesState()
|
entryValues = if (themeFamily == KeyboardTheme.THEME_STYLE_HOLO) KeyboardTheme.THEME_VARIANTS
|
||||||
updateAfterPreferenceChanged()
|
else KeyboardTheme.THEME_VARIANTS.filterNot { it == KeyboardTheme.THEME_HOLO_WHITE }.toTypedArray()
|
||||||
}
|
entries = entryValues.map {
|
||||||
|
val resId = resources.getIdentifier("theme_name_$it", "string", requireContext().packageName)
|
||||||
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)
|
|
||||||
if (resId == 0) it else getString(resId)
|
if (resId == 0) it else getString(resId)
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
entryValues = variants
|
if (value !in entryValues)
|
||||||
value = variant
|
value = entryValues.first().toString()
|
||||||
val name = if (variant == "user_dark") "theme_name_user" else "theme_name_$variant"
|
summary = entries[entryValues.indexOfFirst { it == value }]
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSelectedThemeId(
|
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
||||||
family: String = themeFamilyPref.value,
|
summary = entries[entryValues.indexOfFirst { it == value }]
|
||||||
variant: String = themeVariantPref.value,
|
userColorsPref.isVisible = value == KeyboardTheme.THEME_USER
|
||||||
keyBorders: Boolean = keyBordersPref.isChecked
|
true
|
||||||
) {
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val variants = KeyboardTheme.THEME_VARIANTS
|
themeVariantNightPref?.apply {
|
||||||
val variant = sharedPreferences.getString(Settings.PREF_THEME_VARIANT, KeyboardTheme.THEME_LIGHT)
|
entryValues = if (themeFamily == KeyboardTheme.THEME_STYLE_HOLO) KeyboardTheme.THEME_VARIANTS_DARK
|
||||||
if (!skipThemeVariant) {
|
else KeyboardTheme.THEME_VARIANTS_DARK.filterNot { it == KeyboardTheme.THEME_HOLO_WHITE }.toTypedArray()
|
||||||
themeVariantPref.apply {
|
entries = entryValues.map {
|
||||||
entries = if (isLegacyFamily) variants // todo: translatable string for holo, not internal name
|
val resId = resources.getIdentifier("theme_name_$it", "string", requireContext().packageName)
|
||||||
else variants.map {
|
if (resId == 0) it else getString(resId)
|
||||||
val resId = resources.getIdentifier("theme_name_$it", "string", requireContext().packageName)
|
}.toTypedArray()
|
||||||
if (resId == 0) it else getString(resId)
|
if (value !in entryValues)
|
||||||
}.toTypedArray()
|
value = entryValues.first().toString()
|
||||||
entryValues = variants
|
summary = entries[entryValues.indexOfFirst { it == value }]
|
||||||
value = variant ?: variants[0]
|
|
||||||
summary = if (isLegacyFamily) variant
|
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
||||||
else {
|
summary = entries[entryValues.indexOfFirst { it == value }]
|
||||||
val resId = resources.getIdentifier("theme_name_$variant", "string", requireContext().packageName)
|
userColorsPrefNight?.isVisible = value == KeyboardTheme.THEME_USER_NIGHT
|
||||||
if (resId == 0) variant else getString(resId)
|
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() {
|
private fun setupTheme() {
|
||||||
themeFamilyPref = preferenceScreen.findPreference(Settings.PREF_THEME_STYLE)!!
|
|
||||||
themeFamilyPref.apply {
|
themeFamilyPref.apply {
|
||||||
entries = KeyboardTheme.THEME_STYLES
|
entries = KeyboardTheme.THEME_STYLES
|
||||||
entryValues = KeyboardTheme.THEME_STYLES
|
entryValues = KeyboardTheme.THEME_STYLES
|
||||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
||||||
summary = entries[entryValues.indexOfFirst { it == value }]
|
summary = entries[entryValues.indexOfFirst { it == value }]
|
||||||
saveSelectedThemeId(family = value as String)
|
setThemeVariantPrefs(value.toString())
|
||||||
updateThemePreferencesState(skipThemeFamily = true)
|
|
||||||
true
|
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 ->
|
dayNightPref?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
||||||
updateThemePreferencesState(skipThemeFamily = true)
|
themeVariantNightPref?.isVisible = value as Boolean
|
||||||
|
userColorsPrefNight?.isVisible = value && themeVariantNightPref?.value == KeyboardTheme.THEME_USER_NIGHT
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
customThemeVariantNightPref = preferenceScreen.findPreference(Settings.PREF_THEME_VARIANT_NIGHT) as? ListPreference
|
themeVariantNightPref?.isVisible = dayNightPref?.isChecked == true
|
||||||
customThemeVariantNightPref?.apply {
|
userColorsPref.isVisible = themeVariantPref.value == KeyboardTheme.THEME_USER
|
||||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
userColorsPrefNight?.isVisible = dayNightPref?.isChecked == true && themeVariantNightPref?.value == KeyboardTheme.THEME_USER_NIGHT
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupKeyboardHeight(prefKey: String, defaultValue: Float) {
|
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 {
|
companion object {
|
||||||
private const val PERCENTAGE_FLOAT = 100.0f
|
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 static org.dslul.openboard.inputmethod.latin.permissions.PermissionsManager.get;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
@ -68,8 +66,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
|
||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
addPreferencesFromResource(R.xml.prefs_screen_correction);
|
addPreferencesFromResource(R.xml.prefs_screen_correction);
|
||||||
|
|
||||||
final Context context = getActivity();
|
final PackageManager pm = requireContext().getPackageManager();
|
||||||
final PackageManager pm = context.getPackageManager();
|
|
||||||
|
|
||||||
final Preference editPersonalDictionary =
|
final Preference editPersonalDictionary =
|
||||||
findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
|
findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
|
||||||
|
@ -79,7 +76,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
|
||||||
if (ri == null) {
|
if (ri == null) {
|
||||||
overwriteUserDictionaryPreference(editPersonalDictionary);
|
overwriteUserDictionaryPreference(editPersonalDictionary);
|
||||||
}
|
}
|
||||||
mLookupContactsPreference = (SwitchPreferenceCompat) findPreference(AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY);
|
mLookupContactsPreference = findPreference(AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY);
|
||||||
|
|
||||||
refreshEnabledSettings();
|
refreshEnabledSettings();
|
||||||
}
|
}
|
||||||
|
@ -91,7 +88,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
|
||||||
&& !PermissionsUtil.checkAllPermissionsGranted(
|
&& !PermissionsUtil.checkAllPermissionsGranted(
|
||||||
getActivity() /* context */, Manifest.permission.READ_CONTACTS)
|
getActivity() /* context */, Manifest.permission.READ_CONTACTS)
|
||||||
) {
|
) {
|
||||||
get(getActivity() /* context */).requestPermissions(this /* PermissionsResultCallback */,
|
get(requireContext()).requestPermissions(this /* PermissionsResultCallback */,
|
||||||
getActivity() /* activity */, Manifest.permission.READ_CONTACTS);
|
getActivity() /* activity */, Manifest.permission.READ_CONTACTS);
|
||||||
}
|
}
|
||||||
refreshEnabledSettings();
|
refreshEnabledSettings();
|
||||||
|
@ -113,15 +110,14 @@ public final class CorrectionSettingsFragment extends SubScreenFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshEnabledSettings() {
|
private void refreshEnabledSettings() {
|
||||||
setPreferenceEnabled(Settings.PREF_AUTO_CORRECTION_CONFIDENCE,
|
setPreferenceVisible(Settings.PREF_AUTO_CORRECTION_CONFIDENCE,
|
||||||
Settings.readAutoCorrectEnabled(getSharedPreferences(), getResources()));
|
Settings.readAutoCorrectEnabled(getSharedPreferences()));
|
||||||
setPreferenceEnabled(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, getSharedPreferences().getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true));
|
setPreferenceVisible(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, getSharedPreferences().getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true));
|
||||||
turnOffLookupContactsIfNoPermission();
|
turnOffLookupContactsIfNoPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
|
private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
|
||||||
final Activity activity = getActivity();
|
final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(requireActivity());
|
||||||
final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
|
|
||||||
if (null == localeList) {
|
if (null == localeList) {
|
||||||
// The locale list is null if and only if the user dictionary service is
|
// 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.
|
// 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_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch";
|
||||||
public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS =
|
public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS =
|
||||||
"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 =
|
public static final String PREF_KEY_PREVIEW_DISMISS_DURATION =
|
||||||
"pref_key_preview_dismiss_duration";
|
"pref_key_preview_dismiss_duration";
|
||||||
public static final String PREF_KEY_PREVIEW_DISMISS_END_X_SCALE =
|
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.Bundle;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceGroup;
|
import androidx.preference.PreferenceGroup;
|
||||||
import androidx.preference.TwoStatePreference;
|
import androidx.preference.TwoStatePreference;
|
||||||
|
@ -57,8 +58,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI);
|
removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI);
|
||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceGroup dictDumpPreferenceGroup =
|
final PreferenceGroup dictDumpPreferenceGroup = findPreference(PREF_KEY_DUMP_DICTS);
|
||||||
(PreferenceGroup)findPreference(PREF_KEY_DUMP_DICTS);
|
|
||||||
for (final String dictName : DictionaryFacilitatorImpl.DICT_TYPE_TO_CLASS.keySet()) {
|
for (final String dictName : DictionaryFacilitatorImpl.DICT_TYPE_TO_CLASS.keySet()) {
|
||||||
final Preference pref = new DictDumpPreference(getActivity(), dictName);
|
final Preference pref = new DictDumpPreference(getActivity(), dictName);
|
||||||
pref.setOnPreferenceClickListener(this);
|
pref.setOnPreferenceClickListener(this);
|
||||||
|
@ -81,11 +81,9 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
defaultKeyPreviewDismissEndScale);
|
defaultKeyPreviewDismissEndScale);
|
||||||
setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
|
setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
|
||||||
defaultKeyPreviewDismissEndScale);
|
defaultKeyPreviewDismissEndScale);
|
||||||
setupKeyboardHeight(
|
|
||||||
DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE);
|
|
||||||
|
|
||||||
mServiceNeedsRestart = false;
|
mServiceNeedsRestart = false;
|
||||||
mDebugMode = (TwoStatePreference) findPreference(DebugSettings.PREF_DEBUG_MODE);
|
mDebugMode = findPreference(DebugSettings.PREF_DEBUG_MODE);
|
||||||
updateDebugMode();
|
updateDebugMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,15 +99,14 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(final Preference pref) {
|
public boolean onPreferenceClick(@NonNull final Preference pref) {
|
||||||
final Context context = getActivity();
|
|
||||||
if (pref instanceof DictDumpPreference) {
|
if (pref instanceof DictDumpPreference) {
|
||||||
final DictDumpPreference dictDumpPref = (DictDumpPreference)pref;
|
final DictDumpPreference dictDumpPref = (DictDumpPreference)pref;
|
||||||
final String dictName = dictDumpPref.mDictName;
|
final String dictName = dictDumpPref.mDictName;
|
||||||
final Intent intent = new Intent(
|
final Intent intent = new Intent(
|
||||||
DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
|
DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
|
||||||
intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName);
|
intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName);
|
||||||
context.sendBroadcast(intent);
|
pref.getContext().sendBroadcast(intent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -119,7 +116,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
if (mServiceNeedsRestart) {
|
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));
|
mDebugMode.setChecked(prefs.getBoolean(DebugSettings.PREF_DEBUG_MODE, false));
|
||||||
updateDebugMode();
|
updateDebugMode();
|
||||||
mServiceNeedsRestart = true;
|
mServiceNeedsRestart = true;
|
||||||
return;
|
} else if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
|
||||||
}
|
|
||||||
if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
|
|
||||||
mServiceNeedsRestart = true;
|
mServiceNeedsRestart = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +147,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
private void setupKeyPreviewAnimationScale(final String prefKey, final float defaultValue) {
|
private void setupKeyPreviewAnimationScale(final String prefKey, final float defaultValue) {
|
||||||
final SharedPreferences prefs = getSharedPreferences();
|
final SharedPreferences prefs = getSharedPreferences();
|
||||||
final Resources res = getResources();
|
final Resources res = getResources();
|
||||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
|
final SeekBarDialogPreference pref = findPreference(prefKey);
|
||||||
if (pref == null) {
|
if (pref == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -205,7 +199,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
private void setupKeyPreviewAnimationDuration(final String prefKey, final int defaultValue) {
|
private void setupKeyPreviewAnimationDuration(final String prefKey, final int defaultValue) {
|
||||||
final SharedPreferences prefs = getSharedPreferences();
|
final SharedPreferences prefs = getSharedPreferences();
|
||||||
final Resources res = getResources();
|
final Resources res = getResources();
|
||||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
|
final SeekBarDialogPreference pref = findPreference(prefKey);
|
||||||
if (pref == null) {
|
if (pref == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -232,7 +226,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getValueText(final int value) {
|
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
|
@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!!
|
||||||
|
}
|
||||||
|
}
|
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