mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-31 11:52:13 +00:00
Compare commits
141 commits
v3.0-alpha
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
b903e10b12 | ||
|
e6a412750d | ||
|
7b91221339 | ||
|
f0d4aaa9c3 | ||
|
0e86978df3 | ||
|
82bea7facf | ||
|
4d441e5bdf | ||
|
92b1907c61 | ||
|
37821ff8ad | ||
|
dbeddcd658 | ||
|
31a8761bfa | ||
|
120734ff41 | ||
|
0d5159c2d7 | ||
|
b44dd29b0c | ||
|
7da068145f | ||
|
deec4d1f98 | ||
|
e21c135b90 | ||
|
154f7c3a1e | ||
|
ed540466e9 | ||
|
05ea8b7f76 | ||
|
bf7e0542f5 | ||
|
c4e7c84608 | ||
|
f72e8f41f4 | ||
|
69540b8d9f | ||
|
d1120807d3 | ||
|
9dbce40fd7 | ||
|
82e6d8a5cb | ||
|
175b5ea197 | ||
|
917edee918 | ||
|
ead8fb36cb | ||
|
3f51bd4da5 | ||
|
7bc74810b1 | ||
|
e25300d832 | ||
|
e034065236 | ||
|
954a27b7c9 | ||
|
b1b357d6b8 | ||
|
e32a0c8e98 | ||
|
d9a779a66e | ||
|
18549151b3 | ||
|
9d38471f72 | ||
|
4289e487e9 | ||
|
27a2300631 | ||
|
900dfa1b9c | ||
|
9709c0d0a2 | ||
|
960f058b7e | ||
|
4ecf185431 | ||
|
e45f0660a2 | ||
|
e7ccf72fc5 | ||
|
e154001d44 | ||
|
44558ceeaa | ||
|
aa8068b5d2 | ||
|
466ecfb78c | ||
|
731c6cdd5e | ||
|
199f177c2d | ||
|
66c3dd7a81 | ||
|
9c9fe392d1 | ||
|
c33c2c5823 | ||
|
4d91702073 | ||
|
a0f77c1392 | ||
|
35df3e7bae | ||
|
f48438f30a | ||
|
c96eec601d | ||
|
c9059f3616 | ||
|
2fe87eea9b | ||
|
c4386df186 | ||
|
95d4bfe97c | ||
|
880c7eaf33 | ||
|
b5837c3380 | ||
|
e6ec1c7bca | ||
|
549675d8d7 | ||
|
a1e05c847e | ||
|
1f8a94f219 | ||
|
3c36033acb | ||
|
97db67d7eb | ||
|
a3dff524cb | ||
|
366ee5ae28 | ||
|
91b177d204 | ||
|
4f356086d7 | ||
|
60a5fe1e03 | ||
|
d8bf27f180 | ||
|
2a7ac3cf79 | ||
|
875491a0e1 | ||
|
9f06394a1a | ||
|
ad375cc3a3 | ||
|
011bc96ec9 | ||
|
da62457c90 | ||
|
38547b0c81 | ||
|
8b36ff1c54 | ||
|
5eff3b992b | ||
|
b5dece2ff4 | ||
|
1d441a8ca6 | ||
|
9c727f342d | ||
|
bedb9d1517 | ||
|
54c2c364a0 | ||
|
106a74d749 | ||
|
322f8f9712 | ||
|
6d9f69a4b6 | ||
|
14b5439a97 | ||
|
01c0cd9de2 | ||
|
e60efba59d | ||
|
5b32118b08 | ||
|
69bcca0a22 | ||
|
49ed863a7e | ||
|
46f9227615 | ||
|
7e59bcc799 | ||
|
d9f17733d9 | ||
|
d15a97ccba | ||
|
da7ab05920 | ||
|
d87ed8e53d | ||
|
1012386c8c | ||
|
7748ed75fe | ||
|
c32b3bada4 | ||
|
e042adc5b8 | ||
|
901e745158 | ||
|
91554b02eb | ||
|
fe7f1a1b38 | ||
|
8fddf94121 | ||
|
003ec854ab | ||
|
00ae92318d | ||
|
e4cd58a722 | ||
|
f4b4705e81 | ||
|
d4960c73dc | ||
|
22eb48ff91 | ||
|
6995266bd1 | ||
|
087f87e95c | ||
|
8edea4f7c5 | ||
|
d79c84d7df | ||
|
57deb82ca7 | ||
|
7a57f5a24f | ||
|
452770566c | ||
|
8247366bdd | ||
|
3dbd9c6ed9 | ||
|
6bbce0b5ca | ||
|
ac805a9286 | ||
|
8932fc84e1 | ||
|
525c4e59b6 | ||
|
a3fcce26a7 | ||
|
58778b1f23 | ||
|
fbfff03541 | ||
|
a1f991088d | ||
|
10af5def2b |
348 changed files with 9997 additions and 10367 deletions
21
README.md
21
README.md
|
@ -14,7 +14,6 @@ Does not use internet permission, and thus is 100% offline.
|
|||
* [Translations](#translations)
|
||||
* [To Community Creation](#to-community)
|
||||
* [Code Contribution](CONTRIBUTING.md)
|
||||
- [To-do](#to-do)
|
||||
- [License](#license)
|
||||
- [Credits](#credits)
|
||||
|
||||
|
@ -41,7 +40,7 @@ Does not use internet permission, and thus is 100% offline.
|
|||
</ul>
|
||||
<li>Clipboard history</li>
|
||||
<li>One-handed mode</li>
|
||||
<li>Split keyboard (only available if the screen is large enough)</li>
|
||||
<li>Split keyboard</li>
|
||||
<li>Number pad</li>
|
||||
<li>Backup and restore your settings and learned word / history data</li>
|
||||
</ul>
|
||||
|
@ -88,24 +87,6 @@ You can share your themes, layouts and dictionaries with other people:
|
|||
## Code Contribution
|
||||
See [Contribution Guidelines](CONTRIBUTING.md)
|
||||
|
||||
# To-do
|
||||
__Planned features and improvements:__
|
||||
* Improve support for modifier keys (_alt_, _ctrl_, _meta_ and _fn_), some ideas:
|
||||
* keep modifier keys on with long press
|
||||
* keep modifier keys on until the next key press
|
||||
* use sliding input
|
||||
* Less complicated addition of new keyboard languages (e.g. #519)
|
||||
* Additional and customizable key swipe functionality
|
||||
* Some functionality will not be possible when using glide typing
|
||||
* Add and enable emoji dictionaries by default (if available for language)
|
||||
* Clearer / more intuitive arrangement of settings
|
||||
* Maybe hide some less used settings by default (similar to color customization)
|
||||
* [Bug fixes](https://github.com/Helium314/HeliBoard/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||
|
||||
__What will _not_ be added:__
|
||||
* Dictionaries for more languages (you can still download them)
|
||||
* Anything that requires additional permissions, unless there is a _very_ good reason
|
||||
|
||||
# License
|
||||
|
||||
HeliBoard (as a fork of OpenBoard) is licensed under GNU General Public License v3.0.
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization") version "2.0.21"
|
||||
kotlin("plugin.serialization") version "2.1.21"
|
||||
kotlin("plugin.compose") version "2.0.0"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "helium314.keyboard"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 3002
|
||||
versionName = "3.0-alpha3"
|
||||
versionCode = 3101
|
||||
versionName = "3.1"
|
||||
ndk {
|
||||
abiFilters.clear()
|
||||
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
|
||||
|
@ -36,14 +35,23 @@ android {
|
|||
isJniDebuggable = false
|
||||
}
|
||||
debug {
|
||||
// "normal" debug has minify for smaller APK to fit the GitHub 25 MB limit when zipped
|
||||
// and for better performance in case users want to install a debug APK
|
||||
isMinifyEnabled = true
|
||||
isJniDebuggable = false
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
create("runTests") { // build variant for running tests on CI that skips tests known to fail
|
||||
isMinifyEnabled = true
|
||||
isMinifyEnabled = false
|
||||
isJniDebuggable = false
|
||||
}
|
||||
create("debugNoMinify") { // for faster builds in IDE
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
isJniDebuggable = false
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
base.archivesBaseName = "HeliBoard_" + defaultConfig.versionName
|
||||
}
|
||||
|
||||
|
@ -60,7 +68,7 @@ android {
|
|||
}
|
||||
ndkVersion = "28.0.13004108"
|
||||
|
||||
packagingOptions {
|
||||
packaging {
|
||||
jniLibs {
|
||||
// shrinks APK by 3 MB, zipped size unchanged
|
||||
useLegacyPackaging = true
|
||||
|
@ -96,27 +104,28 @@ android {
|
|||
|
||||
dependencies {
|
||||
// androidx
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
implementation("androidx.core:core-ktx:1.16.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("androidx.autofill:autofill:1.1.0")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||
|
||||
// kotlin
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
||||
|
||||
// compose
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
implementation(platform("androidx.compose:compose-bom:2025.02.00"))
|
||||
implementation(platform("androidx.compose:compose-bom:2025.05.00"))
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
implementation("androidx.navigation:navigation-compose:2.8.8")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.0")
|
||||
implementation("sh.calvin.reorderable:reorderable:2.4.3") // for easier re-ordering
|
||||
implementation("com.github.skydoves:colorpicker-compose:1.1.2") // for user-defined colors
|
||||
|
||||
// test
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.mockito:mockito-core:5.15.2")
|
||||
testImplementation("org.mockito:mockito-core:5.17.0")
|
||||
testImplementation("org.robolectric:robolectric:4.14.1")
|
||||
testImplementation("androidx.test:runner:1.6.2")
|
||||
testImplementation("androidx.test:core:1.6.1")
|
||||
|
|
9
app/src/debugNoMinify/res/values/strings.xml
Normal file
9
app/src/debugNoMinify/res/values/strings.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
-->
|
||||
<resources>
|
||||
<string name="english_ime_name" translatable="false">HeliBoard debug</string>
|
||||
<string name="spell_checker_service_name" translatable="false">HeliBoard debug Spell Checker</string>
|
||||
<string name="ime_settings" translatable="false">HeliBoard debug Settings</string>
|
||||
</resources>
|
|
@ -98,7 +98,15 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
|||
<queries>
|
||||
<!-- To detect other IMEs -->
|
||||
<intent>
|
||||
<action android:name="android.view.InputMethod"/>
|
||||
<!-- changed to * as it's supposed to help with finding other keyboard apps, see https://github.com/Helium314/HeliBoard/issues/1340 -->
|
||||
<!--<action android:name="android.view.InputMethod" />-->
|
||||
<action android:name="*" />
|
||||
</intent>
|
||||
|
||||
<!-- To detect names of installed apps -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
@ -1,114 +1,116 @@
|
|||
main,af,exp
|
||||
main,ar,
|
||||
main,ar,exp
|
||||
main,hy,
|
||||
main,as,
|
||||
main,bn_BD,exp
|
||||
main,bn,
|
||||
main,bn,exp
|
||||
main,eu,
|
||||
main,be,
|
||||
main,bg,
|
||||
main,bg,exp
|
||||
main,ca,
|
||||
main,ca,exp
|
||||
main,hr,
|
||||
main,hr,exp
|
||||
main,cs,
|
||||
main,cs,exp
|
||||
main,da,
|
||||
main,da,exp
|
||||
main,nl,
|
||||
main,nl,exp
|
||||
main,en_AU,
|
||||
main,en_CA,exp
|
||||
main,en_GB,
|
||||
main,en_GB,exp
|
||||
main,en_US,
|
||||
main,en_US,exp
|
||||
symbols,en,exp
|
||||
emoji,en,
|
||||
main,eo,
|
||||
main,eo,exp
|
||||
main,et,exp
|
||||
main,fi,
|
||||
main,fi,exp
|
||||
emoji,fr,
|
||||
symbols,fr,exp
|
||||
main,fr,
|
||||
main,fr,exp
|
||||
main,gl,
|
||||
main,gl,exp
|
||||
main,ka,
|
||||
main,de_AT,exp
|
||||
main,de_CH,
|
||||
main,de,
|
||||
main,de,exp
|
||||
main,gom,
|
||||
main,el,
|
||||
main,gu,
|
||||
main,he,
|
||||
main,iw,
|
||||
main,he,exp
|
||||
main,hi,
|
||||
main,hi_ZZ,
|
||||
main,hu,
|
||||
main,hu,exp
|
||||
main,is,exp
|
||||
main,id,exp
|
||||
main,it,
|
||||
main,it,exp
|
||||
main,kab,exp
|
||||
main,kn,
|
||||
main,ks,
|
||||
main,kk,exp
|
||||
main,km,
|
||||
main,la,
|
||||
main,lv,
|
||||
main,lv,exp
|
||||
main,lt,
|
||||
main,lt,exp
|
||||
main,lb,
|
||||
main,mai,
|
||||
addon,ml_ZZ,exp
|
||||
main,ml,
|
||||
main,mr,
|
||||
main,ne,exp
|
||||
main,nb,
|
||||
main,nb,exp
|
||||
main,or,
|
||||
main,pms,exp
|
||||
main,pl,
|
||||
main,pl,exp
|
||||
main,pt_BR,
|
||||
main,pt_PT,
|
||||
main,pt_PT,exp
|
||||
main,pa,
|
||||
main,ro,
|
||||
main,ro,exp
|
||||
emoji,ru,
|
||||
main,ru,
|
||||
main,ru,exp
|
||||
main,sa,
|
||||
main,sat,
|
||||
main,sr_ZZ,
|
||||
main,sr,
|
||||
main,sd,
|
||||
main,sk,exp
|
||||
main,sl,
|
||||
main,sl,exp
|
||||
main,es,
|
||||
main,es,exp
|
||||
main,zgh_ZZ,
|
||||
main,zgh,
|
||||
main,sv,
|
||||
main,sv,exp
|
||||
main,ta,
|
||||
main,te,
|
||||
main,tok,
|
||||
main,tcy,
|
||||
main,tr,
|
||||
main,tr,exp
|
||||
emoji,uk,
|
||||
main,uk,
|
||||
main,ur,
|
||||
main,af,exp
|
||||
main,ar,exp
|
||||
main,bn_BD,exp
|
||||
main,bn,exp
|
||||
main,bg,exp
|
||||
main,ca,exp
|
||||
main,hr,exp
|
||||
main,cs,exp
|
||||
main,da,exp
|
||||
main,nl,exp
|
||||
main,en_GB,exp
|
||||
main,en_US,exp
|
||||
symbols,en,exp
|
||||
main,eo,exp
|
||||
main,et,exp
|
||||
main,fi,exp
|
||||
symbols,fr,exp
|
||||
main,fr,exp
|
||||
main,gl,exp
|
||||
main,de_AT,exp
|
||||
main,de,exp
|
||||
main,he,exp
|
||||
main,hu,exp
|
||||
main,is,exp
|
||||
main,id,exp
|
||||
main,it,exp
|
||||
main,kab,exp
|
||||
main,kk,exp
|
||||
main,lv,exp
|
||||
main,lt,exp
|
||||
addon,ml_ZZ,exp
|
||||
main,ne,exp
|
||||
main,nb,exp
|
||||
main,pms,exp
|
||||
main,pl,exp
|
||||
main,pt_PT,exp
|
||||
main,ro,exp
|
||||
main,ru,exp
|
||||
main,sk,exp
|
||||
main,sl,exp
|
||||
main,es,exp
|
||||
main,sv,exp
|
||||
main,tr,exp
|
||||
main,uk,exp
|
||||
main,ur,
|
||||
main,vi,exp
|
||||
|
|
|
85
app/src/main/assets/emoji/ACTIVITIES.txt
Normal file
85
app/src/main/assets/emoji/ACTIVITIES.txt
Normal file
|
@ -0,0 +1,85 @@
|
|||
🎃
|
||||
🎄
|
||||
🎆
|
||||
🎇
|
||||
🧨
|
||||
✨
|
||||
🎈
|
||||
🎉
|
||||
🎊
|
||||
🎋
|
||||
🎍
|
||||
🎎
|
||||
🎏
|
||||
🎐
|
||||
🎑
|
||||
🧧
|
||||
🎀
|
||||
🎁
|
||||
🎗️
|
||||
🎟️
|
||||
🎫
|
||||
🎖️
|
||||
🏆
|
||||
🏅
|
||||
🥇
|
||||
🥈
|
||||
🥉
|
||||
⚽
|
||||
⚾
|
||||
🥎
|
||||
🏀
|
||||
🏐
|
||||
🏈
|
||||
🏉
|
||||
🎾
|
||||
🥏
|
||||
🎳
|
||||
🏏
|
||||
🏑
|
||||
🏒
|
||||
🥍
|
||||
🏓
|
||||
🏸
|
||||
🥊
|
||||
🥋
|
||||
🥅
|
||||
⛳
|
||||
⛸️
|
||||
🎣
|
||||
🤿
|
||||
🎽
|
||||
🎿
|
||||
🛷
|
||||
🥌
|
||||
🎯
|
||||
🪀
|
||||
🪁
|
||||
🔫
|
||||
🎱
|
||||
🔮
|
||||
🪄
|
||||
🎮
|
||||
🕹️
|
||||
🎰
|
||||
🎲
|
||||
🧩
|
||||
🧸
|
||||
🪅
|
||||
🪩
|
||||
🪆
|
||||
♠️
|
||||
♥️
|
||||
♦️
|
||||
♣️
|
||||
♟️
|
||||
🃏
|
||||
🀄
|
||||
🎴
|
||||
🎭
|
||||
🖼️
|
||||
🎨
|
||||
🧵
|
||||
🪡
|
||||
🧶
|
||||
🪢
|
159
app/src/main/assets/emoji/ANIMALS_AND_NATURE.txt
Normal file
159
app/src/main/assets/emoji/ANIMALS_AND_NATURE.txt
Normal file
|
@ -0,0 +1,159 @@
|
|||
🐵
|
||||
🐒
|
||||
🦍
|
||||
🦧
|
||||
🐶
|
||||
🐕
|
||||
🦮
|
||||
🐕🦺
|
||||
🐩
|
||||
🐺
|
||||
🦊
|
||||
🦝
|
||||
🐱
|
||||
🐈
|
||||
🐈⬛
|
||||
🦁
|
||||
🐯
|
||||
🐅
|
||||
🐆
|
||||
🐴
|
||||
🫎
|
||||
🫏
|
||||
🐎
|
||||
🦄
|
||||
🦓
|
||||
🦌
|
||||
🦬
|
||||
🐮
|
||||
🐂
|
||||
🐃
|
||||
🐄
|
||||
🐷
|
||||
🐖
|
||||
🐗
|
||||
🐽
|
||||
🐏
|
||||
🐑
|
||||
🐐
|
||||
🐪
|
||||
🐫
|
||||
🦙
|
||||
🦒
|
||||
🐘
|
||||
🦣
|
||||
🦏
|
||||
🦛
|
||||
🐭
|
||||
🐁
|
||||
🐀
|
||||
🐹
|
||||
🐰
|
||||
🐇
|
||||
🐿️
|
||||
🦫
|
||||
🦔
|
||||
🦇
|
||||
🐻
|
||||
🐻❄️
|
||||
🐨
|
||||
🐼
|
||||
🦥
|
||||
🦦
|
||||
🦨
|
||||
🦘
|
||||
🦡
|
||||
🐾
|
||||
🦃
|
||||
🐔
|
||||
🐓
|
||||
🐣
|
||||
🐤
|
||||
🐥
|
||||
🐦
|
||||
🐧
|
||||
🕊️
|
||||
🦅
|
||||
🦆
|
||||
🦢
|
||||
🦉
|
||||
🦤
|
||||
🪶
|
||||
🦩
|
||||
🦚
|
||||
🦜
|
||||
🪽
|
||||
🐦⬛
|
||||
🪿
|
||||
🐦🔥
|
||||
🐸
|
||||
🐊
|
||||
🐢
|
||||
🦎
|
||||
🐍
|
||||
🐲
|
||||
🐉
|
||||
🦕
|
||||
🦖
|
||||
🐳
|
||||
🐋
|
||||
🐬
|
||||
🦭
|
||||
🐟
|
||||
🐠
|
||||
🐡
|
||||
🦈
|
||||
🐙
|
||||
🐚
|
||||
🪸
|
||||
🪼
|
||||
🦀
|
||||
🦞
|
||||
🦐
|
||||
🦑
|
||||
🦪
|
||||
🐌
|
||||
🦋
|
||||
🐛
|
||||
🐜
|
||||
🐝
|
||||
🪲
|
||||
🐞
|
||||
🦗
|
||||
🪳
|
||||
🕷️
|
||||
🕸️
|
||||
🦂
|
||||
🦟
|
||||
🪰
|
||||
🪱
|
||||
🦠
|
||||
💐
|
||||
🌸
|
||||
💮
|
||||
🪷
|
||||
🏵️
|
||||
🌹
|
||||
🥀
|
||||
🌺
|
||||
🌻
|
||||
🌼
|
||||
🌷
|
||||
🪻
|
||||
🌱
|
||||
🪴
|
||||
🌲
|
||||
🌳
|
||||
🌴
|
||||
🌵
|
||||
🌾
|
||||
🌿
|
||||
☘️
|
||||
🍀
|
||||
🍁
|
||||
🍂
|
||||
🍃
|
||||
🪹
|
||||
🪺
|
||||
🍄
|
||||
|
25
app/src/main/assets/emoji/EMOTICONS.txt
Normal file
25
app/src/main/assets/emoji/EMOTICONS.txt
Normal file
|
@ -0,0 +1,25 @@
|
|||
:-)
|
||||
;-)
|
||||
:-(
|
||||
:-!
|
||||
:-$
|
||||
B-)
|
||||
=-O
|
||||
:-P
|
||||
:O
|
||||
:-*
|
||||
:-D
|
||||
:\'(
|
||||
:-\\
|
||||
O:-)
|
||||
:-[
|
||||
(╯°
|
||||
□°)
|
||||
╯︵
|
||||
┻━┻
|
||||
¯\\_
|
||||
(ツ)
|
||||
_/¯
|
||||
┬─┬
|
||||
︵ /(
|
||||
.□.\\
|
270
app/src/main/assets/emoji/FLAGS.txt
Normal file
270
app/src/main/assets/emoji/FLAGS.txt
Normal file
|
@ -0,0 +1,270 @@
|
|||
🏁
|
||||
🚩
|
||||
🎌
|
||||
🏴
|
||||
🏳️
|
||||
🏳️🌈
|
||||
🏳️⚧️
|
||||
🏴☠️
|
||||
🇦🇨
|
||||
🇦🇩
|
||||
🇦🇪
|
||||
🇦🇫
|
||||
🇦🇬
|
||||
🇦🇮
|
||||
🇦🇱
|
||||
🇦🇲
|
||||
🇦🇴
|
||||
🇦🇶
|
||||
🇦🇷
|
||||
🇦🇸
|
||||
🇦🇹
|
||||
🇦🇺
|
||||
🇦🇼
|
||||
🇦🇽
|
||||
🇦🇿
|
||||
🇧🇦
|
||||
🇧🇧
|
||||
🇧🇩
|
||||
🇧🇪
|
||||
🇧🇫
|
||||
🇧🇬
|
||||
🇧🇭
|
||||
🇧🇮
|
||||
🇧🇯
|
||||
🇧🇱
|
||||
🇧🇲
|
||||
🇧🇳
|
||||
🇧🇴
|
||||
🇧🇶
|
||||
🇧🇷
|
||||
🇧🇸
|
||||
🇧🇹
|
||||
🇧🇻
|
||||
🇧🇼
|
||||
🇧🇾
|
||||
🇧🇿
|
||||
🇨🇦
|
||||
🇨🇨
|
||||
🇨🇩
|
||||
🇨🇫
|
||||
🇨🇬
|
||||
🇨🇭
|
||||
🇨🇮
|
||||
🇨🇰
|
||||
🇨🇱
|
||||
🇨🇲
|
||||
🇨🇳
|
||||
🇨🇴
|
||||
🇨🇵
|
||||
🇨🇶
|
||||
🇨🇷
|
||||
🇨🇺
|
||||
🇨🇻
|
||||
🇨🇼
|
||||
🇨🇽
|
||||
🇨🇾
|
||||
🇨🇿
|
||||
🇩🇪
|
||||
🇩🇬
|
||||
🇩🇯
|
||||
🇩🇰
|
||||
🇩🇲
|
||||
🇩🇴
|
||||
🇩🇿
|
||||
🇪🇦
|
||||
🇪🇨
|
||||
🇪🇪
|
||||
🇪🇬
|
||||
🇪🇭
|
||||
🇪🇷
|
||||
🇪🇸
|
||||
🇪🇹
|
||||
🇪🇺
|
||||
🇫🇮
|
||||
🇫🇯
|
||||
🇫🇰
|
||||
🇫🇲
|
||||
🇫🇴
|
||||
🇫🇷
|
||||
🇬🇦
|
||||
🇬🇧
|
||||
🇬🇩
|
||||
🇬🇪
|
||||
🇬🇫
|
||||
🇬🇬
|
||||
🇬🇭
|
||||
🇬🇮
|
||||
🇬🇱
|
||||
🇬🇲
|
||||
🇬🇳
|
||||
🇬🇵
|
||||
🇬🇶
|
||||
🇬🇷
|
||||
🇬🇸
|
||||
🇬🇹
|
||||
🇬🇺
|
||||
🇬🇼
|
||||
🇬🇾
|
||||
🇭🇰
|
||||
🇭🇲
|
||||
🇭🇳
|
||||
🇭🇷
|
||||
🇭🇹
|
||||
🇭🇺
|
||||
🇮🇨
|
||||
🇮🇩
|
||||
🇮🇪
|
||||
🇮🇱
|
||||
🇮🇲
|
||||
🇮🇳
|
||||
🇮🇴
|
||||
🇮🇶
|
||||
🇮🇷
|
||||
🇮🇸
|
||||
🇮🇹
|
||||
🇯🇪
|
||||
🇯🇲
|
||||
🇯🇴
|
||||
🇯🇵
|
||||
🇰🇪
|
||||
🇰🇬
|
||||
🇰🇭
|
||||
🇰🇮
|
||||
🇰🇲
|
||||
🇰🇳
|
||||
🇰🇵
|
||||
🇰🇷
|
||||
🇰🇼
|
||||
🇰🇾
|
||||
🇰🇿
|
||||
🇱🇦
|
||||
🇱🇧
|
||||
🇱🇨
|
||||
🇱🇮
|
||||
🇱🇰
|
||||
🇱🇷
|
||||
🇱🇸
|
||||
🇱🇹
|
||||
🇱🇺
|
||||
🇱🇻
|
||||
🇱🇾
|
||||
🇲🇦
|
||||
🇲🇨
|
||||
🇲🇩
|
||||
🇲🇪
|
||||
🇲🇫
|
||||
🇲🇬
|
||||
🇲🇭
|
||||
🇲🇰
|
||||
🇲🇱
|
||||
🇲🇲
|
||||
🇲🇳
|
||||
🇲🇴
|
||||
🇲🇵
|
||||
🇲🇶
|
||||
🇲🇷
|
||||
🇲🇸
|
||||
🇲🇹
|
||||
🇲🇺
|
||||
🇲🇻
|
||||
🇲🇼
|
||||
🇲🇽
|
||||
🇲🇾
|
||||
🇲🇿
|
||||
🇳🇦
|
||||
🇳🇨
|
||||
🇳🇪
|
||||
🇳🇫
|
||||
🇳🇬
|
||||
🇳🇮
|
||||
🇳🇱
|
||||
🇳🇴
|
||||
🇳🇵
|
||||
🇳🇷
|
||||
🇳🇺
|
||||
🇳🇿
|
||||
🇴🇲
|
||||
🇵🇦
|
||||
🇵🇪
|
||||
🇵🇫
|
||||
🇵🇬
|
||||
🇵🇭
|
||||
🇵🇰
|
||||
🇵🇱
|
||||
🇵🇲
|
||||
🇵🇳
|
||||
🇵🇷
|
||||
🇵🇸
|
||||
🇵🇹
|
||||
🇵🇼
|
||||
🇵🇾
|
||||
🇶🇦
|
||||
🇷🇪
|
||||
🇷🇴
|
||||
🇷🇸
|
||||
🇷🇺
|
||||
🇷🇼
|
||||
🇸🇦
|
||||
🇸🇧
|
||||
🇸🇨
|
||||
🇸🇩
|
||||
🇸🇪
|
||||
🇸🇬
|
||||
🇸🇭
|
||||
🇸🇮
|
||||
🇸🇯
|
||||
🇸🇰
|
||||
🇸🇱
|
||||
🇸🇲
|
||||
🇸🇳
|
||||
🇸🇴
|
||||
🇸🇷
|
||||
🇸🇸
|
||||
🇸🇹
|
||||
🇸🇻
|
||||
🇸🇽
|
||||
🇸🇾
|
||||
🇸🇿
|
||||
🇹🇦
|
||||
🇹🇨
|
||||
🇹🇩
|
||||
🇹🇫
|
||||
🇹🇬
|
||||
🇹🇭
|
||||
🇹🇯
|
||||
🇹🇰
|
||||
🇹🇱
|
||||
🇹🇲
|
||||
🇹🇳
|
||||
🇹🇴
|
||||
🇹🇷
|
||||
🇹🇹
|
||||
🇹🇻
|
||||
🇹🇼
|
||||
🇹🇿
|
||||
🇺🇦
|
||||
🇺🇬
|
||||
🇺🇲
|
||||
🇺🇳
|
||||
🇺🇸
|
||||
🇺🇾
|
||||
🇺🇿
|
||||
🇻🇦
|
||||
🇻🇨
|
||||
🇻🇪
|
||||
🇻🇬
|
||||
🇻🇮
|
||||
🇻🇳
|
||||
🇻🇺
|
||||
🇼🇫
|
||||
🇼🇸
|
||||
🇽🇰
|
||||
🇾🇪
|
||||
🇾🇹
|
||||
🇿🇦
|
||||
🇿🇲
|
||||
🇿🇼
|
||||
🏴
|
||||
🏴
|
||||
🏴
|
131
app/src/main/assets/emoji/FOOD_AND_DRINK.txt
Normal file
131
app/src/main/assets/emoji/FOOD_AND_DRINK.txt
Normal file
|
@ -0,0 +1,131 @@
|
|||
🍇
|
||||
🍈
|
||||
🍉
|
||||
🍊
|
||||
🍋
|
||||
🍋🟩
|
||||
🍌
|
||||
🍍
|
||||
🥭
|
||||
🍎
|
||||
🍏
|
||||
🍐
|
||||
🍑
|
||||
🍒
|
||||
🍓
|
||||
🫐
|
||||
🥝
|
||||
🍅
|
||||
🫒
|
||||
🥥
|
||||
🥑
|
||||
🍆
|
||||
🥔
|
||||
🥕
|
||||
🌽
|
||||
🌶️
|
||||
🫑
|
||||
🥒
|
||||
🥬
|
||||
🥦
|
||||
🧄
|
||||
🧅
|
||||
🥜
|
||||
🫘
|
||||
🌰
|
||||
🫚
|
||||
🫛
|
||||
🍄🟫
|
||||
|
||||
🍞
|
||||
🥐
|
||||
🥖
|
||||
🫓
|
||||
🥨
|
||||
🥯
|
||||
🥞
|
||||
🧇
|
||||
🧀
|
||||
🍖
|
||||
🍗
|
||||
🥩
|
||||
🥓
|
||||
🍔
|
||||
🍟
|
||||
🍕
|
||||
🌭
|
||||
🥪
|
||||
🌮
|
||||
🌯
|
||||
🫔
|
||||
🥙
|
||||
🧆
|
||||
🥚
|
||||
🍳
|
||||
🥘
|
||||
🍲
|
||||
🫕
|
||||
🥣
|
||||
🥗
|
||||
🍿
|
||||
🧈
|
||||
🧂
|
||||
🥫
|
||||
🍱
|
||||
🍘
|
||||
🍙
|
||||
🍚
|
||||
🍛
|
||||
🍜
|
||||
🍝
|
||||
🍠
|
||||
🍢
|
||||
🍣
|
||||
🍤
|
||||
🍥
|
||||
🥮
|
||||
🍡
|
||||
🥟
|
||||
🥠
|
||||
🥡
|
||||
🍦
|
||||
🍧
|
||||
🍨
|
||||
🍩
|
||||
🍪
|
||||
🎂
|
||||
🍰
|
||||
🧁
|
||||
🥧
|
||||
🍫
|
||||
🍬
|
||||
🍭
|
||||
🍮
|
||||
🍯
|
||||
🍼
|
||||
🥛
|
||||
☕
|
||||
🫖
|
||||
🍵
|
||||
🍶
|
||||
🍾
|
||||
🍷
|
||||
🍸
|
||||
🍹
|
||||
🍺
|
||||
🍻
|
||||
🥂
|
||||
🥃
|
||||
🫗
|
||||
🥤
|
||||
🧋
|
||||
🧃
|
||||
🧉
|
||||
🧊
|
||||
🥢
|
||||
🍽️
|
||||
🍴
|
||||
🥄
|
||||
🔪
|
||||
🫙
|
||||
🏺
|
264
app/src/main/assets/emoji/OBJECTS.txt
Normal file
264
app/src/main/assets/emoji/OBJECTS.txt
Normal file
|
@ -0,0 +1,264 @@
|
|||
👓
|
||||
🕶️
|
||||
🥽
|
||||
🥼
|
||||
🦺
|
||||
👔
|
||||
👕
|
||||
👖
|
||||
🧣
|
||||
🧤
|
||||
🧥
|
||||
🧦
|
||||
👗
|
||||
👘
|
||||
🥻
|
||||
🩱
|
||||
🩲
|
||||
🩳
|
||||
👙
|
||||
👚
|
||||
🪭
|
||||
👛
|
||||
👜
|
||||
👝
|
||||
🛍️
|
||||
🎒
|
||||
🩴
|
||||
👞
|
||||
👟
|
||||
🥾
|
||||
🥿
|
||||
👠
|
||||
👡
|
||||
🩰
|
||||
👢
|
||||
🪮
|
||||
👑
|
||||
👒
|
||||
🎩
|
||||
🎓
|
||||
🧢
|
||||
🪖
|
||||
⛑️
|
||||
📿
|
||||
💄
|
||||
💍
|
||||
💎
|
||||
🔇
|
||||
🔈
|
||||
🔉
|
||||
🔊
|
||||
📢
|
||||
📣
|
||||
📯
|
||||
🔔
|
||||
🔕
|
||||
🎼
|
||||
🎵
|
||||
🎶
|
||||
🎙️
|
||||
🎚️
|
||||
🎛️
|
||||
🎤
|
||||
🎧
|
||||
📻
|
||||
🎷
|
||||
🪗
|
||||
🎸
|
||||
🎹
|
||||
🎺
|
||||
🎻
|
||||
🪕
|
||||
🥁
|
||||
🪘
|
||||
🪇
|
||||
🪈
|
||||
|
||||
📱
|
||||
📲
|
||||
☎️
|
||||
📞
|
||||
📟
|
||||
📠
|
||||
🔋
|
||||
🪫
|
||||
🔌
|
||||
💻
|
||||
🖥️
|
||||
🖨️
|
||||
⌨️
|
||||
🖱️
|
||||
🖲️
|
||||
💽
|
||||
💾
|
||||
💿
|
||||
📀
|
||||
🧮
|
||||
🎥
|
||||
🎞️
|
||||
📽️
|
||||
🎬
|
||||
📺
|
||||
📷
|
||||
📸
|
||||
📹
|
||||
📼
|
||||
🔍
|
||||
🔎
|
||||
🕯️
|
||||
💡
|
||||
🔦
|
||||
🏮
|
||||
🪔
|
||||
📔
|
||||
📕
|
||||
📖
|
||||
📗
|
||||
📘
|
||||
📙
|
||||
📚
|
||||
📓
|
||||
📒
|
||||
📃
|
||||
📜
|
||||
📄
|
||||
📰
|
||||
🗞️
|
||||
📑
|
||||
🔖
|
||||
🏷️
|
||||
💰
|
||||
🪙
|
||||
💴
|
||||
💵
|
||||
💶
|
||||
💷
|
||||
💸
|
||||
💳
|
||||
🧾
|
||||
💹
|
||||
✉️
|
||||
📧
|
||||
📨
|
||||
📩
|
||||
📤
|
||||
📥
|
||||
📦
|
||||
📫
|
||||
📪
|
||||
📬
|
||||
📭
|
||||
📮
|
||||
🗳️
|
||||
✏️
|
||||
✒️
|
||||
🖋️
|
||||
🖊️
|
||||
🖌️
|
||||
🖍️
|
||||
📝
|
||||
💼
|
||||
📁
|
||||
📂
|
||||
🗂️
|
||||
📅
|
||||
📆
|
||||
🗒️
|
||||
🗓️
|
||||
📇
|
||||
📈
|
||||
📉
|
||||
📊
|
||||
📋
|
||||
📌
|
||||
📍
|
||||
📎
|
||||
🖇️
|
||||
📏
|
||||
📐
|
||||
✂️
|
||||
🗃️
|
||||
🗄️
|
||||
🗑️
|
||||
🔒
|
||||
🔓
|
||||
🔏
|
||||
🔐
|
||||
🔑
|
||||
🗝️
|
||||
🔨
|
||||
🪓
|
||||
⛏️
|
||||
⚒️
|
||||
🛠️
|
||||
🗡️
|
||||
⚔️
|
||||
💣
|
||||
🪃
|
||||
🏹
|
||||
🛡️
|
||||
🪚
|
||||
🔧
|
||||
🪛
|
||||
🔩
|
||||
⚙️
|
||||
🗜️
|
||||
⚖️
|
||||
🦯
|
||||
🔗
|
||||
⛓️💥
|
||||
⛓️
|
||||
🪝
|
||||
🧰
|
||||
🧲
|
||||
🪜
|
||||
|
||||
⚗️
|
||||
🧪
|
||||
🧫
|
||||
🧬
|
||||
🔬
|
||||
🔭
|
||||
📡
|
||||
💉
|
||||
🩸
|
||||
💊
|
||||
🩹
|
||||
🩼
|
||||
🩺
|
||||
🩻
|
||||
🚪
|
||||
🛗
|
||||
🪞
|
||||
🪟
|
||||
🛏️
|
||||
🛋️
|
||||
🪑
|
||||
🚽
|
||||
🪠
|
||||
🚿
|
||||
🛁
|
||||
🪤
|
||||
🪒
|
||||
🧴
|
||||
🧷
|
||||
🧹
|
||||
🧺
|
||||
🧻
|
||||
🪣
|
||||
🧼
|
||||
🫧
|
||||
🪥
|
||||
🧽
|
||||
🧯
|
||||
🛒
|
||||
🚬
|
||||
⚰️
|
||||
🪦
|
||||
⚱️
|
||||
🧿
|
||||
🪬
|
||||
🗿
|
||||
🪧
|
||||
🪪
|
386
app/src/main/assets/emoji/PEOPLE_AND_BODY.txt
Normal file
386
app/src/main/assets/emoji/PEOPLE_AND_BODY.txt
Normal file
|
@ -0,0 +1,386 @@
|
|||
👋 👋🏻 👋🏼 👋🏽 👋🏾 👋🏿
|
||||
🤚 🤚🏻 🤚🏼 🤚🏽 🤚🏾 🤚🏿
|
||||
🖐️ 🖐🏻 🖐🏼 🖐🏽 🖐🏾 🖐🏿
|
||||
✋ ✋🏻 ✋🏼 ✋🏽 ✋🏾 ✋🏿
|
||||
🖖 🖖🏻 🖖🏼 🖖🏽 🖖🏾 🖖🏿
|
||||
🫱 🫱🏻 🫱🏼 🫱🏽 🫱🏾 🫱🏿
|
||||
🫲 🫲🏻 🫲🏼 🫲🏽 🫲🏾 🫲🏿
|
||||
🫳 🫳🏻 🫳🏼 🫳🏽 🫳🏾 🫳🏿
|
||||
🫴 🫴🏻 🫴🏼 🫴🏽 🫴🏾 🫴🏿
|
||||
🫷 🫷🏻 🫷🏼 🫷🏽 🫷🏾 🫷🏿
|
||||
🫸 🫸🏻 🫸🏼 🫸🏽 🫸🏾 🫸🏿
|
||||
👌 👌🏻 👌🏼 👌🏽 👌🏾 👌🏿
|
||||
🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿
|
||||
🤏 🤏🏻 🤏🏼 🤏🏽 🤏🏾 🤏🏿
|
||||
✌️ ✌🏻 ✌🏼 ✌🏽 ✌🏾 ✌🏿
|
||||
🤞 🤞🏻 🤞🏼 🤞🏽 🤞🏾 🤞🏿
|
||||
🫰 🫰🏻 🫰🏼 🫰🏽 🫰🏾 🫰🏿
|
||||
🤟 🤟🏻 🤟🏼 🤟🏽 🤟🏾 🤟🏿
|
||||
🤘 🤘🏻 🤘🏼 🤘🏽 🤘🏾 🤘🏿
|
||||
🤙 🤙🏻 🤙🏼 🤙🏽 🤙🏾 🤙🏿
|
||||
👈 👈🏻 👈🏼 👈🏽 👈🏾 👈🏿
|
||||
👉 👉🏻 👉🏼 👉🏽 👉🏾 👉🏿
|
||||
👆 👆🏻 👆🏼 👆🏽 👆🏾 👆🏿
|
||||
🖕 🖕🏻 🖕🏼 🖕🏽 🖕🏾 🖕🏿
|
||||
👇 👇🏻 👇🏼 👇🏽 👇🏾 👇🏿
|
||||
☝️ ☝🏻 ☝🏼 ☝🏽 ☝🏾 ☝🏿
|
||||
🫵 🫵🏻 🫵🏼 🫵🏽 🫵🏾 🫵🏿
|
||||
👍 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿
|
||||
👎 👎🏻 👎🏼 👎🏽 👎🏾 👎🏿
|
||||
✊ ✊🏻 ✊🏼 ✊🏽 ✊🏾 ✊🏿
|
||||
👊 👊🏻 👊🏼 👊🏽 👊🏾 👊🏿
|
||||
🤛 🤛🏻 🤛🏼 🤛🏽 🤛🏾 🤛🏿
|
||||
🤜 🤜🏻 🤜🏼 🤜🏽 🤜🏾 🤜🏿
|
||||
👏 👏🏻 👏🏼 👏🏽 👏🏾 👏🏿
|
||||
🙌 🙌🏻 🙌🏼 🙌🏽 🙌🏾 🙌🏿
|
||||
🫶 🫶🏻 🫶🏼 🫶🏽 🫶🏾 🫶🏿
|
||||
👐 👐🏻 👐🏼 👐🏽 👐🏾 👐🏿
|
||||
🤲 🤲🏻 🤲🏼 🤲🏽 🤲🏾 🤲🏿
|
||||
🤝 🤝🏻 🤝🏼 🤝🏽 🤝🏾 🤝🏿
|
||||
🙏 🙏🏻 🙏🏼 🙏🏽 🙏🏾 🙏🏿
|
||||
✍️ ✍🏻 ✍🏼 ✍🏽 ✍🏾 ✍🏿
|
||||
💅 💅🏻 💅🏼 💅🏽 💅🏾 💅🏿
|
||||
🤳 🤳🏻 🤳🏼 🤳🏽 🤳🏾 🤳🏿
|
||||
💪 💪🏻 💪🏼 💪🏽 💪🏾 💪🏿
|
||||
🦾
|
||||
🦿
|
||||
🦵 🦵🏻 🦵🏼 🦵🏽 🦵🏾 🦵🏿
|
||||
🦶 🦶🏻 🦶🏼 🦶🏽 🦶🏾 🦶🏿
|
||||
👂 👂🏻 👂🏼 👂🏽 👂🏾 👂🏿
|
||||
🦻 🦻🏻 🦻🏼 🦻🏽 🦻🏾 🦻🏿
|
||||
👃 👃🏻 👃🏼 👃🏽 👃🏾 👃🏿
|
||||
🧠
|
||||
🫀
|
||||
🫁
|
||||
🦷
|
||||
🦴
|
||||
👀
|
||||
👁️
|
||||
👅
|
||||
👄
|
||||
🫦
|
||||
👶 👶🏻 👶🏼 👶🏽 👶🏾 👶🏿
|
||||
🧒 🧒🏻 🧒🏼 🧒🏽 🧒🏾 🧒🏿
|
||||
👦 👦🏻 👦🏼 👦🏽 👦🏾 👦🏿
|
||||
👧 👧🏻 👧🏼 👧🏽 👧🏾 👧🏿
|
||||
🧑 🧑🏻 🧑🏼 🧑🏽 🧑🏾 🧑🏿
|
||||
👱 👱🏻 👱🏼 👱🏽 👱🏾 👱🏿
|
||||
👨 👨🏻 👨🏼 👨🏽 👨🏾 👨🏿
|
||||
🧔 🧔🏻 🧔🏼 🧔🏽 🧔🏾 🧔🏿
|
||||
🧔♂️ 🧔🏻♂️ 🧔🏼♂️ 🧔🏽♂️ 🧔🏾♂️ 🧔🏿♂️
|
||||
🧔♀️ 🧔🏻♀️ 🧔🏼♀️ 🧔🏽♀️ 🧔🏾♀️ 🧔🏿♀️
|
||||
👨🦰 👨🏻🦰 👨🏼🦰 👨🏽🦰 👨🏾🦰 👨🏿🦰
|
||||
👨🦱 👨🏻🦱 👨🏼🦱 👨🏽🦱 👨🏾🦱 👨🏿🦱
|
||||
👨🦳 👨🏻🦳 👨🏼🦳 👨🏽🦳 👨🏾🦳 👨🏿🦳
|
||||
👨🦲 👨🏻🦲 👨🏼🦲 👨🏽🦲 👨🏾🦲 👨🏿🦲
|
||||
👩 👩🏻 👩🏼 👩🏽 👩🏾 👩🏿
|
||||
👩🦰 👩🏻🦰 👩🏼🦰 👩🏽🦰 👩🏾🦰 👩🏿🦰
|
||||
🧑🦰 🧑🏻🦰 🧑🏼🦰 🧑🏽🦰 🧑🏾🦰 🧑🏿🦰
|
||||
👩🦱 👩🏻🦱 👩🏼🦱 👩🏽🦱 👩🏾🦱 👩🏿🦱
|
||||
🧑🦱 🧑🏻🦱 🧑🏼🦱 🧑🏽🦱 🧑🏾🦱 🧑🏿🦱
|
||||
👩🦳 👩🏻🦳 👩🏼🦳 👩🏽🦳 👩🏾🦳 👩🏿🦳
|
||||
🧑🦳 🧑🏻🦳 🧑🏼🦳 🧑🏽🦳 🧑🏾🦳 🧑🏿🦳
|
||||
👩🦲 👩🏻🦲 👩🏼🦲 👩🏽🦲 👩🏾🦲 👩🏿🦲
|
||||
🧑🦲 🧑🏻🦲 🧑🏼🦲 🧑🏽🦲 🧑🏾🦲 🧑🏿🦲
|
||||
👱♀️ 👱🏻♀️ 👱🏼♀️ 👱🏽♀️ 👱🏾♀️ 👱🏿♀️
|
||||
👱♂️ 👱🏻♂️ 👱🏼♂️ 👱🏽♂️ 👱🏾♂️ 👱🏿♂️
|
||||
🧓 🧓🏻 🧓🏼 🧓🏽 🧓🏾 🧓🏿
|
||||
👴 👴🏻 👴🏼 👴🏽 👴🏾 👴🏿
|
||||
👵 👵🏻 👵🏼 👵🏽 👵🏾 👵🏿
|
||||
🙍 🙍🏻 🙍🏼 🙍🏽 🙍🏾 🙍🏿
|
||||
🙍♂️ 🙍🏻♂️ 🙍🏼♂️ 🙍🏽♂️ 🙍🏾♂️ 🙍🏿♂️
|
||||
🙍♀️ 🙍🏻♀️ 🙍🏼♀️ 🙍🏽♀️ 🙍🏾♀️ 🙍🏿♀️
|
||||
🙎 🙎🏻 🙎🏼 🙎🏽 🙎🏾 🙎🏿
|
||||
🙎♂️ 🙎🏻♂️ 🙎🏼♂️ 🙎🏽♂️ 🙎🏾♂️ 🙎🏿♂️
|
||||
🙎♀️ 🙎🏻♀️ 🙎🏼♀️ 🙎🏽♀️ 🙎🏾♀️ 🙎🏿♀️
|
||||
🙅 🙅🏻 🙅🏼 🙅🏽 🙅🏾 🙅🏿
|
||||
🙅♂️ 🙅🏻♂️ 🙅🏼♂️ 🙅🏽♂️ 🙅🏾♂️ 🙅🏿♂️
|
||||
🙅♀️ 🙅🏻♀️ 🙅🏼♀️ 🙅🏽♀️ 🙅🏾♀️ 🙅🏿♀️
|
||||
🙆 🙆🏻 🙆🏼 🙆🏽 🙆🏾 🙆🏿
|
||||
🙆♂️ 🙆🏻♂️ 🙆🏼♂️ 🙆🏽♂️ 🙆🏾♂️ 🙆🏿♂️
|
||||
🙆♀️ 🙆🏻♀️ 🙆🏼♀️ 🙆🏽♀️ 🙆🏾♀️ 🙆🏿♀️
|
||||
💁 💁🏻 💁🏼 💁🏽 💁🏾 💁🏿
|
||||
💁♂️ 💁🏻♂️ 💁🏼♂️ 💁🏽♂️ 💁🏾♂️ 💁🏿♂️
|
||||
💁♀️ 💁🏻♀️ 💁🏼♀️ 💁🏽♀️ 💁🏾♀️ 💁🏿♀️
|
||||
🙋 🙋🏻 🙋🏼 🙋🏽 🙋🏾 🙋🏿
|
||||
🙋♂️ 🙋🏻♂️ 🙋🏼♂️ 🙋🏽♂️ 🙋🏾♂️ 🙋🏿♂️
|
||||
🙋♀️ 🙋🏻♀️ 🙋🏼♀️ 🙋🏽♀️ 🙋🏾♀️ 🙋🏿♀️
|
||||
🧏 🧏🏻 🧏🏼 🧏🏽 🧏🏾 🧏🏿
|
||||
🧏♂️ 🧏🏻♂️ 🧏🏼♂️ 🧏🏽♂️ 🧏🏾♂️ 🧏🏿♂️
|
||||
🧏♀️ 🧏🏻♀️ 🧏🏼♀️ 🧏🏽♀️ 🧏🏾♀️ 🧏🏿♀️
|
||||
🙇 🙇🏻 🙇🏼 🙇🏽 🙇🏾 🙇🏿
|
||||
🙇♂️ 🙇🏻♂️ 🙇🏼♂️ 🙇🏽♂️ 🙇🏾♂️ 🙇🏿♂️
|
||||
🙇♀️ 🙇🏻♀️ 🙇🏼♀️ 🙇🏽♀️ 🙇🏾♀️ 🙇🏿♀️
|
||||
🤦 🤦🏻 🤦🏼 🤦🏽 🤦🏾 🤦🏿
|
||||
🤦♂️ 🤦🏻♂️ 🤦🏼♂️ 🤦🏽♂️ 🤦🏾♂️ 🤦🏿♂️
|
||||
🤦♀️ 🤦🏻♀️ 🤦🏼♀️ 🤦🏽♀️ 🤦🏾♀️ 🤦🏿♀️
|
||||
🤷 🤷🏻 🤷🏼 🤷🏽 🤷🏾 🤷🏿
|
||||
🤷♂️ 🤷🏻♂️ 🤷🏼♂️ 🤷🏽♂️ 🤷🏾♂️ 🤷🏿♂️
|
||||
🤷♀️ 🤷🏻♀️ 🤷🏼♀️ 🤷🏽♀️ 🤷🏾♀️ 🤷🏿♀️
|
||||
🧑⚕️ 🧑🏻⚕️ 🧑🏼⚕️ 🧑🏽⚕️ 🧑🏾⚕️ 🧑🏿⚕️
|
||||
👨⚕️ 👨🏻⚕️ 👨🏼⚕️ 👨🏽⚕️ 👨🏾⚕️ 👨🏿⚕️
|
||||
👩⚕️ 👩🏻⚕️ 👩🏼⚕️ 👩🏽⚕️ 👩🏾⚕️ 👩🏿⚕️
|
||||
🧑🎓 🧑🏻🎓 🧑🏼🎓 🧑🏽🎓 🧑🏾🎓 🧑🏿🎓
|
||||
👨🎓 👨🏻🎓 👨🏼🎓 👨🏽🎓 👨🏾🎓 👨🏿🎓
|
||||
👩🎓 👩🏻🎓 👩🏼🎓 👩🏽🎓 👩🏾🎓 👩🏿🎓
|
||||
🧑🏫 🧑🏻🏫 🧑🏼🏫 🧑🏽🏫 🧑🏾🏫 🧑🏿🏫
|
||||
👨🏫 👨🏻🏫 👨🏼🏫 👨🏽🏫 👨🏾🏫 👨🏿🏫
|
||||
👩🏫 👩🏻🏫 👩🏼🏫 👩🏽🏫 👩🏾🏫 👩🏿🏫
|
||||
🧑⚖️ 🧑🏻⚖️ 🧑🏼⚖️ 🧑🏽⚖️ 🧑🏾⚖️ 🧑🏿⚖️
|
||||
👨⚖️ 👨🏻⚖️ 👨🏼⚖️ 👨🏽⚖️ 👨🏾⚖️ 👨🏿⚖️
|
||||
👩⚖️ 👩🏻⚖️ 👩🏼⚖️ 👩🏽⚖️ 👩🏾⚖️ 👩🏿⚖️
|
||||
🧑🌾 🧑🏻🌾 🧑🏼🌾 🧑🏽🌾 🧑🏾🌾 🧑🏿🌾
|
||||
👨🌾 👨🏻🌾 👨🏼🌾 👨🏽🌾 👨🏾🌾 👨🏿🌾
|
||||
👩🌾 👩🏻🌾 👩🏼🌾 👩🏽🌾 👩🏾🌾 👩🏿🌾
|
||||
🧑🍳 🧑🏻🍳 🧑🏼🍳 🧑🏽🍳 🧑🏾🍳 🧑🏿🍳
|
||||
👨🍳 👨🏻🍳 👨🏼🍳 👨🏽🍳 👨🏾🍳 👨🏿🍳
|
||||
👩🍳 👩🏻🍳 👩🏼🍳 👩🏽🍳 👩🏾🍳 👩🏿🍳
|
||||
🧑🔧 🧑🏻🔧 🧑🏼🔧 🧑🏽🔧 🧑🏾🔧 🧑🏿🔧
|
||||
👨🔧 👨🏻🔧 👨🏼🔧 👨🏽🔧 👨🏾🔧 👨🏿🔧
|
||||
👩🔧 👩🏻🔧 👩🏼🔧 👩🏽🔧 👩🏾🔧 👩🏿🔧
|
||||
🧑🏭 🧑🏻🏭 🧑🏼🏭 🧑🏽🏭 🧑🏾🏭 🧑🏿🏭
|
||||
👨🏭 👨🏻🏭 👨🏼🏭 👨🏽🏭 👨🏾🏭 👨🏿🏭
|
||||
👩🏭 👩🏻🏭 👩🏼🏭 👩🏽🏭 👩🏾🏭 👩🏿🏭
|
||||
🧑💼 🧑🏻💼 🧑🏼💼 🧑🏽💼 🧑🏾💼 🧑🏿💼
|
||||
👨💼 👨🏻💼 👨🏼💼 👨🏽💼 👨🏾💼 👨🏿💼
|
||||
👩💼 👩🏻💼 👩🏼💼 👩🏽💼 👩🏾💼 👩🏿💼
|
||||
🧑🔬 🧑🏻🔬 🧑🏼🔬 🧑🏽🔬 🧑🏾🔬 🧑🏿🔬
|
||||
👨🔬 👨🏻🔬 👨🏼🔬 👨🏽🔬 👨🏾🔬 👨🏿🔬
|
||||
👩🔬 👩🏻🔬 👩🏼🔬 👩🏽🔬 👩🏾🔬 👩🏿🔬
|
||||
🧑💻 🧑🏻💻 🧑🏼💻 🧑🏽💻 🧑🏾💻 🧑🏿💻
|
||||
👨💻 👨🏻💻 👨🏼💻 👨🏽💻 👨🏾💻 👨🏿💻
|
||||
👩💻 👩🏻💻 👩🏼💻 👩🏽💻 👩🏾💻 👩🏿💻
|
||||
🧑🎤 🧑🏻🎤 🧑🏼🎤 🧑🏽🎤 🧑🏾🎤 🧑🏿🎤
|
||||
👨🎤 👨🏻🎤 👨🏼🎤 👨🏽🎤 👨🏾🎤 👨🏿🎤
|
||||
👩🎤 👩🏻🎤 👩🏼🎤 👩🏽🎤 👩🏾🎤 👩🏿🎤
|
||||
🧑🎨 🧑🏻🎨 🧑🏼🎨 🧑🏽🎨 🧑🏾🎨 🧑🏿🎨
|
||||
👨🎨 👨🏻🎨 👨🏼🎨 👨🏽🎨 👨🏾🎨 👨🏿🎨
|
||||
👩🎨 👩🏻🎨 👩🏼🎨 👩🏽🎨 👩🏾🎨 👩🏿🎨
|
||||
🧑✈️ 🧑🏻✈️ 🧑🏼✈️ 🧑🏽✈️ 🧑🏾✈️ 🧑🏿✈️
|
||||
👨✈️ 👨🏻✈️ 👨🏼✈️ 👨🏽✈️ 👨🏾✈️ 👨🏿✈️
|
||||
👩✈️ 👩🏻✈️ 👩🏼✈️ 👩🏽✈️ 👩🏾✈️ 👩🏿✈️
|
||||
🧑🚀 🧑🏻🚀 🧑🏼🚀 🧑🏽🚀 🧑🏾🚀 🧑🏿🚀
|
||||
👨🚀 👨🏻🚀 👨🏼🚀 👨🏽🚀 👨🏾🚀 👨🏿🚀
|
||||
👩🚀 👩🏻🚀 👩🏼🚀 👩🏽🚀 👩🏾🚀 👩🏿🚀
|
||||
🧑🚒 🧑🏻🚒 🧑🏼🚒 🧑🏽🚒 🧑🏾🚒 🧑🏿🚒
|
||||
👨🚒 👨🏻🚒 👨🏼🚒 👨🏽🚒 👨🏾🚒 👨🏿🚒
|
||||
👩🚒 👩🏻🚒 👩🏼🚒 👩🏽🚒 👩🏾🚒 👩🏿🚒
|
||||
👮 👮🏻 👮🏼 👮🏽 👮🏾 👮🏿
|
||||
👮♂️ 👮🏻♂️ 👮🏼♂️ 👮🏽♂️ 👮🏾♂️ 👮🏿♂️
|
||||
👮♀️ 👮🏻♀️ 👮🏼♀️ 👮🏽♀️ 👮🏾♀️ 👮🏿♀️
|
||||
🕵️ 🕵🏻 🕵🏼 🕵🏽 🕵🏾 🕵🏿
|
||||
🕵️♂️ 🕵🏻♂️ 🕵🏼♂️ 🕵🏽♂️ 🕵🏾♂️ 🕵🏿♂️
|
||||
🕵️♀️ 🕵🏻♀️ 🕵🏼♀️ 🕵🏽♀️ 🕵🏾♀️ 🕵🏿♀️
|
||||
💂 💂🏻 💂🏼 💂🏽 💂🏾 💂🏿
|
||||
💂♂️ 💂🏻♂️ 💂🏼♂️ 💂🏽♂️ 💂🏾♂️ 💂🏿♂️
|
||||
💂♀️ 💂🏻♀️ 💂🏼♀️ 💂🏽♀️ 💂🏾♀️ 💂🏿♀️
|
||||
🥷 🥷🏻 🥷🏼 🥷🏽 🥷🏾 🥷🏿
|
||||
👷 👷🏻 👷🏼 👷🏽 👷🏾 👷🏿
|
||||
👷♂️ 👷🏻♂️ 👷🏼♂️ 👷🏽♂️ 👷🏾♂️ 👷🏿♂️
|
||||
👷♀️ 👷🏻♀️ 👷🏼♀️ 👷🏽♀️ 👷🏾♀️ 👷🏿♀️
|
||||
🫅 🫅🏻 🫅🏼 🫅🏽 🫅🏾 🫅🏿
|
||||
🤴 🤴🏻 🤴🏼 🤴🏽 🤴🏾 🤴🏿
|
||||
👸 👸🏻 👸🏼 👸🏽 👸🏾 👸🏿
|
||||
👳 👳🏻 👳🏼 👳🏽 👳🏾 👳🏿
|
||||
👳♂️ 👳🏻♂️ 👳🏼♂️ 👳🏽♂️ 👳🏾♂️ 👳🏿♂️
|
||||
👳♀️ 👳🏻♀️ 👳🏼♀️ 👳🏽♀️ 👳🏾♀️ 👳🏿♀️
|
||||
👲 👲🏻 👲🏼 👲🏽 👲🏾 👲🏿
|
||||
🧕 🧕🏻 🧕🏼 🧕🏽 🧕🏾 🧕🏿
|
||||
🤵 🤵🏻 🤵🏼 🤵🏽 🤵🏾 🤵🏿
|
||||
🤵♂️ 🤵🏻♂️ 🤵🏼♂️ 🤵🏽♂️ 🤵🏾♂️ 🤵🏿♂️
|
||||
🤵♀️ 🤵🏻♀️ 🤵🏼♀️ 🤵🏽♀️ 🤵🏾♀️ 🤵🏿♀️
|
||||
👰 👰🏻 👰🏼 👰🏽 👰🏾 👰🏿
|
||||
👰♂️ 👰🏻♂️ 👰🏼♂️ 👰🏽♂️ 👰🏾♂️ 👰🏿♂️
|
||||
👰♀️ 👰🏻♀️ 👰🏼♀️ 👰🏽♀️ 👰🏾♀️ 👰🏿♀️
|
||||
🤰 🤰🏻 🤰🏼 🤰🏽 🤰🏾 🤰🏿
|
||||
🫃 🫃🏻 🫃🏼 🫃🏽 🫃🏾 🫃🏿
|
||||
🫄 🫄🏻 🫄🏼 🫄🏽 🫄🏾 🫄🏿
|
||||
🤱 🤱🏻 🤱🏼 🤱🏽 🤱🏾 🤱🏿
|
||||
👩🍼 👩🏻🍼 👩🏼🍼 👩🏽🍼 👩🏾🍼 👩🏿🍼
|
||||
👨🍼 👨🏻🍼 👨🏼🍼 👨🏽🍼 👨🏾🍼 👨🏿🍼
|
||||
🧑🍼 🧑🏻🍼 🧑🏼🍼 🧑🏽🍼 🧑🏾🍼 🧑🏿🍼
|
||||
👼 👼🏻 👼🏼 👼🏽 👼🏾 👼🏿
|
||||
🎅 🎅🏻 🎅🏼 🎅🏽 🎅🏾 🎅🏿
|
||||
🤶 🤶🏻 🤶🏼 🤶🏽 🤶🏾 🤶🏿
|
||||
🧑🎄 🧑🏻🎄 🧑🏼🎄 🧑🏽🎄 🧑🏾🎄 🧑🏿🎄
|
||||
🦸 🦸🏻 🦸🏼 🦸🏽 🦸🏾 🦸🏿
|
||||
🦸♂️ 🦸🏻♂️ 🦸🏼♂️ 🦸🏽♂️ 🦸🏾♂️ 🦸🏿♂️
|
||||
🦸♀️ 🦸🏻♀️ 🦸🏼♀️ 🦸🏽♀️ 🦸🏾♀️ 🦸🏿♀️
|
||||
🦹 🦹🏻 🦹🏼 🦹🏽 🦹🏾 🦹🏿
|
||||
🦹♂️ 🦹🏻♂️ 🦹🏼♂️ 🦹🏽♂️ 🦹🏾♂️ 🦹🏿♂️
|
||||
🦹♀️ 🦹🏻♀️ 🦹🏼♀️ 🦹🏽♀️ 🦹🏾♀️ 🦹🏿♀️
|
||||
🧙 🧙🏻 🧙🏼 🧙🏽 🧙🏾 🧙🏿
|
||||
🧙♂️ 🧙🏻♂️ 🧙🏼♂️ 🧙🏽♂️ 🧙🏾♂️ 🧙🏿♂️
|
||||
🧙♀️ 🧙🏻♀️ 🧙🏼♀️ 🧙🏽♀️ 🧙🏾♀️ 🧙🏿♀️
|
||||
🧚 🧚🏻 🧚🏼 🧚🏽 🧚🏾 🧚🏿
|
||||
🧚♂️ 🧚🏻♂️ 🧚🏼♂️ 🧚🏽♂️ 🧚🏾♂️ 🧚🏿♂️
|
||||
🧚♀️ 🧚🏻♀️ 🧚🏼♀️ 🧚🏽♀️ 🧚🏾♀️ 🧚🏿♀️
|
||||
🧛 🧛🏻 🧛🏼 🧛🏽 🧛🏾 🧛🏿
|
||||
🧛♂️ 🧛🏻♂️ 🧛🏼♂️ 🧛🏽♂️ 🧛🏾♂️ 🧛🏿♂️
|
||||
🧛♀️ 🧛🏻♀️ 🧛🏼♀️ 🧛🏽♀️ 🧛🏾♀️ 🧛🏿♀️
|
||||
🧜 🧜🏻 🧜🏼 🧜🏽 🧜🏾 🧜🏿
|
||||
🧜♂️ 🧜🏻♂️ 🧜🏼♂️ 🧜🏽♂️ 🧜🏾♂️ 🧜🏿♂️
|
||||
🧜♀️ 🧜🏻♀️ 🧜🏼♀️ 🧜🏽♀️ 🧜🏾♀️ 🧜🏿♀️
|
||||
🧝 🧝🏻 🧝🏼 🧝🏽 🧝🏾 🧝🏿
|
||||
🧝♂️ 🧝🏻♂️ 🧝🏼♂️ 🧝🏽♂️ 🧝🏾♂️ 🧝🏿♂️
|
||||
🧝♀️ 🧝🏻♀️ 🧝🏼♀️ 🧝🏽♀️ 🧝🏾♀️ 🧝🏿♀️
|
||||
🧞
|
||||
🧞♂️
|
||||
🧞♀️
|
||||
🧟
|
||||
🧟♂️
|
||||
🧟♀️
|
||||
🧌
|
||||
💆 💆🏻 💆🏼 💆🏽 💆🏾 💆🏿
|
||||
💆♂️ 💆🏻♂️ 💆🏼♂️ 💆🏽♂️ 💆🏾♂️ 💆🏿♂️
|
||||
💆♀️ 💆🏻♀️ 💆🏼♀️ 💆🏽♀️ 💆🏾♀️ 💆🏿♀️
|
||||
💇 💇🏻 💇🏼 💇🏽 💇🏾 💇🏿
|
||||
💇♂️ 💇🏻♂️ 💇🏼♂️ 💇🏽♂️ 💇🏾♂️ 💇🏿♂️
|
||||
💇♀️ 💇🏻♀️ 💇🏼♀️ 💇🏽♀️ 💇🏾♀️ 💇🏿♀️
|
||||
🚶 🚶🏻 🚶🏼 🚶🏽 🚶🏾 🚶🏿
|
||||
🚶♂️ 🚶🏻♂️ 🚶🏼♂️ 🚶🏽♂️ 🚶🏾♂️ 🚶🏿♂️
|
||||
🚶♀️ 🚶🏻♀️ 🚶🏼♀️ 🚶🏽♀️ 🚶🏾♀️ 🚶🏿♀️
|
||||
🚶➡️ 🚶🏻➡️ 🚶🏼➡️ 🚶🏽➡️ 🚶🏾➡️ 🚶🏿➡️
|
||||
🚶♀️➡️ 🚶🏻♀️➡️ 🚶🏼♀️➡️ 🚶🏽♀️➡️ 🚶🏾♀️➡️ 🚶🏿♀️➡️
|
||||
🚶♂️➡️ 🚶🏻♂️➡️ 🚶🏼♂️➡️ 🚶🏽♂️➡️ 🚶🏾♂️➡️ 🚶🏿♂️➡️
|
||||
🧍 🧍🏻 🧍🏼 🧍🏽 🧍🏾 🧍🏿
|
||||
🧍♂️ 🧍🏻♂️ 🧍🏼♂️ 🧍🏽♂️ 🧍🏾♂️ 🧍🏿♂️
|
||||
🧍♀️ 🧍🏻♀️ 🧍🏼♀️ 🧍🏽♀️ 🧍🏾♀️ 🧍🏿♀️
|
||||
🧎 🧎🏻 🧎🏼 🧎🏽 🧎🏾 🧎🏿
|
||||
🧎♂️ 🧎🏻♂️ 🧎🏼♂️ 🧎🏽♂️ 🧎🏾♂️ 🧎🏿♂️
|
||||
🧎♀️ 🧎🏻♀️ 🧎🏼♀️ 🧎🏽♀️ 🧎🏾♀️ 🧎🏿♀️
|
||||
🧎➡️ 🧎🏻➡️ 🧎🏼➡️ 🧎🏽➡️ 🧎🏾➡️ 🧎🏿➡️
|
||||
🧎♀️➡️ 🧎🏻♀️➡️ 🧎🏼♀️➡️ 🧎🏽♀️➡️ 🧎🏾♀️➡️ 🧎🏿♀️➡️
|
||||
🧎♂️➡️ 🧎🏻♂️➡️ 🧎🏼♂️➡️ 🧎🏽♂️➡️ 🧎🏾♂️➡️ 🧎🏿♂️➡️
|
||||
🧑🦯 🧑🏻🦯 🧑🏼🦯 🧑🏽🦯 🧑🏾🦯 🧑🏿🦯
|
||||
🧑🦯➡️ 🧑🏻🦯➡️ 🧑🏼🦯➡️ 🧑🏽🦯➡️ 🧑🏾🦯➡️ 🧑🏿🦯➡️
|
||||
👨🦯 👨🏻🦯 👨🏼🦯 👨🏽🦯 👨🏾🦯 👨🏿🦯
|
||||
👨🦯➡️ 👨🏻🦯➡️ 👨🏼🦯➡️ 👨🏽🦯➡️ 👨🏾🦯➡️ 👨🏿🦯➡️
|
||||
👩🦯 👩🏻🦯 👩🏼🦯 👩🏽🦯 👩🏾🦯 👩🏿🦯
|
||||
👩🦯➡️ 👩🏻🦯➡️ 👩🏼🦯➡️ 👩🏽🦯➡️ 👩🏾🦯➡️ 👩🏿🦯➡️
|
||||
🧑🦼 🧑🏻🦼 🧑🏼🦼 🧑🏽🦼 🧑🏾🦼 🧑🏿🦼
|
||||
🧑🦼➡️ 🧑🏻🦼➡️ 🧑🏼🦼➡️ 🧑🏽🦼➡️ 🧑🏾🦼➡️ 🧑🏿🦼➡️
|
||||
👨🦼 👨🏻🦼 👨🏼🦼 👨🏽🦼 👨🏾🦼 👨🏿🦼
|
||||
👨🦼➡️ 👨🏻🦼➡️ 👨🏼🦼➡️ 👨🏽🦼➡️ 👨🏾🦼➡️ 👨🏿🦼➡️
|
||||
👩🦼 👩🏻🦼 👩🏼🦼 👩🏽🦼 👩🏾🦼 👩🏿🦼
|
||||
👩🦼➡️ 👩🏻🦼➡️ 👩🏼🦼➡️ 👩🏽🦼➡️ 👩🏾🦼➡️ 👩🏿🦼➡️
|
||||
🧑🦽 🧑🏻🦽 🧑🏼🦽 🧑🏽🦽 🧑🏾🦽 🧑🏿🦽
|
||||
🧑🦽➡️ 🧑🏻🦽➡️ 🧑🏼🦽➡️ 🧑🏽🦽➡️ 🧑🏾🦽➡️ 🧑🏿🦽➡️
|
||||
👨🦽 👨🏻🦽 👨🏼🦽 👨🏽🦽 👨🏾🦽 👨🏿🦽
|
||||
👨🦽➡️ 👨🏻🦽➡️ 👨🏼🦽➡️ 👨🏽🦽➡️ 👨🏾🦽➡️ 👨🏿🦽➡️
|
||||
👩🦽 👩🏻🦽 👩🏼🦽 👩🏽🦽 👩🏾🦽 👩🏿🦽
|
||||
👩🦽➡️ 👩🏻🦽➡️ 👩🏼🦽➡️ 👩🏽🦽➡️ 👩🏾🦽➡️ 👩🏿🦽➡️
|
||||
🏃 🏃🏻 🏃🏼 🏃🏽 🏃🏾 🏃🏿
|
||||
🏃♂️ 🏃🏻♂️ 🏃🏼♂️ 🏃🏽♂️ 🏃🏾♂️ 🏃🏿♂️
|
||||
🏃♀️ 🏃🏻♀️ 🏃🏼♀️ 🏃🏽♀️ 🏃🏾♀️ 🏃🏿♀️
|
||||
🏃➡️ 🏃🏻➡️ 🏃🏼➡️ 🏃🏽➡️ 🏃🏾➡️ 🏃🏿➡️
|
||||
🏃♀️➡️ 🏃🏻♀️➡️ 🏃🏼♀️➡️ 🏃🏽♀️➡️ 🏃🏾♀️➡️ 🏃🏿♀️➡️
|
||||
🏃♂️➡️ 🏃🏻♂️➡️ 🏃🏼♂️➡️ 🏃🏽♂️➡️ 🏃🏾♂️➡️ 🏃🏿♂️➡️
|
||||
💃 💃🏻 💃🏼 💃🏽 💃🏾 💃🏿
|
||||
🕺 🕺🏻 🕺🏼 🕺🏽 🕺🏾 🕺🏿
|
||||
🕴️ 🕴🏻 🕴🏼 🕴🏽 🕴🏾 🕴🏿
|
||||
👯
|
||||
👯♂️
|
||||
👯♀️
|
||||
🧖 🧖🏻 🧖🏼 🧖🏽 🧖🏾 🧖🏿
|
||||
🧖♂️ 🧖🏻♂️ 🧖🏼♂️ 🧖🏽♂️ 🧖🏾♂️ 🧖🏿♂️
|
||||
🧖♀️ 🧖🏻♀️ 🧖🏼♀️ 🧖🏽♀️ 🧖🏾♀️ 🧖🏿♀️
|
||||
🧗 🧗🏻 🧗🏼 🧗🏽 🧗🏾 🧗🏿
|
||||
🧗♂️ 🧗🏻♂️ 🧗🏼♂️ 🧗🏽♂️ 🧗🏾♂️ 🧗🏿♂️
|
||||
🧗♀️ 🧗🏻♀️ 🧗🏼♀️ 🧗🏽♀️ 🧗🏾♀️ 🧗🏿♀️
|
||||
🤺
|
||||
🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿
|
||||
⛷️
|
||||
🏂 🏂🏻 🏂🏼 🏂🏽 🏂🏾 🏂🏿
|
||||
🏌️ 🏌🏻 🏌🏼 🏌🏽 🏌🏾 🏌🏿
|
||||
🏌️♂️ 🏌🏻♂️ 🏌🏼♂️ 🏌🏽♂️ 🏌🏾♂️ 🏌🏿♂️
|
||||
🏌️♀️ 🏌🏻♀️ 🏌🏼♀️ 🏌🏽♀️ 🏌🏾♀️ 🏌🏿♀️
|
||||
🏄 🏄🏻 🏄🏼 🏄🏽 🏄🏾 🏄🏿
|
||||
🏄♂️ 🏄🏻♂️ 🏄🏼♂️ 🏄🏽♂️ 🏄🏾♂️ 🏄🏿♂️
|
||||
🏄♀️ 🏄🏻♀️ 🏄🏼♀️ 🏄🏽♀️ 🏄🏾♀️ 🏄🏿♀️
|
||||
🚣 🚣🏻 🚣🏼 🚣🏽 🚣🏾 🚣🏿
|
||||
🚣♂️ 🚣🏻♂️ 🚣🏼♂️ 🚣🏽♂️ 🚣🏾♂️ 🚣🏿♂️
|
||||
🚣♀️ 🚣🏻♀️ 🚣🏼♀️ 🚣🏽♀️ 🚣🏾♀️ 🚣🏿♀️
|
||||
🏊 🏊🏻 🏊🏼 🏊🏽 🏊🏾 🏊🏿
|
||||
🏊♂️ 🏊🏻♂️ 🏊🏼♂️ 🏊🏽♂️ 🏊🏾♂️ 🏊🏿♂️
|
||||
🏊♀️ 🏊🏻♀️ 🏊🏼♀️ 🏊🏽♀️ 🏊🏾♀️ 🏊🏿♀️
|
||||
⛹️ ⛹🏻 ⛹🏼 ⛹🏽 ⛹🏾 ⛹🏿
|
||||
⛹️♂️ ⛹🏻♂️ ⛹🏼♂️ ⛹🏽♂️ ⛹🏾♂️ ⛹🏿♂️
|
||||
⛹️♀️ ⛹🏻♀️ ⛹🏼♀️ ⛹🏽♀️ ⛹🏾♀️ ⛹🏿♀️
|
||||
🏋️ 🏋🏻 🏋🏼 🏋🏽 🏋🏾 🏋🏿
|
||||
🏋️♂️ 🏋🏻♂️ 🏋🏼♂️ 🏋🏽♂️ 🏋🏾♂️ 🏋🏿♂️
|
||||
🏋️♀️ 🏋🏻♀️ 🏋🏼♀️ 🏋🏽♀️ 🏋🏾♀️ 🏋🏿♀️
|
||||
🚴 🚴🏻 🚴🏼 🚴🏽 🚴🏾 🚴🏿
|
||||
🚴♂️ 🚴🏻♂️ 🚴🏼♂️ 🚴🏽♂️ 🚴🏾♂️ 🚴🏿♂️
|
||||
🚴♀️ 🚴🏻♀️ 🚴🏼♀️ 🚴🏽♀️ 🚴🏾♀️ 🚴🏿♀️
|
||||
🚵 🚵🏻 🚵🏼 🚵🏽 🚵🏾 🚵🏿
|
||||
🚵♂️ 🚵🏻♂️ 🚵🏼♂️ 🚵🏽♂️ 🚵🏾♂️ 🚵🏿♂️
|
||||
🚵♀️ 🚵🏻♀️ 🚵🏼♀️ 🚵🏽♀️ 🚵🏾♀️ 🚵🏿♀️
|
||||
🤸 🤸🏻 🤸🏼 🤸🏽 🤸🏾 🤸🏿
|
||||
🤸♂️ 🤸🏻♂️ 🤸🏼♂️ 🤸🏽♂️ 🤸🏾♂️ 🤸🏿♂️
|
||||
🤸♀️ 🤸🏻♀️ 🤸🏼♀️ 🤸🏽♀️ 🤸🏾♀️ 🤸🏿♀️
|
||||
🤼
|
||||
🤼♂️
|
||||
🤼♀️
|
||||
🤽 🤽🏻 🤽🏼 🤽🏽 🤽🏾 🤽🏿
|
||||
🤽♂️ 🤽🏻♂️ 🤽🏼♂️ 🤽🏽♂️ 🤽🏾♂️ 🤽🏿♂️
|
||||
🤽♀️ 🤽🏻♀️ 🤽🏼♀️ 🤽🏽♀️ 🤽🏾♀️ 🤽🏿♀️
|
||||
🤾 🤾🏻 🤾🏼 🤾🏽 🤾🏾 🤾🏿
|
||||
🤾♂️ 🤾🏻♂️ 🤾🏼♂️ 🤾🏽♂️ 🤾🏾♂️ 🤾🏿♂️
|
||||
🤾♀️ 🤾🏻♀️ 🤾🏼♀️ 🤾🏽♀️ 🤾🏾♀️ 🤾🏿♀️
|
||||
🤹 🤹🏻 🤹🏼 🤹🏽 🤹🏾 🤹🏿
|
||||
🤹♂️ 🤹🏻♂️ 🤹🏼♂️ 🤹🏽♂️ 🤹🏾♂️ 🤹🏿♂️
|
||||
🤹♀️ 🤹🏻♀️ 🤹🏼♀️ 🤹🏽♀️ 🤹🏾♀️ 🤹🏿♀️
|
||||
🧘 🧘🏻 🧘🏼 🧘🏽 🧘🏾 🧘🏿
|
||||
🧘♂️ 🧘🏻♂️ 🧘🏼♂️ 🧘🏽♂️ 🧘🏾♂️ 🧘🏿♂️
|
||||
🧘♀️ 🧘🏻♀️ 🧘🏼♀️ 🧘🏽♀️ 🧘🏾♀️ 🧘🏿♀️
|
||||
🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿
|
||||
🛌 🛌🏻 🛌🏼 🛌🏽 🛌🏾 🛌🏿
|
||||
🧑🤝🧑 🧑🏻🤝🧑🏻 🧑🏻🤝🧑🏼 🧑🏻🤝🧑🏽 🧑🏻🤝🧑🏾 🧑🏻🤝🧑🏿 🧑🏼🤝🧑🏻 🧑🏼🤝🧑🏼 🧑🏼🤝🧑🏽 🧑🏼🤝🧑🏾 🧑🏼🤝🧑🏿 🧑🏽🤝🧑🏻 🧑🏽🤝🧑🏼 🧑🏽🤝🧑🏽 🧑🏽🤝🧑🏾 🧑🏽🤝🧑🏿 🧑🏾🤝🧑🏻 🧑🏾🤝🧑🏼 🧑🏾🤝🧑🏽 🧑🏾🤝🧑🏾 🧑🏾🤝🧑🏿 🧑🏿🤝🧑🏻 🧑🏿🤝🧑🏼 🧑🏿🤝🧑🏽 🧑🏿🤝🧑🏾 🧑🏿🤝🧑🏿
|
||||
👭 👭🏻 👭🏼 👭🏽 👭🏾 👭🏿
|
||||
👫 👫🏻 👫🏼 👫🏽 👫🏾 👫🏿
|
||||
👬 👬🏻 👬🏼 👬🏽 👬🏾 👬🏿
|
||||
💏 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿
|
||||
👩❤️💋👨 👩🏻❤️💋👨🏻 👩🏻❤️💋👨🏼 👩🏻❤️💋👨🏽 👩🏻❤️💋👨🏾 👩🏻❤️💋👨🏿 👩🏼❤️💋👨🏻 👩🏼❤️💋👨🏼 👩🏼❤️💋👨🏽 👩🏼❤️💋👨🏾 👩🏼❤️💋👨🏿 👩🏽❤️💋👨🏻 👩🏽❤️💋👨🏼 👩🏽❤️💋👨🏽 👩🏽❤️💋👨🏾 👩🏽❤️💋👨🏿 👩🏾❤️💋👨🏻 👩🏾❤️💋👨🏼 👩🏾❤️💋👨🏽 👩🏾❤️💋👨🏾 👩🏾❤️💋👨🏿 👩🏿❤️💋👨🏻 👩🏿❤️💋👨🏼 👩🏿❤️💋👨🏽 👩🏿❤️💋👨🏾 👩🏿❤️💋👨🏿
|
||||
👨❤️💋👨 👨🏻❤️💋👨🏻 👨🏻❤️💋👨🏼 👨🏻❤️💋👨🏽 👨🏻❤️💋👨🏾 👨🏻❤️💋👨🏿 👨🏼❤️💋👨🏻 👨🏼❤️💋👨🏼 👨🏼❤️💋👨🏽 👨🏼❤️💋👨🏾 👨🏼❤️💋👨🏿 👨🏽❤️💋👨🏻 👨🏽❤️💋👨🏼 👨🏽❤️💋👨🏽 👨🏽❤️💋👨🏾 👨🏽❤️💋👨🏿 👨🏾❤️💋👨🏻 👨🏾❤️💋👨🏼 👨🏾❤️💋👨🏽 👨🏾❤️💋👨🏾 👨🏾❤️💋👨🏿 👨🏿❤️💋👨🏻 👨🏿❤️💋👨🏼 👨🏿❤️💋👨🏽 👨🏿❤️💋👨🏾 👨🏿❤️💋👨🏿
|
||||
👩❤️💋👩 👩🏻❤️💋👩🏻 👩🏻❤️💋👩🏼 👩🏻❤️💋👩🏽 👩🏻❤️💋👩🏾 👩🏻❤️💋👩🏿 👩🏼❤️💋👩🏻 👩🏼❤️💋👩🏼 👩🏼❤️💋👩🏽 👩🏼❤️💋👩🏾 👩🏼❤️💋👩🏿 👩🏽❤️💋👩🏻 👩🏽❤️💋👩🏼 👩🏽❤️💋👩🏽 👩🏽❤️💋👩🏾 👩🏽❤️💋👩🏿 👩🏾❤️💋👩🏻 👩🏾❤️💋👩🏼 👩🏾❤️💋👩🏽 👩🏾❤️💋👩🏾 👩🏾❤️💋👩🏿 👩🏿❤️💋👩🏻 👩🏿❤️💋👩🏼 👩🏿❤️💋👩🏽 👩🏿❤️💋👩🏾 👩🏿❤️💋👩🏿
|
||||
💑 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿
|
||||
👩❤️👨 👩🏻❤️👨🏻 👩🏻❤️👨🏼 👩🏻❤️👨🏽 👩🏻❤️👨🏾 👩🏻❤️👨🏿 👩🏼❤️👨🏻 👩🏼❤️👨🏼 👩🏼❤️👨🏽 👩🏼❤️👨🏾 👩🏼❤️👨🏿 👩🏽❤️👨🏻 👩🏽❤️👨🏼 👩🏽❤️👨🏽 👩🏽❤️👨🏾 👩🏽❤️👨🏿 👩🏾❤️👨🏻 👩🏾❤️👨🏼 👩🏾❤️👨🏽 👩🏾❤️👨🏾 👩🏾❤️👨🏿 👩🏿❤️👨🏻 👩🏿❤️👨🏼 👩🏿❤️👨🏽 👩🏿❤️👨🏾 👩🏿❤️👨🏿
|
||||
👨❤️👨 👨🏻❤️👨🏻 👨🏻❤️👨🏼 👨🏻❤️👨🏽 👨🏻❤️👨🏾 👨🏻❤️👨🏿 👨🏼❤️👨🏻 👨🏼❤️👨🏼 👨🏼❤️👨🏽 👨🏼❤️👨🏾 👨🏼❤️👨🏿 👨🏽❤️👨🏻 👨🏽❤️👨🏼 👨🏽❤️👨🏽 👨🏽❤️👨🏾 👨🏽❤️👨🏿 👨🏾❤️👨🏻 👨🏾❤️👨🏼 👨🏾❤️👨🏽 👨🏾❤️👨🏾 👨🏾❤️👨🏿 👨🏿❤️👨🏻 👨🏿❤️👨🏼 👨🏿❤️👨🏽 👨🏿❤️👨🏾 👨🏿❤️👨🏿
|
||||
👩❤️👩 👩🏻❤️👩🏻 👩🏻❤️👩🏼 👩🏻❤️👩🏽 👩🏻❤️👩🏾 👩🏻❤️👩🏿 👩🏼❤️👩🏻 👩🏼❤️👩🏼 👩🏼❤️👩🏽 👩🏼❤️👩🏾 👩🏼❤️👩🏿 👩🏽❤️👩🏻 👩🏽❤️👩🏼 👩🏽❤️👩🏽 👩🏽❤️👩🏾 👩🏽❤️👩🏿 👩🏾❤️👩🏻 👩🏾❤️👩🏼 👩🏾❤️👩🏽 👩🏾❤️👩🏾 👩🏾❤️👩🏿 👩🏿❤️👩🏻 👩🏿❤️👩🏼 👩🏿❤️👩🏽 👩🏿❤️👩🏾 👩🏿❤️👩🏿
|
||||
👨👩👦
|
||||
👨👩👧
|
||||
👨👩👧👦
|
||||
👨👩👦👦
|
||||
👨👩👧👧
|
||||
👨👨👦
|
||||
👨👨👧
|
||||
👨👨👧👦
|
||||
👨👨👦👦
|
||||
👨👨👧👧
|
||||
👩👩👦
|
||||
👩👩👧
|
||||
👩👩👧👦
|
||||
👩👩👦👦
|
||||
👩👩👧👧
|
||||
👨👦
|
||||
👨👦👦
|
||||
👨👧
|
||||
👨👧👦
|
||||
👨👧👧
|
||||
👩👦
|
||||
👩👦👦
|
||||
👩👧
|
||||
👩👧👦
|
||||
👩👧👧
|
||||
🗣️
|
||||
👤
|
||||
👥
|
||||
🫂
|
||||
👪
|
||||
🧑🧑🧒
|
||||
🧑🧑🧒🧒
|
||||
🧑🧒
|
||||
🧑🧒🧒
|
||||
👣
|
||||
|
169
app/src/main/assets/emoji/SMILEYS_AND_EMOTION.txt
Normal file
169
app/src/main/assets/emoji/SMILEYS_AND_EMOTION.txt
Normal file
|
@ -0,0 +1,169 @@
|
|||
😀
|
||||
😃
|
||||
😄
|
||||
😁
|
||||
😆
|
||||
😅
|
||||
🤣
|
||||
😂
|
||||
🙂
|
||||
🙃
|
||||
🫠
|
||||
😉
|
||||
😊
|
||||
😇
|
||||
🥰
|
||||
😍
|
||||
🤩
|
||||
😘
|
||||
😗
|
||||
☺️
|
||||
😚
|
||||
😙
|
||||
🥲
|
||||
😋
|
||||
😛
|
||||
😜
|
||||
🤪
|
||||
😝
|
||||
🤑
|
||||
🤗
|
||||
🤭
|
||||
🫢
|
||||
🫣
|
||||
🤫
|
||||
🤔
|
||||
🫡
|
||||
🤐
|
||||
🤨
|
||||
😐
|
||||
😑
|
||||
😶
|
||||
🫥
|
||||
😶🌫️
|
||||
😏
|
||||
😒
|
||||
🙄
|
||||
😬
|
||||
😮💨
|
||||
🤥
|
||||
🫨
|
||||
🙂↔️
|
||||
🙂↕️
|
||||
😌
|
||||
😔
|
||||
😪
|
||||
🤤
|
||||
😴
|
||||
|
||||
😷
|
||||
🤒
|
||||
🤕
|
||||
🤢
|
||||
🤮
|
||||
🤧
|
||||
🥵
|
||||
🥶
|
||||
🥴
|
||||
😵
|
||||
😵💫
|
||||
🤯
|
||||
🤠
|
||||
🥳
|
||||
🥸
|
||||
😎
|
||||
🤓
|
||||
🧐
|
||||
😕
|
||||
🫤
|
||||
😟
|
||||
🙁
|
||||
☹️
|
||||
😮
|
||||
😯
|
||||
😲
|
||||
😳
|
||||
🥺
|
||||
🥹
|
||||
😦
|
||||
😧
|
||||
😨
|
||||
😰
|
||||
😥
|
||||
😢
|
||||
😭
|
||||
😱
|
||||
😖
|
||||
😣
|
||||
😞
|
||||
😓
|
||||
😩
|
||||
😫
|
||||
🥱
|
||||
😤
|
||||
😡
|
||||
😠
|
||||
🤬
|
||||
😈
|
||||
👿
|
||||
💀
|
||||
☠️
|
||||
💩
|
||||
🤡
|
||||
👹
|
||||
👺
|
||||
👻
|
||||
👽
|
||||
👾
|
||||
🤖
|
||||
😺
|
||||
😸
|
||||
😹
|
||||
😻
|
||||
😼
|
||||
😽
|
||||
🙀
|
||||
😿
|
||||
😾
|
||||
🙈
|
||||
🙉
|
||||
🙊
|
||||
💌
|
||||
💘
|
||||
💝
|
||||
💖
|
||||
💗
|
||||
💓
|
||||
💞
|
||||
💕
|
||||
💟
|
||||
❣️
|
||||
💔
|
||||
❤️🔥
|
||||
❤️🩹
|
||||
❤️
|
||||
🩷
|
||||
🧡
|
||||
💛
|
||||
💚
|
||||
💙
|
||||
🩵
|
||||
💜
|
||||
🤎
|
||||
🖤
|
||||
🩶
|
||||
🤍
|
||||
💋
|
||||
💯
|
||||
💢
|
||||
💥
|
||||
💫
|
||||
💦
|
||||
💨
|
||||
🕳️
|
||||
💬
|
||||
👁️🗨️
|
||||
🗨️
|
||||
🗯️
|
||||
💭
|
||||
💤
|
250
app/src/main/assets/emoji/SYMBOLS.txt
Normal file
250
app/src/main/assets/emoji/SYMBOLS.txt
Normal file
|
@ -0,0 +1,250 @@
|
|||
🏧
|
||||
🚮
|
||||
🚰
|
||||
♿
|
||||
🚹
|
||||
🚺
|
||||
🚻
|
||||
🚼
|
||||
🚾
|
||||
🛂
|
||||
🛃
|
||||
🛄
|
||||
🛅
|
||||
⚠️
|
||||
🚸
|
||||
⛔
|
||||
🚫
|
||||
🚳
|
||||
🚭
|
||||
🚯
|
||||
🚱
|
||||
🚷
|
||||
📵
|
||||
🔞
|
||||
☢️
|
||||
☣️
|
||||
⬆️
|
||||
↗️
|
||||
➡️
|
||||
↘️
|
||||
⬇️
|
||||
↙️
|
||||
⬅️
|
||||
↖️
|
||||
↕️
|
||||
↔️
|
||||
↩️
|
||||
↪️
|
||||
⤴️
|
||||
⤵️
|
||||
🔃
|
||||
🔄
|
||||
🔙
|
||||
🔚
|
||||
🔛
|
||||
🔜
|
||||
🔝
|
||||
🛐
|
||||
⚛️
|
||||
🕉️
|
||||
✡️
|
||||
☸️
|
||||
☯️
|
||||
✝️
|
||||
☦️
|
||||
☪️
|
||||
☮️
|
||||
🕎
|
||||
🔯
|
||||
🪯
|
||||
♈
|
||||
♉
|
||||
♊
|
||||
♋
|
||||
♌
|
||||
♍
|
||||
♎
|
||||
♏
|
||||
♐
|
||||
♑
|
||||
♒
|
||||
♓
|
||||
⛎
|
||||
🔀
|
||||
🔁
|
||||
🔂
|
||||
▶️
|
||||
⏩
|
||||
⏭️
|
||||
⏯️
|
||||
◀️
|
||||
⏪
|
||||
⏮️
|
||||
🔼
|
||||
⏫
|
||||
🔽
|
||||
⏬
|
||||
⏸️
|
||||
⏹️
|
||||
⏺️
|
||||
⏏️
|
||||
🎦
|
||||
🔅
|
||||
🔆
|
||||
📶
|
||||
🛜
|
||||
📳
|
||||
📴
|
||||
♀️
|
||||
♂️
|
||||
⚧️
|
||||
✖️
|
||||
➕
|
||||
➖
|
||||
➗
|
||||
🟰
|
||||
♾️
|
||||
‼️
|
||||
⁉️
|
||||
❓
|
||||
❔
|
||||
❕
|
||||
❗
|
||||
〰️
|
||||
💱
|
||||
💲
|
||||
⚕️
|
||||
♻️
|
||||
⚜️
|
||||
🔱
|
||||
📛
|
||||
🔰
|
||||
⭕
|
||||
✅
|
||||
☑️
|
||||
✔️
|
||||
❌
|
||||
❎
|
||||
➰
|
||||
➿
|
||||
〽️
|
||||
✳️
|
||||
✴️
|
||||
❇️
|
||||
©️
|
||||
®️
|
||||
™️
|
||||
|
||||
🇦
|
||||
🇧
|
||||
🇨
|
||||
🇩
|
||||
🇪
|
||||
🇫
|
||||
🇬
|
||||
🇭
|
||||
🇮
|
||||
🇯
|
||||
🇰
|
||||
🇱
|
||||
🇲
|
||||
🇳
|
||||
🇴
|
||||
🇵
|
||||
🇶
|
||||
🇷
|
||||
🇸
|
||||
🇹
|
||||
🇺
|
||||
🇻
|
||||
🇼
|
||||
🇽
|
||||
🇾
|
||||
🇿
|
||||
#️⃣
|
||||
*️⃣
|
||||
0️⃣
|
||||
1️⃣
|
||||
2️⃣
|
||||
3️⃣
|
||||
4️⃣
|
||||
5️⃣
|
||||
6️⃣
|
||||
7️⃣
|
||||
8️⃣
|
||||
9️⃣
|
||||
🔟
|
||||
🔠
|
||||
🔡
|
||||
🔢
|
||||
🔣
|
||||
🔤
|
||||
🅰️
|
||||
🆎
|
||||
🅱️
|
||||
🆑
|
||||
🆒
|
||||
🆓
|
||||
ℹ️
|
||||
🆔
|
||||
Ⓜ️
|
||||
🆕
|
||||
🆖
|
||||
🅾️
|
||||
🆗
|
||||
🅿️
|
||||
🆘
|
||||
🆙
|
||||
🆚
|
||||
🈁
|
||||
🈂️
|
||||
🈷️
|
||||
🈶
|
||||
🈯
|
||||
🉐
|
||||
🈹
|
||||
🈚
|
||||
🈲
|
||||
🉑
|
||||
🈸
|
||||
🈴
|
||||
🈳
|
||||
㊗️
|
||||
㊙️
|
||||
🈺
|
||||
🈵
|
||||
🔴
|
||||
🟠
|
||||
🟡
|
||||
🟢
|
||||
🔵
|
||||
🟣
|
||||
🟤
|
||||
⚫
|
||||
⚪
|
||||
🟥
|
||||
🟧
|
||||
🟨
|
||||
🟩
|
||||
🟦
|
||||
🟪
|
||||
🟫
|
||||
⬛
|
||||
⬜
|
||||
◼️
|
||||
◻️
|
||||
◾
|
||||
◽
|
||||
▪️
|
||||
▫️
|
||||
🔶
|
||||
🔷
|
||||
🔸
|
||||
🔹
|
||||
🔺
|
||||
🔻
|
||||
💠
|
||||
🔘
|
||||
🔳
|
||||
🔲
|
218
app/src/main/assets/emoji/TRAVEL_AND_PLACES.txt
Normal file
218
app/src/main/assets/emoji/TRAVEL_AND_PLACES.txt
Normal file
|
@ -0,0 +1,218 @@
|
|||
🌍
|
||||
🌎
|
||||
🌏
|
||||
🌐
|
||||
🗺️
|
||||
🗾
|
||||
🧭
|
||||
🏔️
|
||||
⛰️
|
||||
🌋
|
||||
🗻
|
||||
🏕️
|
||||
🏖️
|
||||
🏜️
|
||||
🏝️
|
||||
🏞️
|
||||
🏟️
|
||||
🏛️
|
||||
🏗️
|
||||
🧱
|
||||
🪨
|
||||
🪵
|
||||
🛖
|
||||
🏘️
|
||||
🏚️
|
||||
🏠
|
||||
🏡
|
||||
🏢
|
||||
🏣
|
||||
🏤
|
||||
🏥
|
||||
🏦
|
||||
🏨
|
||||
🏩
|
||||
🏪
|
||||
🏫
|
||||
🏬
|
||||
🏭
|
||||
🏯
|
||||
🏰
|
||||
💒
|
||||
🗼
|
||||
🗽
|
||||
⛪
|
||||
🕌
|
||||
🛕
|
||||
🕍
|
||||
⛩️
|
||||
🕋
|
||||
⛲
|
||||
⛺
|
||||
🌁
|
||||
🌃
|
||||
🏙️
|
||||
🌄
|
||||
🌅
|
||||
🌆
|
||||
🌇
|
||||
🌉
|
||||
♨️
|
||||
🎠
|
||||
🛝
|
||||
🎡
|
||||
🎢
|
||||
💈
|
||||
🎪
|
||||
🚂
|
||||
🚃
|
||||
🚄
|
||||
🚅
|
||||
🚆
|
||||
🚇
|
||||
🚈
|
||||
🚉
|
||||
🚊
|
||||
🚝
|
||||
🚞
|
||||
🚋
|
||||
🚌
|
||||
🚍
|
||||
🚎
|
||||
🚐
|
||||
🚑
|
||||
🚒
|
||||
🚓
|
||||
🚔
|
||||
🚕
|
||||
🚖
|
||||
🚗
|
||||
🚘
|
||||
🚙
|
||||
🛻
|
||||
🚚
|
||||
🚛
|
||||
🚜
|
||||
🏎️
|
||||
🏍️
|
||||
🛵
|
||||
🦽
|
||||
🦼
|
||||
🛺
|
||||
🚲
|
||||
🛴
|
||||
🛹
|
||||
🛼
|
||||
🚏
|
||||
🛣️
|
||||
🛤️
|
||||
🛢️
|
||||
⛽
|
||||
🛞
|
||||
🚨
|
||||
🚥
|
||||
🚦
|
||||
🛑
|
||||
🚧
|
||||
⚓
|
||||
🛟
|
||||
⛵
|
||||
🛶
|
||||
🚤
|
||||
🛳️
|
||||
⛴️
|
||||
🛥️
|
||||
🚢
|
||||
✈️
|
||||
🛩️
|
||||
🛫
|
||||
🛬
|
||||
🪂
|
||||
💺
|
||||
🚁
|
||||
🚟
|
||||
🚠
|
||||
🚡
|
||||
🛰️
|
||||
🚀
|
||||
🛸
|
||||
🛎️
|
||||
🧳
|
||||
⌛
|
||||
⏳
|
||||
⌚
|
||||
⏰
|
||||
⏱️
|
||||
⏲️
|
||||
🕰️
|
||||
🕛
|
||||
🕧
|
||||
🕐
|
||||
🕜
|
||||
🕑
|
||||
🕝
|
||||
🕒
|
||||
🕞
|
||||
🕓
|
||||
🕟
|
||||
🕔
|
||||
🕠
|
||||
🕕
|
||||
🕡
|
||||
🕖
|
||||
🕢
|
||||
🕗
|
||||
🕣
|
||||
🕘
|
||||
🕤
|
||||
🕙
|
||||
🕥
|
||||
🕚
|
||||
🕦
|
||||
🌑
|
||||
🌒
|
||||
🌓
|
||||
🌔
|
||||
🌕
|
||||
🌖
|
||||
🌗
|
||||
🌘
|
||||
🌙
|
||||
🌚
|
||||
🌛
|
||||
🌜
|
||||
🌡️
|
||||
☀️
|
||||
🌝
|
||||
🌞
|
||||
🪐
|
||||
⭐
|
||||
🌟
|
||||
🌠
|
||||
🌌
|
||||
☁️
|
||||
⛅
|
||||
⛈️
|
||||
🌤️
|
||||
🌥️
|
||||
🌦️
|
||||
🌧️
|
||||
🌨️
|
||||
🌩️
|
||||
🌪️
|
||||
🌫️
|
||||
🌬️
|
||||
🌀
|
||||
🌈
|
||||
🌂
|
||||
☂️
|
||||
☔
|
||||
⛱️
|
||||
⚡
|
||||
❄️
|
||||
☃️
|
||||
⛄
|
||||
☄️
|
||||
🔥
|
||||
💧
|
||||
🌊
|
12
app/src/main/assets/emoji/minApi.txt
Normal file
12
app/src/main/assets/emoji/minApi.txt
Normal file
File diff suppressed because one or more lines are too long
|
@ -15,7 +15,7 @@
|
|||
{ "$": "keyboard_state_selector", "emojiKeyEnabled": { "$": "keyboard_state_selector", "alphabet": { "label": "emoji" }}},
|
||||
{ "$": "keyboard_state_selector", "symbols": { "label": "numpad" }},
|
||||
{ "label": "space" },
|
||||
{ "label": "period" },
|
||||
{ "label": "period", "labelFlags": 1073741824 },
|
||||
{ "label": "action", "width": 0.15 }
|
||||
]
|
||||
]
|
||||
|
|
28
app/src/main/assets/layouts/main/akan.txt
Normal file
28
app/src/main/assets/layouts/main/akan.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ɛ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ɔ x
|
||||
c ¢
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
29
app/src/main/assets/layouts/main/bemba.txt
Normal file
29
app/src/main/assets/layouts/main/bemba.txt
Normal file
|
@ -0,0 +1,29 @@
|
|||
q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
ŋ
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
x
|
||||
c
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
28
app/src/main/assets/layouts/main/dagbani.txt
Normal file
28
app/src/main/assets/layouts/main/dagbani.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
q
|
||||
w
|
||||
ɛ e
|
||||
r ¢
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
ɔ o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
ɣ g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
ʒ z
|
||||
x x
|
||||
c
|
||||
v
|
||||
b
|
||||
ŋ n
|
||||
m
|
28
app/src/main/assets/layouts/main/ewe.txt
Normal file
28
app/src/main/assets/layouts/main/ewe.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ɛ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
ɣ y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ɔ x
|
||||
c ¢
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
28
app/src/main/assets/layouts/main/ga.txt
Normal file
28
app/src/main/assets/layouts/main/ga.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ɛ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ɔ x
|
||||
ŋ c ¢
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
28
app/src/main/assets/layouts/main/hausa.txt
Normal file
28
app/src/main/assets/layouts/main/hausa.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ẹ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ọ x
|
||||
c
|
||||
v
|
||||
b
|
||||
n ₦
|
||||
m
|
28
app/src/main/assets/layouts/main/igbo.txt
Normal file
28
app/src/main/assets/layouts/main/igbo.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ṅ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ọ x
|
||||
c
|
||||
ụ v
|
||||
b
|
||||
n ₦
|
||||
m
|
28
app/src/main/assets/layouts/main/kikuyu.txt
Normal file
28
app/src/main/assets/layouts/main/kikuyu.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ĩ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ũ x
|
||||
c
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
55
app/src/main/assets/layouts/main/korean_phonetic.json
Normal file
55
app/src/main/assets/layouts/main/korean_phonetic.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
[
|
||||
[
|
||||
{ "label": "\u3147" },
|
||||
{ "label": "\u3161" },
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3156" },
|
||||
"default": { "label": "\u3154", "popup": { "main": { "label": "\u3156" } } }
|
||||
},
|
||||
{ "label": "\u3139" },
|
||||
{ "label": "\u314c" },
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3152" },
|
||||
"default": { "label": "\u3150", "popup": { "main": { "label": "\u3152" } } }
|
||||
},
|
||||
{ "label": "\u315c" },
|
||||
{ "label": "\u3163" },
|
||||
{ "label": "\u3157" },
|
||||
{ "label": "\u314d" }
|
||||
],
|
||||
[
|
||||
{ "label": "\u314f" },
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3146" },
|
||||
"default": { "label": "\u3145", "popup": { "main": { "label": "\u3146" } } }
|
||||
},
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3138" },
|
||||
"default": { "label": "\u3137", "popup": { "main": { "label": "\u3138" } } }
|
||||
},
|
||||
{ "label": "\u3151" },
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3132" },
|
||||
"default": { "label": "\u3131", "popup": { "main": { "label": "\u3132" } } }
|
||||
},
|
||||
{ "label": "\u314e" },
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3149" },
|
||||
"default": { "label": "\u3148", "popup": { "main": { "label": "\u3149" } } }
|
||||
},
|
||||
{ "label": "\u314b" },
|
||||
{ "label": "\u315b" }
|
||||
],
|
||||
[
|
||||
{ "label": "\u3155" },
|
||||
{ "label": "\u3160" },
|
||||
{ "label": "\u314a" },
|
||||
{ "label": "\u3153" },
|
||||
{ "$": "shift_state_selector",
|
||||
"manualOrLocked": { "label": "\u3143" },
|
||||
"default": { "label": "\u3142", "popup": { "main": { "label": "\u3143" } } }
|
||||
},
|
||||
{ "label": "\u3134" },
|
||||
{ "label": "\u3141" }
|
||||
]
|
||||
]
|
28
app/src/main/assets/layouts/main/lingala.txt
Normal file
28
app/src/main/assets/layouts/main/lingala.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
q
|
||||
w
|
||||
ɛ e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
ɔ o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
x
|
||||
c
|
||||
̌ v
|
||||
b
|
||||
n
|
||||
m
|
29
app/src/main/assets/layouts/main/luganda.txt
Normal file
29
app/src/main/assets/layouts/main/luganda.txt
Normal file
|
@ -0,0 +1,29 @@
|
|||
q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
ŋ
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
x
|
||||
c
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
28
app/src/main/assets/layouts/main/sesotho.txt
Normal file
28
app/src/main/assets/layouts/main/sesotho.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
š s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
x
|
||||
c
|
||||
v
|
||||
b
|
||||
n
|
||||
m
|
28
app/src/main/assets/layouts/main/yoruba.txt
Normal file
28
app/src/main/assets/layouts/main/yoruba.txt
Normal file
|
@ -0,0 +1,28 @@
|
|||
ẹ q
|
||||
w
|
||||
e
|
||||
r
|
||||
t
|
||||
y
|
||||
u
|
||||
i
|
||||
o
|
||||
p
|
||||
|
||||
a
|
||||
s
|
||||
d
|
||||
f
|
||||
g
|
||||
h
|
||||
j
|
||||
k
|
||||
l
|
||||
|
||||
z
|
||||
ọ x
|
||||
c
|
||||
ṣ v
|
||||
b
|
||||
n ₦
|
||||
m
|
|
@ -14,7 +14,7 @@ import android.view.MotionEvent
|
|||
import helium314.keyboard.accessibility.AccessibilityLongPressTimer.LongPressTimerCallback
|
||||
import helium314.keyboard.keyboard.*
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils.displayName
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in [MainKeyboardView] to enhance
|
||||
|
@ -86,9 +86,7 @@ class MainKeyboardAccessibilityDelegate(
|
|||
* @param keyboard The new keyboard.
|
||||
*/
|
||||
private fun announceKeyboardLanguage(keyboard: Keyboard) {
|
||||
val languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
|
||||
keyboard.mId.mSubtype.rawSubtype)
|
||||
sendWindowStateChanged(languageText)
|
||||
sendWindowStateChanged(keyboard.mId.mSubtype.rawSubtype.displayName())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package helium314.keyboard.keyboard
|
||||
|
||||
import android.text.InputType
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.InputMethodSubtype
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
||||
|
@ -13,9 +14,12 @@ import helium314.keyboard.latin.common.loopOverCodePointsBackwards
|
|||
import helium314.keyboard.latin.inputlogic.InputLogic
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener {
|
||||
|
||||
private val connection = inputLogic.mConnection
|
||||
|
||||
private val keyboardSwitcher = KeyboardSwitcher.getInstance()
|
||||
private val settings = Settings.getInstance()
|
||||
private var metaState = 0 // is this enough, or are there threading issues with the different PointerTrackers?
|
||||
|
@ -28,9 +32,15 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
|||
private fun adjustMetaState(code: Int, remove: Boolean) {
|
||||
val metaCode = when (code) {
|
||||
KeyCode.CTRL -> KeyEvent.META_CTRL_ON
|
||||
KeyCode.CTRL_LEFT -> KeyEvent.META_CTRL_LEFT_ON
|
||||
KeyCode.CTRL_RIGHT -> KeyEvent.META_CTRL_RIGHT_ON
|
||||
KeyCode.ALT -> KeyEvent.META_ALT_ON
|
||||
KeyCode.ALT_LEFT -> KeyEvent.META_ALT_LEFT_ON
|
||||
KeyCode.ALT_RIGHT -> KeyEvent.META_ALT_RIGHT_ON
|
||||
KeyCode.FN -> KeyEvent.META_FUNCTION_ON
|
||||
KeyCode.META -> KeyEvent.META_META_ON
|
||||
KeyCode.META_LEFT -> KeyEvent.META_META_LEFT_ON
|
||||
KeyCode.META_RIGHT -> KeyEvent.META_META_RIGHT_ON
|
||||
else -> return
|
||||
}
|
||||
metaState = if (remove) metaState and metaCode.inv()
|
||||
|
@ -70,8 +80,9 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
|||
keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
|
||||
|
||||
override fun onCustomRequest(requestCode: Int): Boolean {
|
||||
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)
|
||||
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) {
|
||||
return latinIME.showInputPickerDialog()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -101,30 +112,34 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
|||
|
||||
override fun onMoveDeletePointer(steps: Int) {
|
||||
inputLogic.finishInput()
|
||||
val end = inputLogic.mConnection.expectedSelectionEnd
|
||||
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
|
||||
val end = connection.expectedSelectionEnd
|
||||
val actualSteps = actualSteps(steps)
|
||||
val start = connection.expectedSelectionStart + actualSteps
|
||||
if (start > end) return
|
||||
connection.setSelection(start, end)
|
||||
}
|
||||
|
||||
private fun actualSteps(steps: Int): Int {
|
||||
var actualSteps = 0
|
||||
// corrected steps to avoid splitting chars belonging to the same codepoint
|
||||
if (steps > 0) {
|
||||
val text = inputLogic.mConnection.getSelectedText(0)
|
||||
if (text == null) actualSteps = steps
|
||||
else loopOverCodePoints(text) {
|
||||
actualSteps += Character.charCount(it)
|
||||
val text = connection.getSelectedText(0) ?: return steps
|
||||
loopOverCodePoints(text) { cp, charCount ->
|
||||
actualSteps += charCount
|
||||
actualSteps >= steps
|
||||
}
|
||||
} else {
|
||||
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0)
|
||||
if (text == null) actualSteps = steps
|
||||
else loopOverCodePointsBackwards(text) {
|
||||
actualSteps -= Character.charCount(it)
|
||||
val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return steps
|
||||
loopOverCodePointsBackwards(text) { cp, charCount ->
|
||||
actualSteps -= charCount
|
||||
actualSteps <= steps
|
||||
}
|
||||
}
|
||||
val start = inputLogic.mConnection.expectedSelectionStart + actualSteps
|
||||
if (start > end) return
|
||||
inputLogic.mConnection.setSelection(start, end)
|
||||
return actualSteps
|
||||
}
|
||||
|
||||
override fun onUpWithDeletePointerActive() {
|
||||
if (!inputLogic.mConnection.hasSelection()) return
|
||||
if (!connection.hasSelection()) return
|
||||
inputLogic.finishInput()
|
||||
onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
|
||||
}
|
||||
|
@ -143,16 +158,17 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
|||
val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype
|
||||
var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1
|
||||
wantedIndex %= subtypes.size
|
||||
if (wantedIndex < 0)
|
||||
if (wantedIndex < 0) {
|
||||
wantedIndex += subtypes.size
|
||||
}
|
||||
val newSubtype = subtypes[wantedIndex]
|
||||
|
||||
// do not switch if we would switch to the initial subtype after cycling all other subtypes
|
||||
if (initialSubtype == null)
|
||||
initialSubtype = current
|
||||
if (initialSubtype == null) initialSubtype = current
|
||||
if (initialSubtype == newSubtype) {
|
||||
if ((subtypeSwitchCount > 0 && steps > 0) || ((subtypeSwitchCount < 0 && steps < 0)))
|
||||
if ((subtypeSwitchCount > 0 && steps > 0) || (subtypeSwitchCount < 0 && steps < 0)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount--
|
||||
|
||||
|
@ -173,17 +189,8 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
|||
val steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps
|
||||
val moveSteps: Int
|
||||
if (steps < 0) {
|
||||
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
|
||||
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) ?: return false
|
||||
loopOverCodePointsBackwards(text) {
|
||||
if (StringUtils.mightBeEmoji(it)) {
|
||||
actualSteps = 0
|
||||
return@loopOverCodePointsBackwards true
|
||||
}
|
||||
actualSteps -= Character.charCount(it)
|
||||
actualSteps <= steps
|
||||
}
|
||||
moveSteps = -text.length.coerceAtMost(abs(actualSteps))
|
||||
val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return false
|
||||
moveSteps = negativeMoveSteps(text, steps)
|
||||
if (moveSteps == 0) {
|
||||
// some apps don't return any text via input connection, and the cursor can't be moved
|
||||
// we fall back to virtually pressing the left/right key one or more times instead
|
||||
|
@ -193,36 +200,61 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
|||
return true
|
||||
}
|
||||
} else {
|
||||
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
|
||||
val text = inputLogic.mConnection.getTextAfterCursor(steps * 4, 0) ?: return false
|
||||
loopOverCodePoints(text) {
|
||||
if (StringUtils.mightBeEmoji(it)) {
|
||||
actualSteps = 0
|
||||
return@loopOverCodePoints true
|
||||
}
|
||||
actualSteps += Character.charCount(it)
|
||||
actualSteps >= steps
|
||||
}
|
||||
moveSteps = text.length.coerceAtMost(actualSteps)
|
||||
val text = connection.getTextAfterCursor(steps * 4, 0) ?: return false
|
||||
moveSteps = positiveMoveSteps(text, steps)
|
||||
if (moveSteps == 0) {
|
||||
// some apps don't return any text via input connection, and the cursor can't be moved
|
||||
// we fall back to virtually pressing the left/right key one or more times instead
|
||||
repeat(steps) {
|
||||
onCodeInput(KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
|
||||
|
||||
// the shortcut below causes issues due to horrible handling of text fields by Firefox and forks
|
||||
// issues:
|
||||
// * setSelection "will cause the editor to call onUpdateSelection", see: https://developer.android.com/reference/android/view/inputmethod/InputConnection#setSelection(int,%20int)
|
||||
// but Firefox is simply not doing this within the same word... WTF?
|
||||
// https://github.com/Helium314/HeliBoard/issues/1139#issuecomment-2588169384
|
||||
// * inputType is NOT if variant InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT (variant appears to always be 0)
|
||||
// so we can't even only do it for browsers (identifying by app name will break for forks)
|
||||
// best "solution" is not doing this for InputType variation 0 but this applies to the majority of text fields...
|
||||
val variation = InputType.TYPE_MASK_VARIATION and Settings.getValues().mInputAttributes.mInputType
|
||||
if (variation != 0 && inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
|
||||
// no need to finish input and restart suggestions if we're still in the word
|
||||
// this is a noticeable performance improvement
|
||||
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
|
||||
inputLogic.mConnection.setSelection(newPosition, newPosition)
|
||||
// this is a noticeable performance improvement when moving through long words
|
||||
val newPosition = connection.expectedSelectionStart + moveSteps
|
||||
connection.setSelection(newPosition, newPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
inputLogic.finishInput()
|
||||
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
|
||||
inputLogic.mConnection.setSelection(newPosition, newPosition)
|
||||
val newPosition = connection.expectedSelectionStart + moveSteps
|
||||
connection.setSelection(newPosition, newPosition)
|
||||
inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun positiveMoveSteps(text: CharSequence, steps: Int): Int {
|
||||
var actualSteps = 0
|
||||
// corrected steps to avoid splitting chars belonging to the same codepoint
|
||||
loopOverCodePoints(text) { cp, charCount ->
|
||||
if (StringUtils.mightBeEmoji(cp)) return 0
|
||||
actualSteps += charCount
|
||||
actualSteps >= steps
|
||||
}
|
||||
return min(actualSteps, text.length)
|
||||
}
|
||||
|
||||
private fun negativeMoveSteps(text: CharSequence, steps: Int): Int {
|
||||
var actualSteps = 0
|
||||
// corrected steps to avoid splitting chars belonging to the same codepoint
|
||||
loopOverCodePointsBackwards(text) { cp, charCount ->
|
||||
if (StringUtils.mightBeEmoji(cp)) return 0
|
||||
actualSteps -= charCount
|
||||
actualSteps <= steps
|
||||
}
|
||||
return -min(-actualSteps, text.length)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ public final class KeyboardLayoutSet {
|
|||
public static void onSystemLocaleChanged() {
|
||||
clearKeyboardCache();
|
||||
LocaleKeyboardInfosKt.clearCache();
|
||||
SubtypeLocaleUtils.clearDisplayNameCache();
|
||||
SubtypeLocaleUtils.clearSubtypeDisplayNameCache();
|
||||
}
|
||||
|
||||
public static void onKeyboardThemeChanged() {
|
||||
|
|
|
@ -157,10 +157,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
} catch (KeyboardLayoutSetException e) {
|
||||
Log.e(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
|
||||
try {
|
||||
final InputMethodSubtype qwerty = SubtypeUtilsAdditional.INSTANCE
|
||||
.createEmojiCapableAdditionalSubtype(mRichImm.getCurrentSubtypeLocale(), SubtypeLocaleUtils.QWERTY, true);
|
||||
final InputMethodSubtype defaults = SubtypeUtilsAdditional.INSTANCE.createDefaultSubtype(mRichImm.getCurrentSubtypeLocale());
|
||||
mKeyboardLayoutSet = builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
|
||||
.setSubtype(RichInputMethodSubtype.Companion.get(qwerty))
|
||||
.setSubtype(RichInputMethodSubtype.Companion.get(defaults))
|
||||
.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey)
|
||||
.setNumberRowEnabled(settingsValues.mShowsNumberRow)
|
||||
.setLanguageSwitchKeyEnabled(settingsValues.isLanguageSwitchKeyEnabled())
|
||||
|
@ -169,9 +168,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
.setOneHandedModeEnabled(oneHandedModeEnabled)
|
||||
.build();
|
||||
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState, oneHandedModeEnabled);
|
||||
showToast("error loading the keyboard, falling back to qwerty", false);
|
||||
showToast("error loading the keyboard, falling back to defaults", false);
|
||||
} catch (KeyboardLayoutSetException e2) {
|
||||
Log.e(TAG, "even fallback to qwerty failed: " + e2.mKeyboardId, e2.getCause());
|
||||
Log.e(TAG, "even fallback to defaults failed: " + e2.mKeyboardId, e2.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -480,7 +479,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
if (mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
|
||||
return;
|
||||
}
|
||||
mEmojiPalettesView.clearKeyboardCache();
|
||||
final Settings settings = Settings.getInstance();
|
||||
mKeyboardViewWrapper.setOneHandedModeEnabled(enabled);
|
||||
mKeyboardViewWrapper.setOneHandedGravity(settings.getCurrent().mOneHandedModeGravity);
|
||||
|
@ -515,9 +513,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
}
|
||||
|
||||
public void reloadKeyboard() {
|
||||
if (mCurrentInputView != null)
|
||||
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
|
||||
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
|
||||
if (mCurrentInputView == null)
|
||||
return;
|
||||
mEmojiPalettesView.clearKeyboardCache();
|
||||
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
|
||||
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -644,6 +644,12 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
|||
}
|
||||
}
|
||||
|
||||
public void trimMemory() {
|
||||
if (mEmojiPalettesView != null) {
|
||||
mEmojiPalettesView.clearKeyboardCache();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public View onCreateInputView(@NonNull Context displayContext, final boolean isHardwareAcceleratedDrawingEnabled) {
|
||||
if (mKeyboardView != null) {
|
||||
|
|
|
@ -405,14 +405,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
|
|||
}
|
||||
|
||||
fun getUnusedThemeName(initialName: String, prefs: SharedPreferences): String {
|
||||
val existingNames = prefs.all.keys.mapNotNull {
|
||||
when {
|
||||
it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX)
|
||||
it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX)
|
||||
it.startsWith(Settings.PREF_USER_MORE_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_MORE_COLORS_PREFIX)
|
||||
else -> null
|
||||
}
|
||||
}.toSortedSet()
|
||||
val existingNames = getExistingThemeNames(prefs)
|
||||
if (initialName !in existingNames) return initialName
|
||||
var i = 1
|
||||
while ("$initialName$i" in existingNames)
|
||||
|
@ -420,11 +413,8 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
|
|||
return "$initialName$i"
|
||||
}
|
||||
|
||||
// returns false if not renamed due to invalid name or collision
|
||||
fun renameUserColors(from: String, to: String, prefs: SharedPreferences): Boolean {
|
||||
if (to.isBlank()) return false // don't want that
|
||||
if (to == from) return true // nothing to do
|
||||
val existingNames = prefs.all.keys.mapNotNull {
|
||||
private fun getExistingThemeNames(prefs: SharedPreferences) =
|
||||
prefs.all.keys.mapNotNull {
|
||||
when {
|
||||
it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX)
|
||||
it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX)
|
||||
|
@ -432,6 +422,12 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
|
|||
else -> null
|
||||
}
|
||||
}.toSortedSet()
|
||||
|
||||
// returns false if not renamed due to invalid name or collision
|
||||
fun renameUserColors(from: String, to: String, prefs: SharedPreferences): Boolean {
|
||||
if (to.isBlank()) return false // don't want that
|
||||
if (to == from) return true // nothing to do
|
||||
val existingNames = getExistingThemeNames(prefs)
|
||||
if (to in existingNames) return false
|
||||
// all good, now rename
|
||||
prefs.edit {
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.view.View;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import helium314.keyboard.keyboard.emoji.EmojiPageKeyboardView;
|
||||
import helium314.keyboard.keyboard.internal.KeyDrawParams;
|
||||
import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
|
||||
|
@ -34,7 +35,7 @@ import helium314.keyboard.latin.R;
|
|||
import helium314.keyboard.latin.common.ColorType;
|
||||
import helium314.keyboard.latin.common.Colors;
|
||||
import helium314.keyboard.latin.common.Constants;
|
||||
import helium314.keyboard.latin.common.StringUtils;
|
||||
import helium314.keyboard.latin.common.StringUtilsKt;
|
||||
import helium314.keyboard.latin.settings.Settings;
|
||||
import helium314.keyboard.latin.suggestions.MoreSuggestions;
|
||||
import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
|
||||
|
@ -147,6 +148,7 @@ public class KeyboardView extends View {
|
|||
|
||||
mPaint.setAntiAlias(true);
|
||||
mTypeface = Settings.getInstance().getCustomTypeface();
|
||||
setFitsSystemWindows(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -191,7 +193,8 @@ public class KeyboardView extends View {
|
|||
invalidateAllKeys();
|
||||
requestLayout();
|
||||
mFontSizeMultiplier = mKeyboard.mId.isEmojiKeyboard()
|
||||
? Settings.getValues().mFontSizeMultiplierEmoji
|
||||
// In the case of EmojiKeyFit, the size of emojis is taken care of by the size of the keys
|
||||
? (Settings.getValues().mEmojiKeyFit? 1 : Settings.getValues().mFontSizeMultiplierEmoji)
|
||||
: Settings.getValues().mFontSizeMultiplier;
|
||||
}
|
||||
|
||||
|
@ -423,10 +426,14 @@ public class KeyboardView extends View {
|
|||
}
|
||||
|
||||
if (key.isEnabled()) {
|
||||
if (StringUtils.mightBeEmoji(label))
|
||||
if (StringUtilsKt.isEmoji(label))
|
||||
paint.setColor(key.selectTextColor(params) | 0xFF000000); // ignore alpha for emojis (though actually color isn't applied anyway and we could just set white)
|
||||
else if (key.hasActionKeyBackground())
|
||||
paint.setColor(mColors.get(ColorType.ACTION_KEY_ICON));
|
||||
else if (this instanceof EmojiPageKeyboardView)
|
||||
paint.setColor(mColors.get(ColorType.EMOJI_KEY_TEXT));
|
||||
else if (this instanceof PopupKeysKeyboardView)
|
||||
paint.setColor(mColors.get(ColorType.POPUP_KEY_TEXT));
|
||||
else
|
||||
paint.setColor(key.selectTextColor(params));
|
||||
// Set a drop shadow for the text if the shadow radius is positive value.
|
||||
|
@ -624,8 +631,7 @@ public class KeyboardView extends View {
|
|||
} else if (key.getBackgroundType() != Key.BACKGROUND_TYPE_NORMAL) {
|
||||
mColors.setColor(icon, ColorType.KEY_ICON);
|
||||
} else if (this instanceof PopupKeysKeyboardView) {
|
||||
// set color filter for long press comma key, should not trigger anywhere else
|
||||
mColors.setColor(icon, ColorType.KEY_ICON);
|
||||
mColors.setColor(icon, ColorType.POPUP_KEY_ICON);
|
||||
} else if (key.getCode() == Constants.CODE_SPACE || key.getCode() == KeyCode.ZWNJ) {
|
||||
// set color of default number pad space bar icon for Holo style, or for zero-width non-joiner (zwnj) on some layouts like nepal
|
||||
mColors.setColor(icon, ColorType.KEY_ICON);
|
||||
|
|
|
@ -57,6 +57,7 @@ import helium314.keyboard.latin.settings.Settings;
|
|||
import helium314.keyboard.latin.utils.KtxKt;
|
||||
import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils;
|
||||
import helium314.keyboard.latin.utils.Log;
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
|
||||
import helium314.keyboard.latin.utils.TypefaceUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
@ -71,6 +71,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
|
|||
getEnabledClipboardToolbarKeys(context.prefs())
|
||||
.forEach { toolbarKeys.add(createToolbarKey(context, KeyboardIconsSet.instance, it)) }
|
||||
keyboardAttr.recycle()
|
||||
fitsSystemWindows = true
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
|
|
|
@ -64,4 +64,4 @@ class ClipboardLayoutParams(ctx: Context) {
|
|||
view.layoutParams = this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import static helium314.keyboard.keyboard.internal.keyboard_parser.EmojiParserKt
|
|||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import helium314.keyboard.latin.common.Constants;
|
||||
import helium314.keyboard.latin.settings.Defaults;
|
||||
import helium314.keyboard.latin.utils.Log;
|
||||
|
||||
|
@ -34,8 +35,6 @@ import java.util.List;
|
|||
*/
|
||||
final class DynamicGridKeyboard extends Keyboard {
|
||||
private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
|
||||
private static final int TEMPLATE_KEY_CODE_0 = 0x30;
|
||||
private static final int TEMPLATE_KEY_CODE_1 = 0x31;
|
||||
private final Object mLock = new Object();
|
||||
|
||||
private final SharedPreferences mPrefs;
|
||||
|
@ -60,8 +59,8 @@ final class DynamicGridKeyboard extends Keyboard {
|
|||
mBaseWidth = width - paddingWidth;
|
||||
mOccupiedWidth = width;
|
||||
final float spacerWidth = Settings.getValues().mSplitKeyboardSpacerRelativeWidth * mBaseWidth;
|
||||
final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
|
||||
final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
|
||||
final Key key0 = getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_0);
|
||||
final Key key1 = getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_1);
|
||||
final int horizontalGap = Math.abs(key1.getX() - key0.getX()) - key0.getWidth();
|
||||
final float widthScale = determineWidthScale(key0.getWidth() + horizontalGap);
|
||||
mHorizontalGap = (int) (horizontalGap * widthScale);
|
||||
|
@ -213,7 +212,7 @@ final class DynamicGridKeyboard extends Keyboard {
|
|||
}
|
||||
|
||||
// fall back to creating the key
|
||||
return new Key(getTemplateKey(TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, code, null);
|
||||
return new Key(getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, code, null);
|
||||
}
|
||||
|
||||
private Key getKeyByOutputText(final Collection<DynamicGridKeyboard> keyboards,
|
||||
|
@ -227,7 +226,7 @@ final class DynamicGridKeyboard extends Keyboard {
|
|||
}
|
||||
|
||||
// fall back to creating the key
|
||||
return new Key(getTemplateKey(TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, 0, outputText);
|
||||
return new Key(getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, 0, outputText);
|
||||
}
|
||||
|
||||
public void loadRecentKeys(final Collection<DynamicGridKeyboard> keyboards) {
|
||||
|
|
|
@ -261,20 +261,6 @@ final class EmojiCategory {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Returns the view pager's page position for the categoryId
|
||||
public int getPagerPageIdFromCategoryAndPageId(final int categoryId, final int categoryPageId) {
|
||||
int sum = 0;
|
||||
for (int i = 0; i < mShownCategories.size(); ++i) {
|
||||
final CategoryProperties props = mShownCategories.get(i);
|
||||
if (props.mCategoryId == categoryId) {
|
||||
return sum + categoryPageId;
|
||||
}
|
||||
sum += props.getPageCount();
|
||||
}
|
||||
Log.w(TAG, "categoryId not found: " + categoryId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getRecentTabId() {
|
||||
return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS);
|
||||
}
|
||||
|
@ -285,11 +271,11 @@ final class EmojiCategory {
|
|||
}
|
||||
|
||||
// Returns a keyboard from the recycler view's adapter position.
|
||||
public DynamicGridKeyboard getKeyboardFromAdapterPosition(final int position) {
|
||||
if (position >= 0 && position < getCurrentCategoryPageCount()) {
|
||||
return getKeyboard(mCurrentCategoryId, position);
|
||||
public DynamicGridKeyboard getKeyboardFromAdapterPosition(int categoryId, final int position) {
|
||||
if (position >= 0 && position < getCategoryPageCount(categoryId)) {
|
||||
return getKeyboard(categoryId, position);
|
||||
}
|
||||
Log.w(TAG, "invalid position for categoryId : " + mCurrentCategoryId);
|
||||
Log.w(TAG, "invalid position for categoryId : " + categoryId);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ package helium314.keyboard.keyboard.emoji
|
|||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
|
@ -47,7 +47,7 @@ internal class EmojiLayoutParams(res: Resources) {
|
|||
emojiKeyboardHeight = emojiListHeight - emojiCategoryPageIdViewHeight - emojiListBottomMargin
|
||||
}
|
||||
|
||||
fun setEmojiListProperties(vp: RecyclerView) {
|
||||
fun setEmojiListProperties(vp: ViewPager2) {
|
||||
val lp = vp.layoutParams as LinearLayout.LayoutParams
|
||||
lp.height = emojiKeyboardHeight
|
||||
lp.bottomMargin = emojiListBottomMargin
|
||||
|
|
|
@ -12,156 +12,32 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import helium314.keyboard.keyboard.Key;
|
||||
import helium314.keyboard.keyboard.Keyboard;
|
||||
import helium314.keyboard.keyboard.KeyboardView;
|
||||
import helium314.keyboard.latin.R;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import helium314.keyboard.latin.settings.Settings;
|
||||
|
||||
final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapter.ViewHolder>{
|
||||
private static final String TAG = EmojiPalettesAdapter.class.getSimpleName();
|
||||
private static final boolean DEBUG_PAGER = false;
|
||||
|
||||
private final int mCategoryId;
|
||||
private final OnKeyEventListener mListener;
|
||||
private final DynamicGridKeyboard mRecentsKeyboard;
|
||||
private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = new SparseArray<>();
|
||||
private final EmojiCategory mEmojiCategory;
|
||||
private int mActivePosition = 0;
|
||||
|
||||
public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
|
||||
final OnKeyEventListener listener) {
|
||||
public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final OnKeyEventListener listener) {
|
||||
mEmojiCategory = emojiCategory;
|
||||
mCategoryId = categoryId;
|
||||
mListener = listener;
|
||||
mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
|
||||
}
|
||||
|
||||
public void flushPendingRecentKeys() {
|
||||
mRecentsKeyboard.flushPendingRecentKeys();
|
||||
final KeyboardView recentKeyboardView =
|
||||
mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
|
||||
if (recentKeyboardView != null) {
|
||||
recentKeyboardView.invalidateAllKeys();
|
||||
}
|
||||
}
|
||||
|
||||
public void addRecentKey(final Key key) {
|
||||
if (Settings.getValues().mIncognitoModeEnabled) {
|
||||
// We do not want to log recent keys while being in incognito
|
||||
return;
|
||||
}
|
||||
if (mEmojiCategory.isInRecentTab()) {
|
||||
mRecentsKeyboard.addPendingKey(key);
|
||||
return;
|
||||
}
|
||||
mRecentsKeyboard.addKeyFirst(key);
|
||||
final KeyboardView recentKeyboardView =
|
||||
mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
|
||||
if (recentKeyboardView != null) {
|
||||
recentKeyboardView.invalidateAllKeys();
|
||||
}
|
||||
}
|
||||
|
||||
public void onPageScrolled() {
|
||||
releaseCurrentKey(false /* withKeyRegistering */);
|
||||
}
|
||||
|
||||
public void releaseCurrentKey(final boolean withKeyRegistering) {
|
||||
// Make sure the delayed key-down event (highlight effect and haptic feedback) will be
|
||||
// canceled.
|
||||
final EmojiPageKeyboardView currentKeyboardView =
|
||||
mActiveKeyboardViews.get(mActivePosition);
|
||||
if (currentKeyboardView == null) {
|
||||
return;
|
||||
}
|
||||
currentKeyboardView.releaseCurrentKey(withKeyRegistering);
|
||||
}
|
||||
|
||||
/*
|
||||
@Override
|
||||
public Object instantiateItem(final ViewGroup container, final int position) {
|
||||
if (DEBUG_PAGER) {
|
||||
Log.d(TAG, "instantiate item: " + position);
|
||||
}
|
||||
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
|
||||
if (oldKeyboardView != null) {
|
||||
oldKeyboardView.deallocateMemory();
|
||||
// This may be redundant but wanted to be safer..
|
||||
mActiveKeyboardViews.remove(position);
|
||||
}
|
||||
final Keyboard keyboard =
|
||||
mEmojiCategory.getKeyboardFromPagePosition(position);
|
||||
final LayoutInflater inflater = LayoutInflater.from(container.getContext());
|
||||
final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView) inflater.inflate(
|
||||
R.layout.emoji_keyboard_page, container, false);
|
||||
keyboardView.setKeyboard(keyboard);
|
||||
keyboardView.setOnKeyEventListener(mListener);
|
||||
container.addView(keyboardView);
|
||||
mActiveKeyboardViews.put(position, keyboardView);
|
||||
return keyboardView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(final ViewGroup container, final int position,
|
||||
final Object object) {
|
||||
if (mActivePosition == position) {
|
||||
return;
|
||||
}
|
||||
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
|
||||
if (oldKeyboardView != null) {
|
||||
oldKeyboardView.releaseCurrentKey(false);
|
||||
oldKeyboardView.deallocateMemory();
|
||||
}
|
||||
mActivePosition = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(final View view, final Object object) {
|
||||
return view == object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(final ViewGroup container, final int position,
|
||||
final Object object) {
|
||||
if (DEBUG_PAGER) {
|
||||
Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName());
|
||||
}
|
||||
final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(position);
|
||||
if (keyboardView != null) {
|
||||
keyboardView.deallocateMemory();
|
||||
mActiveKeyboardViews.remove(position);
|
||||
}
|
||||
if (object instanceof View) {
|
||||
container.removeView((View)object);
|
||||
} else {
|
||||
Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
/*if (DEBUG_PAGER) {
|
||||
Log.d(TAG, "instantiate item: " + viewType);
|
||||
}
|
||||
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(viewType);
|
||||
if (oldKeyboardView != null) {
|
||||
oldKeyboardView.deallocateMemory();
|
||||
// This may be redundant but wanted to be safer..
|
||||
mActiveKeyboardViews.remove(viewType);
|
||||
}
|
||||
final Keyboard keyboard =
|
||||
mEmojiCategory.getKeyboardFromPagePosition(parent.getVerticalScrollbarPosition());*/
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate(
|
||||
R.layout.emoji_keyboard_page, parent, false);
|
||||
/*keyboardView.setKeyboard(keyboard);
|
||||
keyboardView.setOnKeyEventListener(mListener);
|
||||
parent.addView(keyboardView);
|
||||
mActiveKeyboardViews.put(parent.getVerticalScrollbarPosition(), keyboardView);*/
|
||||
return new ViewHolder(keyboardView);
|
||||
}
|
||||
|
||||
|
@ -170,33 +46,22 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
|
|||
if (DEBUG_PAGER) {
|
||||
Log.d(TAG, "instantiate item: " + position);
|
||||
}
|
||||
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
|
||||
if (oldKeyboardView != null) {
|
||||
oldKeyboardView.deallocateMemory();
|
||||
// This may be redundant but wanted to be safer..
|
||||
mActiveKeyboardViews.remove(position);
|
||||
}
|
||||
|
||||
final Keyboard keyboard =
|
||||
mEmojiCategory.getKeyboardFromAdapterPosition(position);
|
||||
mEmojiCategory.getKeyboardFromAdapterPosition(mCategoryId, position);
|
||||
holder.getKeyboardView().setKeyboard(keyboard);
|
||||
holder.getKeyboardView().setOnKeyEventListener(mListener);
|
||||
//parent.addView(keyboardView);
|
||||
mActiveKeyboardViews.put(position, holder.getKeyboardView());
|
||||
}
|
||||
|
||||
/*if (mActivePosition == position) {
|
||||
return;
|
||||
}
|
||||
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
|
||||
if (oldKeyboardView != null) {
|
||||
oldKeyboardView.releaseCurrentKey(false);
|
||||
oldKeyboardView.deallocateMemory();
|
||||
}
|
||||
mActivePosition = position;*/
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
|
||||
holder.getKeyboardView().releaseCurrentKey(false);
|
||||
holder.getKeyboardView().deallocateMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mEmojiCategory.getCurrentCategoryPageCount();
|
||||
return mEmojiCategory.getCategoryPageCount(mCategoryId);
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
@ -210,7 +75,6 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
|
|||
public EmojiPageKeyboardView getKeyboardView() {
|
||||
return customView;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,15 @@
|
|||
|
||||
package helium314.keyboard.keyboard.emoji;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
@ -20,6 +24,7 @@ import android.widget.LinearLayout;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import helium314.keyboard.keyboard.Key;
|
||||
import helium314.keyboard.keyboard.Keyboard;
|
||||
import helium314.keyboard.keyboard.KeyboardActionListener;
|
||||
|
@ -41,8 +46,6 @@ import helium314.keyboard.latin.settings.Settings;
|
|||
import helium314.keyboard.latin.settings.SettingsValues;
|
||||
import helium314.keyboard.latin.utils.ResourceUtils;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
|
||||
|
||||
/**
|
||||
|
@ -58,26 +61,135 @@ import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
|
|||
*/
|
||||
public final class EmojiPalettesView extends LinearLayout
|
||||
implements View.OnClickListener, OnKeyEventListener {
|
||||
private static final class PagerViewHolder extends RecyclerView.ViewHolder {
|
||||
private long mCategoryId;
|
||||
|
||||
private PagerViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
private final class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder> {
|
||||
private boolean mInitialized;
|
||||
private Map<Integer, RecyclerView> mViews = new HashMap<>(mEmojiCategory.getShownCategories().size());
|
||||
|
||||
private PagerAdapter(ViewPager2 pager) {
|
||||
setHasStableIds(true);
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
var categoryId = (int) getItemId(position);
|
||||
setCurrentCategoryId(categoryId, false);
|
||||
var recyclerView = mViews.get(position);
|
||||
if (recyclerView != null) {
|
||||
updateState(recyclerView, categoryId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
recyclerView.setItemViewCacheSize(mEmojiCategory.getShownCategories().size());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
var view = LayoutInflater.from(parent.getContext()).inflate(R.layout.emoji_category_view, parent, false);
|
||||
var viewHolder = new PagerViewHolder(view);
|
||||
var emojiRecyclerView = getRecyclerView(view);
|
||||
|
||||
emojiRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
super.onScrollStateChanged(recyclerView, newState);
|
||||
// Ignore this message. Only want the actual page selected.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
updateState(recyclerView, viewHolder.mCategoryId);
|
||||
}
|
||||
});
|
||||
|
||||
emojiRecyclerView.setPersistentDrawingCache(PERSISTENT_NO_CACHE);
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PagerViewHolder holder, int position) {
|
||||
holder.mCategoryId = getItemId(position);
|
||||
var recyclerView = getRecyclerView(holder.itemView);
|
||||
mViews.put(position, recyclerView);
|
||||
recyclerView.setAdapter(new EmojiPalettesAdapter(mEmojiCategory, (int) holder.mCategoryId,
|
||||
EmojiPalettesView.this));
|
||||
|
||||
if (! mInitialized) {
|
||||
recyclerView.scrollToPosition(mEmojiCategory.getCurrentCategoryPageId());
|
||||
mInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mEmojiCategory.getShownCategories().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(PagerViewHolder holder) {
|
||||
if (holder.mCategoryId == EmojiCategory.ID_RECENTS) {
|
||||
// Needs to save pending updates for recent keys when we get out of the recents
|
||||
// category because we don't want to move the recent emojis around while the user
|
||||
// is in the recents category.
|
||||
getRecentsKeyboard().flushPendingRecentKeys();
|
||||
getRecyclerView(holder.itemView).getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mEmojiCategory.getShownCategories().get(position).mCategoryId;
|
||||
}
|
||||
|
||||
private static RecyclerView getRecyclerView(View view) {
|
||||
return view.findViewById(R.id.emoji_keyboard_list);
|
||||
}
|
||||
|
||||
private void updateState(@NonNull RecyclerView recyclerView, long categoryId) {
|
||||
if (categoryId != mEmojiCategory.getCurrentCategoryId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int offset = recyclerView.computeVerticalScrollOffset();
|
||||
final int extent = recyclerView.computeVerticalScrollExtent();
|
||||
final int range = recyclerView.computeVerticalScrollRange();
|
||||
final float percentage = offset / (float) (range - extent);
|
||||
|
||||
final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageCount();
|
||||
final int a = (int) (percentage * currentCategorySize);
|
||||
final float b = percentage * currentCategorySize - a;
|
||||
mEmojiCategoryPageIndicatorView.setCategoryPageId(currentCategorySize, a, b);
|
||||
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
|
||||
final int firstCompleteVisibleBoard = layoutManager.findFirstCompletelyVisibleItemPosition();
|
||||
final int firstVisibleBoard = layoutManager.findFirstVisibleItemPosition();
|
||||
mEmojiCategory.setCurrentCategoryPageId(
|
||||
firstCompleteVisibleBoard > 0 ? firstCompleteVisibleBoard : firstVisibleBoard);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean initialized = false;
|
||||
// keep the indicator in case emoji view is changed to tabs / viewpager
|
||||
private final boolean mCategoryIndicatorEnabled;
|
||||
private final int mCategoryIndicatorDrawableResId;
|
||||
private final int mCategoryIndicatorBackgroundResId;
|
||||
private final int mCategoryPageIndicatorColor;
|
||||
private final Colors mColors;
|
||||
private EmojiPalettesAdapter mEmojiPalettesAdapter;
|
||||
private final EmojiLayoutParams mEmojiLayoutParams;
|
||||
private final LinearLayoutManager mEmojiLayoutManager;
|
||||
|
||||
private LinearLayout mTabStrip;
|
||||
private RecyclerView mEmojiRecyclerView;
|
||||
private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
|
||||
|
||||
private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
|
||||
|
||||
private final EmojiCategory mEmojiCategory;
|
||||
|
||||
private ImageView mCurrentTab = null;
|
||||
private ViewPager2 mPager;
|
||||
|
||||
public EmojiPalettesView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.emojiPalettesViewStyle);
|
||||
|
@ -96,16 +208,8 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
|
||||
mEmojiCategory = new EmojiCategory(context, layoutSet, emojiPalettesViewAttr);
|
||||
mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean(
|
||||
R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false);
|
||||
mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId(
|
||||
R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0);
|
||||
mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId(
|
||||
R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
|
||||
mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor( // todo: remove this and related attr
|
||||
R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
|
||||
emojiPalettesViewAttr.recycle();
|
||||
mEmojiLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
|
||||
setFitsSystemWindows(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -121,13 +225,6 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
// todo (maybe): bring back the holo indicator thing?
|
||||
// just some 2 dp high strip
|
||||
// would probably need a vertical linear layout
|
||||
// better not, would complicate stuff again
|
||||
// when decided to definitely not bring it back:
|
||||
// remove mCategoryIndicatorEnabled, mCategoryIndicatorDrawableResId, mCategoryIndicatorBackgroundResId
|
||||
// and the attrs categoryIndicatorDrawable, categoryIndicatorEnabled, categoryIndicatorBackground (and the connected drawables)
|
||||
private void addTab(final LinearLayout host, final int categoryId) {
|
||||
final ImageView iconView = new ImageView(getContext());
|
||||
mColors.setBackground(iconView, ColorType.STRIP_BACKGROUND);
|
||||
|
@ -149,59 +246,14 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
for (final EmojiCategory.CategoryProperties properties : mEmojiCategory.getShownCategories()) {
|
||||
addTab(mTabStrip, properties.mCategoryId);
|
||||
}
|
||||
// mTabStrip.setOnTabChangedListener(this); // now onClickListener
|
||||
/* final TabWidget tabWidget = mTabStrip.getTabWidget();
|
||||
tabWidget.setStripEnabled(mCategoryIndicatorEnabled);
|
||||
if (mCategoryIndicatorEnabled) {
|
||||
// On TabWidget's strip, what looks like an indicator is actually a background.
|
||||
// And what looks like a background are actually left and right drawables.
|
||||
tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId);
|
||||
tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId);
|
||||
tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId);
|
||||
tabWidget.setBackgroundColor(mColors.get(ColorType.EMOJI_CATEGORY_SELECTED));
|
||||
}
|
||||
*/
|
||||
mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
|
||||
|
||||
mEmojiRecyclerView = findViewById(R.id.emoji_keyboard_list);
|
||||
mEmojiRecyclerView.setLayoutManager(mEmojiLayoutManager);
|
||||
mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter);
|
||||
mEmojiRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull @NotNull RecyclerView recyclerView, int newState) {
|
||||
super.onScrollStateChanged(recyclerView, newState);
|
||||
// Ignore this message. Only want the actual page selected.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull @NotNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
mEmojiPalettesAdapter.onPageScrolled();
|
||||
|
||||
final int offset = recyclerView.computeVerticalScrollOffset();
|
||||
final int extent = recyclerView.computeVerticalScrollExtent();
|
||||
final int range = recyclerView.computeVerticalScrollRange();
|
||||
final float percentage = offset / (float) (range - extent);
|
||||
|
||||
final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageCount();
|
||||
final int a = (int) (percentage * currentCategorySize);
|
||||
final float b = percentage * currentCategorySize - a;
|
||||
mEmojiCategoryPageIndicatorView.setCategoryPageId(currentCategorySize, a, b);
|
||||
|
||||
final int firstCompleteVisibleBoard = mEmojiLayoutManager.findFirstCompletelyVisibleItemPosition();
|
||||
final int firstVisibleBoard = mEmojiLayoutManager.findFirstVisibleItemPosition();
|
||||
mEmojiCategory.setCurrentCategoryPageId(
|
||||
firstCompleteVisibleBoard > 0 ? firstCompleteVisibleBoard : firstVisibleBoard);
|
||||
}
|
||||
});
|
||||
|
||||
mEmojiRecyclerView.setPersistentDrawingCache(PERSISTENT_NO_CACHE);
|
||||
mEmojiLayoutParams.setEmojiListProperties(mEmojiRecyclerView);
|
||||
|
||||
mPager = findViewById(R.id.emoji_pager);
|
||||
mPager.setAdapter(new PagerAdapter(mPager));
|
||||
mEmojiLayoutParams.setEmojiListProperties(mPager);
|
||||
mEmojiCategoryPageIndicatorView = findViewById(R.id.emoji_category_page_id_view);
|
||||
mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView);
|
||||
|
||||
setCurrentCategoryAndPageId(mEmojiCategory.getCurrentCategoryId(), mEmojiCategory.getCurrentCategoryPageId(), true);
|
||||
setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true);
|
||||
|
||||
mEmojiCategoryPageIndicatorView.setColors(mColors.get(ColorType.EMOJI_CATEGORY_SELECTED), mColors.get(ColorType.STRIP_BACKGROUND));
|
||||
initialized = true;
|
||||
|
@ -219,7 +271,7 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this);
|
||||
final int categoryId = ((Long) tag).intValue();
|
||||
if (categoryId != mEmojiCategory.getCurrentCategoryId()) {
|
||||
setCurrentCategoryAndPageId(categoryId, 0, false);
|
||||
setCurrentCategoryId(categoryId, false);
|
||||
updateEmojiCategoryPageIdView();
|
||||
}
|
||||
}
|
||||
|
@ -244,7 +296,7 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
*/
|
||||
@Override
|
||||
public void onReleaseKey(final Key key) {
|
||||
mEmojiPalettesAdapter.addRecentKey(key);
|
||||
addRecentKey(key);
|
||||
final int code = key.getCode();
|
||||
if (code == KeyCode.MULTIPLE_CODE_POINTS) {
|
||||
mKeyboardActionListener.onTextInput(key.getOutputText());
|
||||
|
@ -269,13 +321,22 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
setupBottomRowKeyboard(editorInfo, keyboardActionListener);
|
||||
final KeyDrawParams params = new KeyDrawParams();
|
||||
params.updateParams(mEmojiLayoutParams.getBottomRowKeyboardHeight(), keyVisualAttr);
|
||||
if (mEmojiRecyclerView.getAdapter() == null) {
|
||||
mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter);
|
||||
setCurrentCategoryAndPageId(mEmojiCategory.getCurrentCategoryId(), mEmojiCategory.getCurrentCategoryPageId(), true);
|
||||
}
|
||||
setupSidePadding();
|
||||
}
|
||||
|
||||
private void addRecentKey(final Key key) {
|
||||
if (Settings.getValues().mIncognitoModeEnabled) {
|
||||
// We do not want to log recent keys while being in incognito
|
||||
return;
|
||||
}
|
||||
if (mEmojiCategory.isInRecentTab()) {
|
||||
getRecentsKeyboard().addPendingKey(key);
|
||||
return;
|
||||
}
|
||||
getRecentsKeyboard().addKeyFirst(key);
|
||||
mPager.getAdapter().notifyItemChanged(mEmojiCategory.getRecentTabId());
|
||||
}
|
||||
|
||||
private void setupBottomRowKeyboard(final EditorInfo editorInfo, final KeyboardActionListener keyboardActionListener) {
|
||||
MainKeyboardView keyboardView = findViewById(R.id.bottom_row_keyboard);
|
||||
keyboardView.setKeyboardActionListener(keyboardActionListener);
|
||||
|
@ -295,11 +356,11 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
final float rightPadding = keyboardAttr.getFraction(R.styleable.Keyboard_keyboardRightPadding,
|
||||
keyboardWidth, keyboardWidth, 0f) * sv.mSidePaddingScale;
|
||||
keyboardAttr.recycle();
|
||||
mEmojiRecyclerView.setPadding(
|
||||
mPager.setPadding(
|
||||
(int) leftPadding,
|
||||
mEmojiRecyclerView.getPaddingTop(),
|
||||
mPager.getPaddingTop(),
|
||||
(int) rightPadding,
|
||||
mEmojiRecyclerView.getPaddingBottom()
|
||||
mPager.getPaddingBottom()
|
||||
);
|
||||
mEmojiCategoryPageIndicatorView.setPadding(
|
||||
(int) leftPadding,
|
||||
|
@ -312,9 +373,11 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
|
||||
public void stopEmojiPalettes() {
|
||||
if (!initialized) return;
|
||||
mEmojiPalettesAdapter.releaseCurrentKey(true);
|
||||
mEmojiPalettesAdapter.flushPendingRecentKeys();
|
||||
mEmojiRecyclerView.setAdapter(null);
|
||||
getRecentsKeyboard().flushPendingRecentKeys();
|
||||
}
|
||||
|
||||
private DynamicGridKeyboard getRecentsKeyboard() {
|
||||
return mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
|
||||
}
|
||||
|
||||
public void setKeyboardActionListener(final KeyboardActionListener listener) {
|
||||
|
@ -330,34 +393,33 @@ public final class EmojiPalettesView extends LinearLayout
|
|||
mEmojiCategory.getCurrentCategoryPageId(), 0.0f);
|
||||
}
|
||||
|
||||
private void setCurrentCategoryAndPageId(final int categoryId, final int categoryPageId, final boolean force) {
|
||||
private void setCurrentCategoryId(final int categoryId, final boolean initial) {
|
||||
final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
|
||||
final int oldCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
|
||||
|
||||
if (oldCategoryId == EmojiCategory.ID_RECENTS && categoryId != EmojiCategory.ID_RECENTS) {
|
||||
// Needs to save pending updates for recent keys when we get out of the recents
|
||||
// category because we don't want to move the recent emojis around while the user
|
||||
// is in the recents category.
|
||||
mEmojiPalettesAdapter.flushPendingRecentKeys();
|
||||
}
|
||||
|
||||
if (force || oldCategoryId != categoryId || oldCategoryPageId != categoryPageId) {
|
||||
if (initial || oldCategoryId != categoryId) {
|
||||
mEmojiCategory.setCurrentCategoryId(categoryId);
|
||||
mEmojiCategory.setCurrentCategoryPageId(categoryPageId);
|
||||
mEmojiPalettesAdapter.notifyDataSetChanged();
|
||||
mEmojiRecyclerView.scrollToPosition(categoryPageId);
|
||||
|
||||
if (mPager.getScrollState() != ViewPager2.SCROLL_STATE_DRAGGING) {
|
||||
// Not swiping
|
||||
mPager.setCurrentItem(mEmojiCategory.getTabIdFromCategoryId(
|
||||
mEmojiCategory.getCurrentCategoryId()), ! initial);
|
||||
}
|
||||
|
||||
final View old = mTabStrip.findViewWithTag((long) oldCategoryId);
|
||||
final View current = mTabStrip.findViewWithTag((long) categoryId);
|
||||
|
||||
if (old instanceof ImageView)
|
||||
Settings.getValues().mColors.setColor((ImageView) old, ColorType.EMOJI_CATEGORY);
|
||||
if (current instanceof ImageView)
|
||||
Settings.getValues().mColors.setColor((ImageView) current, ColorType.EMOJI_CATEGORY_SELECTED);
|
||||
}
|
||||
|
||||
final View old = mTabStrip.findViewWithTag((long) oldCategoryId);
|
||||
final View current = mTabStrip.findViewWithTag((long) categoryId);
|
||||
|
||||
if (old instanceof ImageView)
|
||||
Settings.getValues().mColors.setColor((ImageView) old, ColorType.EMOJI_CATEGORY);
|
||||
if (current instanceof ImageView)
|
||||
Settings.getValues().mColors.setColor((ImageView) current, ColorType.EMOJI_CATEGORY_SELECTED);
|
||||
}
|
||||
|
||||
public void clearKeyboardCache() {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
mEmojiCategory.clearKeyboardCache();
|
||||
mPager.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package helium314.keyboard.keyboard.emoji
|
||||
|
||||
import android.content.Context
|
||||
import helium314.keyboard.latin.settings.Defaults
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
|
||||
object SupportedEmojis {
|
||||
private val unsupportedEmojis = hashSetOf<String>()
|
||||
|
||||
fun load(context: Context) {
|
||||
val maxSdk = context.prefs().getInt(Settings.PREF_EMOJI_MAX_SDK, Defaults.PREF_EMOJI_MAX_SDK)
|
||||
unsupportedEmojis.clear()
|
||||
context.assets.open("emoji/minApi.txt").reader().readLines().forEach {
|
||||
val s = it.split(" ")
|
||||
val minApi = s.first().toInt()
|
||||
if (minApi > maxSdk)
|
||||
unsupportedEmojis.addAll(s.drop(1))
|
||||
}
|
||||
}
|
||||
|
||||
fun isUnsupported(emoji: String) = emoji in unsupportedEmojis
|
||||
}
|
|
@ -108,7 +108,7 @@ public final class KeyPreviewChoreographer {
|
|||
final boolean hasPopupKeys = (key.getPopupKeys() != null);
|
||||
keyPreviewView.setPreviewBackground(hasPopupKeys, keyPreviewPosition);
|
||||
final Colors colors = Settings.getValues().mColors;
|
||||
colors.setBackground(keyPreviewView, ColorType.KEY_PREVIEW);
|
||||
colors.setBackground(keyPreviewView, ColorType.KEY_PREVIEW_BACKGROUND);
|
||||
|
||||
// The key preview is placed vertically above the top edge of the parent key with an
|
||||
// arbitrary offset.
|
||||
|
|
|
@ -129,7 +129,7 @@ public final class KeyVisualAttributes {
|
|||
// when? -> hasShiftedLetterHint and isShiftedLetterActivated -> both are label flags
|
||||
mShiftedLetterHintActivatedColor = keyAttr.getColor(
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
|
||||
mPreviewTextColor = colors.get(ColorType.KEY_TEXT);
|
||||
mPreviewTextColor = colors.get(ColorType.KEY_PREVIEW_TEXT);
|
||||
|
||||
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f);
|
||||
|
|
|
@ -48,7 +48,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
if (id.isEmojiKeyboard) {
|
||||
mParams.mAllowRedundantPopupKeys = true
|
||||
readAttributes(R.xml.kbd_emoji)
|
||||
keysInRows = EmojiParser(mParams, mContext, Settings.getValues().mEmojiMaxSdk).parse()
|
||||
keysInRows = EmojiParser(mParams, mContext).parse()
|
||||
} else {
|
||||
try {
|
||||
setupParams()
|
||||
|
@ -176,7 +176,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
|
|||
val relativeWidthSumNew = row.sumOf { it.mWidth }
|
||||
val widthFactor = relativeWidthSum / relativeWidthSumNew
|
||||
// re-calculate absolute sizes and positions
|
||||
var currentX = 0f
|
||||
var currentX = mParams.mLeftPadding.toFloat()
|
||||
row.forEach {
|
||||
it.mWidth *= widthFactor
|
||||
it.setAbsoluteDimensions(currentX, y)
|
||||
|
|
|
@ -5,38 +5,63 @@ import android.content.Context
|
|||
import helium314.keyboard.keyboard.Key
|
||||
import helium314.keyboard.keyboard.Key.KeyParams
|
||||
import helium314.keyboard.keyboard.KeyboardId
|
||||
import helium314.keyboard.keyboard.emoji.SupportedEmojis
|
||||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.StringUtils
|
||||
import helium314.keyboard.latin.common.splitOnWhitespace
|
||||
import helium314.keyboard.latin.settings.Defaults
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.ResourceUtils
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import java.util.Collections
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class EmojiParser(private val params: KeyboardParams, private val context: Context, private val maxSdk: Int) {
|
||||
class EmojiParser(private val params: KeyboardParams, private val context: Context) {
|
||||
|
||||
fun parse(): ArrayList<ArrayList<KeyParams>> {
|
||||
val emojiArrayId = when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_EMOJI_RECENTS -> R.array.emoji_recents
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY1 -> R.array.emoji_smileys_emotion
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY2 -> R.array.emoji_people_body
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY3 -> R.array.emoji_animals_nature
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY4 -> R.array.emoji_food_drink
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY5 -> R.array.emoji_travel_places
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY6 -> R.array.emoji_activities
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY7 -> R.array.emoji_objects
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY8 -> R.array.emoji_symbols
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY9 -> R.array.emoji_flags
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY10 -> R.array.emoji_emoticons
|
||||
else -> throw(IllegalStateException("can only parse emoji categories where an array exists"))
|
||||
val emojiFileName = when (params.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY1 -> "SMILEYS_AND_EMOTION.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY2 -> "PEOPLE_AND_BODY.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY3 -> "ANIMALS_AND_NATURE.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY4 -> "FOOD_AND_DRINK.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY5 -> "TRAVEL_AND_PLACES.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY6 -> "ACTIVITIES.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY7 -> "OBJECTS.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY8 -> "SYMBOLS.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY9 -> "FLAGS.txt"
|
||||
KeyboardId.ELEMENT_EMOJI_CATEGORY10 -> "EMOTICONS.txt"
|
||||
else -> null
|
||||
}
|
||||
val emojiArray = context.resources.getStringArray(emojiArrayId)
|
||||
val popupEmojisArray = if (params.mId.mElementId != KeyboardId.ELEMENT_EMOJI_CATEGORY2) null
|
||||
else context.resources.getStringArray(R.array.emoji_people_body_more)
|
||||
if (popupEmojisArray != null && emojiArray.size != popupEmojisArray.size)
|
||||
throw(IllegalStateException("Inconsistent array size between codesArray and popupKeysArray"))
|
||||
val emojiLines = if (emojiFileName == null) {
|
||||
listOf( // special template keys for recents category
|
||||
StringUtils.newSingleCodePointString(Constants.RECENTS_TEMPLATE_KEY_CODE_0),
|
||||
StringUtils.newSingleCodePointString(Constants.RECENTS_TEMPLATE_KEY_CODE_1),
|
||||
)
|
||||
} else {
|
||||
context.assets.open("emoji/$emojiFileName").reader().use { it.readLines() }
|
||||
}
|
||||
val defaultSkinTone = context.prefs().getString(Settings.PREF_EMOJI_SKIN_TONE, Defaults.PREF_EMOJI_SKIN_TONE)!!
|
||||
if (params.mId.mElementId == KeyboardId.ELEMENT_EMOJI_CATEGORY2 && defaultSkinTone != "") {
|
||||
// adjust PEOPLE_AND_BODY if we have a non-yellow default skin tone
|
||||
val modifiedLines = emojiLines.map {
|
||||
val split = it.splitOnWhitespace().toMutableList()
|
||||
// find the line containing the skin tone, and swap with first
|
||||
val foundIndex = split.indexOfFirst { it.contains(defaultSkinTone) }
|
||||
if (foundIndex > 0) {
|
||||
Collections.swap(split, 0, foundIndex)
|
||||
}
|
||||
split.joinToString(" ")
|
||||
}
|
||||
return parseLines(modifiedLines)
|
||||
}
|
||||
return parseLines(emojiLines)
|
||||
}
|
||||
|
||||
val row = ArrayList<KeyParams>(emojiArray.size)
|
||||
private fun parseLines(lines: List<String>): ArrayList<ArrayList<KeyParams>> {
|
||||
val row = ArrayList<KeyParams>(lines.size)
|
||||
var currentX = params.mLeftPadding.toFloat()
|
||||
val currentY = params.mTopPadding.toFloat() // no need to ever change, assignment to rows into rows is done in DynamicGridKeyboard
|
||||
|
||||
|
@ -44,14 +69,20 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
|
|||
// this is a bit long, but ensures that emoji size stays the same, independent of these settings
|
||||
// we also ignore side padding for key width, and prefer fewer keys per row over narrower keys
|
||||
val defaultKeyWidth = ResourceUtils.getDefaultKeyboardWidth(context) * params.mDefaultKeyWidth
|
||||
val keyWidth = defaultKeyWidth * sqrt(Settings.getValues().mKeyboardHeightScale)
|
||||
var keyWidth = defaultKeyWidth * sqrt(Settings.getValues().mKeyboardHeightScale)
|
||||
val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
|
||||
val defaultBottomPadding = context.resources.getFraction(R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight)
|
||||
val emojiKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false) * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
|
||||
val keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getValues().mKeyboardHeightScale // still apply height scale to key
|
||||
var keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getValues().mKeyboardHeightScale // still apply height scale to key
|
||||
|
||||
emojiArray.forEachIndexed { i, codeArraySpec ->
|
||||
val keyParams = parseEmojiKey(codeArraySpec, popupEmojisArray?.get(i)?.takeIf { it.isNotEmpty() }) ?: return@forEachIndexed
|
||||
if (Settings.getValues().mEmojiKeyFit) {
|
||||
keyWidth *= Settings.getValues().mFontSizeMultiplierEmoji
|
||||
keyHeight *= Settings.getValues().mFontSizeMultiplierEmoji
|
||||
}
|
||||
|
||||
|
||||
lines.forEach { line ->
|
||||
val keyParams = parseEmojiKeyNew(line) ?: return@forEach
|
||||
keyParams.xPos = currentX
|
||||
keyParams.yPos = currentY
|
||||
keyParams.mAbsoluteWidth = keyWidth
|
||||
|
@ -62,44 +93,30 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
|
|||
return arrayListOf(row)
|
||||
}
|
||||
|
||||
private fun getLabelAndCode(spec: String): Pair<String, Int>? {
|
||||
val specAndSdk = spec.split("||")
|
||||
if (specAndSdk.getOrNull(1)?.toIntOrNull()?.let { it > maxSdk } == true) return null
|
||||
if ("," !in specAndSdk.first()) {
|
||||
val code = specAndSdk.first().toIntOrNull(16) ?: return specAndSdk.first() to KeyCode.MULTIPLE_CODE_POINTS // text emojis
|
||||
val label = StringUtils.newSingleCodePointString(code)
|
||||
return label to code
|
||||
private fun parseEmojiKeyNew(line: String): KeyParams? {
|
||||
if (!line.contains(" ") || params.mId.mElementId == KeyboardId.ELEMENT_EMOJI_CATEGORY10) {
|
||||
// single emoji without popups, or emoticons (there is one that contains space...)
|
||||
return if (SupportedEmojis.isUnsupported(line)) null
|
||||
else KeyParams(line, line.getCode(), null, null, Key.LABEL_FLAGS_FONT_NORMAL, params)
|
||||
}
|
||||
val labelBuilder = StringBuilder()
|
||||
for (codePointString in specAndSdk.first().split(",")) {
|
||||
val cp = codePointString.toInt(16)
|
||||
labelBuilder.appendCodePoint(cp)
|
||||
}
|
||||
return labelBuilder.toString() to KeyCode.MULTIPLE_CODE_POINTS
|
||||
}
|
||||
|
||||
private fun parseEmojiKey(spec: String, popupKeysString: String? = null): KeyParams? {
|
||||
val (label, code) = getLabelAndCode(spec) ?: return null
|
||||
val sb = StringBuilder()
|
||||
popupKeysString?.split(";")?.let { popupKeys ->
|
||||
popupKeys.forEach {
|
||||
val (mkLabel, _) = getLabelAndCode(it) ?: return@forEach
|
||||
sb.append(mkLabel).append(",")
|
||||
}
|
||||
}
|
||||
val popupKeysSpec = if (sb.isNotEmpty()) {
|
||||
sb.deleteCharAt(sb.length - 1)
|
||||
sb.toString()
|
||||
} else null
|
||||
val split = line.split(" ")
|
||||
val label = split.first()
|
||||
if (SupportedEmojis.isUnsupported(label)) return null
|
||||
val popupKeysSpec = split.drop(1).filterNot { SupportedEmojis.isUnsupported(it) }
|
||||
.takeIf { it.isNotEmpty() }?.joinToString(",")
|
||||
return KeyParams(
|
||||
label,
|
||||
code,
|
||||
label.getCode(),
|
||||
if (popupKeysSpec != null) EMOJI_HINT_LABEL else null,
|
||||
popupKeysSpec,
|
||||
Key.LABEL_FLAGS_FONT_NORMAL,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.getCode(): Int =
|
||||
if (StringUtils.codePointCount(this) != 1) KeyCode.MULTIPLE_CODE_POINTS
|
||||
else Character.codePointAt(this, 0)
|
||||
}
|
||||
|
||||
const val EMOJI_HINT_LABEL = "◥"
|
||||
const val EMOJI_HINT_LABEL = "◥"
|
||||
|
|
|
@ -11,6 +11,7 @@ import helium314.keyboard.latin.R
|
|||
import helium314.keyboard.latin.common.splitOnFirstSpacesOnly
|
||||
import helium314.keyboard.latin.common.splitOnWhitespace
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.utils.SpacedTokens
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
|
||||
import java.io.InputStream
|
||||
import java.util.Locale
|
||||
|
@ -83,7 +84,7 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
|
|||
READER_MODE_EXTRA_KEYS -> if (!onlyPopupKeys) addExtraKey(line.split(colonSpaceRegex, 2))
|
||||
READER_MODE_LABELS -> if (!onlyPopupKeys) addLabel(line.split(colonSpaceRegex, 2))
|
||||
READER_MODE_NUMBER_ROW -> localizedNumberKeys = line.splitOnWhitespace()
|
||||
READER_MODE_TLD -> line.splitOnWhitespace().forEach { tlds.add(".$it") }
|
||||
READER_MODE_TLD -> SpacedTokens(line).forEach { tlds.add(".$it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -226,7 +227,7 @@ private fun getLocaleTlds(locale: Locale): LinkedHashSet<String> {
|
|||
return tlds
|
||||
specialCountryTlds.forEach {
|
||||
if (ccLower != it.first) return@forEach
|
||||
tlds.addAll(it.second.splitOnWhitespace())
|
||||
tlds.addAll(SpacedTokens(it.second))
|
||||
return@getLocaleTlds tlds
|
||||
}
|
||||
tlds.add(".$ccLower")
|
||||
|
@ -235,7 +236,7 @@ private fun getLocaleTlds(locale: Locale): LinkedHashSet<String> {
|
|||
|
||||
private fun getDefaultTlds(locale: Locale): LinkedHashSet<String> {
|
||||
val tlds = linkedSetOf<String>()
|
||||
tlds.addAll(defaultTlds.splitOnWhitespace())
|
||||
tlds.addAll(SpacedTokens(defaultTlds))
|
||||
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
|
||||
tlds.add(".eu")
|
||||
return tlds
|
||||
|
@ -264,9 +265,9 @@ private fun getCurrencyKey(locale: Locale): Pair<String, List<String>> {
|
|||
return euro
|
||||
if (locale.toString().matches(euroLocales))
|
||||
return euro
|
||||
if (locale.language.matches("ca|eu|lb|mt".toRegex()))
|
||||
if (locale.language.matches("ca|eu|lb|mt|pms".toRegex()))
|
||||
return euro
|
||||
if (locale.language.matches("fa|iw|ko|lo|mn|ne|si|th|uk|vi|km".toRegex()))
|
||||
if (locale.language.matches("ak|dag|ee|fa|gaa|ha|he|ig|iw|lo|ko|km|mn|ne|si|th|uk|vi|yo".toRegex()))
|
||||
return genericCurrencyKey(getCurrency(locale))
|
||||
if (locale.language == "hy")
|
||||
return dram
|
||||
|
@ -292,17 +293,24 @@ private fun getCurrency(locale: Locale): String {
|
|||
if (locale.country == "BD") return "৳"
|
||||
if (locale.country == "LK") return "රු"
|
||||
return when (locale.language) {
|
||||
"ak" -> "¢"
|
||||
"dag" -> "¢"
|
||||
"ee" -> "¢"
|
||||
"fa" -> "﷼"
|
||||
"iw" -> "₪"
|
||||
"ko" -> "₩"
|
||||
"gaa" -> "¢"
|
||||
"ha" -> "₦"
|
||||
"ig" -> "₦"
|
||||
"iw", "he" -> "₪"
|
||||
"lo" -> "₭"
|
||||
"km" -> "៛"
|
||||
"ko" -> "₩"
|
||||
"mn" -> "₮"
|
||||
"ne" -> "रु."
|
||||
"si" -> "රු"
|
||||
"th" -> "฿"
|
||||
"uk" -> "₴"
|
||||
"vi" -> "₫"
|
||||
"km" -> "៛"
|
||||
"yo" -> "₦"
|
||||
else -> "$"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,15 @@ object KeyCode {
|
|||
const val UNSPECIFIED = 0
|
||||
|
||||
const val CTRL = -1
|
||||
const val CTRL_LOCK = -2
|
||||
//const val CTRL_LOCK = -2
|
||||
const val ALT = -3
|
||||
const val ALT_LOCK = -4
|
||||
//const val ALT_LOCK = -4
|
||||
const val FN = -5
|
||||
const val FN_LOCK = -6
|
||||
//const val FN_LOCK = -6
|
||||
const val DELETE = -7
|
||||
const val DELETE_WORD = -8
|
||||
const val FORWARD_DELETE = -9
|
||||
const val FORWARD_DELETE_WORD = -10
|
||||
//const val DELETE_WORD = -8
|
||||
//const val FORWARD_DELETE = -9
|
||||
//const val FORWARD_DELETE_WORD = -10
|
||||
const val SHIFT = -11
|
||||
const val CAPS_LOCK = -13
|
||||
|
||||
|
@ -51,21 +51,21 @@ object KeyCode {
|
|||
const val CLIPBOARD_SELECT_WORD = -34 // CLIPBOARD_SELECT
|
||||
const val CLIPBOARD_SELECT_ALL = -35
|
||||
const val CLIPBOARD_CLEAR_HISTORY = -36
|
||||
const val CLIPBOARD_CLEAR_FULL_HISTORY = -37
|
||||
const val CLIPBOARD_CLEAR_PRIMARY_CLIP = -38
|
||||
//const val CLIPBOARD_CLEAR_FULL_HISTORY = -37
|
||||
//const val CLIPBOARD_CLEAR_PRIMARY_CLIP = -38
|
||||
|
||||
const val COMPACT_LAYOUT_TO_LEFT = -111
|
||||
const val COMPACT_LAYOUT_TO_RIGHT = -112
|
||||
//const val COMPACT_LAYOUT_TO_LEFT = -111
|
||||
//const val COMPACT_LAYOUT_TO_RIGHT = -112
|
||||
const val SPLIT_LAYOUT = -113
|
||||
const val MERGE_LAYOUT = -114
|
||||
//const val MERGE_LAYOUT = -114
|
||||
|
||||
const val UNDO = -131
|
||||
const val REDO = -132
|
||||
|
||||
const val ALPHA = -201 // VIEW_CHARACTERS
|
||||
const val SYMBOL = -202 // VIEW_SYMBOLS
|
||||
const val VIEW_SYMBOLS2 = -203
|
||||
const val VIEW_NUMERIC = -204
|
||||
//const val VIEW_SYMBOLS2 = -203
|
||||
//const val VIEW_NUMERIC = -204
|
||||
const val NUMPAD = -205 // VIEW_NUMERIC_ADVANCED
|
||||
const val VIEW_PHONE = -206
|
||||
const val VIEW_PHONE2 = -207
|
||||
|
@ -74,21 +74,21 @@ object KeyCode {
|
|||
const val EMOJI = -212 // IME_UI_MODE_MEDIA
|
||||
const val CLIPBOARD = -213 // IME_UI_MODE_CLIPBOARD
|
||||
|
||||
const val SYSTEM_INPUT_METHOD_PICKER = -221
|
||||
const val SYSTEM_PREV_INPUT_METHOD = -222
|
||||
const val SYSTEM_NEXT_INPUT_METHOD = -223
|
||||
const val IME_SUBTYPE_PICKER = -224
|
||||
const val IME_PREV_SUBTYPE = -225
|
||||
const val IME_NEXT_SUBTYPE = -226
|
||||
//const val SYSTEM_INPUT_METHOD_PICKER = -221
|
||||
//const val SYSTEM_PREV_INPUT_METHOD = -222
|
||||
//const val SYSTEM_NEXT_INPUT_METHOD = -223
|
||||
//const val IME_SUBTYPE_PICKER = -224
|
||||
//const val IME_PREV_SUBTYPE = -225
|
||||
//const val IME_NEXT_SUBTYPE = -226
|
||||
const val LANGUAGE_SWITCH = -227
|
||||
|
||||
const val IME_SHOW_UI = -231
|
||||
const val IME_HIDE_UI = -232
|
||||
//const val IME_SHOW_UI = -231
|
||||
//const val IME_HIDE_UI = -232
|
||||
const val VOICE_INPUT = -233
|
||||
|
||||
const val TOGGLE_SMARTBAR_VISIBILITY = -241
|
||||
const val TOGGLE_ACTIONS_OVERFLOW = -242
|
||||
const val TOGGLE_ACTIONS_EDITOR = -243
|
||||
//const val TOGGLE_SMARTBAR_VISIBILITY = -241
|
||||
//const val TOGGLE_ACTIONS_OVERFLOW = -242
|
||||
//const val TOGGLE_ACTIONS_EDITOR = -243
|
||||
const val TOGGLE_INCOGNITO_MODE = -244
|
||||
const val TOGGLE_AUTOCORRECT = -245
|
||||
|
||||
|
@ -104,18 +104,18 @@ object KeyCode {
|
|||
const val CURRENCY_SLOT_6 = -806
|
||||
|
||||
const val MULTIPLE_CODE_POINTS = -902
|
||||
const val DRAG_MARKER = -991
|
||||
const val NOOP = -999
|
||||
//const val DRAG_MARKER = -991
|
||||
//const val NOOP = -999
|
||||
|
||||
const val CHAR_WIDTH_SWITCHER = -9701
|
||||
const val CHAR_WIDTH_FULL = -9702
|
||||
const val CHAR_WIDTH_HALF = -9703
|
||||
//const val CHAR_WIDTH_SWITCHER = -9701
|
||||
//const val CHAR_WIDTH_FULL = -9702
|
||||
//const val CHAR_WIDTH_HALF = -9703
|
||||
|
||||
const val KANA_SMALL = 12307
|
||||
const val KANA_SWITCHER = -9710
|
||||
const val KANA_HIRA = -9711
|
||||
const val KANA_KATA = -9712
|
||||
const val KANA_HALF_KATA = -9713
|
||||
//const val KANA_SMALL = 12307
|
||||
//const val KANA_SWITCHER = -9710
|
||||
//const val KANA_HIRA = -9711
|
||||
//const val KANA_KATA = -9712
|
||||
//const val KANA_HALF_KATA = -9713
|
||||
|
||||
const val KESHIDA = 1600
|
||||
const val ZWNJ = 8204 // 0x200C, named HALF_SPACE in FlorisBoard
|
||||
|
@ -137,7 +137,7 @@ object KeyCode {
|
|||
const val PAGE_UP = -10010
|
||||
const val PAGE_DOWN = -10011
|
||||
const val META = -10012
|
||||
const val META_LOCK = -10013 // to be consistent with the CTRL/ALT/FN LOCK codes, not sure whether this will be used
|
||||
//const val META_LOCK = -10013 // to be consistent with the CTRL/ALT/FN LOCK codes, not sure whether this will be used
|
||||
const val TAB = -10014
|
||||
const val WORD_LEFT = -10015
|
||||
const val WORD_RIGHT = -10016
|
||||
|
@ -165,8 +165,15 @@ object KeyCode {
|
|||
const val F11 = -10038
|
||||
const val F12 = -10039
|
||||
const val BACK = -10040
|
||||
const val SELECT_LEFT = -10041
|
||||
const val SELECT_RIGHT = -10042
|
||||
//const val SELECT_LEFT = -10041
|
||||
//const val SELECT_RIGHT = -10042
|
||||
const val TIMESTAMP = -10043
|
||||
const val CTRL_LEFT = -10044
|
||||
const val CTRL_RIGHT = -10045
|
||||
const val ALT_LEFT = -10046
|
||||
const val ALT_RIGHT = -10047
|
||||
const val META_LEFT = -10048
|
||||
const val META_RIGHT = -10049
|
||||
|
||||
/** to make sure a FlorisBoard code works when reading a JSON layout */
|
||||
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
|
||||
|
@ -182,7 +189,8 @@ object KeyCode {
|
|||
SYMBOL_ALPHA, TOGGLE_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SPLIT_LAYOUT, SHIFT_ENTER,
|
||||
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED, CLIPBOARD_COPY_ALL, WORD_LEFT, WORD_RIGHT, PAGE_UP,
|
||||
PAGE_DOWN, META, TAB, ESCAPE, INSERT, SLEEP, MEDIA_PLAY, MEDIA_PAUSE, MEDIA_PLAY_PAUSE, MEDIA_NEXT,
|
||||
MEDIA_PREVIOUS, VOL_UP, VOL_DOWN, MUTE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, BACK
|
||||
MEDIA_PREVIOUS, VOL_UP, VOL_DOWN, MUTE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, BACK,
|
||||
TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT
|
||||
-> this
|
||||
|
||||
// conversion
|
||||
|
@ -194,8 +202,12 @@ object KeyCode {
|
|||
else -> throw IllegalStateException("key code $this not yet supported")
|
||||
}
|
||||
|
||||
// todo: three are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
|
||||
/** convert a keyCode / codePoint to a KeyEvent.KEYCODE_<xxx>, fallback to KeyEvent.KEYCODE_UNKNOWN */
|
||||
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
|
||||
/**
|
||||
* Convert a keyCode / codePoint to a KeyEvent.KEYCODE_<xxx>.
|
||||
* Fallback to KeyEvent.KEYCODE_UNKNOWN.
|
||||
* To be uses for fake hardware key press.
|
||||
* */
|
||||
fun Int.toKeyEventCode(): Int = if (this > 0)
|
||||
when (this.toChar().uppercaseChar()) {
|
||||
'/' -> KeyEvent.KEYCODE_SLASH
|
||||
|
|
|
@ -31,6 +31,7 @@ object KeyLabel {
|
|||
const val META = "meta"
|
||||
const val TAB = "tab"
|
||||
const val ESCAPE = "esc"
|
||||
const val TIMESTAMP = "timestamp"
|
||||
|
||||
/** to make sure a FlorisBoard label works when reading a JSON layout */
|
||||
// resulting special labels should be names of FunctionalKey enum, case insensitive
|
||||
|
|
|
@ -49,6 +49,6 @@ open class PopupSet<T : AbstractKeyData>(
|
|||
}
|
||||
|
||||
class SimplePopups(val popupKeys: Collection<String>?) : PopupSet<AbstractKeyData>() {
|
||||
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys
|
||||
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys?.map { KeyData.processLabel(it, params) }
|
||||
override fun isEmpty(): Boolean = popupKeys.isNullOrEmpty()
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import kotlinx.serialization.Transient
|
|||
import helium314.keyboard.keyboard.Key
|
||||
import helium314.keyboard.keyboard.KeyboardId
|
||||
import helium314.keyboard.keyboard.KeyboardTheme
|
||||
import helium314.keyboard.keyboard.internal.KeyboardCodesSet
|
||||
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
|
||||
import helium314.keyboard.keyboard.internal.KeyboardParams
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
|
||||
|
@ -131,6 +132,9 @@ sealed interface KeyData : AbstractKeyData {
|
|||
keys.add("!icon/start_onehanded_mode_key|!code/key_toggle_onehanded")
|
||||
if (!params.mId.mDeviceLocked)
|
||||
keys.add("!icon/settings_key|!code/key_settings")
|
||||
if (shouldShowTldPopups(params)) {
|
||||
keys.add(",")
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
|
@ -285,6 +289,40 @@ sealed interface KeyData : AbstractKeyData {
|
|||
return getStringInLocale(id, params)
|
||||
}
|
||||
|
||||
fun processLabel(label: String, params: KeyboardParams): String = when (label) {
|
||||
KeyLabel.SYMBOL_ALPHA -> if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelSymbol else params.mLocaleKeyboardInfos.labelAlphabet
|
||||
KeyLabel.SYMBOL -> params.mLocaleKeyboardInfos.labelSymbol
|
||||
KeyLabel.ALPHA -> params.mLocaleKeyboardInfos.labelAlphabet
|
||||
KeyLabel.COMMA -> params.mLocaleKeyboardInfos.labelComma
|
||||
KeyLabel.PERIOD -> getPeriodLabel(params)
|
||||
KeyLabel.SPACE -> getSpaceLabel(params)
|
||||
KeyLabel.ACTION -> "${getActionKeyLabel(params)}|${getActionKeyCode(params)}"
|
||||
KeyLabel.DELETE -> "!icon/delete_key|!code/key_delete"
|
||||
KeyLabel.SHIFT -> "${getShiftLabel(params)}|!code/key_shift"
|
||||
KeyLabel.COM -> params.mLocaleKeyboardInfos.tlds.first()
|
||||
KeyLabel.LANGUAGE_SWITCH -> "!icon/language_switch_key|!code/key_language_switch"
|
||||
KeyLabel.ZWNJ -> "!icon/zwnj_key|\u200C"
|
||||
KeyLabel.CURRENCY -> params.mLocaleKeyboardInfos.currencyKey.first
|
||||
KeyLabel.CURRENCY1 -> params.mLocaleKeyboardInfos.currencyKey.second[0]
|
||||
KeyLabel.CURRENCY2 -> params.mLocaleKeyboardInfos.currencyKey.second[1]
|
||||
KeyLabel.CURRENCY3 -> params.mLocaleKeyboardInfos.currencyKey.second[2]
|
||||
KeyLabel.CURRENCY4 -> params.mLocaleKeyboardInfos.currencyKey.second[3]
|
||||
KeyLabel.CURRENCY5 -> params.mLocaleKeyboardInfos.currencyKey.second[4]
|
||||
KeyLabel.CTRL, KeyLabel.ALT, KeyLabel.FN, KeyLabel.META , KeyLabel.ESCAPE -> label.uppercase(Locale.US)
|
||||
KeyLabel.TAB -> "!icon/tab_key|!code/${KeyCode.TAB}"
|
||||
KeyLabel.TIMESTAMP -> "⌚|!code/${KeyCode.TIMESTAMP}"
|
||||
else -> {
|
||||
if (label in toolbarKeyStrings.values) {
|
||||
"!icon/$label|!code/${getCodeForToolbarKey(ToolbarKey.valueOf(label.uppercase(Locale.US)))}"
|
||||
} else label
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowTldPopups(params: KeyboardParams): Boolean =
|
||||
(Settings.getInstance().current.mShowTldPopupKeys
|
||||
&& params.mId.mSubtype.layouts[LayoutType.FUNCTIONAL] != "functional_keys_tablet"
|
||||
&& params.mId.mMode in setOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL))
|
||||
|
||||
// could make arrays right away, but they need to be copied anyway as popupKeys arrays are modified when creating KeyParams
|
||||
private const val POPUP_EYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
|
||||
private const val POPUP_EYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
|
||||
|
@ -301,12 +339,13 @@ sealed interface KeyData : AbstractKeyData {
|
|||
// so better only do it in case the popup stuff needs more improvements
|
||||
// idea: directly create PopupKeySpec, but need to deal with needsToUpcase and popupKeysColumnAndFlags
|
||||
fun getPopupLabel(params: KeyboardParams): String {
|
||||
val newLabel = processLabel(params)
|
||||
val newLabel = processLabel(label, params)
|
||||
if (code == KeyCode.UNSPECIFIED) {
|
||||
if (newLabel == label) return label
|
||||
if (newLabel == label || newLabel.contains(KeyboardCodesSet.PREFIX_CODE))
|
||||
return newLabel
|
||||
val newCode = processCode()
|
||||
if (newLabel.endsWith("|")) return "${newLabel}!code/$newCode" // for toolbar keys
|
||||
return if (newCode == code) newLabel else "${newLabel}|!code/$newCode"
|
||||
if (newLabel.endsWith("|")) return "${newLabel}${KeyboardCodesSet.PREFIX_CODE}$newCode" // maybe not used any more
|
||||
return if (newCode == code) newLabel else "${newLabel}|${KeyboardCodesSet.PREFIX_CODE}$newCode"
|
||||
}
|
||||
if (code >= 32) {
|
||||
if (newLabel.startsWith(KeyboardIconsSet.PREFIX_ICON)) {
|
||||
|
@ -324,12 +363,12 @@ sealed interface KeyData : AbstractKeyData {
|
|||
val outputText = String(codePoints, 0, codePoints.size)
|
||||
return "${newLabel}|$outputText"
|
||||
}
|
||||
return if (newLabel.endsWith("|")) "$newLabel!code/${processCode()}" // for toolbar keys
|
||||
else "$newLabel|!code/${processCode()}"
|
||||
return if (newLabel.endsWith("|")) "$newLabel${KeyboardCodesSet.PREFIX_CODE}${processCode()}" // for toolbar keys
|
||||
else "$newLabel|${KeyboardCodesSet.PREFIX_CODE}${processCode()}"
|
||||
}
|
||||
|
||||
fun getCurrencyLabel(params: KeyboardParams): String {
|
||||
val newLabel = processLabel(params)
|
||||
val newLabel = processLabel(label, params)
|
||||
return when (code) {
|
||||
// consider currency codes for label
|
||||
KeyCode.CURRENCY_SLOT_1 -> "$newLabel|${params.mLocaleKeyboardInfos.currencyKey.first}"
|
||||
|
@ -388,9 +427,9 @@ sealed interface KeyData : AbstractKeyData {
|
|||
newLabel = getCurrencyLabel(params)
|
||||
} else {
|
||||
newCode = processCode()
|
||||
newLabel = processLabel(params)
|
||||
newLabel = processLabel(label, params)
|
||||
}
|
||||
val newLabelFlags = labelFlags or additionalLabelFlags or getAdditionalLabelFlags(params)
|
||||
var newLabelFlags = labelFlags or additionalLabelFlags or getAdditionalLabelFlags(params)
|
||||
val newPopupKeys = popup.merge(getAdditionalPopupKeys(params))
|
||||
|
||||
val background = when (type) {
|
||||
|
@ -402,6 +441,9 @@ sealed interface KeyData : AbstractKeyData {
|
|||
KeyType.LOCK -> getShiftBackground(params)
|
||||
null -> getDefaultBackground(params)
|
||||
}
|
||||
if (background == Key.BACKGROUND_TYPE_FUNCTIONAL
|
||||
|| background == Key.BACKGROUND_TYPE_STICKY_ON || background == Key.BACKGROUND_TYPE_STICKY_OFF)
|
||||
newLabelFlags = newLabelFlags or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
|
||||
|
||||
return if (newCode == KeyCode.UNSPECIFIED || newCode == KeyCode.MULTIPLE_CODE_POINTS) {
|
||||
// code will be determined from label if possible (i.e. label is single code point)
|
||||
|
@ -471,36 +513,6 @@ sealed interface KeyData : AbstractKeyData {
|
|||
else params.mDefaultKeyWidth
|
||||
}
|
||||
|
||||
// todo (later): encoding the code in the label should be avoided, because we know it already
|
||||
private fun processLabel(params: KeyboardParams): String = when (label) {
|
||||
KeyLabel.SYMBOL_ALPHA -> if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelSymbol else params.mLocaleKeyboardInfos.labelAlphabet
|
||||
KeyLabel.SYMBOL -> params.mLocaleKeyboardInfos.labelSymbol
|
||||
KeyLabel.ALPHA -> params.mLocaleKeyboardInfos.labelAlphabet
|
||||
KeyLabel.COMMA -> params.mLocaleKeyboardInfos.labelComma
|
||||
KeyLabel.PERIOD -> getPeriodLabel(params)
|
||||
KeyLabel.SPACE -> getSpaceLabel(params)
|
||||
KeyLabel.ACTION -> "${getActionKeyLabel(params)}|${getActionKeyCode(params)}"
|
||||
KeyLabel.DELETE -> "!icon/delete_key|!code/key_delete"
|
||||
KeyLabel.SHIFT -> "${getShiftLabel(params)}|!code/key_shift"
|
||||
// KeyLabel.EMOJI -> "!icon/emoji_normal_key|!code/key_emoji"
|
||||
KeyLabel.COM -> params.mLocaleKeyboardInfos.tlds.first()
|
||||
KeyLabel.LANGUAGE_SWITCH -> "!icon/language_switch_key|!code/key_language_switch"
|
||||
KeyLabel.ZWNJ -> "!icon/zwnj_key|\u200C"
|
||||
KeyLabel.CURRENCY -> params.mLocaleKeyboardInfos.currencyKey.first
|
||||
KeyLabel.CURRENCY1 -> params.mLocaleKeyboardInfos.currencyKey.second[0]
|
||||
KeyLabel.CURRENCY2 -> params.mLocaleKeyboardInfos.currencyKey.second[1]
|
||||
KeyLabel.CURRENCY3 -> params.mLocaleKeyboardInfos.currencyKey.second[2]
|
||||
KeyLabel.CURRENCY4 -> params.mLocaleKeyboardInfos.currencyKey.second[3]
|
||||
KeyLabel.CURRENCY5 -> params.mLocaleKeyboardInfos.currencyKey.second[4]
|
||||
KeyLabel.CTRL, KeyLabel.ALT, KeyLabel.FN, KeyLabel.META , KeyLabel.ESCAPE -> label.uppercase(Locale.US)
|
||||
KeyLabel.TAB -> "!icon/tab_key|"
|
||||
else -> {
|
||||
if (label in toolbarKeyStrings.values) {
|
||||
"!icon/$label|"
|
||||
} else label
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCode(): Int {
|
||||
if (code != KeyCode.UNSPECIFIED) return code
|
||||
return when (label) {
|
||||
|
@ -513,6 +525,7 @@ sealed interface KeyData : AbstractKeyData {
|
|||
KeyLabel.META -> KeyCode.META
|
||||
KeyLabel.TAB -> KeyCode.TAB
|
||||
KeyLabel.ESCAPE -> KeyCode.ESCAPE
|
||||
KeyLabel.TIMESTAMP -> KeyCode.TIMESTAMP
|
||||
else -> {
|
||||
if (label in toolbarKeyStrings.values) {
|
||||
getCodeForToolbarKey(ToolbarKey.valueOf(label.uppercase(Locale.US)))
|
||||
|
@ -524,20 +537,20 @@ sealed interface KeyData : AbstractKeyData {
|
|||
// todo (later): add explanations / reasoning, often this is just taken from conversion from OpenBoard / AOSP layouts
|
||||
private fun getAdditionalLabelFlags(params: KeyboardParams): Int {
|
||||
return when (label) {
|
||||
KeyLabel.ALPHA, KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL -> Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
|
||||
KeyLabel.ALPHA, KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL -> Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
KeyLabel.COMMA -> Key.LABEL_FLAGS_HAS_POPUP_HINT
|
||||
// essentially the first term only changes the appearance of the armenian period key in holo theme
|
||||
KeyLabel.PERIOD -> (Key.LABEL_FLAGS_HAS_POPUP_HINT and
|
||||
if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelFlags else 0) or
|
||||
(if (shouldShowTldPopups(params)) 0 else Key.LABEL_FLAGS_DISABLE_HINT_LABEL) or
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or
|
||||
// in functional_keys.json the label flag is already defined, let's not override it in case it's removed by the user
|
||||
if (!params.mId.isAlphaOrSymbolKeyboard && shouldShowTldPopups(params)) Key.LABEL_FLAGS_DISABLE_HINT_LABEL else 0
|
||||
KeyLabel.ACTION -> {
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_AUTO_X_SCALE or
|
||||
Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR or
|
||||
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO or
|
||||
Key.LABEL_FLAGS_HAS_POPUP_HINT or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
|
||||
}
|
||||
KeyLabel.SPACE -> if (params.mId.isNumberLayout) Key.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM else 0
|
||||
KeyLabel.SHIFT -> Key.LABEL_FLAGS_PRESERVE_CASE or if (!params.mId.isAlphabetKeyboard) Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR else 0
|
||||
KeyLabel.SHIFT -> Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
toolbarKeyStrings[ToolbarKey.EMOJI] -> KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
|
||||
KeyLabel.COM -> Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE
|
||||
KeyLabel.ZWNJ -> Key.LABEL_FLAGS_HAS_POPUP_HINT
|
||||
|
@ -580,11 +593,6 @@ sealed interface KeyData : AbstractKeyData {
|
|||
if (shouldShowTldPopups(params)) params.mLocaleKeyboardInfos.tlds
|
||||
else getPunctuationPopupKeys(params)
|
||||
)
|
||||
|
||||
private fun shouldShowTldPopups(params: KeyboardParams): Boolean =
|
||||
(Settings.getInstance().current.mShowTldPopupKeys
|
||||
&& params.mId.mSubtype.layouts[LayoutType.FUNCTIONAL] != "functional_keys_tablet"
|
||||
&& params.mId.mMode in setOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.Context
|
|||
import androidx.core.content.edit
|
||||
import helium314.keyboard.keyboard.ColorSetting
|
||||
import helium314.keyboard.keyboard.KeyboardTheme
|
||||
import helium314.keyboard.keyboard.emoji.SupportedEmojis
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
|
||||
import helium314.keyboard.latin.common.ColorType
|
||||
import helium314.keyboard.latin.common.Constants.Separators
|
||||
|
@ -23,6 +24,7 @@ import helium314.keyboard.latin.utils.DictionaryInfoUtils.USER_DICTIONARY_SUFFIX
|
|||
import helium314.keyboard.latin.utils.LayoutType
|
||||
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
|
||||
import helium314.keyboard.latin.utils.LayoutUtilsCustom
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.script
|
||||
import helium314.keyboard.latin.utils.SubtypeSettings
|
||||
|
@ -32,8 +34,8 @@ import helium314.keyboard.latin.utils.defaultPinnedToolbarPref
|
|||
import helium314.keyboard.latin.utils.getResourceSubtypes
|
||||
import helium314.keyboard.latin.utils.locale
|
||||
import helium314.keyboard.latin.utils.mainLayoutName
|
||||
import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import helium314.keyboard.latin.utils.protectedPrefs
|
||||
import helium314.keyboard.latin.utils.upgradeToolbarPrefs
|
||||
import helium314.keyboard.latin.utils.writeCustomKeyCodes
|
||||
import helium314.keyboard.settings.screens.colorPrefsAndResIds
|
||||
|
@ -51,6 +53,16 @@ class App : Application() {
|
|||
checkVersionUpgrade(this)
|
||||
app = this
|
||||
Defaults.initDynamicDefaults(this)
|
||||
LayoutUtilsCustom.removeMissingLayouts(this) // only after version upgrade
|
||||
SupportedEmojis.load(this)
|
||||
|
||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
@Suppress("DEPRECATION")
|
||||
Log.i(
|
||||
"startup", "Starting ${applicationInfo.processName} version ${packageInfo.versionName} (${
|
||||
packageInfo.versionCode
|
||||
}) on Android ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT})"
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -74,16 +86,13 @@ fun checkVersionUpgrade(context: Context) {
|
|||
if (oldVersion == BuildConfig.VERSION_CODE)
|
||||
return
|
||||
// clear extracted dictionaries, in case updated version contains newer ones
|
||||
DictionaryInfoUtils.getCachedDirectoryList(context)?.forEach {
|
||||
if (!it.isDirectory) return@forEach
|
||||
DictionaryInfoUtils.getCacheDirectories(context).forEach {
|
||||
val files = it.listFiles() ?: return@forEach
|
||||
for (file in files) {
|
||||
if (!file.name.endsWith(USER_DICTIONARY_SUFFIX))
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
if (oldVersion == 0) // new install or restoring settings from old app name
|
||||
upgradesWhenComingFromOldAppName(context)
|
||||
if (oldVersion <= 1000) { // upgrade old custom layouts name
|
||||
val oldShiftSymbolsFile = getCustomLayoutFile("custom.shift_symbols", context)
|
||||
if (oldShiftSymbolsFile.exists()) {
|
||||
|
@ -437,19 +446,19 @@ fun checkVersionUpgrade(context: Context) {
|
|||
val mainLayoutName = oldSplit[1]
|
||||
// we now need more information than just locale and main layout name, get it from existing subtypes
|
||||
val filtered = additionalSubtypes.filter {
|
||||
it.locale().toLanguageTag() == languageTag && (it.mainLayoutName() ?: "qwerty") == mainLayoutName
|
||||
it.locale().toLanguageTag() == languageTag && (it.mainLayoutNameOrQwerty()) == mainLayoutName
|
||||
}
|
||||
if (filtered.isNotEmpty())
|
||||
return@joinToString filtered.first().toSettingsSubtype().toPref()
|
||||
// find best matching resource subtype
|
||||
val goodMatch = resourceSubtypes.filter {
|
||||
it.locale().toLanguageTag() == languageTag && (it.mainLayoutName() ?: "qwerty") == mainLayoutName
|
||||
it.locale().toLanguageTag() == languageTag && (it.mainLayoutNameOrQwerty()) == mainLayoutName
|
||||
}
|
||||
if (goodMatch.isNotEmpty())
|
||||
return@joinToString goodMatch.first().toSettingsSubtype().toPref()
|
||||
// not sure how we can get here, but better deal with it
|
||||
val okMatch = resourceSubtypes.filter {
|
||||
it.locale().language == languageTag.constructLocale().language && (it.mainLayoutName() ?: "qwerty") == mainLayoutName
|
||||
it.locale().language == languageTag.constructLocale().language && (it.mainLayoutNameOrQwerty()) == mainLayoutName
|
||||
}
|
||||
if (okMatch.isNotEmpty())
|
||||
okMatch.first().toSettingsSubtype().toPref()
|
||||
|
@ -545,103 +554,15 @@ fun checkVersionUpgrade(context: Context) {
|
|||
if (oldVersion <= 3001 && prefs.getInt(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, Defaults.PREF_CLIPBOARD_HISTORY_RETENTION_TIME) <= 0) {
|
||||
prefs.edit().putInt(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, 121).apply()
|
||||
}
|
||||
if (oldVersion <= 3002) {
|
||||
prefs.all.filterKeys { it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) }.forEach {
|
||||
val oldValue = prefs.getString(it.key, "")!!
|
||||
if ("KEY_PREVIEW" !in oldValue) return@forEach
|
||||
val newValue = oldValue.replace("KEY_PREVIEW", "KEY_PREVIEW_BACKGROUND")
|
||||
prefs.edit().putString(it.key, newValue).apply()
|
||||
}
|
||||
}
|
||||
upgradeToolbarPrefs(prefs)
|
||||
LayoutUtilsCustom.onLayoutFileChanged() // just to be sure
|
||||
prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) }
|
||||
}
|
||||
|
||||
// todo (later): remove it when most users probably have upgraded
|
||||
private fun upgradesWhenComingFromOldAppName(context: Context) {
|
||||
// move layout files
|
||||
try {
|
||||
File(context.filesDir, "layouts").listFiles()?.forEach {
|
||||
it.copyTo(getCustomLayoutFile(it.name, context), true)
|
||||
it.delete()
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
// move background images
|
||||
try {
|
||||
val bgDay = File(context.filesDir, "custom_background_image")
|
||||
if (bgDay.isFile) {
|
||||
bgDay.copyTo(Settings.getCustomBackgroundFile(context, false, false), true)
|
||||
bgDay.delete()
|
||||
}
|
||||
val bgNight = File(context.filesDir, "custom_background_image_night")
|
||||
if (bgNight.isFile) {
|
||||
bgNight.copyTo(Settings.getCustomBackgroundFile(context, true, false), true)
|
||||
bgNight.delete()
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
// upgrade prefs
|
||||
val prefs = context.prefs()
|
||||
if (prefs.all.containsKey("theme_variant")) {
|
||||
prefs.edit().putString(Settings.PREF_THEME_COLORS, prefs.getString("theme_variant", "")).apply()
|
||||
prefs.edit().remove("theme_variant").apply()
|
||||
}
|
||||
if (prefs.all.containsKey("theme_variant_night")) {
|
||||
prefs.edit().putString(Settings.PREF_THEME_COLORS_NIGHT, prefs.getString("theme_variant_night", "")).apply()
|
||||
prefs.edit().remove("theme_variant_night").apply()
|
||||
}
|
||||
prefs.all.toMap().forEach {
|
||||
if (it.key.startsWith("pref_key_") && it.key != "pref_key_longpress_timeout") {
|
||||
var remove = true
|
||||
when (val value = it.value) {
|
||||
is Boolean -> prefs.edit().putBoolean(it.key.substringAfter("pref_key_"), value).apply()
|
||||
is Int -> prefs.edit().putInt(it.key.substringAfter("pref_key_"), value).apply()
|
||||
is Long -> prefs.edit().putLong(it.key.substringAfter("pref_key_"), value).apply()
|
||||
is String -> prefs.edit().putString(it.key.substringAfter("pref_key_"), value).apply()
|
||||
is Float -> prefs.edit().putFloat(it.key.substringAfter("pref_key_"), value).apply()
|
||||
else -> remove = false
|
||||
}
|
||||
if (remove)
|
||||
prefs.edit().remove(it.key).apply()
|
||||
} else if (it.key.startsWith("pref_")) {
|
||||
var remove = true
|
||||
when (val value = it.value) {
|
||||
is Boolean -> prefs.edit().putBoolean(it.key.substringAfter("pref_"), value).apply()
|
||||
is Int -> prefs.edit().putInt(it.key.substringAfter("pref_"), value).apply()
|
||||
is Long -> prefs.edit().putLong(it.key.substringAfter("pref_"), value).apply()
|
||||
is String -> prefs.edit().putString(it.key.substringAfter("pref_"), value).apply()
|
||||
is Float -> prefs.edit().putFloat(it.key.substringAfter("pref_"), value).apply()
|
||||
else -> remove = false
|
||||
}
|
||||
if (remove)
|
||||
prefs.edit().remove(it.key).apply()
|
||||
}
|
||||
}
|
||||
// change more_keys to popup_keys
|
||||
if (prefs.contains("more_keys_order")) {
|
||||
prefs.edit().putString(Settings.PREF_POPUP_KEYS_ORDER, prefs.getString("more_keys_order", "")?.replace("more_", "popup_")).apply()
|
||||
prefs.edit().remove("more_keys_order").apply()
|
||||
}
|
||||
if (prefs.contains("more_keys_labels_order")) {
|
||||
prefs.edit().putString(Settings.PREF_POPUP_KEYS_LABELS_ORDER, prefs.getString("more_keys_labels_order", "")?.replace("more_", "popup_")).apply()
|
||||
prefs.edit().remove("more_keys_labels_order").apply()
|
||||
}
|
||||
if (prefs.contains("more_more_keys")) {
|
||||
prefs.edit().putString(Settings.PREF_MORE_POPUP_KEYS, prefs.getString("more_more_keys", "")).apply()
|
||||
prefs.edit().remove("more_more_keys").apply()
|
||||
}
|
||||
if (prefs.contains("spellcheck_use_contacts")) {
|
||||
prefs.edit().putBoolean(Settings.PREF_USE_CONTACTS, prefs.getBoolean("spellcheck_use_contacts", false)).apply()
|
||||
prefs.edit().remove("spellcheck_use_contacts").apply()
|
||||
}
|
||||
// upgrade additional subtype locale strings
|
||||
if (prefs.contains(Settings.PREF_ADDITIONAL_SUBTYPES)) {
|
||||
val additionalSubtypes = mutableListOf<String>()
|
||||
prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, "")!!.split(";").forEach {
|
||||
val localeString = it.substringBefore(":")
|
||||
additionalSubtypes.add(it.replace(localeString, localeString.constructLocale().toLanguageTag()))
|
||||
}
|
||||
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, additionalSubtypes.joinToString(";")).apply()
|
||||
}
|
||||
// move pinned clips to credential protected storage if device is not locked (should never happen)
|
||||
if (!prefs.contains(Settings.PREF_PINNED_CLIPS)) return
|
||||
try {
|
||||
val defaultProtectedPrefs = context.protectedPrefs()
|
||||
defaultProtectedPrefs.edit { putString(Settings.PREF_PINNED_CLIPS, prefs.getString(Settings.PREF_PINNED_CLIPS, "")) }
|
||||
prefs.edit { remove(Settings.PREF_PINNED_CLIPS) }
|
||||
} catch (_: IllegalStateException) {
|
||||
// SharedPreferences in credential encrypted storage are not available until after user is unlocked
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
package helium314.keyboard.latin;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.inputmethod.latin.BinaryDictionary;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
|
||||
import helium314.keyboard.latin.common.StringUtils;
|
||||
import helium314.keyboard.latin.utils.Log;
|
||||
import helium314.keyboard.latin.utils.SpacedTokens;
|
||||
|
||||
public class AppsBinaryDictionary extends ExpandableBinaryDictionary {
|
||||
private static final String TAG = AppsBinaryDictionary.class.getSimpleName();
|
||||
private static final String NAME = "apps";
|
||||
|
||||
private static final int FREQUENCY_FOR_APPS = 100;
|
||||
private static final int FREQUENCY_FOR_APPS_BIGRAM = 200;
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final boolean DEBUG_DUMP = false;
|
||||
|
||||
private final AppsManager mAppsManager;
|
||||
|
||||
protected AppsBinaryDictionary(final Context ctx, final Locale locale,
|
||||
final File dictFile, final String name) {
|
||||
super(ctx, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_APPS, dictFile);
|
||||
mAppsManager = new AppsManager(ctx);
|
||||
reloadDictionaryIfRequired();
|
||||
}
|
||||
|
||||
public static AppsBinaryDictionary getDictionary(final Context context, final Locale locale,
|
||||
final File dictFile, final String dictNamePrefix) {
|
||||
return new AppsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Typically called whenever the dictionary is created for the first time or recreated when we
|
||||
* think that there are updates to the dictionary. This is called asynchronously.
|
||||
*/
|
||||
@Override
|
||||
public void loadInitialContentsLocked() {
|
||||
loadDictionaryLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads app names to the dictionary.
|
||||
*/
|
||||
private void loadDictionaryLocked() {
|
||||
for (final String name : mAppsManager.getNames()) {
|
||||
addNameLocked(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the words in an app label to the binary dictionary along with their n-grams.
|
||||
*/
|
||||
private void addNameLocked(final String appLabel) {
|
||||
NgramContext ngramContext = NgramContext.getEmptyPrevWordsContext(
|
||||
BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM);
|
||||
// TODO: Better tokenization for non-Latin writing systems
|
||||
for (final String word : new SpacedTokens(appLabel)) {
|
||||
if (DEBUG_DUMP) {
|
||||
Log.d(TAG, "addName word = " + word);
|
||||
}
|
||||
final int wordLen = StringUtils.codePointCount(word);
|
||||
// Don't add single letter words, possibly confuses capitalization of i.
|
||||
if (1 < wordLen && wordLen <= MAX_WORD_LENGTH) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addName " + appLabel + ", " + word + ", " + ngramContext);
|
||||
}
|
||||
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
||||
addUnigramLocked(word, FREQUENCY_FOR_APPS,
|
||||
null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */,
|
||||
false /* isPossiblyOffensive */,
|
||||
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
|
||||
if (ngramContext.isValid()) {
|
||||
runGCIfRequiredLocked(true /* mindsBlockByGC */);
|
||||
addNgramEntryLocked(ngramContext,
|
||||
word,
|
||||
FREQUENCY_FOR_APPS_BIGRAM,
|
||||
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
|
||||
}
|
||||
ngramContext = ngramContext.getNextNgramContext(
|
||||
new NgramContext.WordInfo(word));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
app/src/main/java/helium314/keyboard/latin/AppsManager.kt
Normal file
30
app/src/main/java/helium314/keyboard/latin/AppsManager.kt
Normal file
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
package helium314.keyboard.latin
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import java.util.HashSet
|
||||
|
||||
class AppsManager(context: Context) {
|
||||
private val mPackageManager: PackageManager = context.packageManager
|
||||
|
||||
/**
|
||||
* Returns all app labels associated with a launcher icon, sorted arbitrarily.
|
||||
*/
|
||||
fun getNames(): HashSet<String> {
|
||||
val filter = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
// activities with an entry/icon for the launcher
|
||||
val launcherApps: List<ResolveInfo> = mPackageManager.queryIntentActivities(filter, 0)
|
||||
|
||||
val names = HashSet<String>(launcherApps.size)
|
||||
for (info in launcherApps) {
|
||||
val name = info.activityInfo.loadLabel(mPackageManager).toString()
|
||||
names.add(name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ import android.provider.ContactsContract.Contacts;
|
|||
import helium314.keyboard.latin.utils.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.inputmethod.latin.BinaryDictionary;
|
||||
|
||||
|
@ -51,7 +50,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
|
|||
}
|
||||
|
||||
public static ContactsBinaryDictionary getDictionary(final Context context, @NonNull final Locale locale,
|
||||
final File dictFile, final String dictNamePrefix, @Nullable final String account) {
|
||||
final File dictFile, final String dictNamePrefix) {
|
||||
return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ public class ContactsDictionaryConstants {
|
|||
/**
|
||||
* Frequency for contacts information into the dictionary
|
||||
*/
|
||||
public static final int FREQUENCY_FOR_CONTACTS = 40;
|
||||
public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
|
||||
public static final int FREQUENCY_FOR_CONTACTS = 100; // much increased from original frequency because contacts were barely suggested
|
||||
public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 200; // todo: seems broken, how to actually get bigrams?
|
||||
|
||||
/**
|
||||
* Do not attempt to query contacts if there are more than this many entries.
|
||||
|
|
|
@ -6,15 +6,15 @@
|
|||
|
||||
package helium314.keyboard.latin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
|
||||
import helium314.keyboard.latin.common.ComposedData;
|
||||
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
|
||||
* strokes.
|
||||
|
@ -48,6 +48,7 @@ public abstract class Dictionary {
|
|||
// phony dictionary instances for them.
|
||||
public static final String TYPE_MAIN = "main";
|
||||
public static final String TYPE_CONTACTS = "contacts";
|
||||
public static final String TYPE_APPS = "apps";
|
||||
// User dictionary, the system-managed one.
|
||||
public static final String TYPE_USER = "user";
|
||||
// User history dictionary internal to LatinIME.
|
||||
|
@ -56,16 +57,6 @@ public abstract class Dictionary {
|
|||
// The locale for this dictionary. May be null if unknown (phony dictionary for example).
|
||||
public final Locale mLocale;
|
||||
|
||||
/**
|
||||
* Set out of the dictionary types listed above that are based on data specific to the user,
|
||||
* e.g., the user's contacts.
|
||||
*/
|
||||
private static final HashSet<String> sUserSpecificDictionaryTypes = new HashSet<>(Arrays.asList(
|
||||
TYPE_USER_TYPED,
|
||||
TYPE_USER,
|
||||
TYPE_CONTACTS,
|
||||
TYPE_USER_HISTORY));
|
||||
|
||||
public Dictionary(final String dictType, final Locale locale) {
|
||||
mDictType = dictType;
|
||||
mLocale = locale;
|
||||
|
@ -178,7 +169,14 @@ public abstract class Dictionary {
|
|||
* @return Whether this dictionary is specific to the user.
|
||||
*/
|
||||
public boolean isUserSpecific() {
|
||||
return sUserSpecificDictionaryTypes.contains(mDictType);
|
||||
return switch (mDictType) {
|
||||
case TYPE_USER_TYPED,
|
||||
TYPE_USER,
|
||||
TYPE_CONTACTS,
|
||||
TYPE_APPS,
|
||||
TYPE_USER_HISTORY -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,30 +6,35 @@
|
|||
|
||||
package helium314.keyboard.latin;
|
||||
|
||||
import helium314.keyboard.latin.utils.Log;
|
||||
|
||||
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
|
||||
import helium314.keyboard.latin.common.ComposedData;
|
||||
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
|
||||
import helium314.keyboard.latin.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Class for a collection of dictionaries that behave like one dictionary.
|
||||
*/
|
||||
public final class DictionaryCollection extends Dictionary {
|
||||
private final String TAG = DictionaryCollection.class.getSimpleName();
|
||||
private final CopyOnWriteArrayList<Dictionary> mDictionaries;
|
||||
private final ArrayList<Dictionary> mDictionaries;
|
||||
private final float[] mWeights;
|
||||
|
||||
public DictionaryCollection(final String dictType, final Locale locale,
|
||||
final Collection<Dictionary> dictionaries) {
|
||||
final Collection<Dictionary> dictionaries, final float[] weights) {
|
||||
super(dictType, locale);
|
||||
mDictionaries = new CopyOnWriteArrayList<>(dictionaries);
|
||||
mDictionaries = new ArrayList<>(dictionaries);
|
||||
mDictionaries.removeAll(Collections.singleton(null));
|
||||
if (mDictionaries.size() > weights.length) {
|
||||
mWeights = new float[mDictionaries.size()];
|
||||
Arrays.fill(mWeights, 1f);
|
||||
Log.w(TAG, "got weights array of length " + weights.length + ", expected "+mDictionaries.size());
|
||||
} else mWeights = weights;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -38,19 +43,19 @@ public final class DictionaryCollection extends Dictionary {
|
|||
final SettingsValuesForSuggestion settingsValuesForSuggestion,
|
||||
final int sessionId, final float weightForLocale,
|
||||
final float[] inOutWeightOfLangModelVsSpatialModel) {
|
||||
final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
|
||||
final ArrayList<Dictionary> dictionaries = mDictionaries;
|
||||
if (dictionaries.isEmpty()) return null;
|
||||
// To avoid creating unnecessary objects, we get the list out of the first
|
||||
// dictionary and add the rest to it if not null, hence the get(0)
|
||||
ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composedData,
|
||||
ngramContext, proximityInfoHandle, settingsValuesForSuggestion, sessionId,
|
||||
weightForLocale, inOutWeightOfLangModelVsSpatialModel);
|
||||
weightForLocale * mWeights[0], inOutWeightOfLangModelVsSpatialModel);
|
||||
if (null == suggestions) suggestions = new ArrayList<>();
|
||||
final int length = dictionaries.size();
|
||||
for (int i = 1; i < length; ++ i) {
|
||||
final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(
|
||||
composedData, ngramContext, proximityInfoHandle, settingsValuesForSuggestion,
|
||||
sessionId, weightForLocale, inOutWeightOfLangModelVsSpatialModel);
|
||||
sessionId, weightForLocale * mWeights[i], inOutWeightOfLangModelVsSpatialModel);
|
||||
if (null != sugg) suggestions.addAll(sugg);
|
||||
}
|
||||
return suggestions;
|
||||
|
@ -93,22 +98,4 @@ public final class DictionaryCollection extends Dictionary {
|
|||
for (final Dictionary dict : mDictionaries)
|
||||
dict.close();
|
||||
}
|
||||
|
||||
// Warning: this is not thread-safe. Take necessary precaution when calling.
|
||||
public void addDictionary(final Dictionary newDict) {
|
||||
if (null == newDict) return;
|
||||
if (mDictionaries.contains(newDict)) {
|
||||
Log.w(TAG, "This collection already contains this dictionary: " + newDict);
|
||||
}
|
||||
mDictionaries.add(newDict);
|
||||
}
|
||||
|
||||
// Warning: this is not thread-safe. Take necessary precaution when calling.
|
||||
public void removeDictionary(final Dictionary dict) {
|
||||
if (mDictionaries.contains(dict)) {
|
||||
mDictionaries.remove(dict);
|
||||
} else {
|
||||
Log.w(TAG, "This collection does not contain this dictionary: " + dict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
/**
|
||||
* Interface that facilitates interaction with different kinds of dictionaries. Provides APIs to
|
||||
* instantiate and select the correct dictionaries (based on language or account), update entries
|
||||
* instantiate and select the correct dictionaries (based on language and settings), update entries
|
||||
* and fetch suggestions. Currently AndroidSpellCheckerService and LatinIME both use
|
||||
* DictionaryFacilitator as a client for interacting with dictionaries.
|
||||
*/
|
||||
|
@ -32,22 +32,20 @@ public interface DictionaryFacilitator {
|
|||
String[] ALL_DICTIONARY_TYPES = new String[] {
|
||||
Dictionary.TYPE_MAIN,
|
||||
Dictionary.TYPE_CONTACTS,
|
||||
Dictionary.TYPE_APPS,
|
||||
Dictionary.TYPE_USER_HISTORY,
|
||||
Dictionary.TYPE_USER};
|
||||
|
||||
String[] DYNAMIC_DICTIONARY_TYPES = new String[] {
|
||||
Dictionary.TYPE_CONTACTS,
|
||||
Dictionary.TYPE_APPS,
|
||||
Dictionary.TYPE_USER_HISTORY,
|
||||
Dictionary.TYPE_USER};
|
||||
|
||||
/**
|
||||
* The facilitator will put words into the cache whenever it decodes them.
|
||||
*/
|
||||
/** The facilitator will put words into the cache whenever it decodes them. */
|
||||
void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache);
|
||||
|
||||
/**
|
||||
* The facilitator will get words from the cache whenever it needs to check their spelling.
|
||||
*/
|
||||
/** The facilitator will get words from the cache whenever it needs to check their spelling. */
|
||||
void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache);
|
||||
|
||||
/**
|
||||
|
@ -77,56 +75,64 @@ public interface DictionaryFacilitator {
|
|||
*/
|
||||
void onFinishInput(Context context);
|
||||
|
||||
/** whether a dictionary is set */
|
||||
boolean isActive();
|
||||
|
||||
/** the locale provided in resetDictionaries */
|
||||
@NonNull Locale getMainLocale();
|
||||
|
||||
// useful for multilingual typing
|
||||
Locale getCurrentLocale();
|
||||
/** the most "trusted" locale, differs from getMainLocale only if multilingual typing is used */
|
||||
@NonNull Locale getCurrentLocale();
|
||||
|
||||
boolean usesSameSettings(
|
||||
@NonNull final List<Locale> locales,
|
||||
final boolean contacts,
|
||||
final boolean personalization,
|
||||
@Nullable final String account
|
||||
final boolean apps,
|
||||
final boolean personalization
|
||||
);
|
||||
|
||||
String getAccount();
|
||||
|
||||
/** switches to newLocale, gets secondary locales from current settings, and sets secondary dictionaries */
|
||||
void resetDictionaries(
|
||||
final Context context,
|
||||
final Locale newLocale,
|
||||
final boolean useContactsDict,
|
||||
final boolean useAppsDict,
|
||||
final boolean usePersonalizedDicts,
|
||||
final boolean forceReloadMainDictionary,
|
||||
@Nullable final String account,
|
||||
final String dictNamePrefix,
|
||||
@Nullable final DictionaryInitializationListener listener);
|
||||
|
||||
/** removes the word from all editable dictionaries, and adds it to a blacklist in case it's in a read-only dictionary */
|
||||
void removeWord(String word);
|
||||
|
||||
void closeDictionaries();
|
||||
|
||||
// The main dictionaries are loaded asynchronously. Don't cache the return value
|
||||
// of these methods.
|
||||
/** main dictionaries are loaded asynchronously after resetDictionaries */
|
||||
boolean hasAtLeastOneInitializedMainDictionary();
|
||||
|
||||
/** main dictionaries are loaded asynchronously after resetDictionaries */
|
||||
boolean hasAtLeastOneUninitializedMainDictionary();
|
||||
|
||||
/** main dictionaries are loaded asynchronously after resetDictionaries */
|
||||
void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
|
||||
throws InterruptedException;
|
||||
|
||||
/** adds the word to user history dictionary, calls adjustConfindences, and might add it to personal dictionary if the setting is enabled */
|
||||
void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
|
||||
@NonNull final NgramContext ngramContext, final long timeStampInSeconds,
|
||||
final boolean blockPotentiallyOffensive);
|
||||
|
||||
/** adjust confidences for multilingual typing */
|
||||
void adjustConfidences(final String word, final boolean wasAutoCapitalized);
|
||||
|
||||
/** a string with all used locales and their current confidences, null if multilingual typing is not used */
|
||||
@Nullable String localesAndConfidences();
|
||||
|
||||
/** completely removes the word from user history (currently not if event is a backspace event) */
|
||||
void unlearnFromUserHistory(final String word,
|
||||
@NonNull final NgramContext ngramContext, final long timeStampInSeconds,
|
||||
final int eventType);
|
||||
|
||||
// TODO: Revise the way to fusion suggestion results.
|
||||
@NonNull SuggestionResults getSuggestionResults(final ComposedData composedData,
|
||||
final NgramContext ngramContext, @NonNull final Keyboard keyboard,
|
||||
final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId,
|
||||
|
@ -136,12 +142,10 @@ public interface DictionaryFacilitator {
|
|||
|
||||
boolean isValidSuggestionWord(final String word);
|
||||
|
||||
boolean clearUserHistoryDictionary(final Context context);
|
||||
void clearUserHistoryDictionary(final Context context);
|
||||
|
||||
String dump(final Context context);
|
||||
|
||||
String localesAndConfidences();
|
||||
|
||||
void dumpDictionaryForDebug(final String dictName);
|
||||
|
||||
@NonNull List<DictionaryStats> getDictionaryStats(final Context context);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,820 @@
|
|||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package helium314.keyboard.latin
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.provider.UserDictionary
|
||||
import android.util.LruCache
|
||||
import helium314.keyboard.keyboard.Keyboard
|
||||
import helium314.keyboard.keyboard.emoji.SupportedEmojis
|
||||
import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
|
||||
import helium314.keyboard.latin.NgramContext.WordInfo
|
||||
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
|
||||
import helium314.keyboard.latin.common.ComposedData
|
||||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.StringUtils
|
||||
import helium314.keyboard.latin.common.decapitalize
|
||||
import helium314.keyboard.latin.common.splitOnWhitespace
|
||||
import helium314.keyboard.latin.permissions.PermissionsUtil
|
||||
import helium314.keyboard.latin.personalization.UserHistoryDictionary
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.SubtypeSettings
|
||||
import helium314.keyboard.latin.utils.SuggestionResults
|
||||
import helium314.keyboard.latin.utils.getSecondaryLocales
|
||||
import helium314.keyboard.latin.utils.locale
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Facilitates interaction with different kinds of dictionaries. Provides APIs
|
||||
* to instantiate and select the correct dictionaries (based on language and settings),
|
||||
* update entries and fetch suggestions.
|
||||
*
|
||||
*
|
||||
* Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
|
||||
* a client for interacting with dictionaries.
|
||||
*/
|
||||
class DictionaryFacilitatorImpl : DictionaryFacilitator {
|
||||
private var dictionaryGroups = listOf(DictionaryGroup())
|
||||
|
||||
@Volatile
|
||||
private var mLatchForWaitingLoadingMainDictionaries = CountDownLatch(0)
|
||||
|
||||
// The 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.
|
||||
// todo: this is awful, find a better solution / workaround
|
||||
// or remove completely? not sure if it's actually an improvement
|
||||
// should be fixed in the library, but that's not feasible with current user-provides-library approach
|
||||
// added in 12cbd43bda7d0f0cd73925e9cf836de751c32ed0 / https://github.com/Helium314/HeliBoard/issues/135
|
||||
private var tryChangingWords = false
|
||||
private var changeFrom = ""
|
||||
private var changeTo = ""
|
||||
|
||||
// todo: write cache never set, and never read (only written)
|
||||
// tried to use read cache for a while, but small performance improvements are not worth the work,
|
||||
// see https://github.com/Helium314/HeliBoard/issues/307
|
||||
private var mValidSpellingWordReadCache: LruCache<String, Boolean>? = null
|
||||
private var mValidSpellingWordWriteCache: LruCache<String, Boolean>? = null
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun setValidSpellingWordReadCache(cache: LruCache<String, Boolean>) {
|
||||
mValidSpellingWordReadCache = cache
|
||||
}
|
||||
|
||||
override fun setValidSpellingWordWriteCache(cache: LruCache<String, Boolean>) {
|
||||
mValidSpellingWordWriteCache = cache
|
||||
}
|
||||
|
||||
// judging by usage before adding multilingual typing, this should check primary group locale only
|
||||
override fun isForLocale(locale: Locale?): Boolean {
|
||||
return locale != null && locale == dictionaryGroups[0].locale
|
||||
}
|
||||
|
||||
override fun onStartInput() {
|
||||
}
|
||||
|
||||
override fun onFinishInput(context: Context) {
|
||||
for (dictGroup in dictionaryGroups) {
|
||||
DictionaryFacilitator.ALL_DICTIONARY_TYPES.forEach { dictGroup.getDict(it)?.onFinishInput() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun isActive(): Boolean {
|
||||
return dictionaryGroups[0].locale.language.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun getMainLocale(): Locale {
|
||||
return dictionaryGroups[0].locale
|
||||
}
|
||||
|
||||
override fun getCurrentLocale(): Locale {
|
||||
return currentlyPreferredDictionaryGroup.locale
|
||||
}
|
||||
|
||||
override fun usesSameSettings(locales: List<Locale>, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
|
||||
val dictGroup = dictionaryGroups[0] // settings are the same for all groups
|
||||
return contacts == dictGroup.hasDict(Dictionary.TYPE_CONTACTS)
|
||||
&& apps == dictGroup.hasDict(Dictionary.TYPE_APPS)
|
||||
&& personalization == dictGroup.hasDict(Dictionary.TYPE_USER_HISTORY)
|
||||
&& locales.size == dictionaryGroups.size
|
||||
&& locales.none { findDictionaryGroupWithLocale(dictionaryGroups, it) == null }
|
||||
}
|
||||
|
||||
// -------------- managing (loading & closing) dictionaries ------------
|
||||
|
||||
override fun resetDictionaries(
|
||||
context: Context,
|
||||
newLocale: Locale,
|
||||
useContactsDict: Boolean,
|
||||
useAppsDict: Boolean,
|
||||
usePersonalizedDicts: Boolean,
|
||||
forceReloadMainDictionary: Boolean,
|
||||
dictNamePrefix: String,
|
||||
listener: DictionaryInitializationListener?
|
||||
) {
|
||||
Log.i(TAG, "resetDictionaries, force reloading main dictionary: $forceReloadMainDictionary")
|
||||
|
||||
val locales = getUsedLocales(newLocale, context)
|
||||
|
||||
val subDictTypesToUse = listOfNotNull(
|
||||
Dictionary.TYPE_USER,
|
||||
if (useAppsDict) Dictionary.TYPE_APPS else null,
|
||||
if (usePersonalizedDicts) Dictionary.TYPE_USER_HISTORY else null,
|
||||
if (useContactsDict && PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
|
||||
Dictionary.TYPE_CONTACTS else null
|
||||
)
|
||||
|
||||
val (newDictionaryGroups, existingDictsToCleanup) =
|
||||
getNewDictGroupsAndDictsToCleanup(locales, subDictTypesToUse, forceReloadMainDictionary, dictNamePrefix, context)
|
||||
|
||||
// Replace Dictionaries.
|
||||
val oldDictionaryGroups: List<DictionaryGroup>
|
||||
synchronized(this) {
|
||||
oldDictionaryGroups = dictionaryGroups
|
||||
dictionaryGroups = newDictionaryGroups
|
||||
if (hasAtLeastOneUninitializedMainDictionary()) {
|
||||
asyncReloadUninitializedMainDictionaries(context, locales, listener)
|
||||
}
|
||||
}
|
||||
|
||||
listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
|
||||
|
||||
// Clean up old dictionaries.
|
||||
existingDictsToCleanup.forEach { (locale, dictTypes) ->
|
||||
val dictGroupToCleanup = findDictionaryGroupWithLocale(oldDictionaryGroups, locale) ?: return@forEach
|
||||
for (dictType in dictTypes) {
|
||||
dictGroupToCleanup.closeDict(dictType)
|
||||
}
|
||||
}
|
||||
|
||||
mValidSpellingWordWriteCache?.evictAll()
|
||||
mValidSpellingWordReadCache?.evictAll()
|
||||
}
|
||||
|
||||
/** creates dictionaryGroups for [newLocales] with given [newSubDictTypes], trying to re-use existing dictionaries.
|
||||
* returns the new dictionaryGroups and unused dictionary types by locale */
|
||||
private fun getNewDictGroupsAndDictsToCleanup(
|
||||
newLocales: Collection<Locale>,
|
||||
newSubDictTypes: Collection<String>,
|
||||
forceReload: Boolean,
|
||||
dictNamePrefix: String,
|
||||
context: Context
|
||||
): Pair<List<DictionaryGroup>, Map<Locale, List<String>>> {
|
||||
// Gather all dictionaries by locale. We may remove some from the list later.
|
||||
val existingDictsToCleanup = HashMap<Locale, MutableList<String>>()
|
||||
for (dictGroup in dictionaryGroups) {
|
||||
existingDictsToCleanup[dictGroup.locale] = DictionaryFacilitator.ALL_DICTIONARY_TYPES
|
||||
.filterTo(mutableListOf()) { dictGroup.hasDict(it) }
|
||||
}
|
||||
|
||||
// create new dictionary groups and remove dictionaries to re-use from existingDictsToCleanup
|
||||
val newDictionaryGroups = mutableListOf<DictionaryGroup>()
|
||||
for (locale in newLocales) {
|
||||
// get existing dictionary group for new locale
|
||||
val oldDictGroupForLocale = findDictionaryGroupWithLocale(dictionaryGroups, locale)
|
||||
val dictTypesToCleanupForLocale = existingDictsToCleanup[locale]
|
||||
|
||||
// create new or re-use already loaded main dict
|
||||
val mainDict: Dictionary?
|
||||
if (forceReload || oldDictGroupForLocale == null
|
||||
|| !oldDictGroupForLocale.hasDict(Dictionary.TYPE_MAIN)
|
||||
) {
|
||||
mainDict = null // null main dicts will be loaded later in asyncReloadUninitializedMainDictionaries
|
||||
} else {
|
||||
mainDict = oldDictGroupForLocale.getDict(Dictionary.TYPE_MAIN)
|
||||
dictTypesToCleanupForLocale?.remove(Dictionary.TYPE_MAIN)
|
||||
}
|
||||
|
||||
// create new or re-use already loaded sub-dicts
|
||||
val subDicts: MutableMap<String, ExpandableBinaryDictionary> = HashMap()
|
||||
for (subDictType in newSubDictTypes) {
|
||||
val subDict: ExpandableBinaryDictionary
|
||||
if (forceReload || oldDictGroupForLocale == null
|
||||
|| !oldDictGroupForLocale.hasDict(subDictType)
|
||||
) {
|
||||
// Create a new dictionary.
|
||||
subDict = createSubDict(subDictType, context, locale, null, dictNamePrefix) ?: continue
|
||||
} else {
|
||||
// Reuse the existing dictionary.
|
||||
subDict = oldDictGroupForLocale.getSubDict(subDictType) ?: continue
|
||||
dictTypesToCleanupForLocale?.remove(subDictType)
|
||||
}
|
||||
subDicts[subDictType] = subDict
|
||||
}
|
||||
val newDictGroup = DictionaryGroup(locale, mainDict, subDicts, context)
|
||||
newDictionaryGroups.add(newDictGroup)
|
||||
}
|
||||
return newDictionaryGroups to existingDictsToCleanup
|
||||
}
|
||||
|
||||
private fun asyncReloadUninitializedMainDictionaries(
|
||||
context: Context, locales: Collection<Locale>, listener: DictionaryInitializationListener?
|
||||
) {
|
||||
val latchForWaitingLoadingMainDictionary = CountDownLatch(1)
|
||||
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary
|
||||
scope.launch {
|
||||
val dictGroupsWithNewMainDict = locales.mapNotNull {
|
||||
val dictionaryGroup = findDictionaryGroupWithLocale(dictionaryGroups, it)
|
||||
if (dictionaryGroup == null) {
|
||||
Log.w(TAG, "Expected a dictionary group for $it but none found")
|
||||
return@mapNotNull null // This should never happen
|
||||
}
|
||||
if (dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true) null
|
||||
else dictionaryGroup to DictionaryFactory.createMainDictionaryCollection(context, it)
|
||||
}
|
||||
synchronized(this) {
|
||||
dictGroupsWithNewMainDict.forEach { (dictGroup, mainDict) ->
|
||||
dictGroup.setMainDict(mainDict)
|
||||
}
|
||||
}
|
||||
|
||||
listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
|
||||
latchForWaitingLoadingMainDictionary.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeDictionaries() {
|
||||
val dictionaryGroupsToClose: List<DictionaryGroup>
|
||||
synchronized(this) {
|
||||
dictionaryGroupsToClose = dictionaryGroups
|
||||
dictionaryGroups = listOf(DictionaryGroup())
|
||||
}
|
||||
for (dictionaryGroup in dictionaryGroupsToClose) {
|
||||
for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
|
||||
dictionaryGroup.closeDict(dictType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The main dictionaries are loaded asynchronously. Don't cache the return value of these methods.
|
||||
override fun hasAtLeastOneInitializedMainDictionary(): Boolean =
|
||||
dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true }
|
||||
|
||||
override fun hasAtLeastOneUninitializedMainDictionary(): Boolean =
|
||||
dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized != true }
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
|
||||
mLatchForWaitingLoadingMainDictionaries.await(timeout, unit)
|
||||
}
|
||||
|
||||
// -------------- actual dictionary stuff like getting suggestions ------------
|
||||
|
||||
override fun addToUserHistory(
|
||||
suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
|
||||
timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
|
||||
) {
|
||||
// Update the spelling cache before learning. Words that are not yet added to user history
|
||||
// and appear in no other language model are not considered valid.
|
||||
putWordIntoValidSpellingWordCache("addToUserHistory", suggestion)
|
||||
|
||||
val words = suggestion.splitOnWhitespace().dropLastWhile { it.isEmpty() }
|
||||
|
||||
// increase / decrease confidence
|
||||
if (words.size == 1) // ignore if more than a single word, which only happens with (badly working) spaceAwareGesture
|
||||
adjustConfidences(suggestion, wasAutoCapitalized)
|
||||
|
||||
// Add word to user dictionary if it is in no other dictionary except user history dictionary (i.e. typed again).
|
||||
val sv = Settings.getValues()
|
||||
if (sv.mAddToPersonalDictionary // require the opt-in
|
||||
&& sv.mAutoCorrectEnabled == sv.mAutoCorrectionEnabledPerUserSettings // don't add if user wants autocorrect but input field does not, see https://github.com/Helium314/HeliBoard/issues/427#issuecomment-1905438000
|
||||
&& dictionaryGroups[0].hasDict(Dictionary.TYPE_USER_HISTORY) // require personalized suggestions
|
||||
&& !wasAutoCapitalized // we can't be 100% sure about what the user intended to type, so better don't add it
|
||||
&& words.size == 1 // only single words
|
||||
) {
|
||||
addToPersonalDictionaryIfInvalidButInHistory(suggestion)
|
||||
}
|
||||
|
||||
var ngramContextForCurrentWord = ngramContext
|
||||
val preferredGroup = currentlyPreferredDictionaryGroup
|
||||
for (i in words.indices) {
|
||||
val currentWord = words[i]
|
||||
val wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized
|
||||
// add to history for preferred dictionary group, to avoid mixing languages in history
|
||||
addWordToUserHistory(
|
||||
preferredGroup, ngramContextForCurrentWord, currentWord,
|
||||
wasCurrentWordAutoCapitalized, timeStampInSeconds.toInt(), blockPotentiallyOffensive
|
||||
)
|
||||
ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(WordInfo(currentWord))
|
||||
|
||||
// remove manually entered blacklisted words from blacklist for likely matching languages
|
||||
dictionaryGroups.filter { it.confidence == preferredGroup.confidence }.forEach {
|
||||
it.removeFromBlacklist(currentWord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addWordToUserHistory(
|
||||
dictionaryGroup: DictionaryGroup, ngramContext: NgramContext, word: String, wasAutoCapitalized: Boolean,
|
||||
timeStampInSeconds: Int, blockPotentiallyOffensive: Boolean
|
||||
) {
|
||||
val userHistoryDictionary = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
|
||||
|
||||
val mainFreq = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(word) ?: Dictionary.NOT_A_PROBABILITY
|
||||
if (mainFreq == 0 && blockPotentiallyOffensive)
|
||||
return
|
||||
if (tryChangingWords)
|
||||
tryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(changeFrom, changeTo)
|
||||
|
||||
val wordToUse: String
|
||||
// Check for isBeginningOfSentenceContext too, because not all text fields auto-capitalize in this case.
|
||||
// Even if the user capitalizes manually, they most likely don't want the capitalized form suggested.
|
||||
if (wasAutoCapitalized || ngramContext.isBeginningOfSentenceContext) {
|
||||
val decapitalizedWord = word.decapitalize(dictionaryGroup.locale) // try undoing auto-capitalization
|
||||
if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
|
||||
&& !isValidWord(decapitalizedWord, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
|
||||
) {
|
||||
// If the word was auto-capitalized and exists only as a capitalized word in the
|
||||
// dictionary, then we must not downcase it before registering it. For example,
|
||||
// the name of the contacts in start-of-sentence position would come here with the
|
||||
// wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
|
||||
// of that contact's name which would end up popping in suggestions.
|
||||
wordToUse = word
|
||||
} else {
|
||||
// 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.
|
||||
wordToUse = decapitalizedWord
|
||||
tryChangingWords = true
|
||||
changeFrom = word
|
||||
changeTo = wordToUse
|
||||
}
|
||||
} else {
|
||||
// HACK: We'd like to avoid adding the capitalized form of common words to the User
|
||||
// History dictionary in order to avoid suggesting them until the dictionary
|
||||
// consolidation is done.
|
||||
// TODO: Remove this hack when ready.
|
||||
val lowerCasedWord = word.lowercase(dictionaryGroup.locale)
|
||||
val lowerCaseFreqInMainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(lowerCasedWord)
|
||||
?: Dictionary.NOT_A_PROBABILITY
|
||||
wordToUse = if (mainFreq < lowerCaseFreqInMainDict
|
||||
&& lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT
|
||||
) {
|
||||
// Use lower cased word as the word can be a distracter of the popular word.
|
||||
lowerCasedWord
|
||||
} else {
|
||||
word
|
||||
}
|
||||
}
|
||||
// We demote unrecognized words (frequency <= 0) by specifying them as "invalid".
|
||||
// We don't add words with 0-frequency (assuming they would be profanity etc.).
|
||||
val isValid = mainFreq > 0
|
||||
UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, wordToUse, isValid, timeStampInSeconds)
|
||||
}
|
||||
|
||||
private fun addToPersonalDictionaryIfInvalidButInHistory(word: String) {
|
||||
val dictionaryGroup = clearlyPreferredDictionaryGroup ?: return
|
||||
val userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER) ?: return
|
||||
val userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
|
||||
if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup))
|
||||
return // valid word, no reason to auto-add it to personal dict
|
||||
if (userDict.isInDictionary(word))
|
||||
return // should never happen, but better be safe
|
||||
|
||||
// User history always reports words as invalid, so we check the frequency instead.
|
||||
// Testing shows that after 2 times adding, the frequency is 111, and then rises slowly with usage (values vary slightly).
|
||||
// 120 is after 3 uses of the word, so we simply require more than that. todo: Could be made configurable.
|
||||
// Words added to dictionaries (user and history) seem to be found only after some delay.
|
||||
// This is not too bad, but it delays adding in case a user wants to fill a dictionary using this functionality
|
||||
if (userHistoryDict.getFrequency(word) > 120) {
|
||||
scope.launch {
|
||||
UserDictionary.Words.addWord(userDict.mContext, word, 250, null, dictionaryGroup.locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun putWordIntoValidSpellingWordCache(caller: String, originalWord: String) {
|
||||
if (mValidSpellingWordWriteCache == null)
|
||||
return
|
||||
|
||||
val lowerCaseWord = originalWord.lowercase(currentLocale)
|
||||
val lowerCaseValid = isValidSpellingWord(lowerCaseWord)
|
||||
mValidSpellingWordWriteCache?.put(lowerCaseWord, lowerCaseValid)
|
||||
|
||||
val capitalWord = StringUtils.capitalizeFirstAndDowncaseRest(originalWord, currentLocale)
|
||||
val capitalValid = if (lowerCaseValid) {
|
||||
true // The lower case form of the word is valid, so the upper case must be valid.
|
||||
} else {
|
||||
isValidSpellingWord(capitalWord)
|
||||
}
|
||||
mValidSpellingWordWriteCache?.put(capitalWord, capitalValid)
|
||||
}
|
||||
|
||||
override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {
|
||||
if (dictionaryGroups.size == 1 || word.contains(Constants.WORD_SEPARATOR))
|
||||
return
|
||||
|
||||
// if suggestion was auto-capitalized, check against both the suggestion and the de-capitalized suggestion
|
||||
val decapitalizedSuggestion = if (wasAutoCapitalized) word.decapitalize(currentLocale) else word
|
||||
dictionaryGroups.forEach {
|
||||
if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it)) {
|
||||
it.increaseConfidence()
|
||||
return@forEach
|
||||
}
|
||||
// also increase confidence if suggestion was auto-capitalized and the lowercase variant it valid
|
||||
if (wasAutoCapitalized && isValidWord(decapitalizedSuggestion, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it))
|
||||
it.increaseConfidence()
|
||||
else it.decreaseConfidence()
|
||||
}
|
||||
}
|
||||
|
||||
/** the dictionaryGroup with most confidence, first group when tied */
|
||||
private val currentlyPreferredDictionaryGroup: DictionaryGroup get() = dictionaryGroups.maxBy { it.confidence }
|
||||
|
||||
/** the only dictionary group, or the dictionaryGroup confidence >= DictionaryGroup.MAX_CONFIDENCE if all others have 0 */
|
||||
private val clearlyPreferredDictionaryGroup: DictionaryGroup? get() {
|
||||
if (dictionaryGroups.size == 1) return dictionaryGroups.first() // confidence not used if we only have a single group
|
||||
|
||||
val preferred = currentlyPreferredDictionaryGroup
|
||||
if (preferred.confidence < DictionaryGroup.MAX_CONFIDENCE) return null
|
||||
if (dictionaryGroups.any { it.confidence > 0 && it !== preferred })
|
||||
return null
|
||||
return preferred
|
||||
}
|
||||
|
||||
override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {
|
||||
// TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
|
||||
if (eventType != Constants.EVENT_BACKSPACE) {
|
||||
currentlyPreferredDictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
|
||||
}
|
||||
|
||||
// Update the spelling cache after unlearning. Words that are removed from user history
|
||||
// and appear in no other language model are not considered valid.
|
||||
putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.lowercase(Locale.getDefault()))
|
||||
}
|
||||
|
||||
// TODO: Revise the way to fusion suggestion results.
|
||||
override fun getSuggestionResults(
|
||||
composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
|
||||
settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
|
||||
): SuggestionResults {
|
||||
val proximityInfoHandle = keyboard.proximityInfo.nativeProximityInfo
|
||||
val weightOfLangModelVsSpatialModel = floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
|
||||
|
||||
val waitForOtherDicts = if (dictionaryGroups.size == 1) null else CountDownLatch(dictionaryGroups.size - 1)
|
||||
val suggestionsArray = Array<List<SuggestedWordInfo>?>(dictionaryGroups.size) { null }
|
||||
for (i in 1..dictionaryGroups.lastIndex) {
|
||||
scope.launch {
|
||||
suggestionsArray[i] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
|
||||
proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[i])
|
||||
waitForOtherDicts?.countDown()
|
||||
}
|
||||
}
|
||||
suggestionsArray[0] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
|
||||
proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[0])
|
||||
val suggestionResults = SuggestionResults(
|
||||
SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext, false
|
||||
)
|
||||
waitForOtherDicts?.await()
|
||||
|
||||
suggestionsArray.forEach {
|
||||
if (it == null) return@forEach
|
||||
suggestionResults.addAll(it)
|
||||
suggestionResults.mRawSuggestions?.addAll(it)
|
||||
}
|
||||
|
||||
return suggestionResults
|
||||
}
|
||||
|
||||
private fun getSuggestions(
|
||||
composedData: ComposedData, ngramContext: NgramContext,
|
||||
settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int,
|
||||
proximityInfoHandle: Long, weightOfLangModelVsSpatialModel: FloatArray, dictGroup: DictionaryGroup
|
||||
): List<SuggestedWordInfo> {
|
||||
val suggestions = ArrayList<SuggestedWordInfo>()
|
||||
val weightForLocale = dictGroup.getWeightForLocale(dictionaryGroups, composedData.mIsBatchMode)
|
||||
for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
|
||||
val dictionary = dictGroup.getDict(dictType) ?: continue
|
||||
val dictionarySuggestions = dictionary.getSuggestions(composedData, ngramContext, proximityInfoHandle,
|
||||
settingsValuesForSuggestion, sessionId, weightForLocale, weightOfLangModelVsSpatialModel
|
||||
) ?: continue
|
||||
|
||||
// For some reason "garbage" words are produced when glide typing. For user history
|
||||
// and main dictionaries we can filter them out by checking whether the dictionary
|
||||
// actually contains the word. But personal and addon dictionaries may contain shortcuts,
|
||||
// which do not pass an isInDictionary check (e.g. emojis).
|
||||
// (if the main dict contains shortcuts to non-words, this will break!)
|
||||
val checkForGarbage = composedData.mIsBatchMode && (dictType == Dictionary.TYPE_USER_HISTORY || dictType == Dictionary.TYPE_MAIN)
|
||||
|
||||
for (info in dictionarySuggestions) {
|
||||
val word = info.word
|
||||
if (isBlacklisted(word) || SupportedEmojis.isUnsupported(word)) // don't add blacklisted words and unsupported emojis
|
||||
continue
|
||||
if (checkForGarbage
|
||||
// consider the user might use custom main dictionary containing shortcuts
|
||||
// assume this is unlikely to happen, and take care about common shortcuts that are not actual words (emoji, symbols)
|
||||
&& word.length > 2 // should exclude most symbol shortcuts
|
||||
&& info.mSourceDict.mDictType == dictType // dictType is always main, but info.mSourceDict.mDictType contains the actual dict (main dict is a dictionary group)
|
||||
&& !StringUtils.mightBeEmoji(word) // simplified check for performance reasons
|
||||
&& !dictionary.isInDictionary(word)
|
||||
)
|
||||
continue
|
||||
|
||||
if (word.length == 1 && info.mSourceDict.mDictType == "emoji" && !StringUtils.mightBeEmoji(word[0].code))
|
||||
continue
|
||||
|
||||
suggestions.add(info)
|
||||
}
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// Spell checker is using this, and has its own instance of DictionaryFacilitatorImpl,
|
||||
// meaning that it always has default mConfidence. So we cannot choose to only check preferred
|
||||
// locale, and instead simply return true if word is in any of the available dictionaries
|
||||
override fun isValidSpellingWord(word: String): Boolean {
|
||||
mValidSpellingWordReadCache?.get(word)?.let { return it }
|
||||
val result = dictionaryGroups.any { isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it) }
|
||||
mValidSpellingWordReadCache?.put(word, result)
|
||||
return result
|
||||
}
|
||||
|
||||
// this is unused, so leave it for now (redirecting to isValidWord seems to defeat the purpose...)
|
||||
override fun isValidSuggestionWord(word: String): Boolean {
|
||||
return isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroups[0])
|
||||
}
|
||||
|
||||
// todo: move into dictionaryGroup?
|
||||
private fun isValidWord(word: String, dictionariesToCheck: Array<String>, dictionaryGroup: DictionaryGroup): Boolean {
|
||||
if (word.isEmpty() || dictionaryGroup.isBlacklisted(word)) return false
|
||||
return dictionariesToCheck.any { dictionaryGroup.getDict(it)?.isValidWord(word) == true }
|
||||
}
|
||||
|
||||
private fun isBlacklisted(word: String): Boolean = dictionaryGroups.any { it.isBlacklisted(word) }
|
||||
|
||||
override fun removeWord(word: String) {
|
||||
for (dictionaryGroup in dictionaryGroups) {
|
||||
dictionaryGroup.removeWord(word)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearUserHistoryDictionary(context: Context) {
|
||||
for (dictionaryGroup in dictionaryGroups) {
|
||||
dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun localesAndConfidences(): String? {
|
||||
if (dictionaryGroups.size < 2) return null
|
||||
return dictionaryGroups.joinToString(", ") { "${it.locale} ${it.confidence}" }
|
||||
}
|
||||
|
||||
override fun dumpDictionaryForDebug(dictName: String) {
|
||||
val dictToDump = dictionaryGroups[0].getSubDict(dictName)
|
||||
if (dictToDump == null) {
|
||||
Log.e(TAG, ("Cannot dump $dictName. The dictionary is not being used for suggestion or cannot be dumped."))
|
||||
return
|
||||
}
|
||||
dictToDump.dumpAllWordsForDebug()
|
||||
}
|
||||
|
||||
override fun getDictionaryStats(context: Context): List<DictionaryStats> =
|
||||
DictionaryFacilitator.DYNAMIC_DICTIONARY_TYPES.flatMap { dictType ->
|
||||
dictionaryGroups.mapNotNull { it.getSubDict(dictType)?.dictionaryStats }
|
||||
}
|
||||
|
||||
override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
|
||||
|
||||
companion object {
|
||||
private val TAG = DictionaryFacilitatorImpl::class.java.simpleName
|
||||
|
||||
// HACK: This threshold is being used when adding a capitalized entry in the User History dictionary.
|
||||
private const val CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140
|
||||
|
||||
private fun createSubDict(
|
||||
dictType: String, context: Context, locale: Locale, dictFile: File?, dictNamePrefix: String
|
||||
): ExpandableBinaryDictionary? {
|
||||
try {
|
||||
return when (dictType) {
|
||||
Dictionary.TYPE_USER_HISTORY -> UserHistoryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
|
||||
Dictionary.TYPE_USER -> UserBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
|
||||
Dictionary.TYPE_CONTACTS -> ContactsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
|
||||
Dictionary.TYPE_APPS -> AppsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
|
||||
else -> throw IllegalArgumentException("unknown dictionary type $dictType")
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Cannot create dictionary: $dictType", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Cannot create dictionary: $dictType", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findDictionaryGroupWithLocale(dictGroups: List<DictionaryGroup>?, locale: Locale): DictionaryGroup? {
|
||||
return dictGroups?.firstOrNull { it.locale == locale }
|
||||
}
|
||||
|
||||
private fun getUsedLocales(mainLocale: Locale, context: Context): Collection<Locale> {
|
||||
val locales = hashSetOf(mainLocale)
|
||||
// adding secondary locales is a bit tricky since they depend on the subtype
|
||||
// but usually this is called with the selected subtype locale
|
||||
val selectedSubtype = SubtypeSettings.getSelectedSubtype(context.prefs())
|
||||
if (selectedSubtype.locale() == mainLocale) {
|
||||
locales.addAll(getSecondaryLocales(selectedSubtype.extraValue))
|
||||
} else {
|
||||
// probably we're called from the spell checker when using a different app as keyboard
|
||||
// so best bet is adding all secondary locales for matching main locale
|
||||
SubtypeSettings.getEnabledSubtypes(false).forEach {
|
||||
if (it.locale() == mainLocale)
|
||||
locales.addAll(getSecondaryLocales(it.extraValue))
|
||||
}
|
||||
}
|
||||
return locales
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A group of dictionaries that work together for a single language. */
|
||||
private class DictionaryGroup(
|
||||
val locale: Locale = Locale(""),
|
||||
private var mainDict: Dictionary? = null,
|
||||
subDicts: Map<String, ExpandableBinaryDictionary> = emptyMap(),
|
||||
context: Context? = null
|
||||
) {
|
||||
private val subDicts: ConcurrentHashMap<String, ExpandableBinaryDictionary> = ConcurrentHashMap(subDicts)
|
||||
|
||||
/** Removes a word from all dictionaries in this group. If the word is in a read-only dictionary, it is blacklisted. */
|
||||
fun removeWord(word: String) {
|
||||
// remove from user history
|
||||
getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
|
||||
|
||||
// and from personal dictionary
|
||||
getSubDict(Dictionary.TYPE_USER)?.removeUnigramEntryDynamically(word)
|
||||
|
||||
val contactsDict = getSubDict(Dictionary.TYPE_CONTACTS)
|
||||
if (contactsDict != null && contactsDict.isInDictionary(word)) {
|
||||
contactsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
|
||||
addToBlacklist(word)
|
||||
return
|
||||
}
|
||||
|
||||
val appsDict = getSubDict(Dictionary.TYPE_APPS)
|
||||
if (appsDict != null && appsDict.isInDictionary(word)) {
|
||||
appsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
|
||||
addToBlacklist(word)
|
||||
return
|
||||
}
|
||||
|
||||
val mainDict = mainDict ?: return
|
||||
if (mainDict.isValidWord(word)) {
|
||||
addToBlacklist(word)
|
||||
return
|
||||
}
|
||||
|
||||
val lowercase = word.lowercase(locale)
|
||||
if (getDict(Dictionary.TYPE_MAIN)!!.isValidWord(lowercase)) {
|
||||
addToBlacklist(lowercase)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- Confidence for multilingual typing -------------------
|
||||
|
||||
// Confidence that the most probable language is actually the language the user is
|
||||
// typing in. For now, this is simply the number of times a word from this language
|
||||
// has been committed in a row, with an exception when typing a single word not contained
|
||||
// in this language.
|
||||
var confidence = 1
|
||||
|
||||
// allow to go above max confidence, for better determination of currently preferred language
|
||||
// when decreasing confidence or getting weight factor, limit to maximum
|
||||
fun increaseConfidence() {
|
||||
confidence += 1
|
||||
}
|
||||
|
||||
// If confidence is above max, drop to max confidence. This does not change weights and
|
||||
// allows conveniently typing single words from the other language without affecting suggestions
|
||||
fun decreaseConfidence() {
|
||||
if (confidence > MAX_CONFIDENCE) confidence = MAX_CONFIDENCE
|
||||
else if (confidence > 0) {
|
||||
confidence -= 1
|
||||
}
|
||||
}
|
||||
|
||||
fun getWeightForLocale(groups: List<DictionaryGroup>, isGesturing: Boolean) =
|
||||
getWeightForLocale(groups, if (isGesturing) 0.05f else 0.15f)
|
||||
|
||||
// might need some more tuning
|
||||
fun getWeightForLocale(groups: List<DictionaryGroup>, step: Float): Float {
|
||||
if (groups.size == 1) return 1f
|
||||
if (confidence < 2) return 1f - step * (MAX_CONFIDENCE - confidence)
|
||||
for (group in groups) {
|
||||
if (group !== this && group.confidence >= confidence) return 1f - step / 2f
|
||||
}
|
||||
return 1f
|
||||
}
|
||||
|
||||
// --------------- Blacklist -------------------
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
// words cannot be (permanently) removed from some dictionaries, so we use a blacklist for "removing" words
|
||||
private val blacklistFile = if (context?.filesDir == null) null
|
||||
else {
|
||||
val file = File(context.filesDir.absolutePath + File.separator + "blacklists" + File.separator + locale.toLanguageTag() + ".txt")
|
||||
if (file.isDirectory) file.delete() // this apparently was an issue in some versions
|
||||
if (file.mkdirs()) file
|
||||
else null
|
||||
}
|
||||
|
||||
private val blacklist = hashSetOf<String>().apply {
|
||||
if (blacklistFile?.exists() != true) return@apply
|
||||
scope.launch {
|
||||
synchronized(this) {
|
||||
try {
|
||||
addAll(blacklistFile.readLines())
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception while trying to read blacklist from ${blacklistFile.name}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isBlacklisted(word: String) = blacklist.contains(word)
|
||||
|
||||
fun addToBlacklist(word: String) {
|
||||
if (!blacklist.add(word) || blacklistFile == null) return
|
||||
scope.launch {
|
||||
synchronized(this) {
|
||||
try {
|
||||
if (blacklistFile.isDirectory) blacklistFile.delete()
|
||||
blacklistFile.appendText("$word\n")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception while trying to add word \"$word\" to blacklist ${blacklistFile.name}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromBlacklist(word: String) {
|
||||
if (!blacklist.remove(word) || blacklistFile == null) return
|
||||
scope.launch {
|
||||
synchronized(this) {
|
||||
try {
|
||||
val newLines = blacklistFile.readLines().filterNot { it == word }
|
||||
blacklistFile.writeText(newLines.joinToString("\n"))
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception while trying to remove word \"$word\" to blacklist ${blacklistFile.name}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- Dictionary handling -------------------
|
||||
|
||||
fun setMainDict(newMainDict: Dictionary?) {
|
||||
// Close old dictionary if exists. Main dictionary can be assigned multiple times.
|
||||
val oldDict = mainDict
|
||||
mainDict = newMainDict
|
||||
if (oldDict != null && newMainDict !== oldDict)
|
||||
oldDict.close()
|
||||
}
|
||||
|
||||
fun getDict(dictType: String): Dictionary? {
|
||||
if (dictType == Dictionary.TYPE_MAIN) {
|
||||
return mainDict
|
||||
}
|
||||
return getSubDict(dictType)
|
||||
}
|
||||
|
||||
fun getSubDict(dictType: String): ExpandableBinaryDictionary? {
|
||||
return subDicts[dictType]
|
||||
}
|
||||
|
||||
fun hasDict(dictType: String): Boolean {
|
||||
if (dictType == Dictionary.TYPE_MAIN) {
|
||||
return mainDict != null
|
||||
}
|
||||
return subDicts.containsKey(dictType)
|
||||
}
|
||||
|
||||
fun closeDict(dictType: String) {
|
||||
val dict = if (Dictionary.TYPE_MAIN == dictType) {
|
||||
mainDict
|
||||
} else {
|
||||
subDicts.remove(dictType)
|
||||
}
|
||||
dict?.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DictionaryGroup::class.java.simpleName
|
||||
const val MAX_CONFIDENCE = 2
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ public class DictionaryFacilitatorLruCache {
|
|||
private final Object mLock = new Object();
|
||||
private final DictionaryFacilitator mDictionaryFacilitator;
|
||||
private boolean mUseContactsDictionary;
|
||||
private boolean mUseAppsDictionary;
|
||||
private Locale mLocale;
|
||||
|
||||
public DictionaryFacilitatorLruCache(final Context context, final String dictionaryNamePrefix) {
|
||||
|
@ -58,10 +59,8 @@ public class DictionaryFacilitatorLruCache {
|
|||
// Nothing to do if the locale is null. This would be the case before any get() calls.
|
||||
if (mLocale != null) {
|
||||
// Note: Given that personalized dictionaries are not used here; we can pass null account.
|
||||
mDictionaryFacilitator.resetDictionaries(mContext, mLocale,
|
||||
mUseContactsDictionary, false /* usePersonalizedDicts */,
|
||||
false /* forceReloadMainDictionary */, null /* account */,
|
||||
mDictionaryNamePrefix, null /* listener */);
|
||||
mDictionaryFacilitator.resetDictionaries(mContext, mLocale, mUseContactsDictionary,
|
||||
mUseAppsDictionary, false, false, mDictionaryNamePrefix, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,6 +76,18 @@ public class DictionaryFacilitatorLruCache {
|
|||
}
|
||||
}
|
||||
|
||||
public void setUseAppsDictionary(final boolean useAppsDictionary) {
|
||||
synchronized (mLock) {
|
||||
if (mUseAppsDictionary == useAppsDictionary) {
|
||||
// The value has not been changed.
|
||||
return;
|
||||
}
|
||||
mUseAppsDictionary = useAppsDictionary;
|
||||
resetDictionariesForLocaleLocked();
|
||||
waitForLoadingMainDictionary(mDictionaryFacilitator);
|
||||
}
|
||||
}
|
||||
|
||||
public DictionaryFacilitator get(final Locale locale) {
|
||||
synchronized (mLock) {
|
||||
if (!mDictionaryFacilitator.isForLocale(locale)) {
|
||||
|
|
|
@ -6,88 +6,87 @@
|
|||
package helium314.keyboard.latin
|
||||
|
||||
import android.content.Context
|
||||
import helium314.keyboard.latin.common.FileUtils
|
||||
import helium314.keyboard.latin.common.LocaleUtils
|
||||
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
|
||||
import helium314.keyboard.latin.utils.DictionaryInfoUtils
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import java.io.File
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Initializes a main dictionary collection from a dictionary pack, with explicit flags.
|
||||
*
|
||||
*
|
||||
* This searches for a content provider providing a dictionary pack for the specified
|
||||
* locale. If none is found, it falls back to the built-in dictionary - if any.
|
||||
* @param context application context for reading resources
|
||||
* @param locale the locale for which to create the dictionary
|
||||
* @return an initialized instance of DictionaryCollection
|
||||
*/
|
||||
fun createMainDictionary(context: Context, locale: Locale): DictionaryCollection {
|
||||
val cacheDir = DictionaryInfoUtils.getAndCreateCacheDirectoryForLocale(locale, context)
|
||||
val dictList = LinkedList<Dictionary>()
|
||||
// get cached dict files
|
||||
val (userDicts, extractedDicts) = DictionaryInfoUtils.getCachedDictsForLocale(locale, context)
|
||||
.partition { it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) }
|
||||
// add user dicts to list
|
||||
userDicts.forEach { checkAndAddDictionaryToListIfNotExisting(it, dictList, locale) }
|
||||
// add extracted dicts to list (after userDicts, to skip extracted dicts of same type)
|
||||
extractedDicts.forEach { checkAndAddDictionaryToListIfNotExisting(it, dictList, locale) }
|
||||
if (dictList.any { it.mDictType == Dictionary.TYPE_MAIN })
|
||||
return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList)
|
||||
|
||||
// no main dict found -> check assets
|
||||
val assetsDicts = DictionaryInfoUtils.getAssetsDictionaryList(context)
|
||||
// file name is <type>_<language tag>.dict
|
||||
val dictsByType = assetsDicts?.groupBy { it.substringBefore("_") }
|
||||
// for each type find the best match
|
||||
dictsByType?.forEach { (dictType, dicts) ->
|
||||
val bestMatch = LocaleUtils.getBestMatch(locale, dicts) { it.substringAfter("_")
|
||||
.substringBefore(".").constructLocale() } ?: return@forEach
|
||||
// extract dict and add extracted file
|
||||
val targetFile = File(cacheDir, "$dictType.dict")
|
||||
FileUtils.copyStreamToNewFile(
|
||||
context.assets.open(DictionaryInfoUtils.ASSETS_DICTIONARY_FOLDER + File.separator + bestMatch),
|
||||
targetFile
|
||||
)
|
||||
checkAndAddDictionaryToListIfNotExisting(targetFile, dictList, locale)
|
||||
}
|
||||
// If the list is empty, that means we should not use any dictionary (for example, the user
|
||||
// explicitly disabled the main dictionary), so the following is okay. dictList is never
|
||||
// null, but if for some reason it is, DictionaryCollection handles it gracefully.
|
||||
return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList)
|
||||
}
|
||||
|
||||
/**
|
||||
* add dictionary created from [file] to [dicts]
|
||||
* if [file] cannot be loaded it is deleted
|
||||
* if the dictionary type already exists in [dicts], the [file] is skipped
|
||||
*/
|
||||
private fun checkAndAddDictionaryToListIfNotExisting(file: File, dicts: MutableList<Dictionary>, locale: Locale) {
|
||||
if (!file.isFile) return
|
||||
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file) ?: return killDictionary(file)
|
||||
val dictType = header.mIdString.split(":").first()
|
||||
if (dicts.any { it.mDictType == dictType }) return
|
||||
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
|
||||
file.absolutePath, 0, file.length(), false, locale, dictType
|
||||
)
|
||||
|
||||
if (readOnlyBinaryDictionary.isValidDictionary) {
|
||||
if (locale.language == "ko") {
|
||||
// Use KoreanDictionary for Korean locale
|
||||
dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
|
||||
} else {
|
||||
dicts.add(readOnlyBinaryDictionary)
|
||||
object DictionaryFactory {
|
||||
/**
|
||||
* Initializes a main dictionary collection for a locale.
|
||||
* Uses all dictionaries in cache folder for locale, and adds built-in
|
||||
* dictionaries of matching locales if type is not already in cache folder.
|
||||
*
|
||||
* @return an initialized instance of DictionaryCollection
|
||||
*/
|
||||
// todo:
|
||||
// expose the weight so users can adjust dictionary "importance" (useful for addons like emoji dict)
|
||||
// allow users to block certain dictionaries (not sure how this should work exactly)
|
||||
fun createMainDictionaryCollection(context: Context, locale: Locale): DictionaryCollection {
|
||||
val dictList = LinkedList<Dictionary>()
|
||||
val (extracted, nonExtracted) = getAvailableDictsForLocale(locale, context)
|
||||
extracted.sortedBy { !it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) }.forEach {
|
||||
// we sort to have user dicts first, so they have priority over internal dicts of the same type
|
||||
checkAndAddDictionaryToListNewType(it, dictList, locale)
|
||||
}
|
||||
} else {
|
||||
readOnlyBinaryDictionary.close()
|
||||
killDictionary(file)
|
||||
nonExtracted.forEach { filename ->
|
||||
val type = filename.substringBefore("_")
|
||||
if (dictList.any { it.mDictType == type }) return@forEach
|
||||
val extractedFile = DictionaryInfoUtils.extractAssetsDictionary(filename, locale, context) ?: return@forEach
|
||||
checkAndAddDictionaryToListNewType(extractedFile, dictList, locale)
|
||||
}
|
||||
return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList, FloatArray(dictList.size) { 1f })
|
||||
}
|
||||
|
||||
fun getAvailableDictsForLocale(locale: Locale, context: Context): Pair<Array<out File>, List<String>> {
|
||||
val cachedDicts = DictionaryInfoUtils.getCachedDictsForLocale(locale, context)
|
||||
|
||||
val nonExtractedDicts = mutableListOf<String>()
|
||||
DictionaryInfoUtils.getAssetsDictionaryList(context)
|
||||
// file name is <type>_<language tag>.dict
|
||||
?.groupBy { it.substringBefore("_") }
|
||||
?.forEach { (dictType, dicts) ->
|
||||
if (cachedDicts.any { it.name == "$dictType.dict" })
|
||||
return@forEach // dictionary is already extracted (can't be old because of cleanup on upgrade)
|
||||
val bestMatch = LocaleUtils.getBestMatch(locale, dicts) {
|
||||
DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(it)
|
||||
} ?: return@forEach
|
||||
nonExtractedDicts.add(bestMatch)
|
||||
}
|
||||
return cachedDicts to nonExtractedDicts
|
||||
}
|
||||
|
||||
/**
|
||||
* add dictionary created from [file] to [dicts]
|
||||
* if [file] cannot be loaded it is deleted
|
||||
* if the dictionary type already exists in [dicts], the [file] is skipped
|
||||
*/
|
||||
private fun checkAndAddDictionaryToListNewType(file: File, dicts: MutableList<Dictionary>, locale: Locale) {
|
||||
if (!file.isFile) return
|
||||
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file) ?: return killDictionary(file)
|
||||
val dictType = header.mIdString.split(":").first()
|
||||
if (dicts.any { it.mDictType == dictType }) return
|
||||
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
|
||||
file.absolutePath, 0, file.length(), false, locale, dictType
|
||||
)
|
||||
|
||||
if (readOnlyBinaryDictionary.isValidDictionary) {
|
||||
if (locale.language == "ko") {
|
||||
// Use KoreanDictionary for Korean locale
|
||||
dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
|
||||
} else {
|
||||
dicts.add(readOnlyBinaryDictionary)
|
||||
}
|
||||
} else {
|
||||
readOnlyBinaryDictionary.close()
|
||||
killDictionary(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun killDictionary(file: File) {
|
||||
Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun killDictionary(file: File) {
|
||||
Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
|
||||
file.delete()
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import helium314.keyboard.latin.common.ColorType;
|
|||
import helium314.keyboard.latin.settings.Settings;
|
||||
import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
|
||||
import helium314.keyboard.latin.suggestions.SuggestionStripView;
|
||||
import kotlin.Unit;
|
||||
|
||||
|
||||
public final class InputView extends FrameLayout {
|
||||
private final Rect mInputViewRect = new Rect();
|
||||
|
@ -43,10 +45,7 @@ public final class InputView extends FrameLayout {
|
|||
mMainKeyboardView, suggestionStripView);
|
||||
mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
|
||||
mMainKeyboardView, suggestionStripView);
|
||||
ViewKt.doOnNextLayout(this, v -> {
|
||||
Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
|
||||
return null;
|
||||
});
|
||||
ViewKt.doOnNextLayout(this, this::onNextLayout);
|
||||
}
|
||||
|
||||
public void setKeyboardTopPadding(final int keyboardTopPadding) {
|
||||
|
@ -104,6 +103,14 @@ public final class InputView extends FrameLayout {
|
|||
return mActiveForwarder.onTouchEvent(x, y, me);
|
||||
}
|
||||
|
||||
private Unit onNextLayout(View v) {
|
||||
Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
|
||||
|
||||
// Work around inset application being unreliable
|
||||
requestApplyInsets();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class forwards series of {@link MotionEvent}s from <code>SenderView</code> to
|
||||
* <code>ReceiverView</code>.
|
||||
|
|
|
@ -78,7 +78,7 @@ class KeyboardWrapperView @JvmOverloads constructor(
|
|||
val changePercent = 2 * sign * (x - motionEvent.rawX) / context.resources.displayMetrics.density
|
||||
if (abs(changePercent) < 1) return@setOnTouchListener true
|
||||
x = motionEvent.rawX
|
||||
val oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
val oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
val newScale = (oldScale + changePercent / 100f).coerceAtMost(2.5f).coerceAtLeast(0.5f)
|
||||
if (newScale == oldScale) return@setOnTouchListener true
|
||||
Settings.getInstance().writeOneHandedModeScale(newScale)
|
||||
|
|
|
@ -737,8 +737,8 @@ public class LatinIME extends InputMethodService implements
|
|||
if (mDictionaryFacilitator.usesSameSettings(
|
||||
locales,
|
||||
mSettings.getCurrent().mUseContactsDictionary,
|
||||
mSettings.getCurrent().mUsePersonalizedDicts,
|
||||
mSettings.getCurrent().mAccount
|
||||
mSettings.getCurrent().mUseAppsDictionary,
|
||||
mSettings.getCurrent().mUsePersonalizedDicts
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
@ -755,8 +755,8 @@ public class LatinIME extends InputMethodService implements
|
|||
private void resetDictionaryFacilitator(@NonNull final Locale locale) {
|
||||
final SettingsValues settingsValues = mSettings.getCurrent();
|
||||
mDictionaryFacilitator.resetDictionaries(this, locale,
|
||||
settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts,
|
||||
false, settingsValues.mAccount, "", this);
|
||||
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
|
||||
settingsValues.mUsePersonalizedDicts, false, "", this);
|
||||
mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
|
||||
}
|
||||
|
||||
|
@ -765,12 +765,9 @@ public class LatinIME extends InputMethodService implements
|
|||
*/
|
||||
/* package private */ void resetSuggestMainDict() {
|
||||
final SettingsValues settingsValues = mSettings.getCurrent();
|
||||
mDictionaryFacilitator.resetDictionaries(this /* context */,
|
||||
mDictionaryFacilitator.getMainLocale(), settingsValues.mUseContactsDictionary,
|
||||
settingsValues.mUsePersonalizedDicts,
|
||||
true /* forceReloadMainDictionary */,
|
||||
settingsValues.mAccount, "" /* dictNamePrefix */,
|
||||
this /* DictionaryInitializationListener */);
|
||||
mDictionaryFacilitator.resetDictionaries(this, mDictionaryFacilitator.getMainLocale(),
|
||||
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
|
||||
settingsValues.mUsePersonalizedDicts, true, "", this);
|
||||
}
|
||||
|
||||
// used for debug
|
||||
|
@ -1464,7 +1461,7 @@ public class LatinIME extends InputMethodService implements
|
|||
// switch IME if wanted and possible
|
||||
if (switchIme && !switchSubtype && switchInputMethod())
|
||||
return;
|
||||
final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
|
||||
final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(true).size() > 1;
|
||||
// switch subtype if wanted, do nothing if no other subtype is available
|
||||
if (switchSubtype && !switchIme) {
|
||||
if (hasMoreThanOneSubtype)
|
||||
|
@ -2021,8 +2018,10 @@ public class LatinIME extends InputMethodService implements
|
|||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
switch (level) {
|
||||
case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE ->
|
||||
KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
|
||||
case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE -> {
|
||||
KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
|
||||
mKeyboardSwitcher.trimMemory();
|
||||
}
|
||||
// deallocateMemory always called on hiding, and should not be called when showing
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import helium314.keyboard.latin.utils.ScriptUtils;
|
|||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
|
||||
import helium314.keyboard.latin.utils.SubtypeSettings;
|
||||
import helium314.keyboard.latin.utils.SubtypeUtilsKt;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -91,9 +92,6 @@ public class RichInputMethodManager {
|
|||
mContext = context;
|
||||
mInputMethodInfoCache = new InputMethodInfoCache(mImm, context.getPackageName());
|
||||
|
||||
// Initialize subtype utils.
|
||||
SubtypeLocaleUtils.init(context);
|
||||
|
||||
// Initialize the current input method subtype and the shortcut IME.
|
||||
refreshSubtypeCaches();
|
||||
}
|
||||
|
@ -148,13 +146,15 @@ public class RichInputMethodManager {
|
|||
if (mCachedThisImeInfo != null) {
|
||||
return mCachedThisImeInfo;
|
||||
}
|
||||
for (final InputMethodInfo imi : mImm.getInputMethodList()) {
|
||||
final var inputMethods = mImm.getInputMethodList();
|
||||
for (final InputMethodInfo imi : inputMethods) {
|
||||
if (imi.getPackageName().equals(mImePackageName)) {
|
||||
mCachedThisImeInfo = imi;
|
||||
return imi;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
|
||||
throw new RuntimeException("Input method id for " + mImePackageName + " not found, only found" +
|
||||
CollectionsKt.map(inputMethods, InputMethodInfo::getPackageName));
|
||||
}
|
||||
|
||||
public synchronized List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
|
||||
|
@ -302,9 +302,8 @@ public class RichInputMethodManager {
|
|||
final int count = myImi.getSubtypeCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
|
||||
final String layoutName = SubtypeLocaleUtils.getMainLayoutName(subtype);
|
||||
if (locale.equals(SubtypeUtilsKt.locale(subtype))
|
||||
&& keyboardLayoutSetName.equals(layoutName)) {
|
||||
final String layoutName = SubtypeUtilsKt.mainLayoutNameOrQwerty(subtype);
|
||||
if (locale.equals(SubtypeUtilsKt.locale(subtype)) && keyboardLayoutSetName.equals(layoutName)) {
|
||||
return subtype;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,11 @@ import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder
|
|||
import helium314.keyboard.latin.common.Constants
|
||||
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
|
||||
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
|
||||
import helium314.keyboard.latin.common.LocaleUtils.isRtlLanguage
|
||||
import helium314.keyboard.latin.utils.LayoutType
|
||||
import helium314.keyboard.latin.utils.LayoutUtilsCustom
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.ScriptUtils
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.script
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
|
||||
import helium314.keyboard.latin.utils.locale
|
||||
import java.util.Locale
|
||||
|
@ -25,7 +26,7 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
|
|||
val locale: Locale = rawSubtype.locale()
|
||||
|
||||
// The subtype is considered RTL if the language of the main subtype is RTL.
|
||||
val isRtlSubtype: Boolean = isRtlLanguage(locale)
|
||||
val isRtlSubtype: Boolean = ScriptUtils.isScriptRtl(locale.script())
|
||||
|
||||
fun getExtraValueOf(key: String): String? = rawSubtype.getExtraValueOf(key)
|
||||
|
||||
|
@ -40,21 +41,9 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
|
|||
|
||||
val isCustom: Boolean get() = LayoutUtilsCustom.isCustomLayout(mainLayoutName)
|
||||
|
||||
val fullDisplayName: String get() {
|
||||
if (isNoLanguage) {
|
||||
return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
|
||||
}
|
||||
return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
|
||||
}
|
||||
val fullDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
|
||||
|
||||
val middleDisplayName: String
|
||||
// Get the RichInputMethodSubtype's middle display name in its locale.
|
||||
get() {
|
||||
if (isNoLanguage) {
|
||||
return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
|
||||
}
|
||||
return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
|
||||
}
|
||||
val middleDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is RichInputMethodSubtype) return false
|
||||
|
@ -81,7 +70,7 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
|
|||
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE)
|
||||
private val DUMMY_NO_LANGUAGE_SUBTYPE = RichInputMethodSubtype(
|
||||
InputMethodSubtypeBuilder()
|
||||
.setSubtypeNameResId(R.string.subtype_no_language_qwerty)
|
||||
.setSubtypeNameResId(R.string.subtype_no_language)
|
||||
.setSubtypeIconResId(R.drawable.ic_ime_switcher)
|
||||
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
|
||||
.setSubtypeMode(Constants.Subtype.KEYBOARD_MODE)
|
||||
|
@ -132,4 +121,4 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
|
|||
return DUMMY_NO_LANGUAGE_SUBTYPE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
package helium314.keyboard.latin
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import helium314.keyboard.keyboard.Keyboard
|
||||
import helium314.keyboard.keyboard.KeyboardSwitcher
|
||||
import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
|
||||
import helium314.keyboard.latin.common.ComposedData
|
||||
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
|
||||
import helium314.keyboard.latin.utils.SuggestionResults
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** Simple DictionaryFacilitator for a single Dictionary. Has some optional special purpose functionality. */
|
||||
class SingleDictionaryFacilitator(private val dict: Dictionary) : DictionaryFacilitator {
|
||||
var suggestionLogger: SuggestionLogger? = null
|
||||
|
||||
// this will not work from spell checker if used together with a different keyboard app
|
||||
fun getSuggestions(word: String): SuggestionResults {
|
||||
val suggestionResults = getSuggestionResults(
|
||||
ComposedData.createForWord(word),
|
||||
NgramContext.getEmptyPrevWordsContext(0),
|
||||
KeyboardSwitcher.getInstance().keyboard, // looks like actual keyboard doesn't matter (composed data doesn't contain coordinates)
|
||||
SettingsValuesForSuggestion(false, false),
|
||||
Suggest.SESSION_ID_TYPING, SuggestedWords.INPUT_STYLE_TYPING
|
||||
)
|
||||
return suggestionResults
|
||||
}
|
||||
|
||||
override fun getSuggestionResults(
|
||||
composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
|
||||
settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
|
||||
): SuggestionResults {
|
||||
val suggestionResults = SuggestionResults(
|
||||
SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext,
|
||||
false
|
||||
)
|
||||
suggestionResults.addAll(
|
||||
dict.getSuggestions(composedData, ngramContext, keyboard.proximityInfo.nativeProximityInfo,
|
||||
settingsValuesForSuggestion, sessionId, 1f,
|
||||
floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
|
||||
)
|
||||
)
|
||||
suggestionLogger?.onNewSuggestions(suggestionResults, composedData, ngramContext, keyboard, inputStyle)
|
||||
|
||||
return suggestionResults
|
||||
}
|
||||
|
||||
// ------------ dummy functionality ----------------
|
||||
|
||||
override fun setValidSpellingWordReadCache(cache: LruCache<String, Boolean>) {}
|
||||
|
||||
override fun setValidSpellingWordWriteCache(cache: LruCache<String, Boolean>) {}
|
||||
|
||||
override fun isForLocale(locale: Locale?): Boolean = locale == dict.mLocale
|
||||
|
||||
override fun onStartInput() {}
|
||||
|
||||
override fun onFinishInput(context: Context) {
|
||||
dict.onFinishInput()
|
||||
}
|
||||
|
||||
override fun closeDictionaries() {
|
||||
dict.close()
|
||||
}
|
||||
|
||||
override fun isActive(): Boolean = true
|
||||
|
||||
override fun getMainLocale(): Locale = dict.mLocale
|
||||
|
||||
override fun getCurrentLocale(): Locale = mainLocale
|
||||
|
||||
override fun usesSameSettings(locales: List<Locale>, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
|
||||
return locales.singleOrNull() == mainLocale
|
||||
}
|
||||
|
||||
override fun resetDictionaries(context: Context, newLocale: Locale, useContactsDict: Boolean, useAppsDict: Boolean,
|
||||
usePersonalizedDicts: Boolean, forceReloadMainDictionary: Boolean, dictNamePrefix: String, listener: DictionaryInitializationListener?
|
||||
) { }
|
||||
|
||||
override fun hasAtLeastOneInitializedMainDictionary(): Boolean = dict.isInitialized
|
||||
|
||||
override fun hasAtLeastOneUninitializedMainDictionary(): Boolean = !dict.isInitialized
|
||||
|
||||
override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
|
||||
}
|
||||
|
||||
override fun addToUserHistory(
|
||||
suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
|
||||
timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
|
||||
) {}
|
||||
|
||||
override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {}
|
||||
|
||||
override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {}
|
||||
|
||||
override fun isValidSpellingWord(word: String): Boolean = dict.isValidWord(word)
|
||||
|
||||
override fun isValidSuggestionWord(word: String) = isValidSpellingWord(word)
|
||||
|
||||
override fun removeWord(word: String) {}
|
||||
|
||||
override fun clearUserHistoryDictionary(context: Context) {}
|
||||
|
||||
override fun localesAndConfidences(): String? = null
|
||||
|
||||
override fun dumpDictionaryForDebug(dictName: String) {}
|
||||
|
||||
override fun getDictionaryStats(context: Context): List<DictionaryStats> = emptyList()
|
||||
|
||||
override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
|
||||
|
||||
companion object {
|
||||
interface SuggestionLogger {
|
||||
/** provides input data and suggestions returned by the library */
|
||||
fun onNewSuggestions(suggestions: SuggestionResults, composedData: ComposedData,
|
||||
ngramContext: NgramContext, keyboard: Keyboard, inputStyle: Int)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,8 +82,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
|
|||
}
|
||||
|
||||
public static UserBinaryDictionary getDictionary(
|
||||
final Context context, final Locale locale, final File dictFile,
|
||||
final String dictNamePrefix, @Nullable final String account) {
|
||||
final Context context, final Locale locale, final File dictFile, final String dictNamePrefix) {
|
||||
return new UserBinaryDictionary(context, locale, false, dictFile, dictNamePrefix + NAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -218,6 +218,11 @@ public final class WordComposer {
|
|||
// TODO: compute where that puts us inside the events
|
||||
}
|
||||
|
||||
public void resetInvalidCursorPosition() {
|
||||
if (mCursorPositionWithinWord > mCodePointSize)
|
||||
mCursorPositionWithinWord = 0;
|
||||
}
|
||||
|
||||
public boolean isCursorFrontOrMiddleOfComposingWord() {
|
||||
if (DebugFlags.DEBUG_ENABLED && mCursorPositionWithinWord > mCodePointSize) {
|
||||
throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
|
||||
|
|
|
@ -274,11 +274,11 @@ class DynamicColors(context: Context, override val themeStyle: String, override
|
|||
override fun get(color: ColorType): Int = when (color) {
|
||||
TOOL_BAR_KEY_ENABLED_BACKGROUND, EMOJI_CATEGORY_SELECTED, ACTION_KEY_BACKGROUND,
|
||||
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
|
||||
AUTOFILL_BACKGROUND_CHIP, GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
|
||||
AUTOFILL_BACKGROUND_CHIP, GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW_BACKGROUND -> adjustedBackground
|
||||
TOOL_BAR_EXPAND_KEY_BACKGROUND -> if (!isNight) accent else doubleAdjustedBackground
|
||||
GESTURE_TRAIL -> gesture
|
||||
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON,
|
||||
KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
|
||||
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON, EMOJI_KEY_TEXT, KEY_PREVIEW_TEXT, POPUP_KEY_TEXT,
|
||||
KEY_ICON, POPUP_KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
|
||||
KEY_HINT_TEXT -> keyHintText
|
||||
SPACE_BAR_TEXT -> spaceBarText
|
||||
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
|
||||
|
@ -327,7 +327,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
|
|||
EMOJI_CATEGORY_SELECTED, CLIPBOARD_PIN, SHIFT_KEY_ICON -> accentColorFilter
|
||||
REMOVE_SUGGESTION_ICON, EMOJI_CATEGORY, KEY_TEXT,
|
||||
KEY_ICON, ONE_HANDED_MODE_BUTTON, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY -> keyTextFilter
|
||||
KEY_PREVIEW -> adjustedBackgroundFilter
|
||||
KEY_PREVIEW_BACKGROUND -> adjustedBackgroundFilter
|
||||
ACTION_KEY_ICON -> actionKeyIconColorFilter
|
||||
else -> colorFilter(get(color))
|
||||
}
|
||||
|
@ -336,7 +336,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
|
|||
if (view.background == null)
|
||||
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
|
||||
when (color) {
|
||||
KEY_PREVIEW -> view.background.colorFilter = adjustedBackgroundFilter
|
||||
KEY_PREVIEW_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
|
||||
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND -> setColor(view.background, color)
|
||||
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
|
||||
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
|
||||
|
@ -472,10 +472,11 @@ class DefaultColors (
|
|||
TOOL_BAR_KEY_ENABLED_BACKGROUND, EMOJI_CATEGORY_SELECTED, ACTION_KEY_BACKGROUND,
|
||||
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
|
||||
AUTOFILL_BACKGROUND_CHIP -> if (themeStyle == STYLE_MATERIAL && !hasKeyBorders) background else adjustedBackground
|
||||
GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
|
||||
GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW_BACKGROUND -> adjustedBackground
|
||||
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
|
||||
GESTURE_TRAIL -> gesture
|
||||
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON -> keyText
|
||||
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON, EMOJI_KEY_TEXT,
|
||||
POPUP_KEY_TEXT, POPUP_KEY_ICON, KEY_PREVIEW_TEXT -> keyText
|
||||
KEY_HINT_TEXT -> keyHintText
|
||||
SPACE_BAR_TEXT -> spaceBarText
|
||||
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
|
||||
|
@ -524,7 +525,7 @@ class DefaultColors (
|
|||
if (view.background == null)
|
||||
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
|
||||
when (color) {
|
||||
KEY_PREVIEW, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
|
||||
KEY_PREVIEW_BACKGROUND, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
|
||||
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> setColor(view.background, color)
|
||||
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
|
||||
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
|
||||
|
@ -547,7 +548,7 @@ class DefaultColors (
|
|||
EMOJI_CATEGORY_SELECTED, CLIPBOARD_PIN, SHIFT_KEY_ICON -> accentColorFilter
|
||||
KEY_TEXT, KEY_ICON -> keyTextFilter
|
||||
REMOVE_SUGGESTION_ICON, EMOJI_CATEGORY, ONE_HANDED_MODE_BUTTON, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY -> suggestionTextFilter
|
||||
KEY_PREVIEW -> adjustedBackgroundFilter
|
||||
KEY_PREVIEW_BACKGROUND -> adjustedBackgroundFilter
|
||||
ACTION_KEY_ICON -> actionKeyIconColorFilter
|
||||
else -> colorFilter(get(color)) // create color filter (not great for performance, so the frequently used filters should be stored)
|
||||
}
|
||||
|
@ -620,6 +621,7 @@ enum class ColorType {
|
|||
CLIPBOARD_PIN,
|
||||
EMOJI_CATEGORY,
|
||||
EMOJI_CATEGORY_SELECTED,
|
||||
EMOJI_KEY_TEXT,
|
||||
FUNCTIONAL_KEY_TEXT,
|
||||
FUNCTIONAL_KEY_BACKGROUND,
|
||||
GESTURE_TRAIL,
|
||||
|
@ -628,11 +630,14 @@ enum class ColorType {
|
|||
KEY_ICON,
|
||||
KEY_TEXT,
|
||||
KEY_HINT_TEXT,
|
||||
KEY_PREVIEW,
|
||||
KEY_PREVIEW_BACKGROUND,
|
||||
KEY_PREVIEW_TEXT,
|
||||
MORE_SUGGESTIONS_HINT,
|
||||
MORE_SUGGESTIONS_BACKGROUND,
|
||||
MORE_SUGGESTIONS_WORD_BACKGROUND,
|
||||
POPUP_KEYS_BACKGROUND,
|
||||
POPUP_KEY_TEXT,
|
||||
POPUP_KEY_ICON,
|
||||
NAVIGATION_BAR,
|
||||
SHIFT_KEY_ICON,
|
||||
SPACE_BAR_BACKGROUND,
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
|
||||
package helium314.keyboard.latin.common;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* An immutable class that encapsulates a snapshot of word composition data.
|
||||
*/
|
||||
public class ComposedData {
|
||||
@NonNull
|
||||
public final InputPointers mInputPointers;
|
||||
public final boolean mIsBatchMode;
|
||||
@NonNull
|
||||
public final String mTypedWord;
|
||||
|
||||
public ComposedData(@NonNull final InputPointers inputPointers, final boolean isBatchMode,
|
||||
@NonNull final String typedWord) {
|
||||
mInputPointers = inputPointers;
|
||||
mIsBatchMode = isBatchMode;
|
||||
mTypedWord = typedWord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the code points in the typed word to a destination array of ints.
|
||||
*
|
||||
* If the array is too small to hold the code points in the typed word, nothing is copied and
|
||||
* -1 is returned.
|
||||
*
|
||||
* @param destination the array of ints.
|
||||
* @return the number of copied code points.
|
||||
*/
|
||||
public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
|
||||
@NonNull final int[] destination) {
|
||||
// lastIndex is exclusive
|
||||
final int lastIndex = mTypedWord.length()
|
||||
- StringUtils.getTrailingSingleQuotesCount(mTypedWord);
|
||||
if (lastIndex <= 0) {
|
||||
// The string is empty or contains only single quotes.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The following function counts the number of code points in the text range which begins
|
||||
// at index 0 and extends to the character at lastIndex.
|
||||
final int codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex);
|
||||
if (codePointSize > destination.length) {
|
||||
return -1;
|
||||
}
|
||||
return StringUtils.copyCodePointsAndReturnCodePointCount(destination, mTypedWord, 0,
|
||||
lastIndex, true /* downCase */);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package helium314.keyboard.latin.common
|
||||
|
||||
import helium314.keyboard.latin.WordComposer
|
||||
import kotlin.random.Random
|
||||
|
||||
/** An immutable class that encapsulates a snapshot of word composition data. */
|
||||
class ComposedData(
|
||||
@JvmField val mInputPointers: InputPointers,
|
||||
@JvmField val mIsBatchMode: Boolean,
|
||||
@JvmField val mTypedWord: String
|
||||
) {
|
||||
/**
|
||||
* Copy the code points in the typed word to a destination array of ints.
|
||||
*
|
||||
* If the array is too small to hold the code points in the typed word, nothing is copied and
|
||||
* -1 is returned.
|
||||
*
|
||||
* @param destination the array of ints.
|
||||
* @return the number of copied code points.
|
||||
*/
|
||||
fun copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
|
||||
destination: IntArray
|
||||
): Int {
|
||||
// lastIndex is exclusive
|
||||
val lastIndex = (mTypedWord.length - StringUtils.getTrailingSingleQuotesCount(mTypedWord))
|
||||
if (lastIndex <= 0) {
|
||||
return 0 // The string is empty or contains only single quotes.
|
||||
}
|
||||
|
||||
// The following function counts the number of code points in the text range which begins
|
||||
// at index 0 and extends to the character at lastIndex.
|
||||
val codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex)
|
||||
if (codePointSize > destination.size) {
|
||||
return -1
|
||||
}
|
||||
return StringUtils.copyCodePointsAndReturnCodePointCount(
|
||||
destination, mTypedWord, 0, lastIndex, true
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createForWord(word: String): ComposedData {
|
||||
val codePoints = StringUtils.toCodePointArray(word)
|
||||
val coordinates = CoordinateUtils.newCoordinateArray(codePoints.size)
|
||||
for (i in codePoints.indices) {
|
||||
CoordinateUtils.setXYInArray(coordinates, i, Random.nextBits(2), Random.nextBits(2))
|
||||
}
|
||||
return WordComposer().apply { setComposingWord(codePoints, coordinates) }.composedDataSnapshot
|
||||
}
|
||||
}
|
||||
}
|
|
@ -197,6 +197,8 @@ public final class Constants {
|
|||
public static final int CODE_GRAVE_ACCENT = '`';
|
||||
public static final int CODE_CIRCUMFLEX_ACCENT = '^';
|
||||
public static final int CODE_TILDE = '~';
|
||||
public static final int RECENTS_TEMPLATE_KEY_CODE_0 = 0x30;
|
||||
public static final int RECENTS_TEMPLATE_KEY_CODE_1 = 0x31;
|
||||
|
||||
public static final String REGEXP_PERIOD = "\\.";
|
||||
public static final String STRING_SPACE = " ";
|
||||
|
|
|
@ -8,10 +8,10 @@ package helium314.keyboard.latin.common
|
|||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import helium314.keyboard.compat.locale
|
||||
import helium314.keyboard.latin.BuildConfig
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.script
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
|
||||
import helium314.keyboard.latin.utils.runInLocale
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
|
@ -171,34 +171,32 @@ object LocaleUtils {
|
|||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isRtlLanguage(locale: Locale): Boolean {
|
||||
val displayName = locale.getDisplayName(locale)
|
||||
if (displayName.isEmpty()) return false
|
||||
return when (Character.getDirectionality(displayName.codePointAt(0))) {
|
||||
Character.DIRECTIONALITY_RIGHT_TO_LEFT, Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
fun Locale.localizedDisplayName(resources: Resources, displayLocale: Locale? = null): String {
|
||||
val languageTag = toLanguageTag()
|
||||
if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE)
|
||||
return resources.getString(R.string.subtype_no_language)
|
||||
|
||||
fun Locale.localizedDisplayName(context: Context) =
|
||||
getLocaleDisplayNameInLocale(this, context.resources, context.resources.configuration.locale())
|
||||
|
||||
@JvmStatic
|
||||
fun getLocaleDisplayNameInLocale(locale: Locale, resources: Resources, displayLocale: Locale): String {
|
||||
val languageTag = locale.toLanguageTag()
|
||||
if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE) return resources.getString(R.string.subtype_no_language)
|
||||
if (locale.script() != locale.language.constructLocale().script() || locale.language == "mns" || locale.language == "xdq" || locale.language=="dru") {
|
||||
val resId = resources.getIdentifier(
|
||||
"subtype_${languageTag.replace("-", "_")}",
|
||||
"string",
|
||||
BuildConfig.APPLICATION_ID // replaces context.packageName, see https://stackoverflow.com/a/24525379
|
||||
)
|
||||
if (resId != 0) return resources.getString(resId)
|
||||
val overrideResId = when (languageTag) {
|
||||
"en-US" -> R.string.subtype_en_US
|
||||
"en-GB" -> R.string.subtype_en_GB
|
||||
"es-US" -> R.string.subtype_es_US
|
||||
"hi-Latn" -> R.string.subtype_hi_Latn
|
||||
"sr-Latn" -> R.string.subtype_sr_Latn
|
||||
"mns" -> R.string.subtype_mns
|
||||
"xdq" -> R.string.subtype_xdq
|
||||
"dru" -> R.string.subtype_xdq
|
||||
"st" -> R.string.subtype_st
|
||||
"dag" -> R.string.subtype_dag
|
||||
else -> 0
|
||||
}
|
||||
val localeDisplayName = locale.getDisplayName(displayLocale)
|
||||
if (overrideResId != 0) {
|
||||
return if (displayLocale == null) resources.getString(overrideResId)
|
||||
else runInLocale(resources, displayLocale) { it.getString(overrideResId) }
|
||||
}
|
||||
|
||||
val localeDisplayName = getDisplayName(displayLocale ?: resources.configuration.locale())
|
||||
return if (localeDisplayName == languageTag) {
|
||||
locale.getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
|
||||
getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
|
||||
} else {
|
||||
localeDisplayName
|
||||
}
|
||||
|
|
|
@ -6,67 +6,66 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
|||
import helium314.keyboard.latin.common.StringUtils.mightBeEmoji
|
||||
import helium314.keyboard.latin.common.StringUtils.newSingleCodePointString
|
||||
import helium314.keyboard.latin.settings.SpacingAndPunctuations
|
||||
import helium314.keyboard.latin.utils.SpacedTokens
|
||||
import java.math.BigInteger
|
||||
import java.util.Locale
|
||||
|
||||
fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) {
|
||||
val text = if (s is String) s else s.toString()
|
||||
fun CharSequence.codePointAt(offset: Int) = Character.codePointAt(this, offset)
|
||||
fun CharSequence.codePointBefore(offset: Int) = Character.codePointBefore(this, offset)
|
||||
|
||||
inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
|
||||
var offset = 0
|
||||
while (offset < text.length) {
|
||||
val codepoint = text.codePointAt(offset)
|
||||
if (run(codepoint)) return
|
||||
offset += Character.charCount(codepoint)
|
||||
val cp = text.codePointAt(offset)
|
||||
val charCount = Character.charCount(cp)
|
||||
if (loop(cp, charCount)) return
|
||||
offset += charCount
|
||||
}
|
||||
}
|
||||
|
||||
fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) {
|
||||
val text = if (s is String) s else s.toString()
|
||||
inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
|
||||
var offset = text.length
|
||||
while (offset > 0) {
|
||||
val codepoint = text.codePointBefore(offset)
|
||||
if (run(codepoint)) return
|
||||
offset -= Character.charCount(codepoint)
|
||||
val cp = text.codePointBefore(offset)
|
||||
val charCount = Character.charCount(cp)
|
||||
if (loop(cp, charCount)) return
|
||||
offset -= charCount
|
||||
}
|
||||
}
|
||||
|
||||
fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
|
||||
fun nonWordCodePointAndNoSpaceBeforeCursor(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
|
||||
var space = false
|
||||
var nonWordCodePoint = false
|
||||
loopOverCodePointsBackwards(s) {
|
||||
if (!space && Character.isWhitespace(it))
|
||||
space = true
|
||||
// treat double quote like a word codepoint for the purpose of this function (not great, maybe clarify name, or extend list of chars?)
|
||||
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it) && it != '"'.code)
|
||||
loopOverCodePointsBackwards(text) { cp, _ ->
|
||||
if (!space && Character.isWhitespace(cp)) space = true
|
||||
// treat double quote like a word codepoint for this function (not great, maybe clarify name or extend list of chars?)
|
||||
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(cp) && cp != '"'.code) {
|
||||
nonWordCodePoint = true
|
||||
}
|
||||
space && nonWordCodePoint // stop if both are found
|
||||
}
|
||||
return nonWordCodePoint && !space // return true if an non-word codepoint and no space was found
|
||||
return nonWordCodePoint && !space // return true if a non-word codepoint and no space was found
|
||||
}
|
||||
|
||||
fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean {
|
||||
var letter = false
|
||||
loopOverCodePointsBackwards(s) {
|
||||
if (Character.isWhitespace(it)) true
|
||||
else if (Character.isLetter(it)) {
|
||||
letter = true
|
||||
true
|
||||
}
|
||||
else false
|
||||
fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean {
|
||||
loopOverCodePointsBackwards(text) { cp, _ ->
|
||||
if (Character.isWhitespace(cp)) return false
|
||||
else if (Character.isLetter(cp)) return true
|
||||
false // continue
|
||||
}
|
||||
return letter
|
||||
return false
|
||||
}
|
||||
|
||||
/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */
|
||||
fun getFullEmojiAtEnd(s: CharSequence): String {
|
||||
val text = if (s is String) s else s.toString()
|
||||
var offset = text.length
|
||||
/** get the complete emoji at end of [text], considering that emojis can be joined with ZWJ resulting in different emojis */
|
||||
fun getFullEmojiAtEnd(text: CharSequence): String {
|
||||
val s = text.toString()
|
||||
var offset = s.length
|
||||
while (offset > 0) {
|
||||
val codepoint = text.codePointBefore(offset)
|
||||
val codepoint = s.codePointBefore(offset)
|
||||
// stop if codepoint can't be emoji
|
||||
if (!mightBeEmoji(codepoint))
|
||||
return text.substring(offset)
|
||||
if (!mightBeEmoji(codepoint)) return text.substring(offset)
|
||||
offset -= Character.charCount(codepoint)
|
||||
if (offset > 0 && text[offset - 1].code == KeyCode.ZWJ) {
|
||||
if (offset > 0 && s[offset - 1].code == KeyCode.ZWJ) {
|
||||
// todo: this appends ZWJ in weird cases like text, ZWJ, emoji
|
||||
// and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd)
|
||||
offset -= 1
|
||||
|
@ -76,19 +75,17 @@ fun getFullEmojiAtEnd(s: CharSequence): String {
|
|||
if (codepoint in 0x1F3FB..0x1F3FF) {
|
||||
// Skin tones are not added with ZWJ, but just appended. This is not nice as they can be emojis on their own,
|
||||
// but that's how it is done. Assume that an emoji before the skin tone will get merged (usually correct in practice)
|
||||
val codepointBefore = text.codePointBefore(offset)
|
||||
val codepointBefore = s.codePointBefore(offset)
|
||||
if (isEmoji(codepointBefore)) {
|
||||
offset -= Character.charCount(codepointBefore)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// check the whole text after offset
|
||||
val textToCheck = text.substring(offset)
|
||||
if (isEmoji(textToCheck)) {
|
||||
return textToCheck
|
||||
}
|
||||
val textToCheck = s.substring(offset)
|
||||
if (isEmoji(textToCheck)) return textToCheck
|
||||
}
|
||||
return text.substring(offset)
|
||||
return s.substring(offset)
|
||||
}
|
||||
|
||||
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
|
||||
|
@ -110,8 +107,7 @@ fun String.splitOnFirstSpacesOnly(): List<String> {
|
|||
sb.append(c)
|
||||
}
|
||||
}
|
||||
if (sb.isNotBlank())
|
||||
out.add(sb.toString())
|
||||
if (sb.isNotBlank()) out.add(sb.toString())
|
||||
return out
|
||||
}
|
||||
|
||||
|
@ -120,8 +116,7 @@ fun CharSequence.isValidNumber(): Boolean {
|
|||
}
|
||||
|
||||
fun String.decapitalize(locale: Locale): String {
|
||||
if (isEmpty() || !this[0].isUpperCase())
|
||||
return this
|
||||
if (isEmpty() || !this[0].isUpperCase()) return this
|
||||
return replaceFirstChar { it.lowercase(locale) }
|
||||
}
|
||||
|
||||
|
@ -136,11 +131,9 @@ fun containsValueWhenSplit(string: String?, value: String, split: String): Boole
|
|||
|
||||
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
|
||||
|
||||
fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)
|
||||
fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex)
|
||||
|
||||
fun String.splitOnWhitespace() = split(whitespaceSplitRegex)
|
||||
|
||||
private val whitespaceSplitRegex = "\\s+".toRegex()
|
||||
fun String.splitOnWhitespace() = SpacedTokens(this).toList()
|
||||
|
||||
// from https://github.com/mathiasbynens/emoji-test-regex-pattern, MIT license
|
||||
// matches single emojis only
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.os.Build
|
|||
import helium314.keyboard.latin.BuildConfig
|
||||
import helium314.keyboard.latin.settings.DebugSettings
|
||||
import helium314.keyboard.latin.settings.Defaults
|
||||
import helium314.keyboard.latin.utils.DeviceProtectedUtils
|
||||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.prefs
|
||||
import java.io.File
|
||||
|
@ -64,11 +65,17 @@ ${Log.getLog(100).joinToString("\n")}
|
|||
|
||||
private fun writeCrashReportToFile(text: String) {
|
||||
try {
|
||||
val dir = appContext.getExternalFilesDir(null) ?: return
|
||||
val dir = appContext.getExternalFilesDir(null)
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
|
||||
val crashReportFile = File(dir, "crash_report_$date.txt")
|
||||
crashReportFile.writeText(text)
|
||||
} catch (ignored: IOException) {
|
||||
crashReportFile.appendText(text)
|
||||
} catch (_: Exception) {
|
||||
// can't write in external files dir, maybe device just booted and is still locked
|
||||
// in this case there shouldn't be any sensitive data and we can put crash logs in unprotected files dir
|
||||
val dir = DeviceProtectedUtils.getFilesDir(appContext) ?: return
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
|
||||
val crashReportFile = File(dir, "crash_report_unprotected_$date.txt")
|
||||
crashReportFile.appendText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ import helium314.keyboard.latin.utils.RecapitalizeStatus;
|
|||
import helium314.keyboard.latin.utils.ScriptUtils;
|
||||
import helium314.keyboard.latin.utils.StatsUtils;
|
||||
import helium314.keyboard.latin.utils.TextRange;
|
||||
import helium314.keyboard.latin.utils.TimestampKt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
@ -439,19 +440,30 @@ public final class InputLogic {
|
|||
mWordBeingCorrectedByCursor = null;
|
||||
mJustRevertedACommit = false;
|
||||
final Event processedEvent;
|
||||
if (currentKeyboardScript.equals(ScriptUtils.SCRIPT_HANGUL)
|
||||
// only use the Hangul chain if codepoint may actually be Hangul
|
||||
// todo: this whole hangul-related logic should probably be somewhere else
|
||||
// need to use hangul combiner for whitespace, because otherwise the current word
|
||||
// seems to get deleted / replaced by space during mConnection.endBatchEdit()
|
||||
// similar for functional keys (codePoint -1)
|
||||
&& (event.getMCodePoint() >= 0x1100 || Character.isWhitespace(event.getMCodePoint()) || event.getMCodePoint() == -1)) {
|
||||
mWordComposer.setHangul(true);
|
||||
final Event hangulDecodedEvent = HangulEventDecoder.decodeSoftwareKeyEvent(event);
|
||||
// todo: here hangul combiner does already consume the event, and appends typed codepoint
|
||||
// to the current word instead of considering the cursor position
|
||||
// position is actually not visible to the combiner, how to fix?
|
||||
processedEvent = mWordComposer.processEvent(hangulDecodedEvent);
|
||||
if (currentKeyboardScript.equals(ScriptUtils.SCRIPT_HANGUL)) {
|
||||
// only use the Hangul chain if codepoint may actually be Hangul
|
||||
// todo: this whole hangul-related logic should probably be somewhere else
|
||||
// need to use hangul combiner for functional keys (codePoint -1), because otherwise the current word
|
||||
// seems to get deleted / replaced by space during mConnection.endBatchEdit()
|
||||
if (event.getMCodePoint() >= 0x1100 || event.getMCodePoint() == -1) {
|
||||
mWordComposer.setHangul(true);
|
||||
final Event hangulDecodedEvent = HangulEventDecoder.decodeSoftwareKeyEvent(event);
|
||||
// todo: here hangul combiner does already consume the event, and appends typed codepoint
|
||||
// to the current word instead of considering the cursor position
|
||||
// position is actually not visible to the combiner, how to fix?
|
||||
processedEvent = mWordComposer.processEvent(hangulDecodedEvent);
|
||||
if (event.getMKeyCode() == KeyCode.DELETE)
|
||||
mWordComposer.resetInvalidCursorPosition();
|
||||
} else {
|
||||
mWordComposer.setHangul(false);
|
||||
final boolean wasComposingWord = mWordComposer.isComposingWord();
|
||||
processedEvent = mWordComposer.processEvent(event);
|
||||
// workaround for space and some other separators deleting / replacing the word
|
||||
if (wasComposingWord && !mWordComposer.isComposingWord()) {
|
||||
mWordComposer.resetInvalidCursorPosition();
|
||||
mConnection.finishComposingText();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mWordComposer.setHangul(false);
|
||||
processedEvent = mWordComposer.processEvent(event);
|
||||
|
@ -755,16 +767,35 @@ public final class InputLogic {
|
|||
}
|
||||
break;
|
||||
case KeyCode.WORD_LEFT:
|
||||
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
|
||||
sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
|
||||
break;
|
||||
case KeyCode.WORD_RIGHT:
|
||||
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
|
||||
sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
|
||||
KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
|
||||
break;
|
||||
case KeyCode.MOVE_START_OF_PAGE:
|
||||
final int selectionEnd = mConnection.getExpectedSelectionEnd();
|
||||
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON);
|
||||
if (mConnection.getExpectedSelectionStart() > 0 && mConnection.getExpectedSelectionEnd() == selectionEnd) {
|
||||
// unchanged, and we're not at the top -> try a different method (necessary for compose fields)
|
||||
mConnection.setSelection(0, 0);
|
||||
}
|
||||
break;
|
||||
case KeyCode.MOVE_END_OF_PAGE:
|
||||
final int selectionStart = mConnection.getExpectedSelectionEnd();
|
||||
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON);
|
||||
if (mConnection.getExpectedSelectionStart() == selectionStart) {
|
||||
// unchanged, try fallback e.g. for compose fields that don't care about ctrl + end
|
||||
// we just move to a very large index, and hope the field is prepared to deal with this
|
||||
// getting the actual length of the text for setting the correct position can be tricky for some apps...
|
||||
try {
|
||||
mConnection.setSelection(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
} catch (Exception e) {
|
||||
// better catch potential errors and just do nothing in this case
|
||||
Log.i(TAG, "error when trying to move cursor to last position: " + e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KeyCode.UNDO:
|
||||
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON);
|
||||
|
@ -775,14 +806,18 @@ public final class InputLogic {
|
|||
case KeyCode.SPLIT_LAYOUT:
|
||||
KeyboardSwitcher.getInstance().toggleSplitKeyboardMode();
|
||||
break;
|
||||
case KeyCode.TIMESTAMP:
|
||||
mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
|
||||
break;
|
||||
case KeyCode.VOICE_INPUT:
|
||||
// switching to shortcut IME, shift state, keyboard,... is handled by LatinIME,
|
||||
// {@link KeyboardSwitcher#onEvent(Event)}, or {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
|
||||
// We need to switch to the shortcut IME. This is handled by LatinIME since the
|
||||
// input logic has no business with IME switching.
|
||||
case KeyCode.CAPS_LOCK, KeyCode.SYMBOL_ALPHA, KeyCode.ALPHA, KeyCode.SYMBOL, KeyCode.NUMPAD, KeyCode.EMOJI,
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE,
|
||||
KeyCode.CTRL, KeyCode.ALT, KeyCode.FN, KeyCode.META:
|
||||
KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE, KeyCode.FN,
|
||||
KeyCode.CTRL, KeyCode.CTRL_LEFT, KeyCode.CTRL_RIGHT, KeyCode.ALT, KeyCode.ALT_LEFT, KeyCode.ALT_RIGHT,
|
||||
KeyCode.META, KeyCode.META_LEFT, KeyCode.META_RIGHT:
|
||||
break;
|
||||
default:
|
||||
if (event.getMMetaState() != 0) {
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.content.Context;
|
|||
import helium314.keyboard.latin.utils.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import helium314.keyboard.latin.common.FileUtils;
|
||||
|
||||
|
@ -31,12 +30,8 @@ public class PersonalizationHelper {
|
|||
sLangUserHistoryDictCache = new ConcurrentHashMap<>();
|
||||
|
||||
@NonNull
|
||||
public static UserHistoryDictionary getUserHistoryDictionary(
|
||||
final Context context, final Locale locale, @Nullable final String accountName) {
|
||||
public static UserHistoryDictionary getUserHistoryDictionary(final Context context, final Locale locale) {
|
||||
String lookupStr = locale.toString();
|
||||
if (accountName != null) {
|
||||
lookupStr += "." + accountName;
|
||||
}
|
||||
synchronized (sLangUserHistoryDictCache) {
|
||||
if (sLangUserHistoryDictCache.containsKey(lookupStr)) {
|
||||
final SoftReference<UserHistoryDictionary> ref =
|
||||
|
@ -50,8 +45,7 @@ public class PersonalizationHelper {
|
|||
return dict;
|
||||
}
|
||||
}
|
||||
final UserHistoryDictionary dict = new UserHistoryDictionary(
|
||||
context, locale, accountName);
|
||||
final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
|
||||
sLangUserHistoryDictCache.put(lookupStr, new SoftReference<>(dict));
|
||||
return dict;
|
||||
}
|
||||
|
|
|
@ -30,9 +30,8 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
|
|||
static final String NAME = UserHistoryDictionary.class.getSimpleName();
|
||||
|
||||
// TODO: Make this constructor private
|
||||
UserHistoryDictionary(final Context context, final Locale locale,
|
||||
@Nullable final String account) {
|
||||
super(context, getUserHistoryDictName(NAME, locale, null /* dictFile */, account), locale, Dictionary.TYPE_USER_HISTORY, null);
|
||||
UserHistoryDictionary(final Context context, final Locale locale) {
|
||||
super(context, getUserHistoryDictName(NAME, locale, null), locale, Dictionary.TYPE_USER_HISTORY, null);
|
||||
if (mLocale != null && mLocale.toString().length() > 1) {
|
||||
reloadDictionaryIfRequired();
|
||||
}
|
||||
|
@ -41,14 +40,13 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
|
|||
/**
|
||||
* @returns the name of the {@link UserHistoryDictionary}.
|
||||
*/
|
||||
static String getUserHistoryDictName(final String name, final Locale locale,
|
||||
@Nullable final File dictFile, @Nullable final String account) {
|
||||
static String getUserHistoryDictName(final String name, final Locale locale, @Nullable final File dictFile) {
|
||||
return getDictName(name, locale, dictFile);
|
||||
}
|
||||
|
||||
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
|
||||
final File dictFile, final String dictNamePrefix, @Nullable final String account) {
|
||||
return PersonalizationHelper.getUserHistoryDictionary(context, locale, account);
|
||||
final File dictFile, final String dictNamePrefix) {
|
||||
return PersonalizationHelper.getUserHistoryDictionary(context, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -89,6 +89,8 @@ object Defaults {
|
|||
const val PREF_SIDE_PADDING_SCALE_LANDSCAPE = 0f
|
||||
const val PREF_FONT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
|
||||
const val PREF_EMOJI_FONT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
|
||||
const val PREF_EMOJI_KEY_FIT = true
|
||||
const val PREF_EMOJI_SKIN_TONE = ""
|
||||
const val PREF_SPACE_HORIZONTAL_SWIPE = "move_cursor"
|
||||
const val PREF_SPACE_VERTICAL_SWIPE = "none"
|
||||
const val PREF_DELETE_SWIPE = true
|
||||
|
@ -115,6 +117,7 @@ object Defaults {
|
|||
const val PREF_GESTURE_TRAIL_FADEOUT_DURATION = 800
|
||||
const val PREF_SHOW_SETUP_WIZARD_ICON = true
|
||||
const val PREF_USE_CONTACTS = false
|
||||
const val PREF_USE_APPS = false
|
||||
const val PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = false
|
||||
const val PREF_ONE_HANDED_MODE = false
|
||||
@SuppressLint("RtlHardcoded")
|
||||
|
@ -154,7 +157,7 @@ object Defaults {
|
|||
const val PREF_ABC_AFTER_NUMPAD_SPACE = false
|
||||
const val PREF_REMOVE_REDUNDANT_POPUPS = false
|
||||
const val PREF_SPACE_BAR_TEXT = ""
|
||||
@JvmField
|
||||
const val PREF_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"
|
||||
val PREF_EMOJI_MAX_SDK = Build.VERSION.SDK_INT
|
||||
const val PREF_EMOJI_RECENT_KEYS = ""
|
||||
const val PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = 0
|
||||
|
|
|
@ -95,13 +95,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static final String PREF_SIDE_PADDING_SCALE_LANDSCAPE = "side_padding_scale_landscape";
|
||||
public static final String PREF_FONT_SCALE = "font_scale";
|
||||
public static final String PREF_EMOJI_FONT_SCALE = "emoji_font_scale";
|
||||
public static final String PREF_EMOJI_KEY_FIT = "emoji_key_fit";
|
||||
public static final String PREF_EMOJI_SKIN_TONE = "emoji_skin_tone";
|
||||
public static final String PREF_SPACE_HORIZONTAL_SWIPE = "horizontal_space_swipe";
|
||||
public static final String PREF_SPACE_VERTICAL_SWIPE = "vertical_space_swipe";
|
||||
public static final String PREF_DELETE_SWIPE = "delete_swipe";
|
||||
public static final String PREF_AUTOSPACE_AFTER_PUNCTUATION = "autospace_after_punctuation";
|
||||
public static final String PREF_AUTOSPACE_AFTER_SUGGESTION = "autospace_after_suggestion";
|
||||
public static final String PREF_AUTOSPACE_AFTER_GESTURE_TYPING = "autospace_after_gesture_typing";
|
||||
public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing";
|
||||
public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing";
|
||||
public static final String PREF_SHIFT_REMOVES_AUTOSPACE = "shift_removes_autospace";
|
||||
public static final String PREF_ALWAYS_INCOGNITO_MODE = "always_incognito_mode";
|
||||
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
|
||||
|
@ -120,6 +122,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static final String PREF_GESTURE_TRAIL_FADEOUT_DURATION = "gesture_trail_fadeout_duration";
|
||||
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "show_setup_wizard_icon";
|
||||
public static final String PREF_USE_CONTACTS = "use_contacts";
|
||||
public static final String PREF_USE_APPS = "use_apps";
|
||||
public static final String PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = "long_press_symbols_for_numpad";
|
||||
|
||||
// one-handed mode gravity, enablement and scale, stored separately per orientation
|
||||
|
@ -164,6 +167,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
public static final String PREF_ABC_AFTER_NUMPAD_SPACE = "abc_after_numpad_space";
|
||||
public static final String PREF_REMOVE_REDUNDANT_POPUPS = "remove_redundant_popups";
|
||||
public static final String PREF_SPACE_BAR_TEXT = "space_bar_text";
|
||||
public static final String PREF_TIMESTAMP_FORMAT = "timestamp_format";
|
||||
|
||||
// Emoji
|
||||
public static final String PREF_EMOJI_MAX_SDK = "emoji_max_sdk";
|
||||
|
@ -360,7 +364,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
|
||||
public void writeOneHandedModeEnabled(final boolean enabled) {
|
||||
mPrefs.edit().putBoolean(PREF_ONE_HANDED_MODE_PREFIX +
|
||||
(mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), enabled).apply();
|
||||
(mSettingsValues.mDisplayOrientation != Configuration.ORIENTATION_LANDSCAPE), enabled).apply();
|
||||
}
|
||||
|
||||
public static float readOneHandedModeScale(final SharedPreferences prefs, final boolean isLandscape) {
|
||||
|
@ -369,7 +373,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
|
||||
public void writeOneHandedModeScale(final Float scale) {
|
||||
mPrefs.edit().putFloat(PREF_ONE_HANDED_SCALE_PREFIX +
|
||||
(mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), scale).apply();
|
||||
(mSettingsValues.mDisplayOrientation != Configuration.ORIENTATION_LANDSCAPE), scale).apply();
|
||||
}
|
||||
|
||||
public static int readOneHandedModeGravity(final SharedPreferences prefs, final boolean isLandscape) {
|
||||
|
@ -378,7 +382,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
|||
|
||||
public void writeOneHandedModeGravity(final int gravity) {
|
||||
mPrefs.edit().putInt(PREF_ONE_HANDED_GRAVITY_PREFIX +
|
||||
(mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), gravity).apply();
|
||||
(mSettingsValues.mDisplayOrientation != Configuration.ORIENTATION_LANDSCAPE), gravity).apply();
|
||||
}
|
||||
|
||||
public void writeSplitKeyboardEnabled(final boolean enabled, final boolean isLandscape) {
|
||||
|
|
|
@ -14,11 +14,13 @@ import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue
|
|||
import helium314.keyboard.latin.utils.Log
|
||||
import helium314.keyboard.latin.utils.ScriptUtils
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.script
|
||||
import helium314.keyboard.latin.utils.SubtypeSettings
|
||||
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
|
||||
import helium314.keyboard.latin.utils.locale
|
||||
import java.util.Locale
|
||||
|
||||
// some kind of intermediate between the string stored in preferences and an InputMethodSubtype
|
||||
// todo: consider using a hashMap or sortedMap instead of a string if we run into comparison issues once again
|
||||
data class SettingsSubtype(val locale: Locale, val extraValues: String) {
|
||||
|
||||
fun toPref() = locale.toLanguageTag() + Separators.SET + extraValues
|
||||
|
@ -70,6 +72,8 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) {
|
|||
prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
|
||||
.split(Separators.SETS).contains(toPref())
|
||||
|
||||
fun isSameAsDefault() = SubtypeSettings.getResourceSubtypesForLocale(locale).any { it.toSettingsSubtype() == this.toPref().toSettingsSubtype() }
|
||||
|
||||
companion object {
|
||||
fun String.toSettingsSubtype(): SettingsSubtype =
|
||||
SettingsSubtype(
|
||||
|
|
|
@ -16,7 +16,6 @@ import android.view.inputmethod.EditorInfo;
|
|||
import android.view.inputmethod.InputMethodSubtype;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.TypedValueCompat;
|
||||
|
||||
import helium314.keyboard.compat.ConfigurationCompatKt;
|
||||
|
@ -109,6 +108,7 @@ public class SettingsValues {
|
|||
public final int mScreenMetrics;
|
||||
public final boolean mAddToPersonalDictionary;
|
||||
public final boolean mUseContactsDictionary;
|
||||
public final boolean mUseAppsDictionary;
|
||||
public final boolean mCustomNavBarColor;
|
||||
public final float mKeyboardHeightScale;
|
||||
public final boolean mUrlDetectionEnabled;
|
||||
|
@ -124,6 +124,7 @@ public class SettingsValues {
|
|||
public final String mSpaceBarText;
|
||||
public final float mFontSizeMultiplier;
|
||||
public final float mFontSizeMultiplierEmoji;
|
||||
public final boolean mEmojiKeyFit;
|
||||
|
||||
// From the input box
|
||||
@NonNull
|
||||
|
@ -144,14 +145,10 @@ public class SettingsValues {
|
|||
public final SettingsValuesForSuggestion mSettingsValuesForSuggestion;
|
||||
public final boolean mIncognitoModeEnabled;
|
||||
public final boolean mLongPressSymbolsForNumpad;
|
||||
public final int mEmojiMaxSdk;
|
||||
|
||||
// User-defined colors
|
||||
public final Colors mColors;
|
||||
|
||||
@Nullable
|
||||
public final String mAccount; // todo: always null, remove?
|
||||
|
||||
// creation of Colors and SpacingAndPunctuations are the slowest parts in here, but still ok
|
||||
public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res,
|
||||
@NonNull final InputAttributes inputAttributes) {
|
||||
|
@ -227,7 +224,6 @@ public class SettingsValues {
|
|||
mGestureFloatingPreviewDynamicEnabled = Settings.readGestureDynamicPreviewEnabled(prefs);
|
||||
mGestureFastTypingCooldown = prefs.getInt(Settings.PREF_GESTURE_FAST_TYPING_COOLDOWN, Defaults.PREF_GESTURE_FAST_TYPING_COOLDOWN);
|
||||
mGestureTrailFadeoutDuration = prefs.getInt(Settings.PREF_GESTURE_TRAIL_FADEOUT_DURATION, Defaults.PREF_GESTURE_TRAIL_FADEOUT_DURATION);
|
||||
mAccount = null; // remove? or can it be useful somewhere?
|
||||
mOverrideShowingSuggestions = mInputAttributes.mMayOverrideShowingSuggestions
|
||||
&& prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS)
|
||||
&& ((inputAttributes.mInputType & InputType.TYPE_MASK_VARIATION) != InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
|
||||
|
@ -268,6 +264,7 @@ public class SettingsValues {
|
|||
mPopupKeyLabelSources = SubtypeUtilsKt.getPopupKeyLabelSources(selectedSubtype, prefs);
|
||||
mAddToPersonalDictionary = prefs.getBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, Defaults.PREF_ADD_TO_PERSONAL_DICTIONARY);
|
||||
mUseContactsDictionary = SettingsValues.readUseContactsEnabled(prefs, context);
|
||||
mUseAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS);
|
||||
mCustomNavBarColor = prefs.getBoolean(Settings.PREF_NAVBAR_COLOR, Defaults.PREF_NAVBAR_COLOR);
|
||||
mNarrowKeyGaps = prefs.getBoolean(Settings.PREF_NARROW_KEY_GAPS, Defaults.PREF_NARROW_KEY_GAPS);
|
||||
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(
|
||||
|
@ -286,9 +283,9 @@ public class SettingsValues {
|
|||
mAlphaAfterNumpadAndSpace = prefs.getBoolean(Settings.PREF_ABC_AFTER_NUMPAD_SPACE, Defaults.PREF_ABC_AFTER_NUMPAD_SPACE);
|
||||
mRemoveRedundantPopups = prefs.getBoolean(Settings.PREF_REMOVE_REDUNDANT_POPUPS, Defaults.PREF_REMOVE_REDUNDANT_POPUPS);
|
||||
mSpaceBarText = prefs.getString(Settings.PREF_SPACE_BAR_TEXT, Defaults.PREF_SPACE_BAR_TEXT);
|
||||
mEmojiMaxSdk = prefs.getInt(Settings.PREF_EMOJI_MAX_SDK, Defaults.PREF_EMOJI_MAX_SDK);
|
||||
mFontSizeMultiplier = prefs.getFloat(Settings.PREF_FONT_SCALE, Defaults.PREF_FONT_SCALE);
|
||||
mFontSizeMultiplierEmoji = prefs.getFloat(Settings.PREF_EMOJI_FONT_SCALE, Defaults.PREF_EMOJI_FONT_SCALE);
|
||||
mEmojiKeyFit = prefs.getBoolean(Settings.PREF_EMOJI_KEY_FIT, Defaults.PREF_EMOJI_KEY_FIT);
|
||||
}
|
||||
|
||||
public boolean isApplicationSpecifiedCompletionsOn() {
|
||||
|
@ -352,11 +349,12 @@ public class SettingsValues {
|
|||
return mDisplayOrientation == configuration.orientation;
|
||||
}
|
||||
|
||||
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
|
||||
private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context ctx) {
|
||||
final boolean setting = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
|
||||
if (!setting) return false;
|
||||
if (PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
|
||||
if (PermissionsUtil.checkAllPermissionsGranted(ctx, Manifest.permission.READ_CONTACTS)) {
|
||||
return true;
|
||||
}
|
||||
// disable if permission not granted
|
||||
prefs.edit().putBoolean(Settings.PREF_USE_CONTACTS, false).apply();
|
||||
return false;
|
||||
|
|
|
@ -83,6 +83,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
|
|||
final SharedPreferences prefs = KtxKt.prefs(this);
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
onSharedPreferenceChanged(prefs, Settings.PREF_USE_CONTACTS);
|
||||
onSharedPreferenceChanged(prefs, Settings.PREF_USE_APPS);
|
||||
final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE);
|
||||
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false);
|
||||
}
|
||||
|
@ -93,13 +94,19 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
|
|||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||
if (Settings.PREF_USE_CONTACTS.equals(key)) {
|
||||
if (key != null) switch (key) {
|
||||
case Settings.PREF_USE_CONTACTS -> {
|
||||
final boolean useContactsDictionary = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
|
||||
mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary);
|
||||
} else if (Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE.equals(key)) {
|
||||
}
|
||||
case Settings.PREF_USE_APPS -> {
|
||||
final boolean useAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS);
|
||||
mDictionaryFacilitatorCache.setUseAppsDictionary(useAppsDictionary);
|
||||
}
|
||||
case Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE -> {
|
||||
final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE);
|
||||
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false);
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -373,7 +373,7 @@ final class SuggestionStripLayoutHelper {
|
|||
x += wordView.getMeasuredWidth();
|
||||
|
||||
if (SuggestionStripView.DEBUG_SUGGESTIONS) {
|
||||
layoutDebugInfo(positionInStrip, placerView, x);
|
||||
layoutDebugInfo(positionInStrip, placerView, (int) stripView.getX() + x);
|
||||
}
|
||||
}
|
||||
return startIndexOfMoreSuggestions;
|
||||
|
@ -436,8 +436,7 @@ final class SuggestionStripLayoutHelper {
|
|||
placerView.addView(debugInfoView);
|
||||
debugInfoView.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
final int infoWidth = debugInfoView.getMeasuredWidth();
|
||||
final int y = debugInfoView.getMeasuredHeight();
|
||||
ViewLayoutUtils.placeViewAt(debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight());
|
||||
ViewLayoutUtils.placeViewAt(debugInfoView, x - infoWidth, 0, infoWidth, debugInfoView.getMeasuredHeight());
|
||||
}
|
||||
|
||||
private int getSuggestionWidth(final int positionInStrip, final int maxWidth) {
|
||||
|
|
|
@ -14,12 +14,9 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
|
@ -214,10 +211,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
|
|||
mToolbarExpandKey.setOnClickListener(this);
|
||||
mToolbarExpandKey.setImageDrawable(Settings.getValues().mIncognitoModeEnabled ? mIncognitoIcon : mToolbarArrowIcon);
|
||||
colors.setColor(mToolbarExpandKey, ColorType.TOOL_BAR_EXPAND_KEY);
|
||||
mToolbarExpandKey.setBackground(new ShapeDrawable(new OvalShape())); // ShapeDrawable color is black, need src_atop filter
|
||||
mToolbarExpandKey.getBackground().setColorFilter(colors.get(ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND), PorterDuff.Mode.SRC_ATOP);
|
||||
mToolbarExpandKey.getLayoutParams().height *= 0.82; // shrink the whole key a little (drawable not affected)
|
||||
mToolbarExpandKey.getLayoutParams().width *= 0.82;
|
||||
colors.setColor(mToolbarExpandKey.getBackground(), ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND);
|
||||
|
||||
for (final ToolbarKey pinnedKey : ToolbarUtilsKt.getPinnedToolbarKeys(prefs)) {
|
||||
final ImageButton button = createToolbarKey(context, iconsSet, pinnedKey);
|
||||
|
@ -488,7 +482,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
|
|||
mSuggestedWords.mIsObsoleteSuggestions, mSuggestedWords.mInputStyle,
|
||||
mSuggestedWords.mSequenceNumber);
|
||||
mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions(
|
||||
getContext(), mSuggestedWords, mSuggestionsStrip, SuggestionStripView.this);
|
||||
getContext(), mSuggestedWords, mSuggestionsStrip, this);
|
||||
mStripVisibilityGroup.showSuggestionsStrip();
|
||||
// Show the toolbar if no suggestions are left and the "Auto show toolbar" setting is enabled
|
||||
if (mSuggestedWords.isEmpty() && Settings.getValues().mAutoShowToolbar){
|
||||
|
@ -707,6 +701,13 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
|
|||
mSuggestionsStrip.setVisibility(VISIBLE);
|
||||
mPinnedKeys.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (DEBUG_SUGGESTIONS) {
|
||||
for (var view : mDebugInfoViews) {
|
||||
view.setVisibility(mSuggestionsStrip.getVisibility());
|
||||
}
|
||||
}
|
||||
|
||||
mToolbarExpandKey.setScaleX((visible && !locked ? -1f : 1f) * mRtl);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
|
||||
package helium314.keyboard.latin.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
|
||||
|
||||
import helium314.keyboard.latin.define.DecoderSpecificConstants;
|
||||
import helium314.keyboard.latin.makedict.DictionaryHeader;
|
||||
import helium314.keyboard.latin.makedict.UnsupportedFormatException;
|
||||
import helium314.keyboard.latin.settings.SpacingAndPunctuations;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class encapsulates the logic for the Latin-IME side of dictionary information management.
|
||||
*/
|
||||
public class DictionaryInfoUtils {
|
||||
private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
|
||||
public static final String DEFAULT_MAIN_DICT = "main";
|
||||
public static final String USER_DICTIONARY_SUFFIX = "user.dict";
|
||||
public static final String MAIN_DICT_PREFIX = DEFAULT_MAIN_DICT + "_";
|
||||
// 6 digits - unicode is limited to 21 bits
|
||||
private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
|
||||
public static final String ASSETS_DICTIONARY_FOLDER = "dicts";
|
||||
public static final String ID_CATEGORY_SEPARATOR = ":";
|
||||
private static final String DICTIONARY_CATEGORY_SEPARATOR_EXPRESSION = "[" + ID_CATEGORY_SEPARATOR + "_]";
|
||||
|
||||
private DictionaryInfoUtils() {
|
||||
// Private constructor to forbid instantation of this helper class.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we may want to use this character as part of a file name.
|
||||
* <p>
|
||||
* This basically only accepts ascii letters and numbers, and rejects everything else.
|
||||
*/
|
||||
private static boolean isFileNameCharacter(int codePoint) {
|
||||
if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
|
||||
if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
|
||||
if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
|
||||
return codePoint == '_' || codePoint == '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for any characters that may be suspicious for a file or directory name.
|
||||
* <p>
|
||||
* Concretely this does a sort of URL-encoding except it will encode everything that's not
|
||||
* alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
|
||||
* we cannot allow here)
|
||||
*/
|
||||
// TODO: create a unit test for this method
|
||||
public static String replaceFileNameDangerousCharacters(final String name) {
|
||||
// This assumes '%' is fully available as a non-separator, normal
|
||||
// character in a file name. This is probably true for all file systems.
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final int nameLength = name.length();
|
||||
for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
|
||||
final int codePoint = name.codePointAt(i);
|
||||
if (DictionaryInfoUtils.isFileNameCharacter(codePoint)) {
|
||||
sb.appendCodePoint(codePoint);
|
||||
} else {
|
||||
sb.append(String.format(Locale.US, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x", codePoint));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the top level cache directory.
|
||||
*/
|
||||
public static String getWordListCacheDirectory(final Context context) {
|
||||
return context.getFilesDir() + File.separator + "dicts";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse escaping done by {@link #replaceFileNameDangerousCharacters(String)}.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getWordListIdFromFileName(@NonNull final String fname) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final int fnameLength = fname.length();
|
||||
for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
|
||||
final int codePoint = fname.codePointAt(i);
|
||||
if ('%' != codePoint) {
|
||||
sb.appendCodePoint(codePoint);
|
||||
} else {
|
||||
// + 1 to pass the % sign
|
||||
final int encodedCodePoint =
|
||||
Integer.parseInt(fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT), 16);
|
||||
i += MAX_HEX_DIGITS_FOR_CODEPOINT;
|
||||
sb.appendCodePoint(encodedCodePoint);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to the list of cache directories, one for each distinct locale.
|
||||
*/
|
||||
@Nullable public static File[] getCachedDirectoryList(final Context context) {
|
||||
return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out the cache directory associated with a specific locale.
|
||||
*/
|
||||
public static String getAndCreateCacheDirectoryForLocale(final Locale locale, final Context context) {
|
||||
final String absoluteDirectoryName = getCacheDirectoryForLocale(locale, context);
|
||||
final File directory = new File(absoluteDirectoryName);
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
Log.e(TAG, "Could not create the directory for locale" + locale);
|
||||
}
|
||||
}
|
||||
return absoluteDirectoryName;
|
||||
}
|
||||
|
||||
public static String getCacheDirectoryForLocale(final Locale locale, final Context context) {
|
||||
final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toLanguageTag());
|
||||
return getWordListCacheDirectory(context) + File.separator + relativeDirectoryName;
|
||||
}
|
||||
|
||||
public static File[] getCachedDictsForLocale(final Locale locale, final Context context) {
|
||||
final File cachedDir = new File(getAndCreateCacheDirectoryForLocale(locale, context));
|
||||
if (!cachedDir.isDirectory())
|
||||
return new File[]{};
|
||||
return cachedDir.listFiles();
|
||||
}
|
||||
|
||||
public static String getExtractedMainDictFilename() {
|
||||
return DEFAULT_MAIN_DICT + ".dict";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file,
|
||||
final long offset, final long length) {
|
||||
try {
|
||||
return BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length);
|
||||
} catch (UnsupportedFormatException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) {
|
||||
try {
|
||||
return BinaryDictionaryUtils.getHeader(file);
|
||||
} catch (UnsupportedFormatException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale for a dictionary file name stored in assets.
|
||||
* <p>
|
||||
* Assumes file name main_[locale].dict
|
||||
* <p>
|
||||
* Returns the locale, or null if file name does not match the pattern
|
||||
*/
|
||||
@Nullable public static String extractLocaleFromAssetsDictionaryFile(final String dictionaryFileName) {
|
||||
if (dictionaryFileName.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX)
|
||||
&& dictionaryFileName.endsWith(".dict")) {
|
||||
return dictionaryFileName.substring(
|
||||
DictionaryInfoUtils.MAIN_DICT_PREFIX.length(),
|
||||
dictionaryFileName.lastIndexOf('.')
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable public static String[] getAssetsDictionaryList(final Context context) {
|
||||
final String[] dictionaryList;
|
||||
try {
|
||||
dictionaryList = context.getAssets().list(ASSETS_DICTIONARY_FOLDER);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
return dictionaryList;
|
||||
}
|
||||
|
||||
public static boolean looksValidForDictionaryInsertion(final CharSequence text,
|
||||
final SpacingAndPunctuations spacingAndPunctuations) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return false;
|
||||
}
|
||||
final int length = text.length();
|
||||
if (length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
int i = 0;
|
||||
int digitCount = 0;
|
||||
while (i < length) {
|
||||
final int codePoint = Character.codePointAt(text, i);
|
||||
final int charCount = Character.charCount(codePoint);
|
||||
i += charCount;
|
||||
if (Character.isDigit(codePoint)) {
|
||||
// Count digits: see below
|
||||
digitCount += charCount;
|
||||
continue;
|
||||
}
|
||||
if (!spacingAndPunctuations.isWordCodePoint(codePoint)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// We reject strings entirely comprised of digits to avoid using PIN codes or credit
|
||||
// card numbers. It would come in handy for word prediction though; a good example is
|
||||
// when writing one's address where the street number is usually quite discriminative,
|
||||
// as well as the postal code.
|
||||
return digitCount < length;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
* modified
|
||||
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
||||
*/
|
||||
package helium314.keyboard.latin.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils
|
||||
import helium314.keyboard.latin.common.FileUtils
|
||||
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
|
||||
import helium314.keyboard.latin.common.loopOverCodePoints
|
||||
import helium314.keyboard.latin.define.DecoderSpecificConstants
|
||||
import helium314.keyboard.latin.makedict.DictionaryHeader
|
||||
import helium314.keyboard.latin.makedict.UnsupportedFormatException
|
||||
import helium314.keyboard.latin.settings.SpacingAndPunctuations
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
|
||||
/** encapsulates the logic for the Latin-IME side of dictionary information management */
|
||||
object DictionaryInfoUtils {
|
||||
private val TAG = DictionaryInfoUtils::class.java.simpleName
|
||||
const val DEFAULT_MAIN_DICT = "main"
|
||||
const val USER_DICTIONARY_SUFFIX = "user.dict"
|
||||
const val MAIN_DICT_PREFIX = DEFAULT_MAIN_DICT + "_"
|
||||
const val ASSETS_DICTIONARY_FOLDER = "dicts"
|
||||
const val MAIN_DICT_FILE_NAME = DEFAULT_MAIN_DICT + ".dict"
|
||||
private const val MAX_HEX_DIGITS_FOR_CODEPOINT = 6 // unicode is limited to 21 bits
|
||||
|
||||
/**
|
||||
* Returns whether we may want to use this character as part of a file name.
|
||||
* This basically only accepts ascii letters and numbers, and rejects everything else.
|
||||
*/
|
||||
private fun isFileNameCharacter(codePoint: Int): Boolean {
|
||||
if (codePoint in 0x30..0x39) return true // Digit
|
||||
if (codePoint in 0x41..0x5A) return true // Uppercase
|
||||
if (codePoint in 0x61..0x7A) return true // Lowercase
|
||||
return codePoint == '_'.code || codePoint == '-'.code
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for any characters that may be suspicious for a file or directory name.
|
||||
*
|
||||
* Concretely this does a sort of URL-encoding except it will encode everything that's not
|
||||
* alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
|
||||
* we cannot allow here)
|
||||
*/
|
||||
private fun replaceFileNameDangerousCharacters(name: String): String {
|
||||
// This assumes '%' is fully available as a non-separator, normal
|
||||
// character in a file name. This is probably true for all file systems.
|
||||
val sb = StringBuilder()
|
||||
loopOverCodePoints(name) { codePoint, _ ->
|
||||
if (isFileNameCharacter(codePoint)) {
|
||||
sb.appendCodePoint(codePoint)
|
||||
} else {
|
||||
sb.append(String.format(Locale.US, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x", codePoint))
|
||||
}
|
||||
false
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
// we cache the extracted dictionaries in filesDir, because actual cache might be cleared at
|
||||
// any time, and we can't permanently check whether the dictionary still exists
|
||||
fun getWordListCacheDirectory(context: Context): String = context.filesDir.toString() + File.separator + "dicts"
|
||||
|
||||
/** Reverse escaping done by replaceFileNameDangerousCharacters. */
|
||||
fun getWordListIdFromFileName(fname: String): String {
|
||||
val sb = StringBuilder()
|
||||
val fnameLength = fname.length
|
||||
var i = 0
|
||||
while (i < fnameLength) {
|
||||
val codePoint = fname.codePointAt(i)
|
||||
if ('%'.code != codePoint) {
|
||||
sb.appendCodePoint(codePoint)
|
||||
} else {
|
||||
// + 1 to pass the % sign
|
||||
val encodedCodePoint = fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT).toInt(16)
|
||||
i += MAX_HEX_DIGITS_FOR_CODEPOINT
|
||||
sb.appendCodePoint(encodedCodePoint)
|
||||
}
|
||||
i = fname.offsetByCodePoints(i, 1)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/** Helper method to the list of cache directories, one for each distinct locale. */
|
||||
fun getCacheDirectories(context: Context) = File(getWordListCacheDirectory(context)).listFiles().orEmpty()
|
||||
|
||||
/** Find out the cache directory associated with a specific locale. */
|
||||
fun getCacheDirectoryForLocale(locale: Locale, context: Context): String? {
|
||||
val relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toLanguageTag())
|
||||
val absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator + relativeDirectoryName
|
||||
val directory = File(absoluteDirectoryName)
|
||||
if (!directory.exists() && !directory.mkdirs()) {
|
||||
Log.e(TAG, "Could not create the directory for locale $locale")
|
||||
return null
|
||||
}
|
||||
return absoluteDirectoryName
|
||||
}
|
||||
|
||||
fun getCachedDictsForLocale(locale: Locale, context: Context) =
|
||||
getCacheDirectoryForLocale(locale, context)?.let { File(it).listFiles() }.orEmpty()
|
||||
|
||||
fun getDictionaryFileHeaderOrNull(file: File, offset: Long, length: Long): DictionaryHeader? {
|
||||
return try {
|
||||
BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length)
|
||||
} catch (e: UnsupportedFormatException) {
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDictionaryFileHeaderOrNull(file: File): DictionaryHeader? {
|
||||
return try {
|
||||
BinaryDictionaryUtils.getHeader(file)
|
||||
} catch (e: UnsupportedFormatException) {
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale for a dictionary file name stored in assets.
|
||||
*
|
||||
* Assumes file name main_[locale].dict
|
||||
* Returns the locale, or null if file name does not match the pattern
|
||||
*/
|
||||
fun extractLocaleFromAssetsDictionaryFile(dictionaryFileName: String): Locale {
|
||||
if (dictionaryFileName.contains('_') && !dictionaryFileName.contains('.'))
|
||||
throw IllegalStateException("invalid asset dictionary name $dictionaryFileName")
|
||||
return dictionaryFileName.substringAfter("_").substringBefore(".").constructLocale()
|
||||
}
|
||||
|
||||
// actually we could extract assets dicts to unprotected storage
|
||||
fun extractAssetsDictionary(dictionaryFileName: String, locale: Locale, context: Context): File? {
|
||||
val cacheDir = getCacheDirectoryForLocale(locale, context) ?: return null
|
||||
val targetFile = File(cacheDir, "${dictionaryFileName.substringBefore("_")}.dict")
|
||||
try {
|
||||
FileUtils.copyStreamToNewFile(
|
||||
context.assets.open(ASSETS_DICTIONARY_FOLDER + File.separator + dictionaryFileName),
|
||||
targetFile
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Could not extract assets dictionary $dictionaryFileName")
|
||||
return null
|
||||
}
|
||||
return targetFile
|
||||
}
|
||||
|
||||
fun getAssetsDictionaryList(context: Context): Array<String>? = try {
|
||||
context.assets.list(ASSETS_DICTIONARY_FOLDER)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun looksValidForDictionaryInsertion(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return false
|
||||
}
|
||||
if (text.length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) {
|
||||
return false
|
||||
}
|
||||
var digitCount = 0
|
||||
loopOverCodePoints(text) { codePoint, charCount ->
|
||||
if (Character.isDigit(codePoint)) {
|
||||
// Count digits: see below
|
||||
digitCount += charCount
|
||||
return@loopOverCodePoints false
|
||||
}
|
||||
if (!spacingAndPunctuations.isWordCodePoint(codePoint)) {
|
||||
return false
|
||||
}
|
||||
false
|
||||
}
|
||||
// We reject strings entirely comprised of digits to avoid using PIN codes or credit
|
||||
// card numbers. It would come in handy for word prediction though; a good example is
|
||||
// when writing one's address where the street number is usually quite discriminative,
|
||||
// as well as the postal code.
|
||||
return digitCount < text.length
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ fun getDictionaryLocales(context: Context): MutableSet<Locale> {
|
|||
val locales = HashSet<Locale>()
|
||||
|
||||
// get cached dictionaries: extracted or user-added dictionaries
|
||||
DictionaryInfoUtils.getCachedDirectoryList(context)?.forEach { directory ->
|
||||
DictionaryInfoUtils.getCacheDirectories(context).forEach { directory ->
|
||||
if (!directory.isDirectory) return@forEach
|
||||
if (!hasAnythingOtherThanExtractedMainDictionary(directory)) return@forEach
|
||||
val locale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name).constructLocale()
|
||||
|
@ -35,8 +35,7 @@ fun getDictionaryLocales(context: Context): MutableSet<Locale> {
|
|||
val assetsDictionaryList = DictionaryInfoUtils.getAssetsDictionaryList(context)
|
||||
if (assetsDictionaryList != null) {
|
||||
for (dictionary in assetsDictionaryList) {
|
||||
val locale = DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary)?.constructLocale() ?: continue
|
||||
locales.add(locale)
|
||||
locales.add(DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary))
|
||||
}
|
||||
}
|
||||
return locales
|
||||
|
@ -135,4 +134,4 @@ fun cleanUnusedMainDicts(context: Context) {
|
|||
}
|
||||
|
||||
private fun hasAnythingOtherThanExtractedMainDictionary(dir: File) =
|
||||
dir.listFiles()?.any { it.name != DictionaryInfoUtils.getExtractedMainDictFilename() } != false
|
||||
dir.listFiles()?.any { it.name != DictionaryInfoUtils.MAIN_DICT_FILE_NAME } != false
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.view.inputmethod.InputMethodSubtype
|
|||
import helium314.keyboard.latin.LatinIME
|
||||
import helium314.keyboard.latin.R
|
||||
import helium314.keyboard.latin.RichInputMethodManager
|
||||
import helium314.keyboard.latin.utils.SubtypeLocaleUtils.displayName
|
||||
|
||||
// similar to what showSubtypePicker does in https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/RichInputMethodManager.java
|
||||
fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodManager, windowToken: IBinder): AlertDialog {
|
||||
|
@ -26,7 +27,7 @@ fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodMa
|
|||
var currentSubtypeIndex = 0
|
||||
enabledImis.forEach { imi ->
|
||||
val subtypes = if (imi != thisImi) richImm.getEnabledInputMethodSubtypeList(imi, true)
|
||||
else richImm.getEnabledInputMethodSubtypeList(imi, true).sortedBy { it.displayName(latinIme).toString() }
|
||||
else richImm.getEnabledInputMethodSubtypeList(imi, true).sortedBy { it.displayName() }
|
||||
if (subtypes.isEmpty()) {
|
||||
enabledSubtypes.add(imi to null)
|
||||
} else {
|
||||
|
@ -44,7 +45,7 @@ fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodMa
|
|||
for (imiAndSubtype in enabledSubtypes) {
|
||||
val (imi, subtype) = imiAndSubtype
|
||||
|
||||
val subtypeName = if (imi == thisImi) subtype?.displayName(latinIme)
|
||||
val subtypeName = if (imi == thisImi) subtype?.displayName()
|
||||
else subtype?.getDisplayName(latinIme, imi.packageName, imi.serviceInfo.applicationInfo)
|
||||
val title = SpannableString(subtypeName?.ifBlank { imi.loadLabel(pm) } ?: imi.loadLabel(pm))
|
||||
val subtitle = SpannableString(if (subtype == null) "" else "\n${imi.loadLabel(pm)}")
|
||||
|
|
|
@ -90,7 +90,7 @@ public final class JniUtils {
|
|||
System.loadLibrary(JNI_LIB_NAME_GOOGLE);
|
||||
sHaveGestureLib = true;
|
||||
} catch (UnsatisfiedLinkError ul) {
|
||||
Log.w(TAG, "Could not load system glide typing library " + JNI_LIB_NAME_GOOGLE, ul);
|
||||
Log.w(TAG, "Could not load system glide typing library " + JNI_LIB_NAME_GOOGLE + ": " + ul.getMessage());
|
||||
}
|
||||
}
|
||||
if (!sHaveGestureLib) {
|
||||
|
|
|
@ -43,16 +43,12 @@ public final class LanguageOnSpacebarUtils {
|
|||
return FORMAT_TYPE_NONE;
|
||||
}
|
||||
final Locale locale = subtype.getLocale();
|
||||
if (locale == null) {
|
||||
return FORMAT_TYPE_NONE;
|
||||
}
|
||||
final String keyboardLanguage = locale.getLanguage();
|
||||
final String keyboardLayout = subtype.getMainLayoutName();
|
||||
int sameLanguageAndLayoutCount = 0;
|
||||
for (final InputMethodSubtype ims : sEnabledSubtypes) {
|
||||
final String language = SubtypeUtilsKt.locale(ims).getLanguage();
|
||||
if (keyboardLanguage.equals(language) && keyboardLayout.equals(
|
||||
SubtypeLocaleUtils.getMainLayoutName(ims))) {
|
||||
if (keyboardLanguage.equals(language) && keyboardLayout.equals(SubtypeUtilsKt.mainLayoutNameOrQwerty(ims))) {
|
||||
sameLanguageAndLayoutCount++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ enum class LayoutType {
|
|||
companion object {
|
||||
fun EnumMap<LayoutType, String>.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY)
|
||||
|
||||
fun getLayoutMap(string: String): EnumMap<LayoutType, String> {
|
||||
fun getLayoutMap(string: String?): EnumMap<LayoutType, String> {
|
||||
val map = EnumMap<LayoutType, String>(LayoutType::class.java)
|
||||
string.split(Separators.ENTRY).forEach {
|
||||
string?.split(Separators.ENTRY)?.forEach {
|
||||
val s = it.split(Separators.KV)
|
||||
runCatching { map[LayoutType.valueOf(s[0])] = s[1] }
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ object LayoutUtils {
|
|||
return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty()
|
||||
if (locale == null)
|
||||
return SubtypeSettings.getAllAvailableSubtypes()
|
||||
.mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
|
||||
.mapTo(HashSet()) { it.mainLayoutNameOrQwerty().substringBefore("+") }
|
||||
.apply { addAll(context.resources.getStringArray(R.array.predefined_layouts)) }
|
||||
val layouts = SubtypeSettings.getResourceSubtypesForLocale(locale).mapNotNullTo(mutableSetOf()) { it.mainLayoutName() }
|
||||
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
package helium314.keyboard.latin.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import helium314.keyboard.keyboard.Key
|
||||
import helium314.keyboard.keyboard.KeyboardId
|
||||
import helium314.keyboard.keyboard.KeyboardLayoutSet
|
||||
|
@ -10,9 +11,14 @@ import helium314.keyboard.keyboard.internal.KeyboardParams
|
|||
import helium314.keyboard.keyboard.internal.keyboard_parser.LayoutParser
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
|
||||
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
|
||||
import helium314.keyboard.latin.common.Constants.Separators
|
||||
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
|
||||
import helium314.keyboard.latin.common.decodeBase36
|
||||
import helium314.keyboard.latin.common.encodeBase36
|
||||
import helium314.keyboard.latin.define.DebugFlags
|
||||
import helium314.keyboard.latin.settings.Defaults
|
||||
import helium314.keyboard.latin.settings.Settings
|
||||
import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
|
||||
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
|
||||
import helium314.keyboard.latin.utils.ScriptUtils.script
|
||||
import kotlinx.serialization.SerializationException
|
||||
|
@ -30,11 +36,13 @@ object LayoutUtilsCustom {
|
|||
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
|
||||
addLocaleKeyTextsToParams(context, params, POPUP_KEYS_NORMAL)
|
||||
try {
|
||||
val keys = LayoutParser.parseJsonString(layoutContent).map { row -> row.mapNotNull { it.compute(params)?.toKeyParams(params) } }
|
||||
return checkKeys(keys)
|
||||
if (layoutContent.trimStart().startsWith("[") || layoutContent.trimStart().startsWith("//")) {
|
||||
val keys = LayoutParser.parseJsonString(layoutContent).map { row -> row.mapNotNull { it.compute(params)?.toKeyParams(params) } }
|
||||
return checkKeys(keys)
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
Log.w(TAG, "json parsing error", e)
|
||||
if (layoutContent.trimStart().startsWith("[") && layoutContent.trimEnd().endsWith("]") && layoutContent.contains("},"))
|
||||
if (layoutContent.trimEnd().endsWith("]") && layoutContent.contains("},"))
|
||||
return false // we're sure enough it's a json
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "json layout parsed, but considered invalid", e)
|
||||
|
@ -124,7 +132,6 @@ object LayoutUtilsCustom {
|
|||
layoutName
|
||||
}
|
||||
|
||||
|
||||
/** @return layoutName for given [displayName]. If [layoutType ]is MAIN, non-null [locale] must be supplied */
|
||||
fun getLayoutName(displayName: String, layoutType: LayoutType, locale: Locale? = null): String {
|
||||
if (layoutType != LayoutType.MAIN)
|
||||
|
@ -143,6 +150,37 @@ object LayoutUtilsCustom {
|
|||
return file
|
||||
}
|
||||
|
||||
// remove layouts without a layout file from custom subtypes and settings
|
||||
// should not be necessary, but better fall back to default instead of crashing when encountering a bug
|
||||
fun removeMissingLayouts(context: Context) {
|
||||
val prefs = context.prefs()
|
||||
fun remove(type: LayoutType, name: String) {
|
||||
val message = "removing custom layout ${getDisplayName(name)} / $name without file"
|
||||
if (DebugFlags.DEBUG_ENABLED)
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
Log.w(TAG, message)
|
||||
SubtypeSettings.onRenameLayout(type, name, null, context)
|
||||
}
|
||||
LayoutType.entries.forEach { type ->
|
||||
val name = Settings.readDefaultLayoutName(type, prefs)
|
||||
if (!isCustomLayout(name) || getLayoutFiles(type, context).any { it.name.startsWith(name) })
|
||||
return@forEach
|
||||
remove(type, name)
|
||||
}
|
||||
prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
|
||||
.split(Separators.SETS).forEach outer@{
|
||||
val subtype = it.toSettingsSubtype()
|
||||
LayoutType.getLayoutMap(subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET) ?: "").forEach { (type, name) ->
|
||||
if (!isCustomLayout(name) || getLayoutFiles(type, context).any { it.name.startsWith(name) })
|
||||
return@forEach
|
||||
remove(type, name)
|
||||
// recursive call: additional subtypes must have changed, so we repeat until nothing needs to be deleted
|
||||
removeMissingLayouts(context)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this goes into prefs and file names, so do not change!
|
||||
const val CUSTOM_LAYOUT_PREFIX = "custom."
|
||||
private const val TAG = "LayoutUtilsCustom"
|
||||
|
|
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