Compare commits

...

194 commits

Author SHA1 Message Date
Eran Leshem
d2217f099a
Export DictionaryPackInstallBroadcastReceiver (#1756) 2025-06-29 20:02:29 +02:00
Eran Leshem
4f58e5d013
Support Latin (#1749) 2025-06-28 16:25:15 +02:00
Helium314
10a5eab3bc don't change insets when view is hidden by hardware keyboard
seems to happen only in some apps, e.g. simplemobiletools notes
issue only occurs when keyboard is suppressed after it has been shown and a new input view is created

fixes GH-1455
fixes GH-702
2025-06-28 16:20:54 +02:00
Helium314
8d876a15f0 fix crash in isShowingKeyboardId with hardware keyboard 2025-06-28 15:16:25 +02:00
Helium314
945a700b71 do the correct checks when loading blacklist file
fixes GH-1475
2025-06-28 14:11:00 +02:00
Helium314
53a899794e register OnSharedPreferenceChangeListener in LatinIME.onCreate
because it's unregistered in onDestroy
(registering twice is not an issue)

though maybe not unregistering would be more correct, as it's registered in app.onCreate?

fixes GH-1670
2025-06-24 06:27:51 +02:00
Eran Leshem
c2068224a0
Change settings search keyboard action to Search. (#1734) 2025-06-22 12:23:46 +02:00
Eran Leshem
9193c95c2b
Ignore spacer columns when calculating emoji page size (#1721) 2025-06-22 12:18:37 +02:00
Helium314
76ebf99921 deal with Android refusing to add word to user dictionary
fixes GH-1735
2025-06-22 12:11:20 +02:00
Helium314
77a728e390 don't pre-select unavailable locale when adding dictionary 2025-06-19 09:35:08 +02:00
Helium314
7b0c511857 add logging when hiding window
may help with GH-1710
2025-06-16 19:43:32 +02:00
Eran Leshem
e062efb3d4
Always default popup VisibleOffset to keyboard.mVerticalGap (#1722) 2025-06-16 19:14:47 +02:00
lurebat
d356f9f54b
Add broadcast intent keys (#1675)
This commit introduces three new keycodes: SEND_INTENT_ONE, SEND_INTENT_TWO, and SEND_INTENT_THREE.

When these keys are pressed, a broadcast intent is sent with the action `helium314.keyboard.latin.ACTION_SEND_INTENT`. The intent includes an extra `EXTRA_NUMBER` (integer) indicating which of the three keys was pressed (1, 2, or 3).

This functionality allows external applications to react to these specific key presses.
2025-06-16 18:55:15 +02:00
Helium314
9549389be7 use different method for hiding keyboard
fixes GH-1719
2025-06-15 21:09:53 +02:00
Helium314
2dc838798d use hasLabels for TLD popups
and add documentation
2025-06-15 19:49:32 +02:00
Helium314
24a2eddc1f avoid StringIndexOutOfBoundsException when initializing stringbuilder from other stringbuilder 2025-06-15 17:43:06 +02:00
Helium314
ef3191a2eb prepare for selecting text with cursor movement keys when shift is enabled manually
not enabled, as it's not working reliably
2025-06-15 17:41:05 +02:00
Helium314
e430d13c4a improvements to hardware keyboard handling 2025-06-14 12:53:46 +02:00
Helium314
9c97a6b9bf move some key press logic from LatinIme into KeyboardActionListener 2025-06-14 12:16:29 +02:00
Helium314
a37de668c0 enable hardware keyboard handling again, with improved handling 2025-06-14 11:29:26 +02:00
Helium314
63dad1549e redirect suggestion strip view code input to KeyboardActionListener 2025-06-13 20:57:49 +02:00
Helium314
f06a553d2c make sure selection start is before end
this should not be necessary according to documentation, but apparently there are apps (even by Google!) that can't deal with documented behavior
see comments in GH-1512
2025-06-13 20:49:53 +02:00
Eran Leshem
e9e3bdac17
Display emoji descriptions in popups (#1542) 2025-06-11 22:17:26 +02:00
Eran Leshem
d5cd18ecaa
Remove regional indicator symbol letters from emoji list (#1680) 2025-06-11 22:17:05 +02:00
Eran Leshem
871ac110ad
Move non-com global TLDs to end of list (#1668) 2025-06-11 22:16:39 +02:00
Helium314
49c9d77978 try more aggressive proguard settings 2025-06-11 22:15:49 +02:00
Eran Leshem
79726f1a9d
Fix onInlineSuggestionsResponse crash (#1700) 2025-06-11 21:59:36 +02:00
Helium314
f2ec441f45 update version and translations 2025-06-11 21:57:06 +02:00
Eran Leshem
62f82d15cf
Show incognito button in all toolbar modes (#1681) 2025-06-11 21:43:04 +02:00
Eran Leshem
f8d3795302
Disable emoji view animation according to system setting (#1696) 2025-06-11 21:29:06 +02:00
Helium314
9cec401e1e don't add single letter words with AddToPersonalDictionary setting
fixes GH-1605
2025-06-11 20:01:59 +02:00
Helium314
83ff9b3345 bring up keyboard when adding a new word in personal dictionary screen
fixes GH-1663
2025-06-11 19:43:42 +02:00
Eran Leshem
8ae241b032
Only show some suggestion settings if toolbar is in compatible mode. (#1693) 2025-06-11 17:24:40 +02:00
Helium314
80ba394b95 move some code from RichInputConnection to StringUtils
so we can easily add unit tests
and maybe improve the awkward behavior
2025-06-09 20:20:27 +02:00
Helium314
52744b7427 full reload after changing icon style
should fix GH-1686
2025-06-09 16:05:41 +02:00
Helium314
af5c41c83c add NO_LANGUAGE inside remember
should fix GH-1678
2025-06-08 21:42:33 +02:00
Helium314
e21168b1d3 merge labelFlags when when replacing comma & period
so e.g. dvorak z key doesn't get .com popup hint by default
2025-06-08 11:40:38 +02:00
Helium314
11f45a6209 show PREF_EMOJI_KEY_FIT setting only if PREF_EMOJI_FONT_SCALE is not 100% 2025-06-08 11:26:23 +02:00
Helium314
c8322dd4a2 make KeyCode.IME_HIDE_UI work
fixes GH-1272
2025-06-08 11:16:00 +02:00
Helium314
896e207c5f remove the fallback to original key type when replacing comma & period
relevant e.g. for dvorak in url fields, where otherwise the q has functional key background
2025-06-08 11:07:51 +02:00
Helium314
5cd184e5c2 upgrade version 2025-06-07 23:04:18 +02:00
fakerat
e98c3210dc
fix forced dark mode issue in android 10 or later (#1671) 2025-06-07 23:03:21 +02:00
Helium314
7cec6b148c remove en-rXC files 2025-06-07 22:35:40 +02:00
Helium314
bd85498810 remove useless translations of strings marked as not translatable (because they reference other strings) 2025-06-07 22:31:48 +02:00
Md. Rifat Hasan Jihan
51a863d840
remove non-translatable string (#1651) 2025-06-07 21:59:12 +02:00
Helium314
defec4a27f update translations 2025-06-07 21:58:25 +02:00
Helium314
ccc287c4ea use CombiningRules extra value for setting the combiner
allows flexibility which is needed for the combiner suggested in GH-214
2025-06-07 21:44:14 +02:00
Helium314
48f6c21b57 fix recognition of our own inputMethod 2025-06-07 21:01:48 +02:00
Eran Leshem
9efe534c03
Fix locale_key_texts loading for he locale (#1669) 2025-06-07 20:38:38 +02:00
Eran Leshem
16ce183942
Fix NextScreenIcon direction for RTL system languages (#1667) 2025-06-07 20:34:47 +02:00
Helium314
867438fdc0 simplify the way of setting suggestions
remove the excursion to latinIME
have Suggest return suggestions instead of calling a callback
now the relevant code can be mostly found in inputLogic (except for handling batch input)
2025-06-07 20:26:06 +02:00
BlackyHawky
0787a79de4
Slightly adjust the dictionary dialog (#1656) 2025-06-03 19:27:01 +02:00
Helium314
97aec851e4 avoid dictionary re-ordering on UI interaction 2025-06-02 20:41:52 +02:00
Helium314
7dbf5ea86d
Add MultiSliderDialog (#1580) 2025-06-02 20:33:10 +02:00
Helium314
d951a1dbdd don't try to back up directories matching the file name patterns
fixes #1653
2025-06-02 17:36:06 +02:00
BlackyHawky
20d4704090
Fix unnecessary indentations in the Subtype screen (#1516) (#1648) 2025-06-02 17:12:11 +02:00
Helium314
d0787201ae get rid of warnings in SuggestionStripView
and add a comment
2025-06-01 19:53:56 +02:00
Helium314
0a961a68db consider that voice input key visibility should depend on the text field 2025-06-01 19:17:44 +02:00
Helium314
7499c38e13 add missing key codes to Key.isModifier, and move the information into KeyCode 2025-06-01 18:40:54 +02:00
Helium314
f81f6a7f7d some additional safety, so we show at least a working keyboard in case dictionary or native library loading fails 2025-06-01 17:03:16 +02:00
Helium314
07ea14ea16 move code related to showing more suggestions into MoreSuggestionsView 2025-06-01 16:38:59 +02:00
Helium314
32099748e5 move PopupSuggestionsView to Kotlin
and rename to MoreSuggestionsView
2025-06-01 13:36:14 +02:00
Helium314
538a26a9b8 no need to provide KeyboardIconsSet when creating a toolbar key 2025-06-01 13:21:03 +02:00
Helium314
8284adb32c small cleanup in ClipbardHistoryView 2025-06-01 13:19:38 +02:00
Helium314
180bd179c5 move device locked check to common place 2025-06-01 13:13:00 +02:00
Helium314
12c1cb0cd2 move SuggestionStripView to Kotlin 2025-06-01 13:06:14 +02:00
Eran Leshem
ec2bbb461d
Add toolbar modes (#1606) 2025-06-01 09:50:55 +02:00
Helium314
38bbcd9a83 add editorconfig, udpate gitignore 2025-06-01 09:50:24 +02:00
Helium314
8a55f57ccf decrease minimum height scale to 30%
resolves GH-1647
2025-05-31 20:46:32 +02:00
Helium314
e01714bbee mark string as not translatable 2025-05-31 20:39:11 +02:00
Helium314
3fcea486be gather crash information more aggressively
especially for crashes while device is locked
might help with #801
2025-05-31 20:30:18 +02:00
Helium314
e4e99a7e5d fix NPE related to recent RichIMM changes 2025-05-31 14:28:02 +02:00
Helium314
c321c2c684 replace AsyncTask 2025-05-31 14:01:36 +02:00
Helium314
7dc6279d87 todos after RichIMM overhaul 2025-05-31 13:56:02 +02:00
Helium314
ad71b49c04 make language slide work with fallback subtypes 2025-05-31 13:31:22 +02:00
Helium314
09eecc0502 move RichInputMethodManager to Kotlin 2025-05-31 13:22:22 +02:00
Helium314
424df5fb0d fix file handling in DictionaryGroup (again) 2025-05-31 11:59:05 +02:00
Eran Leshem
b903e10b12
Add some missing emoji variants (#1622) 2025-05-31 10:44:46 +02:00
Helium314
e6a412750d update version and changelogs 2025-05-31 10:16:54 +02:00
Helium314
7b91221339 update translations, fix broken string references escaping the @ in @string/ 2025-05-31 10:06:03 +02:00
Helium314
f0d4aaa9c3 update reading dictionaries available for download 2025-05-31 09:58:29 +02:00
Helium314
0e86978df3 different way of implementing larger toolbar key
see GH-1556, fixes GH-1621
2025-05-31 09:35:33 +02:00
Eran Leshem
82bea7facf
Don't show dictionary debug info on toolbar (#1644) 2025-05-31 08:00:00 +02:00
Eran Leshem
4d441e5bdf
Fix partial emoji swiping (#1645) 2025-05-30 22:49:37 +02:00
Eran Leshem
92b1907c61
Drop non-emoji single character suggestions from emoji dictionary (#1643) 2025-05-30 22:40:52 +02:00
Helium314
37821ff8ad use old way of getting main dictionary suggestions on facilitator thread
apparently this fixes GH-1614 (spurious crash in the native google library on Android 7)
though maybe the reason is a difference between ExecutorUtils and coroutines?
2025-05-30 20:35:32 +02:00
Helium314
dbeddcd658 don't show tld hint label in numpad
see #1640
2025-05-30 20:19:26 +02:00
Helium314
31a8761bfa don't always use the same coordinate when creating fake ComposedData for a word
avoids some sort of cache returning previous suggestions, see GH-1542
2025-05-29 09:32:12 +02:00
Helium314
120734ff41 avoid issues when resetting DictionaryFacilitatorImpl
see GH-801 (probably does not fix the OnePlus issue)
2025-05-28 22:31:18 +02:00
Helium314
0d5159c2d7 comment currently unsupported keycodes to avoid confusion 2025-05-27 20:25:05 +02:00
Helium314
b44dd29b0c consider implicitly enabled subtypes when switching with language switch button
fixes GH-1608
2025-05-27 18:14:05 +02:00
Helium314
7da068145f fix internal dictionaty file names
resulted in assets dicts being extracted on every app start
2025-05-25 16:52:56 +02:00
Helium314
deec4d1f98 upgrade version, add changelog, fix lint issues with strings 2025-05-24 18:23:29 +02:00
Helium314
e21c135b90 don't include default latin non-qwerty layout in subtype display name
so now we're back at "French" and not "French (AZERTY)"
2025-05-24 16:50:51 +02:00
Helium314
154f7c3a1e don't create unnecessary additional subtypes when manually selecting qwerty 2025-05-24 16:46:18 +02:00
Helium314
ed540466e9 remove the xliff tags from stings
caused some issues with weblate, and meaning of %s is now in comment
2025-05-24 16:04:56 +02:00
Helium314
05ea8b7f76 remove unused strings (and rename a string) 2025-05-24 15:43:44 +02:00
Helium314
bf7e0542f5 remove the explicit "alphabet" from no_language (layout is more visible than previously) 2025-05-24 15:09:22 +02:00
Helium314
c4e7c84608 remove complicated and obsolete generic ubtype_with_layout_ and subtype_no_language_ strings
and simplify the place where they were used
2025-05-24 14:56:58 +02:00
Helium314
f72e8f41f4 make SubtypeLocaleUtils less convoluted
still harder to understand than necessary...
2025-05-24 12:45:36 +02:00
Helium314
69540b8d9f improve localizedDisplayName
don't use resoruces.getIdentifier
use override names e.g. for English (UK) for consistent layout names
merge with getLocaleDisplayNameInLocale
2025-05-24 11:35:58 +02:00
Helium314
d1120807d3 move InputMethodSubtype.displayName to SubtypeLocaleUtils 2025-05-24 09:29:11 +02:00
Helium314
9dbce40fd7 remove essentially duplicate functions 2025-05-24 09:14:35 +02:00
Helium314
82e6d8a5cb move SubtypeLocaleUtils to Kotlin 2025-05-24 08:25:18 +02:00
Bernardo do Amaral Teodosio
175b5ea197
Fix Wrong "\" in pt-BR translations for the onboarding (#1581) 2025-05-24 07:09:11 +02:00
Helium314
917edee918 fix obviously broken links in description
fixes GH-1568
2025-05-24 07:04:49 +02:00
Helium314
ead8fb36cb update translations 2025-05-24 07:01:05 +02:00
Helium314
3f51bd4da5 tune layout edit dialog 2025-05-23 21:25:37 +02:00
Helium314
7bc74810b1 fix broken file handling in DictionaryGroup 2025-05-23 20:42:52 +02:00
Eran Leshem
e25300d832
Add comma to the repurposed comma key's popup in URL and email mode (#1594) 2025-05-21 22:48:33 +02:00
Helium314
e034065236 add simple DicitonaryFacilitator for a single dictionary (to be used later) 2025-05-21 22:43:01 +02:00
Helium314
954a27b7c9 refactor creation of main dictionary 2025-05-21 22:13:53 +02:00
Helium314
b1b357d6b8 move DictionaryInfoUtils to Kotlin 2025-05-21 21:02:16 +02:00
Helium314
e32a0c8e98 move ComposedData to Kotlin
and add method to create ComposedData for a given word
2025-05-21 18:13:58 +02:00
Helium314
d9a779a66e update comments and documentation
currently irrelevant changes to dump methods
2025-05-21 17:35:24 +02:00
Helium314
18549151b3 add color for popup key icons
fixes GH-1577
2025-05-20 23:05:01 +02:00
Eran Leshem
9d38471f72
Enlarge toolbar button and icon (#1556) 2025-05-20 22:57:02 +02:00
Helium314
4289e487e9 prepare for adding customizable weights for user-provided dictionaries 2025-05-20 22:17:10 +02:00
Helium314
27a2300631 remove unused "account" 2025-05-20 21:32:23 +02:00
Helium314
900dfa1b9c no need for special lock object 2025-05-20 21:18:38 +02:00
Helium314
9709c0d0a2 more detailed comment on feature 2025-05-20 21:17:08 +02:00
Helium314
960f058b7e move DictionaryFacilitatoryImpl to Kotlin
only very minor changes to behavior
using coroutines instead of ExecutorUtils
some code moved out of "main" facilitator
2025-05-20 20:44:57 +02:00
Helium314
4ecf185431 add label for timestamp key 2025-05-18 22:03:04 +02:00
Helium314
e45f0660a2 process label for simple popup keys
now toolbar keys and defined key labels work in simple layout popup keys
2025-05-18 21:52:25 +02:00
Helium314
e7ccf72fc5 reduce unnecessary json parse attempts when editing layouts 2025-05-18 21:32:53 +02:00
Helium314
e154001d44 fix broken links, remove outdated information 2025-05-18 20:48:28 +02:00
Henré Botha
44558ceeaa
Fix broken internal link (#1574) 2025-05-18 20:44:23 +02:00
Helium314
aa8068b5d2 functional key background now also sets functional key text color
for consistency, fixes GH-1576
2025-05-18 20:15:16 +02:00
Helium314
466ecfb78c reload keyboard theme when changing number row setting
otherwise emoji keyboards are not properly reloaded and may end up misaligned
2025-05-18 19:20:07 +02:00
Eran Leshem
731c6cdd5e
Allow for switching between emoji categories using swipe left/right (#1488) 2025-05-18 19:18:23 +02:00
Helium314
199f177c2d add left/right variants of alt, ctrl and meta
for picky apps, fixes GH-1579
2025-05-18 15:25:10 +02:00
Helium314
66c3dd7a81 use correct checksum 2025-05-17 20:33:31 +02:00
Helium314
9c9fe392d1 fix dealing with prases without non-whitespace letters in SpacedTokens 2025-05-17 16:56:22 +02:00
Helium314
c33c2c5823 remove test logging 2025-05-17 09:54:27 +02:00
Helium314
4d91702073 add setting for default emoji skin tone
fixes GH-817
2025-05-16 21:48:07 +02:00
Helium314
a0f77c1392 don't suggest unsuported emojis
and invert isSupported to isUnsupported to avoid confusion what happens when asking about non-emojis
2025-05-16 21:20:34 +02:00
Helium314
35df3e7bae upgrade dependencies 2025-05-16 21:09:16 +02:00
Helium314
f48438f30a use SpacedTokens instead of regex for splitting on whitespace
and where possible directly used SpacedTokens to avoid unnecessary list
2025-05-14 17:04:31 +02:00
Helium314
c96eec601d remove unused code / comments in EmojiPalettesView and related resources 2025-05-14 16:49:37 +02:00
Devy Ballard
c9059f3616
Apps dictionary (#1361) 2025-05-14 16:41:50 +02:00
Devy Ballard
2fe87eea9b
Inline code point loops (#1408) 2025-05-14 16:24:21 +02:00
Eran Leshem
c4386df186
Display a toast after restoring a backup. Seems like useful feedback. (#1531) 2025-05-14 16:23:39 +02:00
Helium314
95d4bfe97c
Update managing emoji categories (#1515) 2025-05-14 16:22:50 +02:00
Helium314
880c7eaf33 update version and translations 2025-05-14 16:11:52 +02:00
Helium314
b5837c3380 try saving crash logs when device is locked 2025-05-14 15:58:24 +02:00
Helium314
e6ec1c7bca fix debug mode crash sometimes occurring when deleting korean
fixes GH-1551
2025-05-10 17:37:20 +02:00
Helium314
549675d8d7 change query for other IME packages
should fix GH-1340
2025-05-10 15:50:58 +02:00
Helium314
a1e05c847e no need for full stack trace when system gesture lib is not found 2025-05-10 09:27:25 +02:00
Eran Leshem
1f8a94f219
Adjust emoji key size according to emoji font size (#1543) 2025-05-09 16:49:23 +02:00
Helium314
3c36033acb add more information to exception when InputMethodInfo is not found
apparently this randomly happens, see GH-1521
2025-05-09 05:16:22 +02:00
Helium314
97db67d7eb increase contacts dictionary frequency
because contacts rarely got suggested, see GH-1361
2025-05-07 18:23:47 +02:00
Helium314
a3dff524cb avoid problems with hacky way of saving AllColors theme name
see GH-1528
2025-05-06 18:58:14 +02:00
Helium314
366ee5ae28 workaround for page start / page end toolbar keys not working in compose text fields
fixes GH-1477
2025-05-04 20:33:41 +02:00
Helium314
91b177d204 update version, translations and dependencies 2025-05-03 07:52:16 +02:00
Eran Leshem
4f356086d7
Fix direction of word-left & word-right with RTL scripts (#1530) 2025-05-03 07:02:38 +02:00
Eran Leshem
60a5fe1e03
Go back to setFitsSystemWindows, with an added workaround that seems to make it reliable. (#1536) 2025-05-02 23:11:58 +02:00
Helium314
d8bf27f180 add missing items in v31 dialog theme
resolves GH-1518
2025-05-02 05:35:27 +02:00
Helium314
2a7ac3cf79 use default font for titles 2025-05-01 20:27:56 +02:00
Eran Leshem
875491a0e1
Recognize the he language code as Hebrew (#1529)
in addition to outdated `iw` that is used on Android 14 and older
2025-05-01 20:17:41 +02:00
Helium314
9f06394a1a consistent indent 2025-04-29 19:36:47 +02:00
Froingo
ad375cc3a3
Add korean phonetic layout (#1500) 2025-04-29 19:32:03 +02:00
Helium314
011bc96ec9 consider padding when adding keyboard split
fixes GH-1520
2025-04-29 16:17:28 +02:00
Helium314
da62457c90 save logs from the internal logger again, but keep logcat for getting older warning and error log entries 2025-04-28 21:20:32 +02:00
Helium314
38547b0c81 fallback to default layouts on parsing errors 2025-04-28 19:50:31 +02:00
Helium314
8b36ff1c54 skip early on empty hint labels 2025-04-28 19:25:28 +02:00
Eran Leshem
5eff3b992b
Restore Key.LABEL_FLAGS_PRESERVE_CASE, to fix uppercase TLDs in popup (#1517) 2025-04-28 14:27:36 +02:00
Helium314
b5dece2ff4 improve behvavior of slider preference
using onConfirm instead of always onValueChange (which is called when sliding, but we're mostly interested in changed settings)
2025-04-28 13:56:36 +02:00
Helium314
1d441a8ca6 more workarounds for Korean
not just space might delete text, but also other separators
now this problem is avoided in a more generic way
fixes GH-1447
2025-04-27 18:59:29 +02:00
Eran Leshem
9c727f342d
Remove TLD popup hint (#1511)
Co-authored-by: Helium314 <helium314@mailbox.org>
2025-04-27 16:19:26 +02:00
Eran Leshem
bedb9d1517
Fix settings search insets by moving top & horizontal insets to the top of the setting screen. (#1510) 2025-04-27 16:03:18 +02:00
Helium314
54c2c364a0 close background image dialog when deleting image 2025-04-27 15:44:59 +02:00
Helium314
106a74d749 upgrade version and translations 2025-04-26 13:55:40 +02:00
Helium314
322f8f9712 also consider default layouts when removing custom layouts without file
was missing in d15a97ccba
also rearrange the code a little
fixes GH-1490
2025-04-26 13:24:10 +02:00
Eran Leshem
6d9f69a4b6
Fix keyboard insets on Android 15, again (#1497) 2025-04-26 07:34:27 +02:00
Gabriele Monaco
14b5439a97
Use the euro sign for PMS (#1504) 2025-04-25 20:04:05 +02:00
Eran Leshem
01c0cd9de2
Saving log using logcat (#1487)
Old logger will be removed later, see discussion in PR
2025-04-25 17:19:58 +02:00
Helium314
e60efba59d remove some unnecessary code
looks like with edge to edge the status bar color is set automatically
also slightly rearrange code for showing welcome wizard and crash report dialog
2025-04-25 17:03:34 +02:00
Eran Leshem
5b32118b08
Add title to spell checker settings (#1501) 2025-04-25 16:53:22 +02:00
Eran Leshem
69bcca0a22
Apply padding on all sides on settings screens (#1496) 2025-04-22 22:29:03 +02:00
Helium314
49ed863a7e upgrade version and translations 2025-04-21 10:23:49 +02:00
Helium314
46f9227615 properly reload emoji keyboards (fixes split layout change not shown) 2025-04-21 09:48:22 +02:00
Helium314
7e59bcc799 remove weird workaround to get acceptable layout edit dialog positioning
not necessary any more after enabling edge to edge
2025-04-21 08:29:50 +02:00
Eran Leshem
d9f17733d9
Make settings screens fully usable on Android 15 (#1484)
enable edge to edge in settings for all Android versions to avoid minor glitches and have more consistent appearance
2025-04-21 08:06:41 +02:00
Helium314
d15a97ccba consider all layout types when checking for custom layouts without file
fixes #1490
2025-04-21 07:37:42 +02:00
Helium314
da7ab05920 remove old upgrade functionality done for migration from previous package name
should be fine after more than a year of HeliBoard
fixes issue when restoring custom layouts

upgrade now would need to be OpenBoard -> HeliBoard <= 2.3 -> HeliBoard >= 3.0
2025-04-21 07:33:58 +02:00
Helium314
d87ed8e53d fix pattern for files to back up
see #1490
2025-04-21 06:03:32 +02:00
Eran Leshem
1012386c8c
more reliable fix for bottom inset padding on edge-to-edge (#1486) 2025-04-20 18:58:58 +02:00
Helium314
7748ed75fe clarify when we use our own name for the language instead of using whatver is provided by the system 2025-04-19 08:35:46 +02:00
Benson Muite
c32b3bada4
Keyboard layouts for languages primarily used in Africa (#1483) 2025-04-19 08:27:58 +02:00
Helium314
e042adc5b8 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-04-18 10:04:19 +02:00
Eran Leshem
901e745158
Fix bottom of keyboard being under navigation bar (#1461)
mostly for Android 15+, but issues may also occur on older Android versions
2025-04-18 10:02:27 +02:00
Helium314
91554b02eb fix unchanged layout list when deleting secondary layout 2025-04-14 18:46:41 +02:00
Helium314
fe7f1a1b38 fix crash when deleting subtype
fixes GH-1476
2025-04-14 17:06:34 +02:00
Helium314
8fddf94121 fix unable to change one-handed mode scale 2025-04-08 16:10:04 +02:00
389 changed files with 12598 additions and 12698 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 140
tab_width = 4
trim_trailing_whitespace = true
[{*.markdown,*.md}]
trim_trailing_whitespace = false
indent_size = 2

2
.gitignore vendored
View file

@ -1,6 +1,7 @@
*.iml
.idea
.gradle
.kotlin
local.properties
.DS_Store
Gemfile
@ -8,5 +9,6 @@ build
app/build
app/release
app/.cxx
app/.attach_*
fastlane/Appfile
tools/*.txt

View file

@ -1,25 +1,24 @@
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 = 3004
versionName = "3.0-beta1"
versionCode = 3201
versionName = "3.2"
ndk {
abiFilters.clear()
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
}
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
buildTypes {
@ -43,7 +42,7 @@ android {
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
@ -69,7 +68,7 @@ android {
}
ndkVersion = "28.0.13004108"
packagingOptions {
packaging {
jniLibs {
// shrinks APK by 3 MB, zipped size unchanged
useLegacyPackaging = true
@ -105,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")

View file

@ -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>

View file

@ -1,115 +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_CA,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

1 main ar af exp
1 main af exp
2 main ar ar
3 main ar exp
4 main hy hy
5 main as as
6 main bn_BD exp
7 main bn bn
8 main bn exp
9 main eu eu
10 main be be
11 main bg bg
12 main bg exp
13 main ca ca
14 main ca exp
15 main hr hr
16 main hr exp
17 main cs cs
18 main cs exp
19 main da da
20 main da exp
21 main nl nl
22 main nl exp
23 main en_AU en_AU
24 main en_CA exp
25 main en_GB en_GB
26 main en_GB exp
27 main en_US en_US
28 main en_US exp
29 symbols en exp
30 emoji en en
31 main eo eo
32 main eo exp
33 main et exp
34 main fi fi
35 main fi exp
36 emoji fr fr
37 symbols fr exp
38 main fr fr
39 main fr exp
40 main gl gl
41 main gl exp
42 main ka ka
43 main de_AT exp
44 main de_CH
45 main de de
46 main de exp
47 main gom gom
48 main el el
49 main gu gu
50 main he he
51 main iw iw
52 main he exp
53 main hi hi
54 main hi_ZZ hi_ZZ
55 main hu hu
56 main hu exp
57 main is exp
58 main id exp
59 main it it
60 main it exp
61 main kab exp
62 main kn kn
63 main ks ks
64 main kk exp
65 main km km
66 main la la
67 main lv lv
68 main lv exp
69 main lt lt
70 main lt exp
71 main lb lb
72 main mai mai
73 addon ml_ZZ exp
74 main ml ml
75 main mr mr
76 main ne exp
77 main nb nb
78 main nb exp
79 main or or
80 main pms exp
81 main pl pl
82 main pl exp
83 main pt_BR pt_BR
84 main pt_PT pt_PT
85 main pt_PT exp
86 main pa pa
87 main ro ro
88 main ro exp
89 emoji ru ru
90 main ru ru
91 main ru exp
92 main sa sa
93 main sat sat
94 main sr_ZZ sr_ZZ
95 main sr sr
96 main sd sd
97 main sk exp
98 main sl sl
99 main sl exp
100 main es es
101 main es exp
102 main zgh_ZZ zgh_ZZ
103 main zgh zgh
104 main sv sv
105 main sv exp
106 main ta ta
107 main te te
108 main tok tok
109 main tcy tcy
110 main tr tr
111 main tr exp
112 emoji uk uk
113 main uk 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_CA 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
114 main uk uk exp exp
115 main ur
116 main vi vi exp exp

View file

@ -0,0 +1,85 @@
🎃
🎄
🎆
🎇
🧨
🎈
🎉
🎊
🎋
🎍
🎎
🎏
🎐
🎑
🧧
🎀
🎁
🎗️
🎟️
🎫
🎖️
🏆
🏅
🥇
🥈
🥉
🥎
🏀
🏐
🏈
🏉
🎾
🥏
🎳
🏏
🏑
🏒
🥍
🏓
🏸
🥊
🥋
🥅
⛸️
🎣
🤿
🎽
🎿
🛷
🥌
🎯
🪀
🪁
🔫
🎱
🔮
🪄
🎮
🕹️
🎰
🎲
🧩
🧸
🪅
🪩
🪆
♠️
♥️
♦️
♣️
♟️
🃏
🀄
🎴
🎭
🖼️
🎨
🧵
🪡
🧶
🪢

View file

@ -0,0 +1,159 @@
🐵
🐒
🦍
🦧
🐶
🐕
🦮
🐕‍🦺
🐩
🐺
🦊
🦝
🐱
🐈
🐈‍⬛
🦁
🐯
🐅
🐆
🐴
🫎
🫏
🐎
🦄
🦓
🦌
🦬
🐮
🐂
🐃
🐄
🐷
🐖
🐗
🐽
🐏
🐑
🐐
🐪
🐫
🦙
🦒
🐘
🦣
🦏
🦛
🐭
🐁
🐀
🐹
🐰
🐇
🐿️
🦫
🦔
🦇
🐻
🐻‍❄️
🐨
🐼
🦥
🦦
🦨
🦘
🦡
🐾
🦃
🐔
🐓
🐣
🐤
🐥
🐦
🐧
🕊️
🦅
🦆
🦢
🦉
🦤
🪶
🦩
🦚
🦜
🪽
🐦‍⬛
🪿
🐦‍🔥
🐸
🐊
🐢
🦎
🐍
🐲
🐉
🦕
🦖
🐳
🐋
🐬
🦭
🐟
🐠
🐡
🦈
🐙
🐚
🪸
🪼
🦀
🦞
🦐
🦑
🦪
🐌
🦋
🐛
🐜
🐝
🪲
🐞
🦗
🪳
🕷️
🕸️
🦂
🦟
🪰
🪱
🦠
💐
🌸
💮
🪷
🏵️
🌹
🥀
🌺
🌻
🌼
🌷
🪻
🌱
🪴
🌲
🌳
🌴
🌵
🌾
🌿
☘️
🍀
🍁
🍂
🍃
🪹
🪺
🍄
🪾

View file

@ -0,0 +1,25 @@
:-)
;-)
:-(
:-!
:-$
B-)
=-O
:-P
:O
:-*
:-D
:\'(
:-\\
O:-)
:-[
(╯°
□°)
╯︵
┻━┻
¯\\_
(ツ)
_/¯
┬─┬
︵ /(
.□.\\

View file

@ -0,0 +1,270 @@
🏁
🚩
🎌
🏴
🏳️
🏳️‍🌈
🏳️‍⚧️
🏴‍☠️
🇦🇨
🇦🇩
🇦🇪
🇦🇫
🇦🇬
🇦🇮
🇦🇱
🇦🇲
🇦🇴
🇦🇶
🇦🇷
🇦🇸
🇦🇹
🇦🇺
🇦🇼
🇦🇽
🇦🇿
🇧🇦
🇧🇧
🇧🇩
🇧🇪
🇧🇫
🇧🇬
🇧🇭
🇧🇮
🇧🇯
🇧🇱
🇧🇲
🇧🇳
🇧🇴
🇧🇶
🇧🇷
🇧🇸
🇧🇹
🇧🇻
🇧🇼
🇧🇾
🇧🇿
🇨🇦
🇨🇨
🇨🇩
🇨🇫
🇨🇬
🇨🇭
🇨🇮
🇨🇰
🇨🇱
🇨🇲
🇨🇳
🇨🇴
🇨🇵
🇨🇶
🇨🇷
🇨🇺
🇨🇻
🇨🇼
🇨🇽
🇨🇾
🇨🇿
🇩🇪
🇩🇬
🇩🇯
🇩🇰
🇩🇲
🇩🇴
🇩🇿
🇪🇦
🇪🇨
🇪🇪
🇪🇬
🇪🇭
🇪🇷
🇪🇸
🇪🇹
🇪🇺
🇫🇮
🇫🇯
🇫🇰
🇫🇲
🇫🇴
🇫🇷
🇬🇦
🇬🇧
🇬🇩
🇬🇪
🇬🇫
🇬🇬
🇬🇭
🇬🇮
🇬🇱
🇬🇲
🇬🇳
🇬🇵
🇬🇶
🇬🇷
🇬🇸
🇬🇹
🇬🇺
🇬🇼
🇬🇾
🇭🇰
🇭🇲
🇭🇳
🇭🇷
🇭🇹
🇭🇺
🇮🇨
🇮🇩
🇮🇪
🇮🇱
🇮🇲
🇮🇳
🇮🇴
🇮🇶
🇮🇷
🇮🇸
🇮🇹
🇯🇪
🇯🇲
🇯🇴
🇯🇵
🇰🇪
🇰🇬
🇰🇭
🇰🇮
🇰🇲
🇰🇳
🇰🇵
🇰🇷
🇰🇼
🇰🇾
🇰🇿
🇱🇦
🇱🇧
🇱🇨
🇱🇮
🇱🇰
🇱🇷
🇱🇸
🇱🇹
🇱🇺
🇱🇻
🇱🇾
🇲🇦
🇲🇨
🇲🇩
🇲🇪
🇲🇫
🇲🇬
🇲🇭
🇲🇰
🇲🇱
🇲🇲
🇲🇳
🇲🇴
🇲🇵
🇲🇶
🇲🇷
🇲🇸
🇲🇹
🇲🇺
🇲🇻
🇲🇼
🇲🇽
🇲🇾
🇲🇿
🇳🇦
🇳🇨
🇳🇪
🇳🇫
🇳🇬
🇳🇮
🇳🇱
🇳🇴
🇳🇵
🇳🇷
🇳🇺
🇳🇿
🇴🇲
🇵🇦
🇵🇪
🇵🇫
🇵🇬
🇵🇭
🇵🇰
🇵🇱
🇵🇲
🇵🇳
🇵🇷
🇵🇸
🇵🇹
🇵🇼
🇵🇾
🇶🇦
🇷🇪
🇷🇴
🇷🇸
🇷🇺
🇷🇼
🇸🇦
🇸🇧
🇸🇨
🇸🇩
🇸🇪
🇸🇬
🇸🇭
🇸🇮
🇸🇯
🇸🇰
🇸🇱
🇸🇲
🇸🇳
🇸🇴
🇸🇷
🇸🇸
🇸🇹
🇸🇻
🇸🇽
🇸🇾
🇸🇿
🇹🇦
🇹🇨
🇹🇩
🇹🇫
🇹🇬
🇹🇭
🇹🇯
🇹🇰
🇹🇱
🇹🇲
🇹🇳
🇹🇴
🇹🇷
🇹🇹
🇹🇻
🇹🇼
🇹🇿
🇺🇦
🇺🇬
🇺🇲
🇺🇳
🇺🇸
🇺🇾
🇺🇿
🇻🇦
🇻🇨
🇻🇪
🇻🇬
🇻🇮
🇻🇳
🇻🇺
🇼🇫
🇼🇸
🇽🇰
🇾🇪
🇾🇹
🇿🇦
🇿🇲
🇿🇼
🏴󠁧󠁢󠁥󠁮󠁧󠁿
🏴󠁧󠁢󠁳󠁣󠁴󠁿
🏴󠁧󠁢󠁷󠁬󠁳󠁿

View file

@ -0,0 +1,131 @@
🍇
🍈
🍉
🍊
🍋
🍋‍🟩
🍌
🍍
🥭
🍎
🍏
🍐
🍑
🍒
🍓
🫐
🥝
🍅
🫒
🥥
🥑
🍆
🥔
🥕
🌽
🌶️
🫑
🥒
🥬
🥦
🧄
🧅
🥜
🫘
🌰
🫚
🫛
🍄‍🟫
🫜
🍞
🥐
🥖
🫓
🥨
🥯
🥞
🧇
🧀
🍖
🍗
🥩
🥓
🍔
🍟
🍕
🌭
🥪
🌮
🌯
🫔
🥙
🧆
🥚
🍳
🥘
🍲
🫕
🥣
🥗
🍿
🧈
🧂
🥫
🍱
🍘
🍙
🍚
🍛
🍜
🍝
🍠
🍢
🍣
🍤
🍥
🥮
🍡
🥟
🥠
🥡
🍦
🍧
🍨
🍩
🍪
🎂
🍰
🧁
🥧
🍫
🍬
🍭
🍮
🍯
🍼
🥛
🫖
🍵
🍶
🍾
🍷
🍸
🍹
🍺
🍻
🥂
🥃
🫗
🥤
🧋
🧃
🧉
🧊
🥢
🍽️
🍴
🥄
🔪
🫙
🏺

View file

@ -0,0 +1,264 @@
👓
🕶️
🥽
🥼
🦺
👔
👕
👖
🧣
🧤
🧥
🧦
👗
👘
🥻
🩱
🩲
🩳
👙
👚
🪭
👛
👜
👝
🛍️
🎒
🩴
👞
👟
🥾
🥿
👠
👡
🩰
👢
🪮
👑
👒
🎩
🎓
🧢
🪖
⛑️
📿
💄
💍
💎
🔇
🔈
🔉
🔊
📢
📣
📯
🔔
🔕
🎼
🎵
🎶
🎙️
🎚️
🎛️
🎤
🎧
📻
🎷
🪗
🎸
🎹
🎺
🎻
🪕
🥁
🪘
🪇
🪈
🪉
📱
📲
☎️
📞
📟
📠
🔋
🪫
🔌
💻
🖥️
🖨️
⌨️
🖱️
🖲️
💽
💾
💿
📀
🧮
🎥
🎞️
📽️
🎬
📺
📷
📸
📹
📼
🔍
🔎
🕯️
💡
🔦
🏮
🪔
📔
📕
📖
📗
📘
📙
📚
📓
📒
📃
📜
📄
📰
🗞️
📑
🔖
🏷️
💰
🪙
💴
💵
💶
💷
💸
💳
🧾
💹
✉️
📧
📨
📩
📤
📥
📦
📫
📪
📬
📭
📮
🗳️
✏️
✒️
🖋️
🖊️
🖌️
🖍️
📝
💼
📁
📂
🗂️
📅
📆
🗒️
🗓️
📇
📈
📉
📊
📋
📌
📍
📎
🖇️
📏
📐
✂️
🗃️
🗄️
🗑️
🔒
🔓
🔏
🔐
🔑
🗝️
🔨
🪓
⛏️
⚒️
🛠️
🗡️
⚔️
💣
🪃
🏹
🛡️
🪚
🔧
🪛
🔩
⚙️
🗜️
⚖️
🦯
🔗
⛓️‍💥
⛓️
🪝
🧰
🧲
🪜
🪏
⚗️
🧪
🧫
🧬
🔬
🔭
📡
💉
🩸
💊
🩹
🩼
🩺
🩻
🚪
🛗
🪞
🪟
🛏️
🛋️
🪑
🚽
🪠
🚿
🛁
🪤
🪒
🧴
🧷
🧹
🧺
🧻
🪣
🧼
🫧
🪥
🧽
🧯
🛒
🚬
⚰️
🪦
⚱️
🧿
🪬
🗿
🪧
🪪

View file

@ -0,0 +1,386 @@
👋 👋🏻 👋🏼 👋🏽 👋🏾 👋🏿
🤚 🤚🏻 🤚🏼 🤚🏽 🤚🏾 🤚🏿
🖐️ 🖐🏻 🖐🏼 🖐🏽 🖐🏾 🖐🏿
✋ ✋🏻 ✋🏼 ✋🏽 ✋🏾 ✋🏿
🖖 🖖🏻 🖖🏼 🖖🏽 🖖🏾 🖖🏿
🫱 🫱🏻 🫱🏼 🫱🏽 🫱🏾 🫱🏿
🫲 🫲🏻 🫲🏼 🫲🏽 🫲🏾 🫲🏿
🫳 🫳🏻 🫳🏼 🫳🏽 🫳🏾 🫳🏿
🫴 🫴🏻 🫴🏼 🫴🏽 🫴🏾 🫴🏿
🫷 🫷🏻 🫷🏼 🫷🏽 🫷🏾 🫷🏿
🫸 🫸🏻 🫸🏼 🫸🏽 🫸🏾 🫸🏿
👌 👌🏻 👌🏼 👌🏽 👌🏾 👌🏿
🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿
🤏 🤏🏻 🤏🏼 🤏🏽 🤏🏾 🤏🏿
✌️ ✌🏻 ✌🏼 ✌🏽 ✌🏾 ✌🏿
🤞 🤞🏻 🤞🏼 🤞🏽 🤞🏾 🤞🏿
🫰 🫰🏻 🫰🏼 🫰🏽 🫰🏾 🫰🏿
🤟 🤟🏻 🤟🏼 🤟🏽 🤟🏾 🤟🏿
🤘 🤘🏻 🤘🏼 🤘🏽 🤘🏾 🤘🏿
🤙 🤙🏻 🤙🏼 🤙🏽 🤙🏾 🤙🏿
👈 👈🏻 👈🏼 👈🏽 👈🏾 👈🏿
👉 👉🏻 👉🏼 👉🏽 👉🏾 👉🏿
👆 👆🏻 👆🏼 👆🏽 👆🏾 👆🏿
🖕 🖕🏻 🖕🏼 🖕🏽 🖕🏾 🖕🏿
👇 👇🏻 👇🏼 👇🏽 👇🏾 👇🏿
☝️ ☝🏻 ☝🏼 ☝🏽 ☝🏾 ☝🏿
🫵 🫵🏻 🫵🏼 🫵🏽 🫵🏾 🫵🏿
👍 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿
👎 👎🏻 👎🏼 👎🏽 👎🏾 👎🏿
✊ ✊🏻 ✊🏼 ✊🏽 ✊🏾 ✊🏿
👊 👊🏻 👊🏼 👊🏽 👊🏾 👊🏿
🤛 🤛🏻 🤛🏼 🤛🏽 🤛🏾 🤛🏿
🤜 🤜🏻 🤜🏼 🤜🏽 🤜🏾 🤜🏿
👏 👏🏻 👏🏼 👏🏽 👏🏾 👏🏿
🙌 🙌🏻 🙌🏼 🙌🏽 🙌🏾 🙌🏿
🫶 🫶🏻 🫶🏼 🫶🏽 🫶🏾 🫶🏿
👐 👐🏻 👐🏼 👐🏽 👐🏾 👐🏿
🤲 🤲🏻 🤲🏼 🤲🏽 🤲🏾 🤲🏿
🤝 🤝🏻 🤝🏼 🤝🏽 🤝🏾 🤝🏿
🙏 🙏🏻 🙏🏼 🙏🏽 🙏🏾 🙏🏿
✍️ ✍🏻 ✍🏼 ✍🏽 ✍🏾 ✍🏿
💅 💅🏻 💅🏼 💅🏽 💅🏾 💅🏿
🤳 🤳🏻 🤳🏼 🤳🏽 🤳🏾 🤳🏿
💪 💪🏻 💪🏼 💪🏽 💪🏾 💪🏿
🦾
🦿
🦵 🦵🏻 🦵🏼 🦵🏽 🦵🏾 🦵🏿
🦶 🦶🏻 🦶🏼 🦶🏽 🦶🏾 🦶🏿
👂 👂🏻 👂🏼 👂🏽 👂🏾 👂🏿
🦻 🦻🏻 🦻🏼 🦻🏽 🦻🏾 🦻🏿
👃 👃🏻 👃🏼 👃🏽 👃🏾 👃🏿
🧠
🫀
🫁
🦷
🦴
👀
👁️
👅
👄
🫦
👶 👶🏻 👶🏼 👶🏽 👶🏾 👶🏿
🧒 🧒🏻 🧒🏼 🧒🏽 🧒🏾 🧒🏿
👦 👦🏻 👦🏼 👦🏽 👦🏾 👦🏿
👧 👧🏻 👧🏼 👧🏽 👧🏾 👧🏿
🧑 🧑🏻 🧑🏼 🧑🏽 🧑🏾 🧑🏿
👱 👱🏻 👱🏼 👱🏽 👱🏾 👱🏿
👨 👨🏻 👨🏼 👨🏽 👨🏾 👨🏿
🧔 🧔🏻 🧔🏼 🧔🏽 🧔🏾 🧔🏿
🧔‍♂️ 🧔🏻‍♂️ 🧔🏼‍♂️ 🧔🏽‍♂️ 🧔🏾‍♂️ 🧔🏿‍♂️
🧔‍♀️ 🧔🏻‍♀️ 🧔🏼‍♀️ 🧔🏽‍♀️ 🧔🏾‍♀️ 🧔🏿‍♀️
👨‍🦰 👨🏻‍🦰 👨🏼‍🦰 👨🏽‍🦰 👨🏾‍🦰 👨🏿‍🦰
👨‍🦱 👨🏻‍🦱 👨🏼‍🦱 👨🏽‍🦱 👨🏾‍🦱 👨🏿‍🦱
👨‍🦳 👨🏻‍🦳 👨🏼‍🦳 👨🏽‍🦳 👨🏾‍🦳 👨🏿‍🦳
👨‍🦲 👨🏻‍🦲 👨🏼‍🦲 👨🏽‍🦲 👨🏾‍🦲 👨🏿‍🦲
👩 👩🏻 👩🏼 👩🏽 👩🏾 👩🏿
👩‍🦰 👩🏻‍🦰 👩🏼‍🦰 👩🏽‍🦰 👩🏾‍🦰 👩🏿‍🦰
🧑‍🦰 🧑🏻‍🦰 🧑🏼‍🦰 🧑🏽‍🦰 🧑🏾‍🦰 🧑🏿‍🦰
👩‍🦱 👩🏻‍🦱 👩🏼‍🦱 👩🏽‍🦱 👩🏾‍🦱 👩🏿‍🦱
🧑‍🦱 🧑🏻‍🦱 🧑🏼‍🦱 🧑🏽‍🦱 🧑🏾‍🦱 🧑🏿‍🦱
👩‍🦳 👩🏻‍🦳 👩🏼‍🦳 👩🏽‍🦳 👩🏾‍🦳 👩🏿‍🦳
🧑‍🦳 🧑🏻‍🦳 🧑🏼‍🦳 🧑🏽‍🦳 🧑🏾‍🦳 🧑🏿‍🦳
👩‍🦲 👩🏻‍🦲 👩🏼‍🦲 👩🏽‍🦲 👩🏾‍🦲 👩🏿‍🦲
🧑‍🦲 🧑🏻‍🦲 🧑🏼‍🦲 🧑🏽‍🦲 🧑🏾‍🦲 🧑🏿‍🦲
👱‍♀️ 👱🏻‍♀️ 👱🏼‍♀️ 👱🏽‍♀️ 👱🏾‍♀️ 👱🏿‍♀️
👱‍♂️ 👱🏻‍♂️ 👱🏼‍♂️ 👱🏽‍♂️ 👱🏾‍♂️ 👱🏿‍♂️
🧓 🧓🏻 🧓🏼 🧓🏽 🧓🏾 🧓🏿
👴 👴🏻 👴🏼 👴🏽 👴🏾 👴🏿
👵 👵🏻 👵🏼 👵🏽 👵🏾 👵🏿
🙍 🙍🏻 🙍🏼 🙍🏽 🙍🏾 🙍🏿
🙍‍♂️ 🙍🏻‍♂️ 🙍🏼‍♂️ 🙍🏽‍♂️ 🙍🏾‍♂️ 🙍🏿‍♂️
🙍‍♀️ 🙍🏻‍♀️ 🙍🏼‍♀️ 🙍🏽‍♀️ 🙍🏾‍♀️ 🙍🏿‍♀️
🙎 🙎🏻 🙎🏼 🙎🏽 🙎🏾 🙎🏿
🙎‍♂️ 🙎🏻‍♂️ 🙎🏼‍♂️ 🙎🏽‍♂️ 🙎🏾‍♂️ 🙎🏿‍♂️
🙎‍♀️ 🙎🏻‍♀️ 🙎🏼‍♀️ 🙎🏽‍♀️ 🙎🏾‍♀️ 🙎🏿‍♀️
🙅 🙅🏻 🙅🏼 🙅🏽 🙅🏾 🙅🏿
🙅‍♂️ 🙅🏻‍♂️ 🙅🏼‍♂️ 🙅🏽‍♂️ 🙅🏾‍♂️ 🙅🏿‍♂️
🙅‍♀️ 🙅🏻‍♀️ 🙅🏼‍♀️ 🙅🏽‍♀️ 🙅🏾‍♀️ 🙅🏿‍♀️
🙆 🙆🏻 🙆🏼 🙆🏽 🙆🏾 🙆🏿
🙆‍♂️ 🙆🏻‍♂️ 🙆🏼‍♂️ 🙆🏽‍♂️ 🙆🏾‍♂️ 🙆🏿‍♂️
🙆‍♀️ 🙆🏻‍♀️ 🙆🏼‍♀️ 🙆🏽‍♀️ 🙆🏾‍♀️ 🙆🏿‍♀️
💁 💁🏻 💁🏼 💁🏽 💁🏾 💁🏿
💁‍♂️ 💁🏻‍♂️ 💁🏼‍♂️ 💁🏽‍♂️ 💁🏾‍♂️ 💁🏿‍♂️
💁‍♀️ 💁🏻‍♀️ 💁🏼‍♀️ 💁🏽‍♀️ 💁🏾‍♀️ 💁🏿‍♀️
🙋 🙋🏻 🙋🏼 🙋🏽 🙋🏾 🙋🏿
🙋‍♂️ 🙋🏻‍♂️ 🙋🏼‍♂️ 🙋🏽‍♂️ 🙋🏾‍♂️ 🙋🏿‍♂️
🙋‍♀️ 🙋🏻‍♀️ 🙋🏼‍♀️ 🙋🏽‍♀️ 🙋🏾‍♀️ 🙋🏿‍♀️
🧏 🧏🏻 🧏🏼 🧏🏽 🧏🏾 🧏🏿
🧏‍♂️ 🧏🏻‍♂️ 🧏🏼‍♂️ 🧏🏽‍♂️ 🧏🏾‍♂️ 🧏🏿‍♂️
🧏‍♀️ 🧏🏻‍♀️ 🧏🏼‍♀️ 🧏🏽‍♀️ 🧏🏾‍♀️ 🧏🏿‍♀️
🙇 🙇🏻 🙇🏼 🙇🏽 🙇🏾 🙇🏿
🙇‍♂️ 🙇🏻‍♂️ 🙇🏼‍♂️ 🙇🏽‍♂️ 🙇🏾‍♂️ 🙇🏿‍♂️
🙇‍♀️ 🙇🏻‍♀️ 🙇🏼‍♀️ 🙇🏽‍♀️ 🙇🏾‍♀️ 🙇🏿‍♀️
🤦 🤦🏻 🤦🏼 🤦🏽 🤦🏾 🤦🏿
🤦‍♂️ 🤦🏻‍♂️ 🤦🏼‍♂️ 🤦🏽‍♂️ 🤦🏾‍♂️ 🤦🏿‍♂️
🤦‍♀️ 🤦🏻‍♀️ 🤦🏼‍♀️ 🤦🏽‍♀️ 🤦🏾‍♀️ 🤦🏿‍♀️
🤷 🤷🏻 🤷🏼 🤷🏽 🤷🏾 🤷🏿
🤷‍♂️ 🤷🏻‍♂️ 🤷🏼‍♂️ 🤷🏽‍♂️ 🤷🏾‍♂️ 🤷🏿‍♂️
🤷‍♀️ 🤷🏻‍♀️ 🤷🏼‍♀️ 🤷🏽‍♀️ 🤷🏾‍♀️ 🤷🏿‍♀️
🧑‍⚕️ 🧑🏻‍⚕️ 🧑🏼‍⚕️ 🧑🏽‍⚕️ 🧑🏾‍⚕️ 🧑🏿‍⚕️
👨‍⚕️ 👨🏻‍⚕️ 👨🏼‍⚕️ 👨🏽‍⚕️ 👨🏾‍⚕️ 👨🏿‍⚕️
👩‍⚕️ 👩🏻‍⚕️ 👩🏼‍⚕️ 👩🏽‍⚕️ 👩🏾‍⚕️ 👩🏿‍⚕️
🧑‍🎓 🧑🏻‍🎓 🧑🏼‍🎓 🧑🏽‍🎓 🧑🏾‍🎓 🧑🏿‍🎓
👨‍🎓 👨🏻‍🎓 👨🏼‍🎓 👨🏽‍🎓 👨🏾‍🎓 👨🏿‍🎓
👩‍🎓 👩🏻‍🎓 👩🏼‍🎓 👩🏽‍🎓 👩🏾‍🎓 👩🏿‍🎓
🧑‍🏫 🧑🏻‍🏫 🧑🏼‍🏫 🧑🏽‍🏫 🧑🏾‍🏫 🧑🏿‍🏫
👨‍🏫 👨🏻‍🏫 👨🏼‍🏫 👨🏽‍🏫 👨🏾‍🏫 👨🏿‍🏫
👩‍🏫 👩🏻‍🏫 👩🏼‍🏫 👩🏽‍🏫 👩🏾‍🏫 👩🏿‍🏫
🧑‍⚖️ 🧑🏻‍⚖️ 🧑🏼‍⚖️ 🧑🏽‍⚖️ 🧑🏾‍⚖️ 🧑🏿‍⚖️
👨‍⚖️ 👨🏻‍⚖️ 👨🏼‍⚖️ 👨🏽‍⚖️ 👨🏾‍⚖️ 👨🏿‍⚖️
👩‍⚖️ 👩🏻‍⚖️ 👩🏼‍⚖️ 👩🏽‍⚖️ 👩🏾‍⚖️ 👩🏿‍⚖️
🧑‍🌾 🧑🏻‍🌾 🧑🏼‍🌾 🧑🏽‍🌾 🧑🏾‍🌾 🧑🏿‍🌾
👨‍🌾 👨🏻‍🌾 👨🏼‍🌾 👨🏽‍🌾 👨🏾‍🌾 👨🏿‍🌾
👩‍🌾 👩🏻‍🌾 👩🏼‍🌾 👩🏽‍🌾 👩🏾‍🌾 👩🏿‍🌾
🧑‍🍳 🧑🏻‍🍳 🧑🏼‍🍳 🧑🏽‍🍳 🧑🏾‍🍳 🧑🏿‍🍳
👨‍🍳 👨🏻‍🍳 👨🏼‍🍳 👨🏽‍🍳 👨🏾‍🍳 👨🏿‍🍳
👩‍🍳 👩🏻‍🍳 👩🏼‍🍳 👩🏽‍🍳 👩🏾‍🍳 👩🏿‍🍳
🧑‍🔧 🧑🏻‍🔧 🧑🏼‍🔧 🧑🏽‍🔧 🧑🏾‍🔧 🧑🏿‍🔧
👨‍🔧 👨🏻‍🔧 👨🏼‍🔧 👨🏽‍🔧 👨🏾‍🔧 👨🏿‍🔧
👩‍🔧 👩🏻‍🔧 👩🏼‍🔧 👩🏽‍🔧 👩🏾‍🔧 👩🏿‍🔧
🧑‍🏭 🧑🏻‍🏭 🧑🏼‍🏭 🧑🏽‍🏭 🧑🏾‍🏭 🧑🏿‍🏭
👨‍🏭 👨🏻‍🏭 👨🏼‍🏭 👨🏽‍🏭 👨🏾‍🏭 👨🏿‍🏭
👩‍🏭 👩🏻‍🏭 👩🏼‍🏭 👩🏽‍🏭 👩🏾‍🏭 👩🏿‍🏭
🧑‍💼 🧑🏻‍💼 🧑🏼‍💼 🧑🏽‍💼 🧑🏾‍💼 🧑🏿‍💼
👨‍💼 👨🏻‍💼 👨🏼‍💼 👨🏽‍💼 👨🏾‍💼 👨🏿‍💼
👩‍💼 👩🏻‍💼 👩🏼‍💼 👩🏽‍💼 👩🏾‍💼 👩🏿‍💼
🧑‍🔬 🧑🏻‍🔬 🧑🏼‍🔬 🧑🏽‍🔬 🧑🏾‍🔬 🧑🏿‍🔬
👨‍🔬 👨🏻‍🔬 👨🏼‍🔬 👨🏽‍🔬 👨🏾‍🔬 👨🏿‍🔬
👩‍🔬 👩🏻‍🔬 👩🏼‍🔬 👩🏽‍🔬 👩🏾‍🔬 👩🏿‍🔬
🧑‍💻 🧑🏻‍💻 🧑🏼‍💻 🧑🏽‍💻 🧑🏾‍💻 🧑🏿‍💻
👨‍💻 👨🏻‍💻 👨🏼‍💻 👨🏽‍💻 👨🏾‍💻 👨🏿‍💻
👩‍💻 👩🏻‍💻 👩🏼‍💻 👩🏽‍💻 👩🏾‍💻 👩🏿‍💻
🧑‍🎤 🧑🏻‍🎤 🧑🏼‍🎤 🧑🏽‍🎤 🧑🏾‍🎤 🧑🏿‍🎤
👨‍🎤 👨🏻‍🎤 👨🏼‍🎤 👨🏽‍🎤 👨🏾‍🎤 👨🏿‍🎤
👩‍🎤 👩🏻‍🎤 👩🏼‍🎤 👩🏽‍🎤 👩🏾‍🎤 👩🏿‍🎤
🧑‍🎨 🧑🏻‍🎨 🧑🏼‍🎨 🧑🏽‍🎨 🧑🏾‍🎨 🧑🏿‍🎨
👨‍🎨 👨🏻‍🎨 👨🏼‍🎨 👨🏽‍🎨 👨🏾‍🎨 👨🏿‍🎨
👩‍🎨 👩🏻‍🎨 👩🏼‍🎨 👩🏽‍🎨 👩🏾‍🎨 👩🏿‍🎨
🧑‍✈️ 🧑🏻‍✈️ 🧑🏼‍✈️ 🧑🏽‍✈️ 🧑🏾‍✈️ 🧑🏿‍✈️
👨‍✈️ 👨🏻‍✈️ 👨🏼‍✈️ 👨🏽‍✈️ 👨🏾‍✈️ 👨🏿‍✈️
👩‍✈️ 👩🏻‍✈️ 👩🏼‍✈️ 👩🏽‍✈️ 👩🏾‍✈️ 👩🏿‍✈️
🧑‍🚀 🧑🏻‍🚀 🧑🏼‍🚀 🧑🏽‍🚀 🧑🏾‍🚀 🧑🏿‍🚀
👨‍🚀 👨🏻‍🚀 👨🏼‍🚀 👨🏽‍🚀 👨🏾‍🚀 👨🏿‍🚀
👩‍🚀 👩🏻‍🚀 👩🏼‍🚀 👩🏽‍🚀 👩🏾‍🚀 👩🏿‍🚀
🧑‍🚒 🧑🏻‍🚒 🧑🏼‍🚒 🧑🏽‍🚒 🧑🏾‍🚒 🧑🏿‍🚒
👨‍🚒 👨🏻‍🚒 👨🏼‍🚒 👨🏽‍🚒 👨🏾‍🚒 👨🏿‍🚒
👩‍🚒 👩🏻‍🚒 👩🏼‍🚒 👩🏽‍🚒 👩🏾‍🚒 👩🏿‍🚒
👮 👮🏻 👮🏼 👮🏽 👮🏾 👮🏿
👮‍♂️ 👮🏻‍♂️ 👮🏼‍♂️ 👮🏽‍♂️ 👮🏾‍♂️ 👮🏿‍♂️
👮‍♀️ 👮🏻‍♀️ 👮🏼‍♀️ 👮🏽‍♀️ 👮🏾‍♀️ 👮🏿‍♀️
🕵️ 🕵🏻 🕵🏼 🕵🏽 🕵🏾 🕵🏿
🕵️‍♂️ 🕵🏻‍♂️ 🕵🏼‍♂️ 🕵🏽‍♂️ 🕵🏾‍♂️ 🕵🏿‍♂️
🕵️‍♀️ 🕵🏻‍♀️ 🕵🏼‍♀️ 🕵🏽‍♀️ 🕵🏾‍♀️ 🕵🏿‍♀️
💂 💂🏻 💂🏼 💂🏽 💂🏾 💂🏿
💂‍♂️ 💂🏻‍♂️ 💂🏼‍♂️ 💂🏽‍♂️ 💂🏾‍♂️ 💂🏿‍♂️
💂‍♀️ 💂🏻‍♀️ 💂🏼‍♀️ 💂🏽‍♀️ 💂🏾‍♀️ 💂🏿‍♀️
🥷 🥷🏻 🥷🏼 🥷🏽 🥷🏾 🥷🏿
👷 👷🏻 👷🏼 👷🏽 👷🏾 👷🏿
👷‍♂️ 👷🏻‍♂️ 👷🏼‍♂️ 👷🏽‍♂️ 👷🏾‍♂️ 👷🏿‍♂️
👷‍♀️ 👷🏻‍♀️ 👷🏼‍♀️ 👷🏽‍♀️ 👷🏾‍♀️ 👷🏿‍♀️
🫅 🫅🏻 🫅🏼 🫅🏽 🫅🏾 🫅🏿
🤴 🤴🏻 🤴🏼 🤴🏽 🤴🏾 🤴🏿
👸 👸🏻 👸🏼 👸🏽 👸🏾 👸🏿
👳 👳🏻 👳🏼 👳🏽 👳🏾 👳🏿
👳‍♂️ 👳🏻‍♂️ 👳🏼‍♂️ 👳🏽‍♂️ 👳🏾‍♂️ 👳🏿‍♂️
👳‍♀️ 👳🏻‍♀️ 👳🏼‍♀️ 👳🏽‍♀️ 👳🏾‍♀️ 👳🏿‍♀️
👲 👲🏻 👲🏼 👲🏽 👲🏾 👲🏿
🧕 🧕🏻 🧕🏼 🧕🏽 🧕🏾 🧕🏿
🤵 🤵🏻 🤵🏼 🤵🏽 🤵🏾 🤵🏿
🤵‍♂️ 🤵🏻‍♂️ 🤵🏼‍♂️ 🤵🏽‍♂️ 🤵🏾‍♂️ 🤵🏿‍♂️
🤵‍♀️ 🤵🏻‍♀️ 🤵🏼‍♀️ 🤵🏽‍♀️ 🤵🏾‍♀️ 🤵🏿‍♀️
👰 👰🏻 👰🏼 👰🏽 👰🏾 👰🏿
👰‍♂️ 👰🏻‍♂️ 👰🏼‍♂️ 👰🏽‍♂️ 👰🏾‍♂️ 👰🏿‍♂️
👰‍♀️ 👰🏻‍♀️ 👰🏼‍♀️ 👰🏽‍♀️ 👰🏾‍♀️ 👰🏿‍♀️
🤰 🤰🏻 🤰🏼 🤰🏽 🤰🏾 🤰🏿
🫃 🫃🏻 🫃🏼 🫃🏽 🫃🏾 🫃🏿
🫄 🫄🏻 🫄🏼 🫄🏽 🫄🏾 🫄🏿
🤱 🤱🏻 🤱🏼 🤱🏽 🤱🏾 🤱🏿
👩‍🍼 👩🏻‍🍼 👩🏼‍🍼 👩🏽‍🍼 👩🏾‍🍼 👩🏿‍🍼
👨‍🍼 👨🏻‍🍼 👨🏼‍🍼 👨🏽‍🍼 👨🏾‍🍼 👨🏿‍🍼
🧑‍🍼 🧑🏻‍🍼 🧑🏼‍🍼 🧑🏽‍🍼 🧑🏾‍🍼 🧑🏿‍🍼
👼 👼🏻 👼🏼 👼🏽 👼🏾 👼🏿
🎅 🎅🏻 🎅🏼 🎅🏽 🎅🏾 🎅🏿
🤶 🤶🏻 🤶🏼 🤶🏽 🤶🏾 🤶🏿
🧑‍🎄 🧑🏻‍🎄 🧑🏼‍🎄 🧑🏽‍🎄 🧑🏾‍🎄 🧑🏿‍🎄
🦸 🦸🏻 🦸🏼 🦸🏽 🦸🏾 🦸🏿
🦸‍♂️ 🦸🏻‍♂️ 🦸🏼‍♂️ 🦸🏽‍♂️ 🦸🏾‍♂️ 🦸🏿‍♂️
🦸‍♀️ 🦸🏻‍♀️ 🦸🏼‍♀️ 🦸🏽‍♀️ 🦸🏾‍♀️ 🦸🏿‍♀️
🦹 🦹🏻 🦹🏼 🦹🏽 🦹🏾 🦹🏿
🦹‍♂️ 🦹🏻‍♂️ 🦹🏼‍♂️ 🦹🏽‍♂️ 🦹🏾‍♂️ 🦹🏿‍♂️
🦹‍♀️ 🦹🏻‍♀️ 🦹🏼‍♀️ 🦹🏽‍♀️ 🦹🏾‍♀️ 🦹🏿‍♀️
🧙 🧙🏻 🧙🏼 🧙🏽 🧙🏾 🧙🏿
🧙‍♂️ 🧙🏻‍♂️ 🧙🏼‍♂️ 🧙🏽‍♂️ 🧙🏾‍♂️ 🧙🏿‍♂️
🧙‍♀️ 🧙🏻‍♀️ 🧙🏼‍♀️ 🧙🏽‍♀️ 🧙🏾‍♀️ 🧙🏿‍♀️
🧚 🧚🏻 🧚🏼 🧚🏽 🧚🏾 🧚🏿
🧚‍♂️ 🧚🏻‍♂️ 🧚🏼‍♂️ 🧚🏽‍♂️ 🧚🏾‍♂️ 🧚🏿‍♂️
🧚‍♀️ 🧚🏻‍♀️ 🧚🏼‍♀️ 🧚🏽‍♀️ 🧚🏾‍♀️ 🧚🏿‍♀️
🧛 🧛🏻 🧛🏼 🧛🏽 🧛🏾 🧛🏿
🧛‍♂️ 🧛🏻‍♂️ 🧛🏼‍♂️ 🧛🏽‍♂️ 🧛🏾‍♂️ 🧛🏿‍♂️
🧛‍♀️ 🧛🏻‍♀️ 🧛🏼‍♀️ 🧛🏽‍♀️ 🧛🏾‍♀️ 🧛🏿‍♀️
🧜 🧜🏻 🧜🏼 🧜🏽 🧜🏾 🧜🏿
🧜‍♂️ 🧜🏻‍♂️ 🧜🏼‍♂️ 🧜🏽‍♂️ 🧜🏾‍♂️ 🧜🏿‍♂️
🧜‍♀️ 🧜🏻‍♀️ 🧜🏼‍♀️ 🧜🏽‍♀️ 🧜🏾‍♀️ 🧜🏿‍♀️
🧝 🧝🏻 🧝🏼 🧝🏽 🧝🏾 🧝🏿
🧝‍♂️ 🧝🏻‍♂️ 🧝🏼‍♂️ 🧝🏽‍♂️ 🧝🏾‍♂️ 🧝🏿‍♂️
🧝‍♀️ 🧝🏻‍♀️ 🧝🏼‍♀️ 🧝🏽‍♀️ 🧝🏾‍♀️ 🧝🏿‍♀️
🧞
🧞‍♂️
🧞‍♀️
🧟
🧟‍♂️
🧟‍♀️
🧌
💆 💆🏻 💆🏼 💆🏽 💆🏾 💆🏿
💆‍♂️ 💆🏻‍♂️ 💆🏼‍♂️ 💆🏽‍♂️ 💆🏾‍♂️ 💆🏿‍♂️
💆‍♀️ 💆🏻‍♀️ 💆🏼‍♀️ 💆🏽‍♀️ 💆🏾‍♀️ 💆🏿‍♀️
💇 💇🏻 💇🏼 💇🏽 💇🏾 💇🏿
💇‍♂️ 💇🏻‍♂️ 💇🏼‍♂️ 💇🏽‍♂️ 💇🏾‍♂️ 💇🏿‍♂️
💇‍♀️ 💇🏻‍♀️ 💇🏼‍♀️ 💇🏽‍♀️ 💇🏾‍♀️ 💇🏿‍♀️
🚶 🚶🏻 🚶🏼 🚶🏽 🚶🏾 🚶🏿
🚶‍♂️ 🚶🏻‍♂️ 🚶🏼‍♂️ 🚶🏽‍♂️ 🚶🏾‍♂️ 🚶🏿‍♂️
🚶‍♀️ 🚶🏻‍♀️ 🚶🏼‍♀️ 🚶🏽‍♀️ 🚶🏾‍♀️ 🚶🏿‍♀️
🚶‍➡️ 🚶🏻‍➡️ 🚶🏼‍➡️ 🚶🏽‍➡️ 🚶🏾‍➡️ 🚶🏿‍➡️
🚶‍♀️‍➡️ 🚶🏻‍♀️‍➡️ 🚶🏼‍♀️‍➡️ 🚶🏽‍♀️‍➡️ 🚶🏾‍♀️‍➡️ 🚶🏿‍♀️‍➡️
🚶‍♂️‍➡️ 🚶🏻‍♂️‍➡️ 🚶🏼‍♂️‍➡️ 🚶🏽‍♂️‍➡️ 🚶🏾‍♂️‍➡️ 🚶🏿‍♂️‍➡️
🧍 🧍🏻 🧍🏼 🧍🏽 🧍🏾 🧍🏿
🧍‍♂️ 🧍🏻‍♂️ 🧍🏼‍♂️ 🧍🏽‍♂️ 🧍🏾‍♂️ 🧍🏿‍♂️
🧍‍♀️ 🧍🏻‍♀️ 🧍🏼‍♀️ 🧍🏽‍♀️ 🧍🏾‍♀️ 🧍🏿‍♀️
🧎 🧎🏻 🧎🏼 🧎🏽 🧎🏾 🧎🏿
🧎‍♂️ 🧎🏻‍♂️ 🧎🏼‍♂️ 🧎🏽‍♂️ 🧎🏾‍♂️ 🧎🏿‍♂️
🧎‍♀️ 🧎🏻‍♀️ 🧎🏼‍♀️ 🧎🏽‍♀️ 🧎🏾‍♀️ 🧎🏿‍♀️
🧎‍➡️ 🧎🏻‍➡️ 🧎🏼‍➡️ 🧎🏽‍➡️ 🧎🏾‍➡️ 🧎🏿‍➡️
🧎‍♀️‍➡️ 🧎🏻‍♀️‍➡️ 🧎🏼‍♀️‍➡️ 🧎🏽‍♀️‍➡️ 🧎🏾‍♀️‍➡️ 🧎🏿‍♀️‍➡️
🧎‍♂️‍➡️ 🧎🏻‍♂️‍➡️ 🧎🏼‍♂️‍➡️ 🧎🏽‍♂️‍➡️ 🧎🏾‍♂️‍➡️ 🧎🏿‍♂️‍➡️
🧑‍🦯 🧑🏻‍🦯 🧑🏼‍🦯 🧑🏽‍🦯 🧑🏾‍🦯 🧑🏿‍🦯
🧑‍🦯‍➡️ 🧑🏻‍🦯‍➡️ 🧑🏼‍🦯‍➡️ 🧑🏽‍🦯‍➡️ 🧑🏾‍🦯‍➡️ 🧑🏿‍🦯‍➡️
👨‍🦯 👨🏻‍🦯 👨🏼‍🦯 👨🏽‍🦯 👨🏾‍🦯 👨🏿‍🦯
👨‍🦯‍➡️ 👨🏻‍🦯‍➡️ 👨🏼‍🦯‍➡️ 👨🏽‍🦯‍➡️ 👨🏾‍🦯‍➡️ 👨🏿‍🦯‍➡️
👩‍🦯 👩🏻‍🦯 👩🏼‍🦯 👩🏽‍🦯 👩🏾‍🦯 👩🏿‍🦯
👩‍🦯‍➡️ 👩🏻‍🦯‍➡️ 👩🏼‍🦯‍➡️ 👩🏽‍🦯‍➡️ 👩🏾‍🦯‍➡️ 👩🏿‍🦯‍➡️
🧑‍🦼 🧑🏻‍🦼 🧑🏼‍🦼 🧑🏽‍🦼 🧑🏾‍🦼 🧑🏿‍🦼
🧑‍🦼‍➡️ 🧑🏻‍🦼‍➡️ 🧑🏼‍🦼‍➡️ 🧑🏽‍🦼‍➡️ 🧑🏾‍🦼‍➡️ 🧑🏿‍🦼‍➡️
👨‍🦼 👨🏻‍🦼 👨🏼‍🦼 👨🏽‍🦼 👨🏾‍🦼 👨🏿‍🦼
👨‍🦼‍➡️ 👨🏻‍🦼‍➡️ 👨🏼‍🦼‍➡️ 👨🏽‍🦼‍➡️ 👨🏾‍🦼‍➡️ 👨🏿‍🦼‍➡️
👩‍🦼 👩🏻‍🦼 👩🏼‍🦼 👩🏽‍🦼 👩🏾‍🦼 👩🏿‍🦼
👩‍🦼‍➡️ 👩🏻‍🦼‍➡️ 👩🏼‍🦼‍➡️ 👩🏽‍🦼‍➡️ 👩🏾‍🦼‍➡️ 👩🏿‍🦼‍➡️
🧑‍🦽 🧑🏻‍🦽 🧑🏼‍🦽 🧑🏽‍🦽 🧑🏾‍🦽 🧑🏿‍🦽
🧑‍🦽‍➡️ 🧑🏻‍🦽‍➡️ 🧑🏼‍🦽‍➡️ 🧑🏽‍🦽‍➡️ 🧑🏾‍🦽‍➡️ 🧑🏿‍🦽‍➡️
👨‍🦽 👨🏻‍🦽 👨🏼‍🦽 👨🏽‍🦽 👨🏾‍🦽 👨🏿‍🦽
👨‍🦽‍➡️ 👨🏻‍🦽‍➡️ 👨🏼‍🦽‍➡️ 👨🏽‍🦽‍➡️ 👨🏾‍🦽‍➡️ 👨🏿‍🦽‍➡️
👩‍🦽 👩🏻‍🦽 👩🏼‍🦽 👩🏽‍🦽 👩🏾‍🦽 👩🏿‍🦽
👩‍🦽‍➡️ 👩🏻‍🦽‍➡️ 👩🏼‍🦽‍➡️ 👩🏽‍🦽‍➡️ 👩🏾‍🦽‍➡️ 👩🏿‍🦽‍➡️
🏃 🏃🏻 🏃🏼 🏃🏽 🏃🏾 🏃🏿
🏃‍♂️ 🏃🏻‍♂️ 🏃🏼‍♂️ 🏃🏽‍♂️ 🏃🏾‍♂️ 🏃🏿‍♂️
🏃‍♀️ 🏃🏻‍♀️ 🏃🏼‍♀️ 🏃🏽‍♀️ 🏃🏾‍♀️ 🏃🏿‍♀️
🏃‍➡️ 🏃🏻‍➡️ 🏃🏼‍➡️ 🏃🏽‍➡️ 🏃🏾‍➡️ 🏃🏿‍➡️
🏃‍♀️‍➡️ 🏃🏻‍♀️‍➡️ 🏃🏼‍♀️‍➡️ 🏃🏽‍♀️‍➡️ 🏃🏾‍♀️‍➡️ 🏃🏿‍♀️‍➡️
🏃‍♂️‍➡️ 🏃🏻‍♂️‍➡️ 🏃🏼‍♂️‍➡️ 🏃🏽‍♂️‍➡️ 🏃🏾‍♂️‍➡️ 🏃🏿‍♂️‍➡️
💃 💃🏻 💃🏼 💃🏽 💃🏾 💃🏿
🕺 🕺🏻 🕺🏼 🕺🏽 🕺🏾 🕺🏿
🕴️ 🕴🏻 🕴🏼 🕴🏽 🕴🏾 🕴🏿
👯
👯‍♂️
👯‍♀️
🧖 🧖🏻 🧖🏼 🧖🏽 🧖🏾 🧖🏿
🧖‍♂️ 🧖🏻‍♂️ 🧖🏼‍♂️ 🧖🏽‍♂️ 🧖🏾‍♂️ 🧖🏿‍♂️
🧖‍♀️ 🧖🏻‍♀️ 🧖🏼‍♀️ 🧖🏽‍♀️ 🧖🏾‍♀️ 🧖🏿‍♀️
🧗 🧗🏻 🧗🏼 🧗🏽 🧗🏾 🧗🏿
🧗‍♂️ 🧗🏻‍♂️ 🧗🏼‍♂️ 🧗🏽‍♂️ 🧗🏾‍♂️ 🧗🏿‍♂️
🧗‍♀️ 🧗🏻‍♀️ 🧗🏼‍♀️ 🧗🏽‍♀️ 🧗🏾‍♀️ 🧗🏿‍♀️
🤺
🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿
⛷️
🏂 🏂🏻 🏂🏼 🏂🏽 🏂🏾 🏂🏿
🏌️ 🏌🏻 🏌🏼 🏌🏽 🏌🏾 🏌🏿
🏌️‍♂️ 🏌🏻‍♂️ 🏌🏼‍♂️ 🏌🏽‍♂️ 🏌🏾‍♂️ 🏌🏿‍♂️
🏌️‍♀️ 🏌🏻‍♀️ 🏌🏼‍♀️ 🏌🏽‍♀️ 🏌🏾‍♀️ 🏌🏿‍♀️
🏄 🏄🏻 🏄🏼 🏄🏽 🏄🏾 🏄🏿
🏄‍♂️ 🏄🏻‍♂️ 🏄🏼‍♂️ 🏄🏽‍♂️ 🏄🏾‍♂️ 🏄🏿‍♂️
🏄‍♀️ 🏄🏻‍♀️ 🏄🏼‍♀️ 🏄🏽‍♀️ 🏄🏾‍♀️ 🏄🏿‍♀️
🚣 🚣🏻 🚣🏼 🚣🏽 🚣🏾 🚣🏿
🚣‍♂️ 🚣🏻‍♂️ 🚣🏼‍♂️ 🚣🏽‍♂️ 🚣🏾‍♂️ 🚣🏿‍♂️
🚣‍♀️ 🚣🏻‍♀️ 🚣🏼‍♀️ 🚣🏽‍♀️ 🚣🏾‍♀️ 🚣🏿‍♀️
🏊 🏊🏻 🏊🏼 🏊🏽 🏊🏾 🏊🏿
🏊‍♂️ 🏊🏻‍♂️ 🏊🏼‍♂️ 🏊🏽‍♂️ 🏊🏾‍♂️ 🏊🏿‍♂️
🏊‍♀️ 🏊🏻‍♀️ 🏊🏼‍♀️ 🏊🏽‍♀️ 🏊🏾‍♀️ 🏊🏿‍♀️
⛹️ ⛹🏻 ⛹🏼 ⛹🏽 ⛹🏾 ⛹🏿
⛹️‍♂️ ⛹🏻‍♂️ ⛹🏼‍♂️ ⛹🏽‍♂️ ⛹🏾‍♂️ ⛹🏿‍♂️
⛹️‍♀️ ⛹🏻‍♀️ ⛹🏼‍♀️ ⛹🏽‍♀️ ⛹🏾‍♀️ ⛹🏿‍♀️
🏋️ 🏋🏻 🏋🏼 🏋🏽 🏋🏾 🏋🏿
🏋️‍♂️ 🏋🏻‍♂️ 🏋🏼‍♂️ 🏋🏽‍♂️ 🏋🏾‍♂️ 🏋🏿‍♂️
🏋️‍♀️ 🏋🏻‍♀️ 🏋🏼‍♀️ 🏋🏽‍♀️ 🏋🏾‍♀️ 🏋🏿‍♀️
🚴 🚴🏻 🚴🏼 🚴🏽 🚴🏾 🚴🏿
🚴‍♂️ 🚴🏻‍♂️ 🚴🏼‍♂️ 🚴🏽‍♂️ 🚴🏾‍♂️ 🚴🏿‍♂️
🚴‍♀️ 🚴🏻‍♀️ 🚴🏼‍♀️ 🚴🏽‍♀️ 🚴🏾‍♀️ 🚴🏿‍♀️
🚵 🚵🏻 🚵🏼 🚵🏽 🚵🏾 🚵🏿
🚵‍♂️ 🚵🏻‍♂️ 🚵🏼‍♂️ 🚵🏽‍♂️ 🚵🏾‍♂️ 🚵🏿‍♂️
🚵‍♀️ 🚵🏻‍♀️ 🚵🏼‍♀️ 🚵🏽‍♀️ 🚵🏾‍♀️ 🚵🏿‍♀️
🤸 🤸🏻 🤸🏼 🤸🏽 🤸🏾 🤸🏿
🤸‍♂️ 🤸🏻‍♂️ 🤸🏼‍♂️ 🤸🏽‍♂️ 🤸🏾‍♂️ 🤸🏿‍♂️
🤸‍♀️ 🤸🏻‍♀️ 🤸🏼‍♀️ 🤸🏽‍♀️ 🤸🏾‍♀️ 🤸🏿‍♀️
🤼
🤼‍♂️
🤼‍♀️
🤽 🤽🏻 🤽🏼 🤽🏽 🤽🏾 🤽🏿
🤽‍♂️ 🤽🏻‍♂️ 🤽🏼‍♂️ 🤽🏽‍♂️ 🤽🏾‍♂️ 🤽🏿‍♂️
🤽‍♀️ 🤽🏻‍♀️ 🤽🏼‍♀️ 🤽🏽‍♀️ 🤽🏾‍♀️ 🤽🏿‍♀️
🤾 🤾🏻 🤾🏼 🤾🏽 🤾🏾 🤾🏿
🤾‍♂️ 🤾🏻‍♂️ 🤾🏼‍♂️ 🤾🏽‍♂️ 🤾🏾‍♂️ 🤾🏿‍♂️
🤾‍♀️ 🤾🏻‍♀️ 🤾🏼‍♀️ 🤾🏽‍♀️ 🤾🏾‍♀️ 🤾🏿‍♀️
🤹 🤹🏻 🤹🏼 🤹🏽 🤹🏾 🤹🏿
🤹‍♂️ 🤹🏻‍♂️ 🤹🏼‍♂️ 🤹🏽‍♂️ 🤹🏾‍♂️ 🤹🏿‍♂️
🤹‍♀️ 🤹🏻‍♀️ 🤹🏼‍♀️ 🤹🏽‍♀️ 🤹🏾‍♀️ 🤹🏿‍♀️
🧘 🧘🏻 🧘🏼 🧘🏽 🧘🏾 🧘🏿
🧘‍♂️ 🧘🏻‍♂️ 🧘🏼‍♂️ 🧘🏽‍♂️ 🧘🏾‍♂️ 🧘🏿‍♂️
🧘‍♀️ 🧘🏻‍♀️ 🧘🏼‍♀️ 🧘🏽‍♀️ 🧘🏾‍♀️ 🧘🏿‍♀️
🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿
🛌 🛌🏻 🛌🏼 🛌🏽 🛌🏾 🛌🏿
🧑‍🤝‍🧑 🧑🏻‍🤝‍🧑🏻 🧑🏻‍🤝‍🧑🏼 🧑🏻‍🤝‍🧑🏽 🧑🏻‍🤝‍🧑🏾 🧑🏻‍🤝‍🧑🏿 🧑🏼‍🤝‍🧑🏻 🧑🏼‍🤝‍🧑🏼 🧑🏼‍🤝‍🧑🏽 🧑🏼‍🤝‍🧑🏾 🧑🏼‍🤝‍🧑🏿 🧑🏽‍🤝‍🧑🏻 🧑🏽‍🤝‍🧑🏼 🧑🏽‍🤝‍🧑🏽 🧑🏽‍🤝‍🧑🏾 🧑🏽‍🤝‍🧑🏿 🧑🏾‍🤝‍🧑🏻 🧑🏾‍🤝‍🧑🏼 🧑🏾‍🤝‍🧑🏽 🧑🏾‍🤝‍🧑🏾 🧑🏾‍🤝‍🧑🏿 🧑🏿‍🤝‍🧑🏻 🧑🏿‍🤝‍🧑🏼 🧑🏿‍🤝‍🧑🏽 🧑🏿‍🤝‍🧑🏾 🧑🏿‍🤝‍🧑🏿
👭 👭🏻 👭🏼 👭🏽 👭🏾 👭🏿
👫 👫🏻 👫🏼 👫🏽 👫🏾 👫🏿
👬 👬🏻 👬🏼 👬🏽 👬🏾 👬🏿
💏 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿
👩‍❤️‍💋‍👨 👩🏻‍❤️‍💋‍👨🏻 👩🏻‍❤️‍💋‍👨🏼 👩🏻‍❤️‍💋‍👨🏽 👩🏻‍❤️‍💋‍👨🏾 👩🏻‍❤️‍💋‍👨🏿 👩🏼‍❤️‍💋‍👨🏻 👩🏼‍❤️‍💋‍👨🏼 👩🏼‍❤️‍💋‍👨🏽 👩🏼‍❤️‍💋‍👨🏾 👩🏼‍❤️‍💋‍👨🏿 👩🏽‍❤️‍💋‍👨🏻 👩🏽‍❤️‍💋‍👨🏼 👩🏽‍❤️‍💋‍👨🏽 👩🏽‍❤️‍💋‍👨🏾 👩🏽‍❤️‍💋‍👨🏿 👩🏾‍❤️‍💋‍👨🏻 👩🏾‍❤️‍💋‍👨🏼 👩🏾‍❤️‍💋‍👨🏽 👩🏾‍❤️‍💋‍👨🏾 👩🏾‍❤️‍💋‍👨🏿 👩🏿‍❤️‍💋‍👨🏻 👩🏿‍❤️‍💋‍👨🏼 👩🏿‍❤️‍💋‍👨🏽 👩🏿‍❤️‍💋‍👨🏾 👩🏿‍❤️‍💋‍👨🏿
👨‍❤️‍💋‍👨 👨🏻‍❤️‍💋‍👨🏻 👨🏻‍❤️‍💋‍👨🏼 👨🏻‍❤️‍💋‍👨🏽 👨🏻‍❤️‍💋‍👨🏾 👨🏻‍❤️‍💋‍👨🏿 👨🏼‍❤️‍💋‍👨🏻 👨🏼‍❤️‍💋‍👨🏼 👨🏼‍❤️‍💋‍👨🏽 👨🏼‍❤️‍💋‍👨🏾 👨🏼‍❤️‍💋‍👨🏿 👨🏽‍❤️‍💋‍👨🏻 👨🏽‍❤️‍💋‍👨🏼 👨🏽‍❤️‍💋‍👨🏽 👨🏽‍❤️‍💋‍👨🏾 👨🏽‍❤️‍💋‍👨🏿 👨🏾‍❤️‍💋‍👨🏻 👨🏾‍❤️‍💋‍👨🏼 👨🏾‍❤️‍💋‍👨🏽 👨🏾‍❤️‍💋‍👨🏾 👨🏾‍❤️‍💋‍👨🏿 👨🏿‍❤️‍💋‍👨🏻 👨🏿‍❤️‍💋‍👨🏼 👨🏿‍❤️‍💋‍👨🏽 👨🏿‍❤️‍💋‍👨🏾 👨🏿‍❤️‍💋‍👨🏿
👩‍❤️‍💋‍👩 👩🏻‍❤️‍💋‍👩🏻 👩🏻‍❤️‍💋‍👩🏼 👩🏻‍❤️‍💋‍👩🏽 👩🏻‍❤️‍💋‍👩🏾 👩🏻‍❤️‍💋‍👩🏿 👩🏼‍❤️‍💋‍👩🏻 👩🏼‍❤️‍💋‍👩🏼 👩🏼‍❤️‍💋‍👩🏽 👩🏼‍❤️‍💋‍👩🏾 👩🏼‍❤️‍💋‍👩🏿 👩🏽‍❤️‍💋‍👩🏻 👩🏽‍❤️‍💋‍👩🏼 👩🏽‍❤️‍💋‍👩🏽 👩🏽‍❤️‍💋‍👩🏾 👩🏽‍❤️‍💋‍👩🏿 👩🏾‍❤️‍💋‍👩🏻 👩🏾‍❤️‍💋‍👩🏼 👩🏾‍❤️‍💋‍👩🏽 👩🏾‍❤️‍💋‍👩🏾 👩🏾‍❤️‍💋‍👩🏿 👩🏿‍❤️‍💋‍👩🏻 👩🏿‍❤️‍💋‍👩🏼 👩🏿‍❤️‍💋‍👩🏽 👩🏿‍❤️‍💋‍👩🏾 👩🏿‍❤️‍💋‍👩🏿
💑 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿
👩‍❤️‍👨 👩🏻‍❤️‍👨🏻 👩🏻‍❤️‍👨🏼 👩🏻‍❤️‍👨🏽 👩🏻‍❤️‍👨🏾 👩🏻‍❤️‍👨🏿 👩🏼‍❤️‍👨🏻 👩🏼‍❤️‍👨🏼 👩🏼‍❤️‍👨🏽 👩🏼‍❤️‍👨🏾 👩🏼‍❤️‍👨🏿 👩🏽‍❤️‍👨🏻 👩🏽‍❤️‍👨🏼 👩🏽‍❤️‍👨🏽 👩🏽‍❤️‍👨🏾 👩🏽‍❤️‍👨🏿 👩🏾‍❤️‍👨🏻 👩🏾‍❤️‍👨🏼 👩🏾‍❤️‍👨🏽 👩🏾‍❤️‍👨🏾 👩🏾‍❤️‍👨🏿 👩🏿‍❤️‍👨🏻 👩🏿‍❤️‍👨🏼 👩🏿‍❤️‍👨🏽 👩🏿‍❤️‍👨🏾 👩🏿‍❤️‍👨🏿
👨‍❤️‍👨 👨🏻‍❤️‍👨🏻 👨🏻‍❤️‍👨🏼 👨🏻‍❤️‍👨🏽 👨🏻‍❤️‍👨🏾 👨🏻‍❤️‍👨🏿 👨🏼‍❤️‍👨🏻 👨🏼‍❤️‍👨🏼 👨🏼‍❤️‍👨🏽 👨🏼‍❤️‍👨🏾 👨🏼‍❤️‍👨🏿 👨🏽‍❤️‍👨🏻 👨🏽‍❤️‍👨🏼 👨🏽‍❤️‍👨🏽 👨🏽‍❤️‍👨🏾 👨🏽‍❤️‍👨🏿 👨🏾‍❤️‍👨🏻 👨🏾‍❤️‍👨🏼 👨🏾‍❤️‍👨🏽 👨🏾‍❤️‍👨🏾 👨🏾‍❤️‍👨🏿 👨🏿‍❤️‍👨🏻 👨🏿‍❤️‍👨🏼 👨🏿‍❤️‍👨🏽 👨🏿‍❤️‍👨🏾 👨🏿‍❤️‍👨🏿
👩‍❤️‍👩 👩🏻‍❤️‍👩🏻 👩🏻‍❤️‍👩🏼 👩🏻‍❤️‍👩🏽 👩🏻‍❤️‍👩🏾 👩🏻‍❤️‍👩🏿 👩🏼‍❤️‍👩🏻 👩🏼‍❤️‍👩🏼 👩🏼‍❤️‍👩🏽 👩🏼‍❤️‍👩🏾 👩🏼‍❤️‍👩🏿 👩🏽‍❤️‍👩🏻 👩🏽‍❤️‍👩🏼 👩🏽‍❤️‍👩🏽 👩🏽‍❤️‍👩🏾 👩🏽‍❤️‍👩🏿 👩🏾‍❤️‍👩🏻 👩🏾‍❤️‍👩🏼 👩🏾‍❤️‍👩🏽 👩🏾‍❤️‍👩🏾 👩🏾‍❤️‍👩🏿 👩🏿‍❤️‍👩🏻 👩🏿‍❤️‍👩🏼 👩🏿‍❤️‍👩🏽 👩🏿‍❤️‍👩🏾 👩🏿‍❤️‍👩🏿
👨‍👩‍👦
👨‍👩‍👧
👨‍👩‍👧‍👦
👨‍👩‍👦‍👦
👨‍👩‍👧‍👧
👨‍👨‍👦
👨‍👨‍👧
👨‍👨‍👧‍👦
👨‍👨‍👦‍👦
👨‍👨‍👧‍👧
👩‍👩‍👦
👩‍👩‍👧
👩‍👩‍👧‍👦
👩‍👩‍👦‍👦
👩‍👩‍👧‍👧
👨‍👦
👨‍👦‍👦
👨‍👧
👨‍👧‍👦
👨‍👧‍👧
👩‍👦
👩‍👦‍👦
👩‍👧
👩‍👧‍👦
👩‍👧‍👧
🗣️
👤
👥
🫂
👪
🧑‍🧑‍🧒
🧑‍🧑‍🧒‍🧒
🧑‍🧒
🧑‍🧒‍🧒
👣
🫆

View file

@ -0,0 +1,169 @@
😀
😃
😄
😁
😆
😅
🤣
😂
🙂
🙃
🫠
😉
😊
😇
🥰
😍
🤩
😘
😗
☺️
😚
😙
🥲
😋
😛
😜
🤪
😝
🤑
🤗
🤭
🫢
🫣
🤫
🤔
🫡
🤐
🤨
😐
😑
😶
🫥
😶‍🌫️
😏
😒
🙄
😬
😮‍💨
🤥
🫨
🙂‍↔️
🙂‍↕️
😌
😔
😪
🤤
😴
🫩
😷
🤒
🤕
🤢
🤮
🤧
🥵
🥶
🥴
😵
😵‍💫
🤯
🤠
🥳
🥸
😎
🤓
🧐
😕
🫤
😟
🙁
☹️
😮
😯
😲
😳
🥺
🥹
😦
😧
😨
😰
😥
😢
😭
😱
😖
😣
😞
😓
😩
😫
🥱
😤
😡
😠
🤬
😈
👿
💀
☠️
💩
🤡
👹
👺
👻
👽
👾
🤖
😺
😸
😹
😻
😼
😽
🙀
😿
😾
🙈
🙉
🙊
💌
💘
💝
💖
💗
💓
💞
💕
💟
❣️
💔
❤️‍🔥
❤️‍🩹
❤️
🩷
🧡
💛
💚
💙
🩵
💜
🤎
🖤
🩶
🤍
💋
💯
💢
💥
💫
💦
💨
🕳️
💬
👁️‍🗨️
🗨️
🗯️
💭
💤

View file

@ -0,0 +1,224 @@
🏧
🚮
🚰
🚹
🚺
🚻
🚼
🚾
🛂
🛃
🛄
🛅
⚠️
🚸
🚫
🚳
🚭
🚯
🚱
🚷
📵
🔞
☢️
☣️
⬆️
↗️
➡️
↘️
⬇️
↙️
⬅️
↖️
↕️
↔️
↩️
↪️
⤴️
⤵️
🔃
🔄
🔙
🔚
🔛
🔜
🔝
🛐
⚛️
🕉️
✡️
☸️
☯️
✝️
☦️
☪️
☮️
🕎
🔯
🪯
🔀
🔁
🔂
▶️
⏭️
⏯️
◀️
⏮️
🔼
🔽
⏸️
⏹️
⏺️
⏏️
🎦
🔅
🔆
📶
🛜
📳
📴
♀️
♂️
⚧️
✖️
🟰
♾️
‼️
⁉️
〰️
💱
💲
⚕️
♻️
⚜️
🔱
📛
🔰
☑️
✔️
〽️
✳️
✴️
❇️
©️
®️
™️
🫟
#️⃣
*️⃣
0
1
2
3
4
5
6
7
8
9
🔟
🔠
🔡
🔢
🔣
🔤
🅰️
🆎
🅱️
🆑
🆒
🆓
🆔
Ⓜ️
🆕
🆖
🅾️
🆗
🅿️
🆘
🆙
🆚
🈁
🈂️
🈷️
🈶
🈯
🉐
🈹
🈚
🈲
🉑
🈸
🈴
🈳
㊗️
㊙️
🈺
🈵
🔴
🟠
🟡
🟢
🔵
🟣
🟤
🟥
🟧
🟨
🟩
🟦
🟪
🟫
◼️
◻️
▪️
▫️
🔶
🔷
🔸
🔹
🔺
🔻
💠
🔘
🔳
🔲

View file

@ -0,0 +1,218 @@
🌍
🌎
🌏
🌐
🗺️
🗾
🧭
🏔️
⛰️
🌋
🗻
🏕️
🏖️
🏜️
🏝️
🏞️
🏟️
🏛️
🏗️
🧱
🪨
🪵
🛖
🏘️
🏚️
🏠
🏡
🏢
🏣
🏤
🏥
🏦
🏨
🏩
🏪
🏫
🏬
🏭
🏯
🏰
💒
🗼
🗽
🕌
🛕
🕍
⛩️
🕋
🌁
🌃
🏙️
🌄
🌅
🌆
🌇
🌉
♨️
🎠
🛝
🎡
🎢
💈
🎪
🚂
🚃
🚄
🚅
🚆
🚇
🚈
🚉
🚊
🚝
🚞
🚋
🚌
🚍
🚎
🚐
🚑
🚒
🚓
🚔
🚕
🚖
🚗
🚘
🚙
🛻
🚚
🚛
🚜
🏎️
🏍️
🛵
🦽
🦼
🛺
🚲
🛴
🛹
🛼
🚏
🛣️
🛤️
🛢️
🛞
🚨
🚥
🚦
🛑
🚧
🛟
🛶
🚤
🛳️
⛴️
🛥️
🚢
✈️
🛩️
🛫
🛬
🪂
💺
🚁
🚟
🚠
🚡
🛰️
🚀
🛸
🛎️
🧳
⏱️
⏲️
🕰️
🕛
🕧
🕐
🕜
🕑
🕝
🕒
🕞
🕓
🕟
🕔
🕠
🕕
🕡
🕖
🕢
🕗
🕣
🕘
🕤
🕙
🕥
🕚
🕦
🌑
🌒
🌓
🌔
🌕
🌖
🌗
🌘
🌙
🌚
🌛
🌜
🌡️
☀️
🌝
🌞
🪐
🌟
🌠
🌌
☁️
⛈️
🌤️
🌥️
🌦️
🌧️
🌨️
🌩️
🌪️
🌫️
🌬️
🌀
🌈
🌂
☂️
⛱️
❄️
☃️
☄️
🔥
💧
🌊

File diff suppressed because one or more lines are too long

View file

@ -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 }
]
]

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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" }
]
]

View 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

View 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

View 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

View 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

View file

@ -70,7 +70,12 @@ public class ProximityInfo {
return;
}
computeNearestNeighbors();
try {
mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
} catch (Throwable e) {
Log.e(TAG, "could not create proximity info", e);
mNativeProximityInfo = 0;
}
}
private long mNativeProximityInfo;

View file

@ -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())
}
/**

View file

@ -0,0 +1,13 @@
package helium314.keyboard.compat
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
fun isDeviceLocked(context: Context): Boolean {
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
keyguardManager.isDeviceLocked
else
keyguardManager.isKeyguardLocked
}

View file

@ -29,30 +29,18 @@ import java.util.*
* cursor: we'll start after this.
* @param initialText The text that has already been combined so far.
*/
class CombinerChain(initialText: String) {
class CombinerChain(initialText: String, combiningSpec: String) {
// The already combined text, as described above
private val mCombinedText = StringBuilder(initialText)
// The feedback on the composing state, as described above
private val mStateFeedback = SpannableStringBuilder()
private val mCombiners = ArrayList<Combiner>()
// Hangul combiner affects other scripts, e.g. period is seen as port of a word for latin,
// so we need to remove the combiner when not writing in hangul script.
// Maybe it would be better to always have the Hangul combiner, but make sure it doesn't affect
// events for other scripts, but how?
// todo: this really should be done properly, hangul combiner should do nothing when it's not needed
var isHangul = false
set(value) {
if (field == value) return
field = value
if (!value)
mCombiners.removeAll { it is HangulCombiner }
else if (mCombiners.none { it is HangulCombiner })
mCombiners.add(HangulCombiner())
}
init {
// The dead key combiner is always active, and always first
mCombiners.add(DeadKeyCombiner())
if (combiningSpec == "hangul")
mCombiners.add(HangulCombiner())
}
fun reset() {

View file

@ -139,6 +139,16 @@ class Event private constructor(
null, if (isKeyRepeat) FLAG_REPEAT else FLAG_NONE, null)
}
// A helper method to split the code point and the key code.
// todo: Ultimately, they should not be squashed into the same variable, and this method should be removed.
@JvmStatic
fun createSoftwareKeypressEvent(keyCodeOrCodePoint: Int, metaState: Int, keyX: Int, keyY: Int, isKeyRepeat: Boolean) =
if (keyCodeOrCodePoint <= 0) {
createSoftwareKeypressEvent(NOT_A_CODE_POINT, keyCodeOrCodePoint, metaState, keyX, keyY, isKeyRepeat)
} else {
createSoftwareKeypressEvent(keyCodeOrCodePoint, NOT_A_KEY_CODE, metaState, keyX, keyY, isKeyRepeat)
}
fun createHardwareKeypressEvent(codePoint: Int, keyCode: Int, metaState: Int, next: Event?, isKeyRepeat: Boolean): Event {
return Event(EVENT_TYPE_INPUT_KEYPRESS, null, codePoint, keyCode, metaState,
Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE,
@ -256,11 +266,9 @@ class Event private constructor(
source.mX, source.mY, source.mSuggestedWordInfo, source.mFlags or FLAG_COMBINING, source.mNextEvent)
}
fun createNotHandledEvent(): Event {
return Event(EVENT_TYPE_NOT_HANDLED, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, 0,
val notHandledEvent = Event(EVENT_TYPE_NOT_HANDLED, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, 0,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, null, FLAG_NONE, null)
}
}
// This method is private - to create a new event, use one of the create* utility methods.
init {

View file

@ -16,6 +16,8 @@ class HangulCombiner : Combiner {
override fun processEvent(previousEvents: ArrayList<Event>?, event: Event): Event {
if (event.mKeyCode == KeyCode.SHIFT) return event
// previously we only used the combiner if codePoint > 0x1100 or codePoint == -1, but looks here it's not necessary
val event = HangulEventDecoder.decodeSoftwareKeyEvent(event)
if (Character.isWhitespace(event.mCodePoint)) {
val text = combiningStateFeedback
reset()

View file

@ -24,7 +24,8 @@ class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
// KeyEvent#getUnicodeChar() does not exactly returns a unicode char, but rather a value
// that includes both the unicode char in the lower 21 bits and flags in the upper bits,
// hence the name "codePointAndFlags". {@see KeyEvent#getUnicodeChar()} for more info.
val codePointAndFlags = keyEvent.unicodeChar
val codePointAndFlags = keyEvent.unicodeChar.takeIf { it != 0 }
?: Event.NOT_A_CODE_POINT // KeyEvent has 0 if no codePoint, but that's actually valid so we convert it to -1
// The keyCode is the abstraction used by the KeyEvent to represent different keys that
// do not necessarily map to a unicode character. This represents a physical key, like
// the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock.
@ -48,6 +49,21 @@ class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
} else Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, metaState, null, isKeyRepeat)
// If not Enter, then this is just a regular keypress event for a normal character
// that can be committed right away, taking into account the current state.
} else Event.createNotHandledEvent()
} else if (isDpadDirection(keyCode)) {
Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, metaState, null, isKeyRepeat)
// } else if (KeyEvent.isModifierKey(keyCode)) {
// todo: we could synchronize meta state across HW and SW keyboard, but that's more work for little benefit (especially with shift & caps lock)
} else {
Event.notHandledEvent
}
}
companion object {
private fun isDpadDirection(keyCode: Int) = when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_DOWN_LEFT, KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, KeyEvent.KEYCODE_DPAD_UP_RIGHT,
KeyEvent.KEYCODE_DPAD_UP_LEFT -> true
else -> false
}
}
}

View file

@ -518,11 +518,7 @@ public class Key implements Comparable<Key> {
}
public final boolean isModifier() {
return switch (mCode) {
case KeyCode.SHIFT, KeyCode.SYMBOL_ALPHA, KeyCode.ALPHA, KeyCode.SYMBOL, KeyCode.NUMPAD, KeyCode.CTRL,
KeyCode.ALT, KeyCode.FN, KeyCode.META -> true;
default -> false;
};
return KeyCode.INSTANCE.isModifier(mCode);
}
public final boolean isRepeatable() {

View file

@ -6,6 +6,8 @@
package helium314.keyboard.keyboard;
import android.view.KeyEvent;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.common.InputPointers;
@ -31,6 +33,12 @@ public interface KeyboardActionListener {
*/
void onReleaseKey(int primaryCode, boolean withSliding);
/** For handling hardware key presses. Returns whether the event was handled. */
boolean onKeyDown(int keyCode, KeyEvent keyEvent);
/** For handling hardware key presses. Returns whether the event was handled. */
boolean onKeyUp(int keyCode, KeyEvent keyEvent);
/**
* Send a key code to the listener.
*
@ -117,6 +125,10 @@ public interface KeyboardActionListener {
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { return false; }
@Override
public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { return false; }
@Override
public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {}
@Override
public void onTextInput(String text) {}

View file

@ -1,22 +1,38 @@
package helium314.keyboard.keyboard
import android.text.InputType
import android.util.SparseArray
import android.view.KeyEvent
import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.event.Event
import helium314.keyboard.event.HangulEventDecoder
import helium314.keyboard.event.HardwareEventDecoder
import helium314.keyboard.event.HardwareKeyboardEventDecoder
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.EmojiAltPhysicalKeyDetector
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputMethodManager
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.InputPointers
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.combiningRange
import helium314.keyboard.latin.common.loopOverCodePoints
import helium314.keyboard.latin.common.loopOverCodePointsBackwards
import helium314.keyboard.latin.define.ProductionFlags
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 emojiAltPhysicalKeyDetector by lazy { EmojiAltPhysicalKeyDetector(latinIME.resources) }
// We expect to have only one decoder in almost all cases, hence the default capacity of 1.
// If it turns out we need several, it will get grown seamlessly.
private val hardwareEventDecoders: SparseArray<HardwareEventDecoder> = SparseArray(1)
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?
@ -29,9 +45,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()
@ -49,9 +71,62 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
keyboardSwitcher.onReleaseKey(primaryCode, withSliding, latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
}
override fun onKeyUp(keyCode: Int, keyEvent: KeyEvent): Boolean {
emojiAltPhysicalKeyDetector.onKeyUp(keyEvent)
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
return false
val keyIdentifier = keyEvent.deviceId.toLong() shl 32 + keyEvent.keyCode
return inputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)
}
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
emojiAltPhysicalKeyDetector.onKeyDown(keyEvent)
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
return false
val event: Event
if (settings.current.mLocale.language == "ko") { // todo: this does not appear to be the right place
val subtype = keyboardSwitcher.keyboard?.mId?.mSubtype ?: RichInputMethodManager.getInstance().currentSubtype
event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent) {
getHardwareKeyEventDecoder(keyEvent.deviceId).decodeHardwareKey(keyEvent)
}
} else {
event = getHardwareKeyEventDecoder(keyEvent.deviceId).decodeHardwareKey(keyEvent)
}
if (event.isHandled) {
inputLogic.onCodeInput(
settings.current, event,
keyboardSwitcher.getKeyboardShiftMode(), // TODO: this is not necessarily correct for a hardware keyboard right now
keyboardSwitcher.getCurrentKeyboardScript(),
latinIME.mHandler
)
return true
}
return false
}
override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) {
when (primaryCode) {
KeyCode.TOGGLE_AUTOCORRECT -> return Settings.getInstance().toggleAutoCorrect()
KeyCode.TOGGLE_INCOGNITO_MODE -> return Settings.getInstance().toggleAlwaysIncognitoMode()
}
val mkv = keyboardSwitcher.mainKeyboardView
latinIME.onCodeInput(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
// checking if the character is a combining accent
val event = if (primaryCode in combiningRange) { // todo: should this be done later, maybe in inputLogic?
Event.createSoftwareDeadEvent(primaryCode, 0, metaState, mkv.getKeyX(x), mkv.getKeyY(y), null)
} else {
// todo:
// setting meta shift should only be done for arrow and similar cursor movement keys
// should only be enabled once it works more reliably (currently depends on app for some reason)
// if (mkv.keyboard?.mId?.isAlphabetShiftedManually == true)
// Event.createSoftwareKeypressEvent(primaryCode, metaState or KeyEvent.META_SHIFT_ON, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
// else Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
}
latinIME.onEvent(event)
}
override fun onTextInput(text: String?) = latinIME.onTextInput(text)
@ -71,8 +146,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
}
@ -102,30 +178,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)
}
@ -136,7 +216,7 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
private fun onLanguageSlide(steps: Int): Boolean {
if (abs(steps) < settings.current.mLanguageSwipeDistance) return false
val subtypes = RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypeList(false)
val subtypes = RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypes(true)
if (subtypes.size <= 1) { // only allow if we have more than one subtype
return false
}
@ -144,17 +224,18 @@ 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--
KeyboardSwitcher.getInstance().switchToSubtype(newSubtype)
@ -174,17 +255,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
@ -194,18 +266,11 @@ 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)
}
@ -225,16 +290,46 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
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 when moving through long words
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
inputLogic.mConnection.setSelection(newPosition, newPosition)
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)
}
private fun getHardwareKeyEventDecoder(deviceId: Int): HardwareEventDecoder {
hardwareEventDecoders.get(deviceId)?.let { return it }
// TODO: create the decoder according to the specification
val newDecoder = HardwareKeyboardEventDecoder(deviceId)
hardwareEventDecoders.put(deviceId, newDecoder)
return newDecoder
}
}

View file

@ -184,6 +184,11 @@ public final class KeyboardId {
|| mElementId == ELEMENT_ALPHABET_AUTOMATIC_SHIFTED || mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public boolean isAlphabetShiftedManually() {
return mElementId == ELEMENT_ALPHABET_SHIFT_LOCKED || mElementId == ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
|| mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public boolean isNumberLayout() {
return mElementId == ELEMENT_NUMBER || mElementId == ELEMENT_NUMPAD
|| mElementId == ELEMENT_PHONE || mElementId == ELEMENT_PHONE_SYMBOLS;

View file

@ -6,12 +6,11 @@
package helium314.keyboard.keyboard;
import android.app.KeyguardManager;
import android.content.Context;
import android.os.Build;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import helium314.keyboard.compat.IsLockedCompatKt;
import helium314.keyboard.keyboard.internal.KeyboardBuilder;
import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.KeyboardParams;
@ -96,7 +95,7 @@ public final class KeyboardLayoutSet {
public static void onSystemLocaleChanged() {
clearKeyboardCache();
LocaleKeyboardInfosKt.clearCache();
SubtypeLocaleUtils.clearDisplayNameCache();
SubtypeLocaleUtils.clearSubtypeDisplayNameCache();
}
public static void onKeyboardThemeChanged() {
@ -207,14 +206,8 @@ public final class KeyboardLayoutSet {
params.mEditorInfo = editorInfo;
params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
// When the device is still locked, features like showing the IME setting app need to
// be locked down.
final KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
params.mDeviceLocked = km.isDeviceLocked();
} else {
params.mDeviceLocked = km.isKeyguardLocked();
}
// When the device is still locked, features like showing the IME setting app need to be locked down.
params.mDeviceLocked = IsLockedCompatKt.isDeviceLocked(context);
}
public static KeyboardLayoutSet buildEmojiClipBottomRow(final Context context, @Nullable final EditorInfo ei) {

View file

@ -20,6 +20,7 @@ import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
@ -49,8 +50,8 @@ import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ResourceUtils;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional;
import helium314.keyboard.latin.utils.ToolbarMode;
public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private static final String TAG = KeyboardSwitcher.class.getSimpleName();
@ -64,6 +65,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private LinearLayout mClipboardStripView;
private HorizontalScrollView mClipboardStripScrollView;
private SuggestionStripView mSuggestionStripView;
private FrameLayout mStripContainer;
private ClipboardHistoryView mClipboardHistoryView;
private TextView mFakeToastView;
private LatinIME mLatinIME;
@ -136,7 +138,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return false;
}
public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
private void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
final int currentAutoCapsState, final int currentRecapitalizeState) {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
@ -157,10 +159,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 +170,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());
}
}
}
@ -308,6 +309,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
@NonNull final SettingsValues settingsValues,
@NonNull final KeyboardSwitchState toggleState) {
final int visibility = isImeSuppressedByHardwareKeyboard(settingsValues, toggleState) ? View.GONE : View.VISIBLE;
final int stripVisibility = settingsValues.mToolbarMode == ToolbarMode.HIDDEN ? View.GONE : View.VISIBLE;
mStripContainer.setVisibility(stripVisibility);
PointerTracker.switchTo(mKeyboardView);
mKeyboardView.setVisibility(visibility);
// The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
@ -318,7 +321,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mEmojiPalettesView.stopEmojiPalettes();
mEmojiTabStripView.setVisibility(View.GONE);
mClipboardStripScrollView.setVisibility(View.GONE);
mSuggestionStripView.setVisibility(View.VISIBLE);
mSuggestionStripView.setVisibility(stripVisibility);
mClipboardHistoryView.setVisibility(View.GONE);
mClipboardHistoryView.stopClipboardHistory();
}
@ -335,6 +338,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
// @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
mKeyboardView.setVisibility(View.GONE);
mSuggestionStripView.setVisibility(View.GONE);
mStripContainer.setVisibility(getSecondaryStripVisibility());
mClipboardStripScrollView.setVisibility(View.GONE);
mEmojiTabStripView.setVisibility(View.VISIBLE);
mClipboardHistoryView.setVisibility(View.GONE);
@ -356,6 +360,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mKeyboardView.setVisibility(View.GONE);
mEmojiTabStripView.setVisibility(View.GONE);
mSuggestionStripView.setVisibility(View.GONE);
mStripContainer.setVisibility(getSecondaryStripVisibility());
mClipboardStripScrollView.post(() -> mClipboardStripScrollView.fullScroll(HorizontalScrollView.FOCUS_RIGHT));
mClipboardStripScrollView.setVisibility(View.VISIBLE);
mEmojiPalettesView.setVisibility(View.GONE);
@ -477,10 +482,13 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setOneHandedModeEnabled(boolean enabled) {
if (mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
setOneHandedModeEnabled(enabled, false);
}
public void setOneHandedModeEnabled(boolean enabled, boolean force) {
if (!force && mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
return;
}
mEmojiPalettesView.clearKeyboardCache();
final Settings settings = Settings.getInstance();
mKeyboardViewWrapper.setOneHandedModeEnabled(enabled);
mKeyboardViewWrapper.setOneHandedGravity(settings.getCurrent().mOneHandedModeGravity);
@ -511,11 +519,18 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
!settings.getCurrent().mIsSplitKeyboardEnabled,
mCurrentOrientation == Configuration.ORIENTATION_LANDSCAPE
);
setOneHandedModeEnabled(settings.getCurrent().mOneHandedModeEnabled, true);
reloadKeyboard();
}
public void reloadKeyboard() {
if (mCurrentInputView != null)
if (mCurrentInputView == null)
return;
mEmojiPalettesView.clearKeyboardCache();
reloadMainKeyboard();
}
public void reloadMainKeyboard() {
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
}
@ -539,6 +554,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
}
private static int getSecondaryStripVisibility() {
return Settings.getValues().mSecondaryStripVisible? View.VISIBLE : View.GONE;
}
// Displays a toast-like message with the provided text for a specified duration.
private void showFakeToast(final String text, final int timeMillis) {
if (mFakeToastView.getVisibility() == View.VISIBLE) return;
@ -582,7 +601,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
if (mKeyboardView == null || !mKeyboardView.isShown()) {
return false;
}
int activeKeyboardId = mKeyboardView.getKeyboard().mId.mElementId;
final Keyboard keyboard = mKeyboardView.getKeyboard();
if (keyboard == null) // may happen when using hardware keyboard
return false;
int activeKeyboardId = keyboard.mId.mElementId;
for (int keyboardId : keyboardIds) {
if (activeKeyboardId == keyboardId) {
return true;
@ -606,6 +628,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return mKeyboardView.isShowingPopupKeysPanel();
}
public boolean isShowingStripContainer() {
return mStripContainer.isShown();
}
public View getVisibleKeyboardView() {
if (isShowingEmojiPalettes()) {
return mEmojiPalettesView;
@ -631,6 +657,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return mKeyboardView;
}
public FrameLayout getStripContainer() { return mStripContainer; }
public void deallocateMemory() {
if (mKeyboardView != null) {
mKeyboardView.cancelAllOngoingEvents();
@ -644,6 +672,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) {
@ -678,6 +712,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mClipboardStripView = mCurrentInputView.findViewById(R.id.clipboard_strip);
mClipboardStripScrollView = mCurrentInputView.findViewById(R.id.clipboard_strip_scroll_view);
mSuggestionStripView = mCurrentInputView.findViewById(R.id.suggestion_strip_view);
mStripContainer = mCurrentInputView.findViewById(R.id.strip_container);
prefs.registerOnSharedPreferenceChangeListener(mSuggestionStripView);
prefs.registerOnSharedPreferenceChangeListener(mClipboardHistoryView);

View file

@ -38,7 +38,7 @@ import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.common.StringUtilsKt;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.suggestions.MoreSuggestions;
import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
import helium314.keyboard.latin.suggestions.MoreSuggestionsView;
import helium314.keyboard.latin.utils.TypefaceUtils;
import java.util.HashSet;
@ -109,7 +109,7 @@ public class KeyboardView extends View {
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
if (this instanceof PopupSuggestionsView)
if (this instanceof MoreSuggestionsView)
mKeyBackground = mColors.selectAndColorDrawable(keyboardViewAttr, ColorType.MORE_SUGGESTIONS_WORD_BACKGROUND);
else if (this instanceof PopupKeysKeyboardView)
mKeyBackground = mColors.selectAndColorDrawable(keyboardViewAttr, ColorType.POPUP_KEYS_BACKGROUND);
@ -148,6 +148,7 @@ public class KeyboardView extends View {
mPaint.setAntiAlias(true);
mTypeface = Settings.getInstance().getCustomTypeface();
setFitsSystemWindows(true);
}
@Nullable
@ -192,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;
}
@ -629,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);

View file

@ -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;
@ -359,25 +360,21 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
public void onKeyPressed(@NonNull final Key key, final boolean withPreview) {
key.onPressed();
invalidateKey(key);
if (withPreview && !key.noKeyPreview()) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
mKeyPreviewDrawParams.setVisibleOffset(-keyboard.mVerticalGap);
if (withPreview && !key.noKeyPreview() && mKeyPreviewDrawParams.isPopupEnabled()) {
showKeyPreview(key);
}
}
private void showKeyPreview(@NonNull final Key key) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
if (!previewParams.isPopupEnabled()) {
previewParams.setVisibleOffset(-keyboard.mVerticalGap);
return;
}
locatePreviewPlacerView();
getLocationInWindow(mOriginCoords);
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, keyboard.mIconsSet, getKeyDrawParams(),
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, getKeyboard().mIconsSet, getKeyDrawParams(),
KeyboardSwitcher.getInstance().getWrapperView().getWidth(), mOriginCoords, mDrawingPreviewPlacerView);
}

View file

@ -12,15 +12,15 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import helium314.keyboard.accessibility.AccessibilityUtils;
import helium314.keyboard.accessibility.PopupKeysKeyboardAccessibilityDelegate;
import helium314.keyboard.keyboard.emoji.OnKeyEventListener;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
@ -39,7 +39,7 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
protected final KeyDetector mKeyDetector;
private Controller mController = EMPTY_CONTROLLER;
protected KeyboardActionListener mListener;
protected OnKeyEventListener mKeyEventListener;
protected EmojiViewCallback mEmojiViewCallback;
private int mOriginX;
private int mOriginY;
private Key mCurrentKey;
@ -122,7 +122,7 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
mListener = listener;
mKeyEventListener = null;
mEmojiViewCallback = null;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@ -131,9 +131,9 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
*/
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final OnKeyEventListener listener) {
final int pointX, final int pointY, final EmojiViewCallback emojiViewCallback) {
mListener = null;
mKeyEventListener = listener;
mEmojiViewCallback = emojiViewCallback;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@ -157,6 +157,9 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
var center = panelX + getMeasuredWidth() / 2;
// This is needed for cases where there's also a long text popup above this keyboard
controller.setLayoutGravity(center < pointX? Gravity.RIGHT : center > pointX? Gravity.LEFT : Gravity.CENTER_HORIZONTAL);
controller.onShowPopupKeysPanel(this);
final PopupKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
@ -222,8 +225,8 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
false /* isKeyRepeat */);
}
}
} else if (mKeyEventListener != null) {
mKeyEventListener.onReleaseKey(key);
} else if (mEmojiViewCallback != null) {
mEmojiViewCallback.onReleaseKey(key);
}
}
@ -314,28 +317,4 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
}
return super.onHoverEvent(event);
}
private View getContainerView() {
return (View)getParent();
}
@Override
public void showInParent(final ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
@Override
public void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
@Override
public boolean isShowingInParent() {
return (getContainerView().getParent() != null);
}
}

View file

@ -8,10 +8,17 @@ package helium314.keyboard.keyboard;
import android.view.View;
import android.view.ViewGroup;
import helium314.keyboard.keyboard.emoji.OnKeyEventListener;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
public interface PopupKeysPanel {
interface Controller {
/**
* Set the layout gravity.
* @param layoutGravity requested by the popup
*/
default void setLayoutGravity(int layoutGravity) {
}
/**
* Add the {@link PopupKeysPanel} to the target view.
* @param panel the panel to be shown.
@ -59,19 +66,18 @@ public interface PopupKeysPanel {
* Initializes the layout and event handling of this {@link PopupKeysPanel} and calls the
* controller's onShowPopupKeysPanel to add the panel's container view.
* Same as {@link PopupKeysPanel#showPopupKeysPanel(View, Controller, int, int, KeyboardActionListener)},
* but with a {@link OnKeyEventListener}.
* but with a {@link EmojiViewCallback}.
*
* @param parentView the parent view of this {@link PopupKeysPanel}
* @param controller the controller that can dismiss this {@link PopupKeysPanel}
* @param pointX x coordinate of this {@link PopupKeysPanel}
* @param pointY y coordinate of this {@link PopupKeysPanel}
* @param listener the listener that will receive keyboard action from this
* {@link PopupKeysPanel}.
* @param emojiViewCallback to receive keyboard actions from this {@link PopupKeysPanel}.
*/
// TODO: Currently the PopupKeysPanel is inside a container view that is added to the parent.
// Consider the simpler approach of placing the PopupKeysPanel itself into the parent view.
void showPopupKeysPanel(View parentView, Controller controller, int pointX,
int pointY, OnKeyEventListener listener);
int pointY, EmojiViewCallback emojiViewCallback);
/**
* Dismisses the popup keys panel and calls the controller's onDismissPopupKeysPanel to remove
@ -127,20 +133,35 @@ public interface PopupKeysPanel {
*/
int translateY(int y);
default View getContainerView() {
return (View) ((View) this).getParent();
}
/**
* Show this {@link PopupKeysPanel} in the parent view.
*
* @param parentView the {@link ViewGroup} that hosts this {@link PopupKeysPanel}.
*/
void showInParent(ViewGroup parentView);
default void showInParent(ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
/**
* Remove this {@link PopupKeysPanel} from the parent view.
*/
void removeFromParent();
default void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
/**
* Return whether the panel is currently being shown.
*/
boolean isShowingInParent();
default boolean isShowingInParent() {
return getContainerView().getParent() != null;
}
}

View file

@ -0,0 +1,121 @@
/*
* Copyright (C) 2011 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.keyboard;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.CoordinateUtils;
import helium314.keyboard.latin.settings.Settings;
/**
* A view that displays popup text.
*/
public class PopupTextView extends TextView implements PopupKeysPanel {
private final int[] mCoordinates = CoordinateUtils.newInstance();
private final Typeface mTypeface;
private Controller mController = EMPTY_CONTROLLER;
private int mOriginX;
private int mOriginY;
private Key mKey;
private EmojiViewCallback mEmojiViewCallback;
public PopupTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.popupKeysKeyboardViewStyle);
}
public PopupTextView(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
mTypeface = Settings.getInstance().getCustomTypeface();
}
public void setKeyDrawParams(Key key, KeyDrawParams drawParams) {
mKey = key;
Settings.getValues().mColors.setBackground(this, ColorType.KEY_PREVIEW_BACKGROUND);
setTextColor(drawParams.mPreviewTextColor);
setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectHintTextSize(drawParams) << 1);
setTypeface(mTypeface == null ? key.selectTypeface(drawParams) : mTypeface);
}
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final EmojiViewCallback emojiViewCallback) {
mEmojiViewCallback = emojiViewCallback;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
private void showPopupKeysPanelInternal(final View parentView, final Controller controller,
final int pointX, final int pointY) {
mController = controller;
final View container = getContainerView();
// The coordinates of panel's left-top corner in parentView's coordinate system.
// We need to consider background drawable paddings.
final int x = pointX - getMeasuredWidth() / 2 - container.getPaddingLeft() - getPaddingLeft();
final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom()
+ getPaddingBottom();
parentView.getLocationInWindow(mCoordinates);
// Ensure the horizontal position of the panel does not extend past the parentView edges.
final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth();
final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates);
final int panelY = y + CoordinateUtils.y(mCoordinates);
container.setX(panelX);
container.setY(panelY);
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
controller.setLayoutGravity(Gravity.NO_GRAVITY);
controller.onShowPopupKeysPanel(this);
}
@Override
public void onDownEvent(final int x, final int y, final int pointerId, final long eventTime) {
}
@Override
public void onMoveEvent(final int x, final int y, final int pointerId, final long eventTime) {
}
@Override
public void onUpEvent(final int x, final int y, final int pointerId, final long eventTime) {
mEmojiViewCallback.onReleaseKey(mKey);
}
@Override
public void dismissPopupKeysPanel() {
if (!isShowingInParent()) {
return;
}
mController.onDismissPopupKeysPanel();
}
@Override
public int translateX(final int x) {
return x - mOriginX;
}
@Override
public int translateY(final int y) {
return y - mOriginY;
}
}

View file

@ -21,7 +21,6 @@ import helium314.keyboard.keyboard.MainKeyboardView
import helium314.keyboard.keyboard.PointerTracker
import helium314.keyboard.keyboard.internal.KeyDrawParams
import helium314.keyboard.keyboard.internal.KeyVisualAttributes
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.ClipboardHistoryManager
import helium314.keyboard.latin.R
@ -49,15 +48,14 @@ class ClipboardHistoryView @JvmOverloads constructor(
private val clipboardLayoutParams = ClipboardLayoutParams(context)
private val pinIconId: Int
private val keyBackgroundId: Int
private var initialized = false
private lateinit var clipboardRecyclerView: ClipboardHistoryRecyclerView
private lateinit var placeholderView: TextView
private val toolbarKeys = mutableListOf<ImageButton>()
private lateinit var clipboardAdapter: ClipboardAdapter
var keyboardActionListener: KeyboardActionListener? = null
var clipboardHistoryManager: ClipboardHistoryManager? = null
lateinit var keyboardActionListener: KeyboardActionListener
private var clipboardHistoryManager: ClipboardHistoryManager? = null
init {
val clipboardViewAttr = context.obtainStyledAttributes(attrs,
@ -67,10 +65,11 @@ class ClipboardHistoryView @JvmOverloads constructor(
val keyboardViewAttr = context.obtainStyledAttributes(attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView)
keyBackgroundId = keyboardViewAttr.getResourceId(R.styleable.KeyboardView_keyBackground, 0)
keyboardViewAttr.recycle()
val keyboardAttr = context.obtainStyledAttributes(attrs, R.styleable.Keyboard, defStyle, R.style.SuggestionStripView)
if (Settings.getValues().mSecondaryStripVisible) {
getEnabledClipboardToolbarKeys(context.prefs())
.forEach { toolbarKeys.add(createToolbarKey(context, KeyboardIconsSet.instance, it)) }
keyboardAttr.recycle()
.forEach { toolbarKeys.add(createToolbarKey(context, it)) }
}
fitsSystemWindows = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@ -78,13 +77,13 @@ class ClipboardHistoryView @JvmOverloads constructor(
val res = context.resources
// The main keyboard expands to the entire this {@link KeyboardView}.
val width = ResourceUtils.getKeyboardWidth(context, Settings.getValues()) + paddingLeft + paddingRight
val height = ResourceUtils.getKeyboardHeight(res, Settings.getValues()) + paddingTop + paddingBottom
val height = ResourceUtils.getSecondaryKeyboardHeight(res, Settings.getValues()) + paddingTop + paddingBottom
setMeasuredDimension(width, height)
}
@SuppressLint("ClickableViewAccessibility")
private fun initialize() { // needs to be delayed for access to ClipboardStrip, which is not a child of this view
if (initialized) return
if (this::clipboardAdapter.isInitialized) return
val colors = Settings.getValues().mColors
clipboardAdapter = ClipboardAdapter(clipboardLayoutParams, this).apply {
itemBackgroundId = keyBackgroundId
@ -107,7 +106,6 @@ class ClipboardHistoryView @JvmOverloads constructor(
colors.setColor(it, ColorType.TOOL_BAR_KEY)
colors.setBackground(it, ColorType.STRIP_BACKGROUND)
}
initialized = true
}
private fun setupClipKey(params: KeyDrawParams) {
@ -188,7 +186,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
}
fun stopClipboardHistory() {
if (!initialized) return
if (!this::clipboardAdapter.isInitialized) return
clipboardRecyclerView.adapter = null
clipboardHistoryManager?.setHistoryChangeListener(null)
clipboardHistoryManager = null
@ -200,7 +198,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
if (tag is ToolbarKey) {
val code = getCodeForToolbarKey(tag)
if (code != KeyCode.UNSPECIFIED) {
keyboardActionListener?.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
keyboardActionListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
return
}
}
@ -211,7 +209,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
if (tag is ToolbarKey) {
val longClickCode = getCodeForToolbarKeyLongClick(tag)
if (longClickCode != KeyCode.UNSPECIFIED) {
keyboardActionListener?.onCodeInput(
keyboardActionListener.onCodeInput(
longClickCode,
Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE,
@ -224,15 +222,15 @@ class ClipboardHistoryView @JvmOverloads constructor(
}
override fun onKeyDown(clipId: Long) {
keyboardActionListener?.onPressKey(KeyCode.NOT_SPECIFIED, 0, true)
keyboardActionListener.onPressKey(KeyCode.NOT_SPECIFIED, 0, true)
}
override fun onKeyUp(clipId: Long) {
val clipContent = clipboardHistoryManager?.getHistoryEntryContent(clipId)
keyboardActionListener?.onTextInput(clipContent?.content.toString())
keyboardActionListener?.onReleaseKey(KeyCode.NOT_SPECIFIED, false)
keyboardActionListener.onTextInput(clipContent?.content.toString())
keyboardActionListener.onReleaseKey(KeyCode.NOT_SPECIFIED, false)
if (Settings.getValues().mAlphaAfterClipHistoryEntry)
keyboardActionListener?.onCodeInput(KeyCode.ALPHA, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
keyboardActionListener.onCodeInput(KeyCode.ALPHA, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
}
override fun onClipboardHistoryEntryAdded(at: Int) {

View file

@ -3,7 +3,6 @@
package helium314.keyboard.keyboard.clipboard
import android.content.Context
import android.content.res.Resources
import android.view.View
import android.widget.FrameLayout
import androidx.recyclerview.widget.RecyclerView
@ -22,7 +21,7 @@ class ClipboardLayoutParams(ctx: Context) {
init {
val res = ctx.resources
val sv = Settings.getValues()
val defaultKeyboardHeight = ResourceUtils.getKeyboardHeight(res, sv)
val defaultKeyboardHeight = ResourceUtils.getSecondaryKeyboardHeight(res, sv)
val defaultKeyboardWidth = ResourceUtils.getKeyboardWidth(ctx, sv)
if (sv.mNarrowKeyGaps) {

View file

@ -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);
@ -113,12 +112,12 @@ final class DynamicGridKeyboard extends Keyboard {
}
public int getDynamicOccupiedHeight() {
final int row = (mGridKeys.size() - 1) / mColumnsNum + 1;
final int row = (mGridKeys.size() - 1) / getOccupiedColumnCount() + 1;
return row * mVerticalStep;
}
public int getColumnsCount() {
return mColumnsNum;
public int getOccupiedColumnCount() {
return mColumnsNum - mEmptyColumnIndices.size();
}
public void addPendingKey(final Key usedKey) {
@ -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) {

View file

@ -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;
}
@ -338,7 +324,7 @@ final class EmojiCategory {
final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
0, 0, ResourceUtils.getKeyboardWidth(mContext, Settings.getValues()));
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getColumnsCount();
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getOccupiedColumnCount();
}
private static final Comparator<Key> EMOJI_KEY_COMPARATOR = (lhs, rhs) -> {

View file

@ -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
@ -22,7 +22,7 @@ internal class EmojiLayoutParams(res: Resources) {
init {
val sv = Settings.getValues()
val defaultKeyboardHeight = ResourceUtils.getKeyboardHeight(res, sv)
val defaultKeyboardHeight = ResourceUtils.getSecondaryKeyboardHeight(res, sv)
val keyVerticalGap = if (sv.mNarrowKeyGaps) {
res.getFraction(R.fraction.config_key_vertical_gap_holo_narrow,
@ -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

View file

@ -13,6 +13,9 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.LinearLayout;
import helium314.keyboard.keyboard.PopupTextView;
import helium314.keyboard.latin.utils.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@ -53,14 +56,18 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
private static final long KEY_PRESS_DELAY_TIME = 250; // msec
private static final long KEY_RELEASE_DELAY_TIME = 30; // msec
private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() {
private static final EmojiViewCallback EMPTY_EMOJI_VIEW_CALLBACK = new EmojiViewCallback() {
@Override
public void onPressKey(final Key key) {}
@Override
public void onReleaseKey(final Key key) {}
@Override
public String getDescription(String emoji) {
return null;
}
};
private OnKeyEventListener mListener = EMPTY_LISTENER;
private EmojiViewCallback mEmojiViewCallback = EMPTY_EMOJI_VIEW_CALLBACK;
private final KeyDetector mKeyDetector = new KeyDetector();
private KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate;
@ -74,6 +81,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
// More keys keyboard
private final View mPopupKeysKeyboardContainer;
private final PopupTextView mDescriptionView;
private final PopupKeysKeyboardView mPopupKeysKeyboardView;
private final WeakHashMap<Key, Keyboard> mPopupKeysKeyboardCache = new WeakHashMap<>();
private final boolean mConfigShowPopupKeysKeyboardAtTouchedPoint;
private final ViewGroup mPopupKeysPlacerView;
@ -102,6 +111,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
final LayoutInflater inflater = LayoutInflater.from(getContext());
mPopupKeysKeyboardContainer = inflater.inflate(popupKeysKeyboardLayoutId, null);
mDescriptionView = mPopupKeysKeyboardContainer.findViewById(R.id.description_view);
mPopupKeysKeyboardView = mPopupKeysKeyboardContainer.findViewById(R.id.popup_keys_keyboard_view);
}
@Override
@ -146,8 +157,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
}
}
public void setOnKeyEventListener(final OnKeyEventListener listener) {
mListener = listener;
public void setEmojiViewCallback(final EmojiViewCallback emojiViewCallback) {
mEmojiViewCallback = emojiViewCallback;
}
/**
@ -169,7 +180,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
}
@Nullable
public PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key, final int lastX, final int lastY) {
private PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key) {
mPopupKeysKeyboardView.setVisibility(GONE);
final PopupKeySpec[] popupKeys = key.getPopupKeys();
if (popupKeys == null) {
return null;
@ -182,21 +194,9 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
}
final View container = mPopupKeysKeyboardContainer;
final PopupKeysKeyboardView popupKeysKeyboardView = container.findViewById(R.id.popup_keys_keyboard_view);
popupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, lastX, lastY);
// The popup keys keyboard is usually horizontally aligned with the center of the parent key.
// If showPopupKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
// keys keyboard is placed at the touch point of the parent key.
final int pointX = mConfigShowPopupKeysKeyboardAtTouchedPoint
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY();
popupKeysKeyboardView.showPopupKeysPanel(this, this, pointX, pointY, mListener);
return popupKeysKeyboardView;
mPopupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
mPopupKeysKeyboardView.setVisibility(VISIBLE);
return mPopupKeysKeyboardView;
}
private void dismissPopupKeysPanel() {
@ -209,6 +209,17 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
return mPopupKeysPanel != null;
}
@Override
public void setLayoutGravity(int layoutGravity) {
var layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = mDescriptionView.getMeasuredWidth() > mPopupKeysKeyboardView.getMeasuredWidth()?
layoutGravity : Gravity.CENTER_HORIZONTAL;
mPopupKeysKeyboardContainer.setLayoutParams(layoutParams);
mDescriptionView.setLayoutParams(layoutParams);
mPopupKeysKeyboardView.setLayoutParams(layoutParams);
}
@Override
public void onShowPopupKeysPanel(final PopupKeysPanel panel) {
// install placer view only when needed instead of when this
@ -290,9 +301,11 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
return;
}
var descriptionPanel = showDescription(key);
final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key);
final int x = mLastX;
final int y = mLastY;
final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key, x, y);
if (popupKeysPanel != null) {
final int translatedX = popupKeysPanel.translateX(x);
final int translatedY = popupKeysPanel.translateY(y);
@ -301,6 +314,34 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
// want any scroll to append during this entire input.
disallowParentInterceptTouchEvent(true);
}
if (popupKeysPanel != null || descriptionPanel != null) {
mPopupKeysKeyboardContainer.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, x, y);
// The popup keys keyboard is usually horizontally aligned with the center of the parent key.
// If showPopupKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
// keys keyboard is placed at the touch point of the parent key.
final int pointX = mConfigShowPopupKeysKeyboardAtTouchedPoint
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY() - getKeyboard().mVerticalGap;
(popupKeysPanel != null? popupKeysPanel : descriptionPanel)
.showPopupKeysPanel(this, this, pointX, pointY, mEmojiViewCallback);
}
}
private PopupKeysPanel showDescription(Key key) {
mDescriptionView.setVisibility(GONE);
var description = mEmojiViewCallback.getDescription(key.getLabel());
if (description == null) {
return null;
}
mDescriptionView.setText(description);
mDescriptionView.setKeyDrawParams(key, getKeyDrawParams());
mDescriptionView.setVisibility(VISIBLE);
return mDescriptionView;
}
private void registerPress(final Key key) {
@ -318,7 +359,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
releasedKey.onReleased();
invalidateKey(releasedKey);
if (withKeyRegistering) {
mListener.onReleaseKey(releasedKey);
mEmojiViewCallback.onReleaseKey(releasedKey);
}
}
@ -326,7 +367,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
mPendingKeyDown = null;
pressedKey.onPressed();
invalidateKey(pressedKey);
mListener.onPressKey(pressedKey);
mEmojiViewCallback.onPressKey(pressedKey);
}
public void releaseCurrentKey(final boolean withKeyRegistering) {

View file

@ -7,161 +7,37 @@
package helium314.keyboard.keyboard.emoji;
import helium314.keyboard.latin.utils.Log;
import android.util.SparseArray;
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 OnKeyEventListener mListener;
private final DynamicGridKeyboard mRecentsKeyboard;
private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = new SparseArray<>();
private final int mCategoryId;
private final EmojiViewCallback mEmojiViewCallback;
private final EmojiCategory mEmojiCategory;
private int mActivePosition = 0;
public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
final OnKeyEventListener listener) {
public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final EmojiViewCallback emojiViewCallback) {
mEmojiCategory = emojiCategory;
mListener = listener;
mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
mCategoryId = categoryId;
mEmojiViewCallback = emojiViewCallback;
}
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);*/
keyboardView.setEmojiViewCallback(mEmojiViewCallback);
return new ViewHolder(keyboardView);
}
@ -170,33 +46,21 @@ 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);
holder.getKeyboardView().setKeyboard(keyboard);
holder.getKeyboardView().setOnKeyEventListener(mListener);
//parent.addView(keyboardView);
mActiveKeyboardViews.put(position, holder.getKeyboardView());
/*if (mActivePosition == position) {
return;
final Keyboard keyboard =
mEmojiCategory.getKeyboardFromAdapterPosition(mCategoryId, position);
holder.getKeyboardView().setKeyboard(keyboard);
}
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 +74,6 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
public EmojiPageKeyboardView getKeyboardView() {
return customView;
}
}
}

View file

@ -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;
@ -33,16 +38,18 @@ import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.AudioAndHapticFeedbackManager;
import helium314.keyboard.latin.DictionaryFactory;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.RichInputMethodManager;
import helium314.keyboard.latin.RichInputMethodSubtype;
import helium314.keyboard.latin.SingleDictionaryFacilitator;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.utils.DictionaryInfoUtils;
import helium314.keyboard.latin.utils.ResourceUtils;
import org.jetbrains.annotations.NotNull;
import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
/**
@ -57,27 +64,135 @@ import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
* Because of the above reasons, this class doesn't extend {@link KeyboardView}.
*/
public final class EmojiPalettesView extends LinearLayout
implements View.OnClickListener, OnKeyEventListener {
implements View.OnClickListener, EmojiViewCallback {
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 final 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 static SingleDictionaryFacilitator sDictionaryFacilitator;
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 +211,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
@ -115,19 +222,12 @@ public final class EmojiPalettesView extends LinearLayout
// The main keyboard expands to the entire this {@link KeyboardView}.
final int width = ResourceUtils.getKeyboardWidth(getContext(), Settings.getValues())
+ getPaddingLeft() + getPaddingRight();
final int height = ResourceUtils.getKeyboardHeight(res, Settings.getValues())
final int height = ResourceUtils.getSecondaryKeyboardHeight(res, Settings.getValues())
+ getPaddingTop() + getPaddingBottom();
mEmojiCategoryPageIndicatorView.mWidth = width;
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);
@ -146,63 +246,18 @@ public final class EmojiPalettesView extends LinearLayout
if (initialized) return;
mEmojiCategory.initialize();
mTabStrip = (LinearLayout) KeyboardSwitcher.getInstance().getEmojiTabStrip();
if (Settings.getValues().mSecondaryStripVisible) {
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,15 +274,14 @@ 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();
}
}
}
/**
* Called from {@link EmojiPageKeyboardView} through
* {@link helium314.keyboard.keyboard.emoji.OnKeyEventListener}
* Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
* interface to handle touch events from non-View-based elements such as Emoji buttons.
*/
@Override
@ -237,14 +291,13 @@ public final class EmojiPalettesView extends LinearLayout
}
/**
* Called from {@link EmojiPageKeyboardView} through
* {@link helium314.keyboard.keyboard.emoji.OnKeyEventListener}
* Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
* interface to handle touch events from non-View-based elements such as Emoji buttons.
* This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)}.
* This may be called without any prior call to {@link EmojiViewCallback#onPressKey(Key)}.
*/
@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());
@ -256,6 +309,20 @@ public final class EmojiPalettesView extends LinearLayout
mKeyboardActionListener.onCodeInput(KeyCode.ALPHA, NOT_A_COORDINATE, NOT_A_COORDINATE, false);
}
@Override
public String getDescription(String emoji) {
if (sDictionaryFacilitator == null) {
return null;
}
var wordProperty = sDictionaryFacilitator.getWordProperty(emoji);
if (wordProperty == null || ! wordProperty.mHasShortcuts) {
return null;
}
return wordProperty.mShortcutTargets.get(0).mWord;
}
public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
if (!enabled) return;
// TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
@ -269,11 +336,21 @@ 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();
initDictionaryFacilitator();
}
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) {
@ -295,11 +372,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 +389,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,24 +409,18 @@ 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 && ! isAnimationsDisabled());
}
if (Settings.getValues().mSecondaryStripVisible) {
final View old = mTabStrip.findViewWithTag((long) oldCategoryId);
final View current = mTabStrip.findViewWithTag((long) categoryId);
@ -356,8 +429,42 @@ public final class EmojiPalettesView extends LinearLayout
if (current instanceof ImageView)
Settings.getValues().mColors.setColor((ImageView) current, ColorType.EMOJI_CATEGORY_SELECTED);
}
}
}
private boolean isAnimationsDisabled() {
return android.provider.Settings.Global.getFloat(getContext().getContentResolver(),
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f;
}
public void clearKeyboardCache() {
if (!initialized) {
return;
}
mEmojiCategory.clearKeyboardCache();
mPager.getAdapter().notifyDataSetChanged();
closeDictionaryFacilitator();
}
private void initDictionaryFacilitator() {
if (Settings.getValues().mShowEmojiDescriptions) {
var locale = RichInputMethodManager.getInstance().getCurrentSubtype().getLocale();
if (sDictionaryFacilitator == null || ! sDictionaryFacilitator.isForLocale(locale)) {
closeDictionaryFacilitator();
var dictFile = DictionaryInfoUtils.getCachedDictForLocaleAndType(locale, "emoji", getContext());
var dictionary = dictFile != null? DictionaryFactory.getDictionary(dictFile, locale) : null;
sDictionaryFacilitator = dictionary != null? new SingleDictionaryFacilitator(dictionary) : null;
}
} else {
closeDictionaryFacilitator();
}
}
private static void closeDictionaryFacilitator() {
if (sDictionaryFacilitator != null) {
sDictionaryFacilitator.closeDictionaries();
sDictionaryFacilitator = null;
}
}
}

View file

@ -5,10 +5,10 @@ package helium314.keyboard.keyboard.emoji;
import helium314.keyboard.keyboard.Key;
/**
* Interface to handle touch events from non-View-based elements
* such as Emoji buttons.
* Interface to handle callbacks from child elements
* such as Emoji buttons and keyboard views.
*/
public interface OnKeyEventListener {
public interface EmojiViewCallback {
/**
* Called when a key is pressed by the user
@ -17,8 +17,13 @@ public interface OnKeyEventListener {
/**
* Called when a key is released.
* This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)},
* This may be called without any prior call to {@link EmojiViewCallback#onPressKey(Key)},
* for example when a key from a popup keys keyboard is selected by releasing touch on it.
*/
void onReleaseKey(Key key);
/**
* Called from keyboard view to get an emoji description
*/
String getDescription(String emoji);
}

View file

@ -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
}

View file

@ -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)

View file

@ -8,14 +8,9 @@ package helium314.keyboard.keyboard.internal;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Insets;
import android.os.Build;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -230,16 +225,6 @@ public class KeyboardParams {
mBottomPadding = (int) (keyboardAttr.getFraction(
R.styleable.Keyboard_keyboardBottomPadding, height, height, 0)
* Settings.getValues().mBottomPaddingScale);
if (Build.VERSION.SDK_INT >= 35) {
WindowManager wm = context.getSystemService(WindowManager.class);
WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
WindowInsets windowInsets = windowMetrics.getWindowInsets();
int insetTypes = WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout();
Insets insets = windowInsets.getInsetsIgnoringVisibility(insetTypes);
mBottomPadding += insets.bottom;
}
mLeftPadding = (int) (keyboardAttr.getFraction(
R.styleable.Keyboard_keyboardLeftPadding, width, width, 0)
* Settings.getValues().mSidePaddingScale);

View file

@ -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 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 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 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
val emojiKeyboardHeight = defaultKeyboardHeight * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
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 = ""

View file

@ -229,13 +229,15 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
return
// replace comma / period if 2 keys in normal bottom row
if (baseKeys.last().size == 2) {
val newComma = baseKeys.last()[0]
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.COMMA || it.groupId == KeyData.GROUP_COMMA},
{ baseKeys.last()[0].copy(newGroupId = 1, newType = baseKeys.last()[0].type ?: it.type) }
{ newComma.copy(newGroupId = 1, newType = newComma.type, newLabelFlags = it.labelFlags or newComma.labelFlags) }
)
val newPeriod = baseKeys.last()[1]
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.PERIOD || it.groupId == KeyData.GROUP_PERIOD},
{ baseKeys.last()[1].copy(newGroupId = 2, newType = baseKeys.last()[1].type ?: it.type) }
{ newPeriod.copy(newGroupId = 2, newType = newPeriod.type, newLabelFlags = it.labelFlags or newPeriod.labelFlags) }
)
baseKeys.removeAt(baseKeys.lastIndex)
}

View file

@ -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
@ -44,7 +45,7 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
"mns" -> Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO
else -> 0
}
val tlds = getLocaleTlds(locale)
val tlds = mutableListOf(Key.POPUP_KEYS_HAS_LABELS)
init {
readStream(dataStream, false, true)
@ -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 -> tlds.addAll(SpacedTokens(line).map { ".$it" })
}
}
}
@ -155,6 +156,16 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
}
}
fun addLocaleTlds(locale: Locale) {
tlds.add(0, comTld)
val ccLower = locale.country.lowercase()
if (ccLower.isNotEmpty() && locale.language != SubtypeLocaleUtils.NO_LANGUAGE) {
specialCountryTlds[ccLower]?.let { tlds.addAll(SpacedTokens(it)) } ?: tlds.add(".$ccLower")
}
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
tlds.addAll(SpacedTokens(otherDefaultTlds))
}
}
private fun addFixedColumnOrder(popupKeys: MutableCollection<String>) {
@ -204,6 +215,7 @@ private fun createLocaleKeyTexts(context: Context, params: KeyboardParams, popup
POPUP_KEYS_MORE -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_more.txt"), false)
POPUP_KEYS_ALL -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_all.txt"), false)
}
lkt.addLocaleTlds(params.mId.locale)
return lkt
}
@ -213,34 +225,12 @@ private fun getStreamForLocale(locale: Locale, context: Context) =
else context.assets.open("$LOCALE_TEXTS_FOLDER/${locale.toLanguageTag()}.txt")
} catch (_: Exception) {
try {
context.assets.open("$LOCALE_TEXTS_FOLDER/${locale.language}.txt")
context.assets.open("$LOCALE_TEXTS_FOLDER/${if (locale.language == "he") "iw" else locale.language}.txt")
} catch (_: Exception) {
null
}
}
private fun getLocaleTlds(locale: Locale): LinkedHashSet<String> {
val tlds = getDefaultTlds(locale)
val ccLower = locale.country.lowercase()
if (ccLower.isEmpty() || locale.language == SubtypeLocaleUtils.NO_LANGUAGE)
return tlds
specialCountryTlds.forEach {
if (ccLower != it.first) return@forEach
tlds.addAll(it.second.splitOnWhitespace())
return@getLocaleTlds tlds
}
tlds.add(".$ccLower")
return tlds
}
private fun getDefaultTlds(locale: Locale): LinkedHashSet<String> {
val tlds = linkedSetOf<String>()
tlds.addAll(defaultTlds.splitOnWhitespace())
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
return tlds
}
fun clearCache() = localeKeyboardInfosCache.clear()
// cache the texts, so they don't need to be read over and over
@ -264,9 +254,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 +282,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 -> "$"
}
}
@ -333,7 +330,7 @@ const val POPUP_KEYS_NORMAL = "normal"
private const val LOCALE_TEXTS_FOLDER = "locale_key_texts"
// either tld is not simply lowercase ISO 3166-1 code, or there are multiple according to some list
private val specialCountryTlds = listOf(
private val specialCountryTlds = hashMapOf<String, String>(
"bd" to ".bd .com.bd",
"bq" to ".bq .an .nl",
"bl" to ".bl .gp .fr",
@ -343,4 +340,5 @@ private val specialCountryTlds = listOf(
"mf" to ".mf .gp .fr",
"tl" to ".tl .tp",
)
private const val defaultTlds = ".com .gov .edu .org .net"
private const val comTld = ".com"
private const val otherDefaultTlds = ".gov .edu .org .net"

View file

@ -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_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,9 +165,21 @@ 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
// Intents
const val SEND_INTENT_ONE = -20000
const val SEND_INTENT_TWO = -20001
const val SEND_INTENT_THREE = -20002
/** to make sure a FlorisBoard code works when reading a JSON layout */
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
@ -177,14 +189,14 @@ object KeyCode {
REDO, ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, CLIPBOARD_COPY, CLIPBOARD_PASTE, CLIPBOARD_SELECT_ALL,
CLIPBOARD_SELECT_WORD, TOGGLE_INCOGNITO_MODE, TOGGLE_AUTOCORRECT, MOVE_START_OF_LINE, MOVE_END_OF_LINE,
MOVE_START_OF_PAGE, MOVE_END_OF_PAGE, SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED, CTRL, ALT,
FN, CLIPBOARD_CLEAR_HISTORY, NUMPAD,
FN, CLIPBOARD_CLEAR_HISTORY, NUMPAD, IME_HIDE_UI,
// heliboard only
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,
TIMESTAMP
TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT, SEND_INTENT_ONE, SEND_INTENT_TWO, SEND_INTENT_THREE,
-> this
// conversion
@ -196,14 +208,62 @@ object KeyCode {
else -> throw IllegalStateException("key code $this not yet supported")
}
fun Int.isModifier() = when (this) {
SHIFT, SYMBOL_ALPHA, ALPHA, SYMBOL, NUMPAD, FN, CTRL, CTRL_LEFT, CTRL_RIGHT, ALT, ALT_LEFT, ALT_RIGHT,
META, META_LEFT, META_RIGHT -> true
else -> false
}
// 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>.
* Convert an internal keyCode to a KeyEvent.KEYCODE_<xxx>.
* Positive codes are passed through, unknown negative codes result in KeyEvent.KEYCODE_UNKNOWN.
* To be uses for fake hardware key press.
*/
@JvmStatic fun keyCodeToKeyEventCode(keyCode: Int) = when (keyCode) {
ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
MOVE_START_OF_LINE -> KeyEvent.KEYCODE_MOVE_HOME
MOVE_END_OF_LINE -> KeyEvent.KEYCODE_MOVE_END
TAB -> KeyEvent.KEYCODE_TAB
PAGE_UP -> KeyEvent.KEYCODE_PAGE_UP
PAGE_DOWN -> KeyEvent.KEYCODE_PAGE_DOWN
ESCAPE -> KeyEvent.KEYCODE_ESCAPE
INSERT -> KeyEvent.KEYCODE_INSERT
SLEEP -> KeyEvent.KEYCODE_SLEEP
MEDIA_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY
MEDIA_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE
MEDIA_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
MEDIA_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
MEDIA_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
VOL_UP -> KeyEvent.KEYCODE_VOLUME_UP
VOL_DOWN -> KeyEvent.KEYCODE_VOLUME_DOWN
MUTE -> KeyEvent.KEYCODE_VOLUME_MUTE
BACK -> KeyEvent.KEYCODE_BACK
F1 -> KeyEvent.KEYCODE_F1
F2 -> KeyEvent.KEYCODE_F2
F3 -> KeyEvent.KEYCODE_F3
F4 -> KeyEvent.KEYCODE_F4
F5 -> KeyEvent.KEYCODE_F5
F6 -> KeyEvent.KEYCODE_F6
F7 -> KeyEvent.KEYCODE_F7
F8 -> KeyEvent.KEYCODE_F8
F9 -> KeyEvent.KEYCODE_F9
F10 -> KeyEvent.KEYCODE_F10
F11 -> KeyEvent.KEYCODE_F11
F12 -> KeyEvent.KEYCODE_F12
else -> if (keyCode < 0) KeyEvent.KEYCODE_UNKNOWN else keyCode
}
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/**
* Convert a 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()) {
*/
@JvmStatic fun codePointToKeyEventCode(codePoint: Int): Int = when (codePoint.toChar().uppercaseChar()) {
'/' -> KeyEvent.KEYCODE_SLASH
'\\' -> KeyEvent.KEYCODE_BACKSLASH
';' -> KeyEvent.KEYCODE_SEMICOLON
@ -257,40 +317,4 @@ object KeyCode {
'Z' -> KeyEvent.KEYCODE_Z
else -> KeyEvent.KEYCODE_UNKNOWN
}
else when (this) {
ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
MOVE_START_OF_LINE -> KeyEvent.KEYCODE_MOVE_HOME
MOVE_END_OF_LINE -> KeyEvent.KEYCODE_MOVE_END
TAB -> KeyEvent.KEYCODE_TAB
PAGE_UP -> KeyEvent.KEYCODE_PAGE_UP
PAGE_DOWN -> KeyEvent.KEYCODE_PAGE_DOWN
ESCAPE -> KeyEvent.KEYCODE_ESCAPE
INSERT -> KeyEvent.KEYCODE_INSERT
SLEEP -> KeyEvent.KEYCODE_SLEEP
MEDIA_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY
MEDIA_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE
MEDIA_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
MEDIA_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
MEDIA_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
VOL_UP -> KeyEvent.KEYCODE_VOLUME_UP
VOL_DOWN -> KeyEvent.KEYCODE_VOLUME_DOWN
MUTE -> KeyEvent.KEYCODE_VOLUME_MUTE
BACK -> KeyEvent.KEYCODE_BACK
F1 -> KeyEvent.KEYCODE_F1
F2 -> KeyEvent.KEYCODE_F2
F3 -> KeyEvent.KEYCODE_F3
F4 -> KeyEvent.KEYCODE_F4
F5 -> KeyEvent.KEYCODE_F5
F6 -> KeyEvent.KEYCODE_F6
F7 -> KeyEvent.KEYCODE_F7
F8 -> KeyEvent.KEYCODE_F8
F9 -> KeyEvent.KEYCODE_F9
F10 -> KeyEvent.KEYCODE_F10
F11 -> KeyEvent.KEYCODE_F11
F12 -> KeyEvent.KEYCODE_F12
else -> KeyEvent.KEYCODE_UNKNOWN
}
}

View file

@ -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

View file

@ -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()
}

View file

@ -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))
}
/**

View file

@ -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
@ -17,12 +18,14 @@ import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.SettingsSubtype
import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.settings.createPrefKeyForBooleanSettings
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.DictionaryInfoUtils
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
@ -31,9 +34,8 @@ import helium314.keyboard.latin.utils.ToolbarKey
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
@ -43,14 +45,24 @@ import java.util.EnumMap
class App : Application() {
override fun onCreate() {
super.onCreate()
Settings.init(this)
DebugFlags.init(this)
Settings.init(this)
SubtypeSettings.init(this)
RichInputMethodManager.init(this)
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()
@ -553,103 +562,58 @@ fun checkVersionUpgrade(context: Context) {
prefs.edit().putString(it.key, newValue).apply()
}
}
if (oldVersion <= 3101) {
val e = prefs.edit()
prefs.all.toMap().forEach { (key, value) ->
if (key == "side_padding_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 0, 2), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 2, 2), value)
} else if (key == "side_padding_scale_landscape") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 1, 2), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 3, 2), value)
} else if (key == "bottom_padding_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_BOTTOM_PADDING_SCALE_PREFIX, 0, 1), value as Float)
} else if (key == "bottom_padding_scale_landscape") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_BOTTOM_PADDING_SCALE_PREFIX, 1, 1), value as Float)
} else if (key == "split_spacer_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SPLIT_SPACER_SCALE_PREFIX, 0, 1), value as Float)
} else if (key == "split_spacer_scale_landscape") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SPLIT_SPACER_SCALE_PREFIX, 1, 1), value as Float)
} else if (key == "one_handed_mode_enabled_p_true") {
e.putBoolean(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_MODE_PREFIX, 0, 2), value as Boolean)
} else if (key == "one_handed_mode_enabled_p_false") {
e.putBoolean(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_MODE_PREFIX, 1, 2), value as Boolean)
} else if (key == "one_handed_mode_scale_p_true") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_SCALE_PREFIX, 0, 2), value as Float)
} else if (key == "one_handed_mode_scale_p_false") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_SCALE_PREFIX, 1, 2), value as Float)
} else if (key == "one_handed_mode_gravity_p_true") {
e.putInt(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_GRAVITY_PREFIX, 0, 2), value as Int)
} else if (key == "one_handed_mode_gravity_p_false") {
e.putInt(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_GRAVITY_PREFIX, 1, 2), value as Int)
} else if (key == "keyboard_height_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value)
} else {
if (key == Settings.PREF_ADDITIONAL_SUBTYPES || key == Settings.PREF_ENABLED_SUBTYPES) {
val subtypes = prefs.getString(key, "")!!.split(Separators.SETS).filter { it.isNotEmpty() }.map {
val st = it.toSettingsSubtype()
if (st.locale.language == "ko") st.with(ExtraValue.COMBINING_RULES, "hangul")
else st
}
e.putString(key, subtypes.joinToString(Separators.SETS) { it.toPref() })
} else if (key == Settings.PREF_SELECTED_SUBTYPE) {
val subtype = prefs.getString(key, "")!!.toSettingsSubtype()
if (subtype.locale.language == "ko")
e.putString(key, subtype.with(ExtraValue.COMBINING_RULES, "hangul").toPref())
}
return@forEach
}
e.remove(key)
}
e.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
}
}

View file

@ -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));
}
}
}
}

View 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
}
}

View file

@ -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);
}

View file

@ -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.

View file

@ -6,15 +6,14 @@
package helium314.keyboard.latin;
import java.util.ArrayList;
import java.util.Locale;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
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 +47,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 +56,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,14 +168,25 @@ 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;
};
}
public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) {
return null;
}
/**
* Not a true dictionary. A placeholder used to indicate suggestions that don't come from any
* real dictionary.
*/
static class PhonyDictionary extends Dictionary {
public static class PhonyDictionary extends Dictionary {
PhonyDictionary(final String type) {
super(type, null);
}

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -0,0 +1,825 @@
/*
* 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.launch
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 {
try {
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()
} catch (e: Throwable) {
Log.e(TAG, "could not initialize main dictionaries for $locales", e)
}
}
}
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) {
if (word.length <= 1) return
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 {
// adding can throw IllegalArgumentException: Unknown URL content://user_dictionary/words
// https://stackoverflow.com/q/41474623 https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/490
// apparently some devices don't have a dictionary? or it's just sporadic hiccups?
runCatching { 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.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) file
else null
}
private val blacklist = hashSetOf<String>().apply {
if (blacklistFile?.isFile != 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
}
}

View file

@ -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)) {

View file

@ -6,58 +6,56 @@
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
object DictionaryFactory {
/**
* Initializes a main dictionary collection from a dictionary pack, with explicit flags.
* 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.
*
*
* 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)
// 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>()
// 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)
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)
}
// 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)
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
}
/**
@ -65,11 +63,27 @@ fun createMainDictionary(context: Context, locale: Locale): DictionaryCollection
* 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)
private fun checkAndAddDictionaryToListNewType(file: File, dicts: MutableList<Dictionary>, locale: Locale) {
val dictionary = getDictionary(file, locale) ?: return
if (dicts.any { it.mDictType == dictionary.mDictType }) {
dictionary.close()
return
}
dicts.add(dictionary)
}
@JvmStatic
fun getDictionary(
file: File,
locale: Locale
): Dictionary? {
if (!file.isFile) return null
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file)
if (header == null) {
killDictionary(file)
return null
}
val dictType = header.mIdString.split(":").first()
if (dicts.any { it.mDictType == dictType }) return
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
file.absolutePath, 0, file.length(), false, locale, dictType
)
@ -77,17 +91,17 @@ private fun checkAndAddDictionaryToListIfNotExisting(file: File, dicts: MutableL
if (readOnlyBinaryDictionary.isValidDictionary) {
if (locale.language == "ko") {
// Use KoreanDictionary for Korean locale
dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
} else {
dicts.add(readOnlyBinaryDictionary)
return KoreanDictionary(readOnlyBinaryDictionary)
}
return readOnlyBinaryDictionary
}
} else {
readOnlyBinaryDictionary.close()
killDictionary(file)
}
return null
}
private fun killDictionary(file: File) {
Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
file.delete()
}
}

View file

@ -23,7 +23,7 @@ import java.util.List;
/**
* A class for detecting Emoji-Alt physical key.
*/
final class EmojiAltPhysicalKeyDetector {
public final class EmojiAltPhysicalKeyDetector {
private static final String TAG = "EmojiAltPhysKeyDetector";
private static final boolean DEBUG = false;

View file

@ -103,7 +103,7 @@ public final class InputAttributes {
|| InputTypeUtils.isEmailVariation(variation)
|| hasNoMicrophoneKeyOption()
|| !RichInputMethodManager.isInitialized() // avoid crash when only using spell checker
|| !RichInputMethodManager.getInstance().hasShortcutIme();
|| !RichInputMethodManager.getInstance().isShortcutImeReady();
mShouldShowVoiceInputKey = !noMicrophone;
mDisableGestureFloatingPreviewText = InputAttributes.inPrivateImeOptions(

View file

@ -19,8 +19,10 @@ import helium314.keyboard.accessibility.AccessibilityUtils;
import helium314.keyboard.keyboard.MainKeyboardView;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
import helium314.keyboard.latin.suggestions.MoreSuggestionsView;
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>.
@ -223,8 +230,8 @@ public final class InputView extends FrameLayout {
/**
* This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to
* {@link SuggestionStripView} when the {@link PopupSuggestionsView} is showing.
* {@link SuggestionStripView} dismisses {@link PopupSuggestionsView} when it receives any event
* {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing.
* {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives any event
* outside of it.
*/
private static class MoreSuggestionsViewCanceler

View file

@ -78,12 +78,13 @@ 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 landscape = Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
val split = Settings.getValues().mIsSplitKeyboardEnabled
val oldScale = Settings.readOneHandedModeScale(context.prefs(), landscape, split)
val newScale = (oldScale + changePercent / 100f).coerceAtMost(2.5f).coerceAtLeast(0.5f)
if (newScale == oldScale) return@setOnTouchListener true
Settings.getInstance().writeOneHandedModeScale(newScale)
oneHandedModeEnabled = false // intentionally putting wrong value, so KeyboardSwitcher.setOneHandedModeEnabled does actually reload
KeyboardSwitcher.getInstance().setOneHandedModeEnabled(true)
KeyboardSwitcher.getInstance().setOneHandedModeEnabled(true, true)
}
else -> x = 0f
}

View file

@ -4,6 +4,7 @@ package helium314.keyboard.latin;
import helium314.keyboard.event.HangulCombiner;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.text.Normalizer;
@ -72,6 +73,11 @@ public class KoreanDictionary extends Dictionary {
return mDictionary.getMaxFrequencyOfExactMatches(processInput(word));
}
@Override
public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
return mDictionary.getWordProperty(processInput(word), isBeginningOfSentence);
}
@Override
protected boolean same(char[] word, int length, String typedWord) {
word = processInput(new String(word)).toCharArray();

View file

@ -26,7 +26,6 @@ import android.os.Process;
import android.text.InputType;
import android.util.PrintWriterPrinter;
import android.util.Printer;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@ -51,16 +50,12 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.common.InsetsOutlineProvider;
import helium314.keyboard.dictionarypack.DictionaryPackConstants;
import helium314.keyboard.event.Event;
import helium314.keyboard.event.HangulEventDecoder;
import helium314.keyboard.event.HardwareEventDecoder;
import helium314.keyboard.event.HardwareKeyboardEventDecoder;
import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardId;
import helium314.keyboard.keyboard.KeyboardLayoutSet;
import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.MainKeyboardView;
import helium314.keyboard.latin.Suggest.OnGetSuggestedWordsCallback;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.Constants;
@ -69,7 +64,6 @@ import helium314.keyboard.latin.common.InputPointers;
import helium314.keyboard.latin.common.LocaleUtils;
import helium314.keyboard.latin.common.ViewOutlineProviderUtilsKt;
import helium314.keyboard.latin.define.DebugFlags;
import helium314.keyboard.latin.define.ProductionFlags;
import helium314.keyboard.latin.inputlogic.InputLogic;
import helium314.keyboard.latin.personalization.PersonalizationHelper;
import helium314.keyboard.latin.settings.Settings;
@ -87,6 +81,7 @@ import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.StatsUtilsManager;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeSettings;
import helium314.keyboard.latin.utils.ToolbarMode;
import helium314.keyboard.latin.utils.ViewLayoutUtils;
import helium314.keyboard.settings.SettingsActivity;
import kotlin.collections.CollectionsKt;
@ -128,12 +123,12 @@ public class LatinIME extends InputMethodService implements
public final KeyboardActionListener mKeyboardActionListener;
private int mOriginalNavBarColor = 0;
private int mOriginalNavBarFlags = 0;
// UIHandler is needed when creating InputLogic
public final UIHandler mHandler = new UIHandler(this);
private final DictionaryFacilitator mDictionaryFacilitator =
DictionaryFacilitatorProvider.getDictionaryFacilitator(false);
final InputLogic mInputLogic = new InputLogic(this, this, mDictionaryFacilitator);
// We expect to have only one decoder in almost all cases, hence the default capacity of 1.
// If it turns out we need several, it will get grown seamlessly.
final SparseArray<HardwareEventDecoder> mHardwareEventDecoders = new SparseArray<>(1);
// TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
private View mInputView;
@ -143,7 +138,6 @@ public class LatinIME extends InputMethodService implements
private RichInputMethodManager mRichImm;
final KeyboardSwitcher mKeyboardSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
private final StatsUtilsManager mStatsUtilsManager;
// Working variable for {@link #startShowingInputView()} and
// {@link #onEvaluateInputViewShown()}.
@ -186,8 +180,6 @@ public class LatinIME extends InputMethodService implements
private final ClipboardHistoryManager mClipboardHistoryManager = new ClipboardHistoryManager(this);
public final UIHandler mHandler = new UIHandler(this);
public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
private static final int MSG_UPDATE_SHIFT_STATE = 0;
private static final int MSG_PENDING_IMS_CALLBACK = 1;
@ -278,9 +270,7 @@ public class LatinIME extends InputMethodService implements
msg.arg2 /* remainingTries */, this /* handler */)) {
// If we were able to reset the caches, then we can reload the keyboard.
// Otherwise, we'll do it when we can.
latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(),
settingsValues, latinIme.getCurrentAutoCapsState(),
latinIme.getCurrentRecapitalizeState());
latinIme.mKeyboardSwitcher.reloadMainKeyboard();
}
break;
case MSG_WAIT_FOR_DICTIONARY_LOAD:
@ -608,7 +598,7 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = false;
}
if (currentSubtypeHasBeenUsed
&& richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
&& SubtypeSettings.INSTANCE.isEnabled(lastActiveSubtype)
&& !currentSubtype.equals(lastActiveSubtype)) {
switchToSubtype(lastActiveSubtype);
return;
@ -636,6 +626,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void onCreate() {
mSettings.startListener();
KeyboardIconsSet.Companion.getInstance().loadIcons(this);
mRichImm = RichInputMethodManager.getInstance();
AudioAndHapticFeedbackManager.init(this);
@ -663,7 +654,8 @@ public class LatinIME extends InputMethodService implements
final IntentFilter newDictFilter = new IntentFilter();
newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
// RECEIVER_EXPORTED is necessary because apparently Android 15 (and others?) don't recognize if the sender and receiver are the same app, see https://github.com/Helium314/HeliBoard/pull/1756
ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_EXPORTED);
final IntentFilter dictDumpFilter = new IntentFilter();
dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
@ -737,8 +729,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;
}
@ -754,9 +746,14 @@ public class LatinIME extends InputMethodService implements
// TODO: make sure the current settings always have the right locales, and read from them.
private void resetDictionaryFacilitator(@NonNull final Locale locale) {
final SettingsValues settingsValues = mSettings.getCurrent();
try {
mDictionaryFacilitator.resetDictionaries(this, locale,
settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts,
false, settingsValues.mAccount, "", this);
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
settingsValues.mUsePersonalizedDicts, false, "", this);
} catch (Throwable e) {
// this should not happen, but in case it does we at least want to show a keyboard
Log.e(TAG, "Could not reset dictionary facilitator, please fix ASAP", e);
}
mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
}
@ -765,12 +762,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
@ -793,14 +787,6 @@ public class LatinIME extends InputMethodService implements
deallocateMemory();
}
public void recycle() {
unregisterReceiver(mDictionaryPackInstallReceiver);
unregisterReceiver(mDictionaryDumpBroadcastReceiver);
unregisterReceiver(mRingerModeChangeReceiver);
unregisterReceiver(mRestartAfterDeviceUnlockReceiver);
mInputLogic.recycle();
}
private boolean isImeSuppressedByHardwareKeyboard() {
final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
return !onEvaluateInputViewShown() && switcher.isImeSuppressedByHardwareKeyboard(
@ -882,7 +868,8 @@ public class LatinIME extends InputMethodService implements
mInputView = view;
mInsetsUpdater = ViewOutlineProviderUtilsKt.setInsetsOutlineProvider(view);
updateSoftInputWindowLayoutParameters();
mSuggestionStripView = view.findViewById(R.id.suggestion_strip_view);
mSuggestionStripView = mSettings.getCurrent().mToolbarMode == ToolbarMode.HIDDEN?
null : view.findViewById(R.id.suggestion_strip_view);
if (hasSuggestionStripView()) {
mSuggestionStripView.setListener(this, view);
}
@ -934,9 +921,10 @@ public class LatinIME extends InputMethodService implements
mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype),
mSettings.getCurrent());
loadKeyboard();
if (mSuggestionStripView != null)
if (hasSuggestionStripView()) {
mSuggestionStripView.setRtl(mRichImm.getCurrentSubtype().isRtlSubtype());
}
}
/** alias to onCurrentInputMethodSubtypeChanged with a better name, as it's also used for internal switching */
public void switchToSubtype(final InputMethodSubtype subtype) {
@ -1023,6 +1011,8 @@ public class LatinIME extends InputMethodService implements
!currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) {
loadSettings();
currentSettingsValues = mSettings.getCurrent();
if (hasSuggestionStripView())
mSuggestionStripView.updateVoiceKey();
}
// ALERT: settings have not been reloaded and there is a chance they may be stale.
// In the practice, if it is, we should have gotten onConfigurationChanged so it should
@ -1072,7 +1062,7 @@ public class LatinIME extends InputMethodService implements
if (isDifferentTextField) {
mainKeyboardView.closing();
suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
switcher.reloadMainKeyboard();
if (needToCallLoadKeyboardLater) {
// If we need to call loadKeyboard again later, we need to save its state now. The
// later call will be done in #retryResetCaches.
@ -1120,6 +1110,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void onWindowHidden() {
super.onWindowHidden();
Log.i(TAG, "onWindowHidden");
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
mainKeyboardView.closing();
@ -1176,8 +1167,12 @@ public class LatinIME extends InputMethodService implements
if (isInputViewShown()
&& mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
composingSpanStart, composingSpanEnd, settingsValues)) {
mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
getCurrentRecapitalizeState());
// we don't want to update a manually set shift state if selection changed towards one side
// because this may end the manual shift, which is unwanted in case of shift + arrow keys for changing selection
// todo: this is not fully implemented yet, and maybe should be behind a setting
if (mKeyboardSwitcher.getKeyboard() != null && mKeyboardSwitcher.getKeyboard().mId.isAlphabetShiftedManually()
&& !((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart)))
mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
}
@ -1218,7 +1213,8 @@ public class LatinIME extends InputMethodService implements
@Override
public void hideWindow() {
if (mSuggestionStripView != null)
Log.i(TAG, "hideWindow");
if (hasSuggestionStripView() && mSettings.getCurrent().mToolbarMode == ToolbarMode.EXPANDABLE)
mSuggestionStripView.setToolbarVisibility(false);
mKeyboardSwitcher.onHideWindow();
@ -1230,6 +1226,12 @@ public class LatinIME extends InputMethodService implements
super.hideWindow();
}
@Override
public void requestHideSelf(int flags) {
super.requestHideSelf(flags);
Log.i(TAG, "requestHideSelf: " + flags);
}
@Override
public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
if (DebugFlags.DEBUG_ENABLED) {
@ -1274,20 +1276,25 @@ public class LatinIME extends InputMethodService implements
return;
}
final View visibleKeyboardView = mKeyboardSwitcher.getWrapperView();
if (visibleKeyboardView == null || !hasSuggestionStripView()) {
if (visibleKeyboardView == null) {
return;
}
final int inputHeight = mInputView.getHeight();
if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) {
// If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen.
outInsets.contentTopInsets = inputHeight;
outInsets.visibleTopInsets = inputHeight;
// for some reason setting contentTopInsets and visibleTopInsets broke somewhere along the
// way from OpenBoard to HeliBoard (GH-702, GH-1455), but not setting anything seems to work
mInsetsUpdater.setInsets(outInsets);
return;
}
final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - mSuggestionStripView.getHeight();
final int stripHeight = mKeyboardSwitcher.isShowingStripContainer() ? mKeyboardSwitcher.getStripContainer().getHeight() : 0;
final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - stripHeight;
if (hasSuggestionStripView()) {
mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
}
// Need to set expanded touchable region only if a keyboard view is being shown.
if (visibleKeyboardView.isShown()) {
final int touchLeft = 0;
@ -1371,6 +1378,10 @@ public class LatinIME extends InputMethodService implements
@RequiresApi(api = Build.VERSION_CODES.R)
public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(@NonNull Bundle uiExtras) {
Log.d(TAG,"onCreateInlineSuggestionsRequest called");
if (Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
return null;
}
return InlineAutofillUtils.createInlineSuggestionRequest(mDisplayContext);
}
@ -1378,6 +1389,10 @@ public class LatinIME extends InputMethodService implements
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
Log.d(TAG,"onInlineSuggestionsResponse called");
if (Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
return false;
}
final List<InlineSuggestion> inlineSuggestions = response.getInlineSuggestions();
if (inlineSuggestions.isEmpty()) {
return false;
@ -1464,7 +1479,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.hasMultipleEnabledSubtypesInThisIme(true);
// switch subtype if wanted, do nothing if no other subtype is available
if (switchSubtype && !switchIme) {
if (hasMoreThanOneSubtype)
@ -1527,25 +1542,7 @@ public class LatinIME extends InputMethodService implements
// Implementation of {@link SuggestionStripView.Listener}.
@Override
public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) {
onCodeInput(codePoint, 0, x, y, isKeyRepeat);
}
public void onCodeInput(final int codePoint, final int metaState, final int x, final int y, final boolean isKeyRepeat) {
if (codePoint < 0) {
switch (codePoint) {
case KeyCode.TOGGLE_AUTOCORRECT -> {mSettings.toggleAutoCorrect(); return; }
case KeyCode.TOGGLE_INCOGNITO_MODE -> {mSettings.toggleAlwaysIncognitoMode(); return; }
}
}
final Event event;
// checking if the character is a combining accent
if (0x300 <= codePoint && codePoint <= 0x35b) {
event = Event.createSoftwareDeadEvent(codePoint, 0, metaState, x, y, null);
} else {
event = createSoftwareKeypressEvent(codePoint, metaState, x, y, isKeyRepeat);
}
onEvent(event);
mKeyboardActionListener.onCodeInput(codePoint, x, y, isKeyRepeat);
}
// This method is public for testability of LatinIME, but also in the future it should
@ -1562,24 +1559,6 @@ public class LatinIME extends InputMethodService implements
mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
// A helper method to split the code point and the key code. Ultimately, they should not be
// squashed into the same variable, and this method should be removed.
// public for testing, as we don't want to copy the same logic into test code
@NonNull
public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int metaState,
final int keyX, final int keyY, final boolean isKeyRepeat) {
final int keyCode;
final int codePoint;
if (keyCodeOrCodePoint <= 0) {
keyCode = keyCodeOrCodePoint;
codePoint = Event.NOT_A_CODE_POINT;
} else {
keyCode = Event.NOT_A_KEY_CODE;
codePoint = keyCodeOrCodePoint;
}
return Event.createSoftwareKeypressEvent(codePoint, keyCode, metaState, keyX, keyY, isKeyRepeat);
}
public void onTextInput(final String rawText) {
// TODO: have the keyboard pass the correct key code when we need it.
final Event event = Event.createSoftwareTextEvent(rawText, KeyCode.MULTIPLE_CODE_POINTS);
@ -1632,7 +1611,7 @@ public class LatinIME extends InputMethodService implements
dismissGestureFloatingPreviewText /* dismissDelayed */);
}
public boolean hasSuggestionStripView() {
private boolean hasSuggestionStripView() {
return null != mSuggestionStripView;
}
@ -1667,18 +1646,6 @@ public class LatinIME extends InputMethodService implements
}
}
// TODO[IL]: Move this out of LatinIME.
public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
final OnGetSuggestedWordsCallback callback) {
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
if (keyboard == null) {
callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
return;
}
mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard,
mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback);
}
@Override
public void showSuggestionStrip(final SuggestedWords suggestedWords) {
if (suggestedWords.isEmpty()) {
@ -1763,8 +1730,7 @@ public class LatinIME extends InputMethodService implements
loadSettings();
if (mKeyboardSwitcher.getMainKeyboardView() != null) {
// Reload keyboard because the current language has been changed.
mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(),
getCurrentAutoCapsState(), getCurrentRecapitalizeState());
mKeyboardSwitcher.reloadMainKeyboard();
}
}
@ -1832,63 +1798,18 @@ public class LatinIME extends InputMethodService implements
feedbackManager.performAudioFeedback(code);
}
private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) {
final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId);
if (null != decoder) return decoder;
// TODO: create the decoder according to the specification
final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId);
mHardwareEventDecoders.put(deviceId, newDecoder);
return newDecoder;
}
// Hooks for hardware keyboard
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
if (mEmojiAltPhysicalKeyDetector == null) {
mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
getApplicationContext().getResources());
}
mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent);
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
return super.onKeyDown(keyCode, keyEvent);
}
final Event event;
if (mRichImm.getCurrentSubtypeLocale().getLanguage().equals("ko")) {
final RichInputMethodSubtype subtype = mKeyboardSwitcher.getKeyboard() == null
? mRichImm.getCurrentSubtype()
: mKeyboardSwitcher.getKeyboard().mId.mSubtype;
event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent,
() -> getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent));
} else {
event = getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent);
}
// If the event is not handled by LatinIME, we just pass it to the parent implementation.
// If it's handled, we return true because we did handle it.
if (event.isHandled()) {
mInputLogic.onCodeInput(mSettings.getCurrent(), event,
mKeyboardSwitcher.getKeyboardShiftMode(),
// TODO: this is not necessarily correct for a hardware keyboard right now
mKeyboardSwitcher.getCurrentKeyboardScript(),
mHandler);
if (mKeyboardActionListener.onKeyDown(keyCode, keyEvent))
return true;
}
return super.onKeyDown(keyCode, keyEvent);
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
if (mEmojiAltPhysicalKeyDetector == null) {
mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
getApplicationContext().getResources());
}
mEmojiAltPhysicalKeyDetector.onKeyUp(keyEvent);
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
return super.onKeyUp(keyCode, keyEvent);
}
final long keyIdentifier = (long) keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode();
if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
if (mKeyboardActionListener.onKeyUp(keyCode, keyEvent))
return true;
}
return super.onKeyUp(keyCode, keyEvent);
}
@ -2021,8 +1942,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 ->
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
}
}

View file

@ -10,6 +10,7 @@ import com.android.inputmethod.latin.BinaryDictionary;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
@ -107,6 +108,18 @@ public final class ReadOnlyBinaryDictionary extends Dictionary {
return NOT_A_PROBABILITY;
}
@Override
public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
if (mLock.readLock().tryLock()) {
try {
return mBinaryDictionary.getWordProperty(word, isBeginningOfSentence);
} finally {
mLock.readLock().unlock();
}
}
return null;
}
@Override
public void close() {
mLock.writeLock().lock();

View file

@ -40,8 +40,6 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.utils.CapsModeUtils;
import helium314.keyboard.latin.utils.DebugLogUtils;
import helium314.keyboard.latin.utils.NgramContextUtils;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.SpannableStringUtils;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.TextRange;
@ -441,7 +439,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// test for this explicitly)
if (INVALID_CURSOR_POSITION != mExpectedSelStart
&& (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText.toString());
// We call #toString() here to create a temporary object.
// In some situations, this method is called on a worker thread, and it's possible
// the main thread touches the contents of mComposingText while this worker thread
@ -718,8 +716,13 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (start < 0 || end < 0) {
return false;
}
if (start > end) {
mExpectedSelStart = end;
mExpectedSelEnd = start;
} else {
mExpectedSelStart = start;
mExpectedSelEnd = end;
}
if (isConnected()) {
final boolean isIcValid = mIC.setSelection(start, end);
if (!isIcValid) {
@ -825,15 +828,6 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return NgramContextUtils.getNgramContextFromNthPreviousWord(prev, spacingAndPunctuations, n);
}
private static boolean isPartOfCompositionForScript(final int codePoint,
final SpacingAndPunctuations spacingAndPunctuations, final String script) {
// We always consider word connectors part of compositions.
return spacingAndPunctuations.isWordConnector(codePoint)
// Otherwise, it's part of composition if it's part of script and not a separator.
|| (!spacingAndPunctuations.isWordSeparator(codePoint)
&& ScriptUtils.isLetterPartOfScript(codePoint, script));
}
/**
* Returns the text surrounding the cursor.
*
@ -860,90 +854,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (before == null || after == null) {
return null;
}
// Going backward, find the first breaking point (separator)
int startIndexInBefore = before.length();
int endIndexInAfter = -1;
while (startIndexInBefore > 0) {
final int codePoint = Character.codePointBefore(before, startIndexInBefore);
if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
break;
// continue to the next whitespace and see whether this contains a sometimesWordConnector
for (int i = startIndexInBefore - 1; i >= 0; i--) {
final char c = before.charAt(i);
if (spacingAndPunctuations.isSometimesWordConnector(c)) {
// if yes -> whitespace is the index
startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter -1;
break;
} else if (Character.isWhitespace(c)) {
// if no, just break normally
break;
}
}
break;
}
--startIndexInBefore;
if (Character.isSupplementaryCodePoint(codePoint)) {
--startIndexInBefore;
}
}
// Find last word separator after the cursor
if (endIndexInAfter == -1) {
while (++endIndexInAfter < after.length()) {
final int codePoint = Character.codePointAt(after, endIndexInAfter);
if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
break;
// continue to the next whitespace and see whether this contains a sometimesWordConnector
for (int i = endIndexInAfter; i < after.length(); i++) {
final char c = after.charAt(i);
if (spacingAndPunctuations.isSometimesWordConnector(c)) {
// if yes -> whitespace is next to the index
startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter - 1;
break;
} else if (Character.isWhitespace(c)) {
// if no, just break normally
break;
}
}
break;
}
if (Character.isSupplementaryCodePoint(codePoint)) {
++endIndexInAfter;
}
}
}
// strip stuff before "//" (i.e. ignore http and other protocols)
final String beforeConsideringStart = before.subSequence(startIndexInBefore, before.length()).toString();
final int protocolEnd = beforeConsideringStart.lastIndexOf("//");
if (protocolEnd != -1)
startIndexInBefore += protocolEnd + 1;
// we don't want the end characters to be word separators
while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after.charAt(endIndexInAfter - 1))) {
--endIndexInAfter;
}
while (startIndexInBefore < before.length() && spacingAndPunctuations.isWordSeparator(before.charAt(startIndexInBefore))) {
++startIndexInBefore;
}
final boolean hasUrlSpans =
SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
|| SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
// We don't use TextUtils#concat because it copies all spans without respect to their
// nature. If the text includes a PARAGRAPH span and it has been split, then
// TextUtils#concat will crash when it tries to concat both sides of it.
return new TextRange(
SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
startIndexInBefore, before.length() + endIndexInAfter, before.length(),
hasUrlSpans);
return StringUtilsKt.getTouchedWordRange(before, after, script, spacingAndPunctuations);
}
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
@ -956,19 +867,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// a composing region should always count as a word
return true;
}
final String textBeforeCursor = mCommittedTextBeforeComposingText.toString();
int indexOfCodePointInJavaChars = textBeforeCursor.length();
int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
: textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
// Search for the first non word-connector char
if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) {
indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint);
consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
: textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
}
return !(Constants.NOT_A_CODE == consideredCodePoint
|| spacingAndPunctuations.isWordSeparator(consideredCodePoint)
|| spacingAndPunctuations.isWordConnector(consideredCodePoint));
return StringUtilsKt.endsWithWordCodepoint(mCommittedTextBeforeComposingText.toString(), spacingAndPunctuations);
}
public boolean isCursorFollowedByWordCharacter(

View file

@ -1,446 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.latin;
import android.content.Context;
import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.os.AsyncTask;
import android.os.IBinder;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import helium314.keyboard.compat.ConfigurationCompatKt;
import helium314.keyboard.latin.common.LocaleUtils;
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.ScriptUtils;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeSettings;
import helium314.keyboard.latin.utils.SubtypeUtilsKt;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static helium314.keyboard.latin.common.Constants.Subtype.KEYBOARD_MODE;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Enrichment class for InputMethodManager to simplify interaction and add functionality.
*/
// non final for easy mocking.
public class RichInputMethodManager {
private static final String TAG = RichInputMethodManager.class.getSimpleName();
private static final boolean DEBUG = false;
private RichInputMethodManager() {
// This utility class is not publicly instantiable.
}
private static final RichInputMethodManager sInstance = new RichInputMethodManager();
private Context mContext;
private InputMethodManager mImm;
private InputMethodInfoCache mInputMethodInfoCache;
private RichInputMethodSubtype mCurrentRichInputMethodSubtype;
private InputMethodInfo mShortcutInputMethodInfo;
private InputMethodSubtype mShortcutSubtype;
private static final int INDEX_NOT_FOUND = -1;
public static RichInputMethodManager getInstance() {
sInstance.checkInitialized();
return sInstance;
}
public static void init(final Context context) {
sInstance.initInternal(context);
}
private boolean isInitializedInternal() {
return mImm != null;
}
public static boolean isInitialized() {
return sInstance.isInitializedInternal();
}
private void checkInitialized() {
if (!isInitializedInternal()) {
throw new RuntimeException(TAG + " is used before initialization");
}
}
private void initInternal(final Context context) {
if (isInitializedInternal()) {
return;
}
mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
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();
}
public InputMethodManager getInputMethodManager() {
checkInitialized();
return mImm;
}
public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
boolean allowsImplicitlySelectedSubtypes) {
return getEnabledInputMethodSubtypeList(
getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes);
}
public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) {
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(true);
final int currentIndex = enabledSubtypes.indexOf(currentSubtype);
if (currentIndex == INDEX_NOT_FOUND) {
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
+ SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
if (onlyCurrentIme) return enabledSubtypes.get(0); // just return first enabled subtype
else return null;
}
final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
if (nextIndex <= currentIndex && !onlyCurrentIme) {
// The current subtype is the last or only enabled one and it needs to switch to next IME.
return null;
}
return enabledSubtypes.get(nextIndex);
}
private static class InputMethodInfoCache {
private final InputMethodManager mImm;
private final String mImePackageName;
private InputMethodInfo mCachedThisImeInfo;
private final HashMap<InputMethodInfo, List<InputMethodSubtype>>
mCachedSubtypeListWithImplicitlySelected;
private final HashMap<InputMethodInfo, List<InputMethodSubtype>>
mCachedSubtypeListOnlyExplicitlySelected;
public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) {
mImm = imm;
mImePackageName = imePackageName;
mCachedSubtypeListWithImplicitlySelected = new HashMap<>();
mCachedSubtypeListOnlyExplicitlySelected = new HashMap<>();
}
public synchronized InputMethodInfo getInputMethodOfThisIme() {
if (mCachedThisImeInfo != null) {
return mCachedThisImeInfo;
}
for (final InputMethodInfo imi : mImm.getInputMethodList()) {
if (imi.getPackageName().equals(mImePackageName)) {
mCachedThisImeInfo = imi;
return imi;
}
}
throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
}
public synchronized List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) {
final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
allowsImplicitlySelectedSubtypes
? mCachedSubtypeListWithImplicitlySelected
: mCachedSubtypeListOnlyExplicitlySelected;
final List<InputMethodSubtype> cachedList = cache.get(imi);
if (cachedList != null) {
return cachedList;
}
final List<InputMethodSubtype> result;
if (imi == getInputMethodOfThisIme()) {
// allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled,
// use it to fall back to system locales or en_US to avoid returning an empty list
result = SubtypeSettings.INSTANCE.getEnabledSubtypes(allowsImplicitlySelectedSubtypes);
} else {
result = mImm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes);
}
cache.put(imi, result);
return result;
}
public synchronized void clear() {
mCachedThisImeInfo = null;
mCachedSubtypeListWithImplicitlySelected.clear();
mCachedSubtypeListOnlyExplicitlySelected.clear();
}
}
public InputMethodInfo getInputMethodInfoOfThisIme() {
return mInputMethodInfoCache.getInputMethodOfThisIme();
}
public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
return getEnabledInputMethodSubtypeList(getInputMethodInfoOfThisIme(), true)
.contains(subtype);
}
public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(final InputMethodSubtype subtype) {
final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
final boolean subtypeExplicitlyEnabled = getMyEnabledInputMethodSubtypeList(false)
.contains(subtype);
return subtypeEnabled && !subtypeExplicitlyEnabled;
}
public void onSubtypeChanged(@NonNull final InputMethodSubtype newSubtype) {
updateCurrentSubtype(newSubtype);
updateShortcutIme();
if (DEBUG) {
Log.w(TAG, "onSubtypeChanged: " + mCurrentRichInputMethodSubtype);
}
}
private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
static void forceSubtype(@NonNull final InputMethodSubtype subtype) {
sForcedSubtypeForTesting = RichInputMethodSubtype.Companion.get(subtype);
}
@NonNull
public Locale getCurrentSubtypeLocale() {
if (null != sForcedSubtypeForTesting) {
return sForcedSubtypeForTesting.getLocale();
}
return getCurrentSubtype().getLocale();
}
@NonNull
public RichInputMethodSubtype getCurrentSubtype() {
if (null != sForcedSubtypeForTesting) {
return sForcedSubtypeForTesting;
}
return mCurrentRichInputMethodSubtype;
}
public String getCombiningRulesExtraValueOfCurrentSubtype() {
return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
}
public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
final List<InputMethodInfo> enabledImis = mImm.getEnabledInputMethodList();
return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
}
public boolean hasMultipleEnabledSubtypesInThisIme(
final boolean shouldIncludeAuxiliarySubtypes) {
final List<InputMethodInfo> imiList = Collections.singletonList(
getInputMethodInfoOfThisIme());
return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
}
private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
final List<InputMethodInfo> imiList) {
// Number of the filtered IMEs
int filteredImisCount = 0;
for (InputMethodInfo imi : imiList) {
// We can return true immediately after we find two or more filtered IMEs.
if (filteredImisCount > 1) return true;
final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true);
// IMEs that have no subtypes should be counted.
if (subtypes.isEmpty()) {
++filteredImisCount;
continue;
}
int auxCount = 0;
for (InputMethodSubtype subtype : subtypes) {
if (subtype.isAuxiliary()) {
++auxCount;
}
}
final int nonAuxCount = subtypes.size() - auxCount;
// IMEs that have one or more non-auxiliary subtypes should be counted.
// If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
// subtypes should be counted as well.
if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
++filteredImisCount;
}
}
if (filteredImisCount > 1) {
return true;
}
final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
int keyboardCount = 0;
// imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
// both explicitly and implicitly enabled input method subtype.
// (The current IME should be LatinIME.)
for (InputMethodSubtype subtype : subtypes) {
if (KEYBOARD_MODE.equals(subtype.getMode())) {
++keyboardCount;
}
}
return keyboardCount > 1;
}
public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final Locale locale,
final String keyboardLayoutSetName) {
final InputMethodInfo myImi = getInputMethodInfoOfThisIme();
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)) {
return subtype;
}
}
return null;
}
public InputMethodSubtype findSubtypeForHintLocale(final Locale locale) {
// Find the best subtype based on a locale matching
final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
InputMethodSubtype bestMatch = LocaleUtils.getBestMatch(locale, subtypes, SubtypeUtilsKt::locale);
if (bestMatch != null) return bestMatch;
// search for first secondary language & script match
final int count = subtypes.size();
final String language = locale.getLanguage();
final String script = ScriptUtils.script(locale);
for (int i = 0; i < count; ++i) {
final InputMethodSubtype subtype = subtypes.get(i);
final Locale subtypeLocale = SubtypeUtilsKt.locale(subtype);
if (!ScriptUtils.script(subtypeLocale).equals(script))
continue; // need compatible script
bestMatch = subtype;
final List<Locale> secondaryLocales = SubtypeUtilsKt.getSecondaryLocales(subtype.getExtraValue());
for (final Locale secondaryLocale : secondaryLocales) {
if (secondaryLocale.getLanguage().equals(language)) {
return bestMatch;
}
}
}
// if wanted script is not compatible to current subtype, return a subtype with compatible script if possible
if (!script.equals(ScriptUtils.script(getCurrentSubtypeLocale()))) {
return bestMatch;
}
return null;
}
public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
final boolean allowsImplicitlySelectedSubtypes) {
return mInputMethodInfoCache.getEnabledInputMethodSubtypeList(
imi, allowsImplicitlySelectedSubtypes);
}
public void refreshSubtypeCaches() {
mInputMethodInfoCache.clear();
SharedPreferences prefs = KtxKt.prefs(mContext);
updateCurrentSubtype(SubtypeSettings.INSTANCE.getSelectedSubtype(prefs));
updateShortcutIme();
}
private void updateCurrentSubtype(final InputMethodSubtype subtype) {
SubtypeSettings.INSTANCE.setSelectedSubtype(KtxKt.prefs(mContext), subtype);
mCurrentRichInputMethodSubtype = RichInputMethodSubtype.Companion.get(subtype);
}
public static boolean canSwitchLanguage() {
if (!isInitialized()) return false;
if (Settings.getValues().mLanguageSwitchKeyToOtherSubtypes && getInstance().hasMultipleEnabledSubtypesInThisIme(false))
return true;
if (Settings.getValues().mLanguageSwitchKeyToOtherImes && getInstance().mImm.getEnabledInputMethodList().size() > 1)
return true;
return false;
}
// todo: is shortcutIme only voice input, or can it be something else?
// if always voice input, rename it and other things like mHasShortcutKey
private void updateShortcutIme() {
if (DEBUG) {
Log.d(TAG, "Update shortcut IME from : "
+ (mShortcutInputMethodInfo == null
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ (mShortcutSubtype == null ? "<null>" : (
mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
}
final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype;
final boolean implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
richSubtype.getRawSubtype());
final Locale systemLocale = ConfigurationCompatKt.locale(mContext.getResources().getConfiguration());
LanguageOnSpacebarUtils.onSubtypeChanged(
richSubtype, implicitlyEnabledSubtype, systemLocale);
LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList(
true /* allowsImplicitlySelectedSubtypes */));
// TODO: Update an icon for shortcut IME
final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
getInputMethodManager().getShortcutInputMethodsAndSubtypes();
mShortcutInputMethodInfo = null;
mShortcutSubtype = null;
for (final InputMethodInfo imi : shortcuts.keySet()) {
final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
// TODO: Returns the first found IMI for now. Should handle all shortcuts as
// appropriate.
mShortcutInputMethodInfo = imi;
// TODO: Pick up the first found subtype for now. Should handle all subtypes
// as appropriate.
mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
break;
}
if (DEBUG) {
Log.d(TAG, "Update shortcut IME to : "
+ (mShortcutInputMethodInfo == null
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ (mShortcutSubtype == null ? "<null>" : (
mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
}
}
public void switchToShortcutIme(final InputMethodService context) {
if (mShortcutInputMethodInfo == null) {
return;
}
final String imiId = mShortcutInputMethodInfo.getId();
switchToTargetIME(imiId, mShortcutSubtype, context);
}
public boolean hasShortcutIme() {
return mShortcutInputMethodInfo != null;
}
private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
final InputMethodService context) {
final IBinder token = context.getWindow().getWindow().getAttributes().token;
if (token == null) {
return;
}
final InputMethodManager imm = getInputMethodManager();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
imm.setInputMethodAndSubtype(token, imiId, subtype);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public boolean isShortcutImeReady() {
return mShortcutInputMethodInfo != null;
}
}

View file

@ -0,0 +1,303 @@
/*
* Copyright (C) 2012 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.latin
import android.content.Context
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.view.inputmethod.InputMethodInfo
import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.compat.locale
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.LocaleUtils.getBestMatch
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.getSecondaryLocales
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.prefs
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.Locale
/** Enrichment class for InputMethodManager to simplify interaction and add functionality. */
class RichInputMethodManager private constructor() {
private lateinit var context: Context
private lateinit var imm: InputMethodManager
private lateinit var inputMethodInfoCache: InputMethodInfoCache
private lateinit var currentRichInputMethodSubtype: RichInputMethodSubtype
private var shortcutInputMethodInfo: InputMethodInfo? = null
private var shortcutSubtype: InputMethodSubtype? = null
private val isInitializedInternal get() = this::imm.isInitialized
val currentSubtypeLocale get() = forcedSubtypeForTesting?.locale ?: currentSubtype.locale
val currentSubtype get() = forcedSubtypeForTesting ?: currentRichInputMethodSubtype
val combiningRulesExtraValueOfCurrentSubtype get() =
SubtypeLocaleUtils.getCombiningRulesExtraValue(currentSubtype.rawSubtype)
val inputMethodInfoOfThisIme get() = inputMethodInfoCache.inputMethodOfThisIme
val inputMethodManager: InputMethodManager get() {
checkInitialized()
return imm
}
val isShortcutImeReady get() = shortcutInputMethodInfo != null
fun getMyEnabledInputMethodSubtypes(allowsImplicitlySelectedSubtypes: Boolean) =
SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes)
fun getEnabledInputMethodSubtypes(imi: InputMethodInfo, allowsImplicitlySelectedSubtypes: Boolean) =
inputMethodInfoCache.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes)
fun hasMultipleEnabledIMEsOrSubtypes(shouldIncludeAuxiliarySubtypes: Boolean) =
hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imm.enabledInputMethodList)
fun hasMultipleEnabledSubtypesInThisIme(shouldIncludeAuxiliarySubtypes: Boolean) =
SubtypeSettings.getEnabledSubtypes(shouldIncludeAuxiliarySubtypes).size > 1
fun getNextSubtypeInThisIme(onlyCurrentIme: Boolean): InputMethodSubtype? {
val currentSubtype = currentSubtype.rawSubtype
val enabledSubtypes = getMyEnabledInputMethodSubtypes(true)
val currentIndex = enabledSubtypes.indexOf(currentSubtype)
if (currentIndex == -1) {
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" +
SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype))
return if (onlyCurrentIme) enabledSubtypes[0] // just return first enabled subtype
else null
}
val nextIndex = (currentIndex + 1) % enabledSubtypes.size
if (nextIndex <= currentIndex && !onlyCurrentIme) {
// The current subtype is the last or only enabled one and it needs to switch to next IME.
return null
}
return enabledSubtypes[nextIndex]
}
fun findSubtypeForHintLocale(locale: Locale): InputMethodSubtype? {
// Find the best subtype based on a locale matching
val subtypes = getMyEnabledInputMethodSubtypes(true)
var bestMatch = getBestMatch(locale, subtypes) { it.locale() }
if (bestMatch != null) return bestMatch
// search for first secondary language & script match
val language = locale.language
val script = locale.script()
for (subtype in subtypes) {
val subtypeLocale = subtype.locale()
if (subtypeLocale.script() != script) continue // need compatible script
bestMatch = subtype
val secondaryLocales = getSecondaryLocales(subtype.extraValue)
for (secondaryLocale in secondaryLocales) {
if (secondaryLocale.language == language) {
return bestMatch
}
}
}
// if wanted script is not compatible to current subtype, return a subtype with compatible script if available
if (script != currentSubtypeLocale.script()) {
return bestMatch
}
return null
}
fun onSubtypeChanged(newSubtype: InputMethodSubtype) {
SubtypeSettings.setSelectedSubtype(context.prefs(), newSubtype)
currentRichInputMethodSubtype = RichInputMethodSubtype.get(newSubtype)
updateShortcutIme()
if (DEBUG) {
Log.w(TAG, "onSubtypeChanged: $currentRichInputMethodSubtype")
}
}
fun refreshSubtypeCaches() {
inputMethodInfoCache.clear()
currentRichInputMethodSubtype = RichInputMethodSubtype.get(SubtypeSettings.getSelectedSubtype(context.prefs()))
updateShortcutIme()
}
fun switchToShortcutIme(inputMethodService: InputMethodService) {
val imiId = shortcutInputMethodInfo?.id ?: return
val token = inputMethodService.window.window?.attributes?.token ?: return
GlobalScope.launch {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
inputMethodService.switchInputMethod(imiId, shortcutSubtype)
else
@Suppress("Deprecation") imm.setInputMethodAndSubtype(token, imiId, shortcutSubtype)
}
}
// todo: is shortcutIme only voice input, or can it be something else?
// if always voice input, rename it and other things like mHasShortcutKey
private fun updateShortcutIme() {
if (DEBUG) {
val subtype = shortcutSubtype?.let { "${it.locale()}, ${it.mode}" } ?: "<null>"
Log.d(TAG, ("Update shortcut IME from: ${shortcutInputMethodInfo?.id ?: "<null>"}, $subtype"))
}
val richSubtype = currentRichInputMethodSubtype
val implicitlyEnabledSubtype = SubtypeSettings.isEnabled(richSubtype.rawSubtype)
&& !SubtypeSettings.getEnabledSubtypes(false).contains(richSubtype.rawSubtype)
val systemLocale = context.resources.configuration.locale()
LanguageOnSpacebarUtils.onSubtypeChanged(richSubtype, implicitlyEnabledSubtype, systemLocale)
LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypes(true))
// TODO: Update an icon for shortcut IME
val shortcuts = inputMethodManager.shortcutInputMethodsAndSubtypes
shortcutInputMethodInfo = null
shortcutSubtype = null
for (imi in shortcuts.keys) {
val subtypes = shortcuts[imi] ?: continue
// TODO: Returns the first found IMI for now. Should handle all shortcuts as appropriate.
shortcutInputMethodInfo = imi
// TODO: Pick up the first found subtype for now. Should handle all subtypes as appropriate.
shortcutSubtype = if (subtypes.size > 0) subtypes[0] else null
break
}
if (DEBUG) {
val subtype = shortcutSubtype?.let { "${it.locale()}, ${it.mode}" } ?: "<null>"
Log.d(TAG, ("Update shortcut IME to: ${shortcutInputMethodInfo?.id ?: "<null>"}, $subtype"))
}
}
private fun hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes: Boolean, imiList: List<InputMethodInfo>): Boolean {
// Number of the filtered IMEs
var filteredImisCount = 0
imiList.forEach { imi ->
// We can return true immediately after we find two or more filtered IMEs.
if (filteredImisCount > 1) return true
val subtypes = getEnabledInputMethodSubtypes(imi, true)
// IMEs that have no subtypes should be counted.
if (subtypes.isEmpty()) {
++filteredImisCount
return@forEach
}
var auxCount = 0
for (subtype in subtypes) {
if (!subtype.isAuxiliary) {
// IMEs that have one or more non-auxiliary subtypes should be counted.
++filteredImisCount
return@forEach
}
++auxCount
}
// If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
// subtypes should be counted as well.
if (shouldIncludeAuxiliarySubtypes && auxCount > 1) {
++filteredImisCount
}
}
if (filteredImisCount > 1) {
return true
}
val subtypes = getMyEnabledInputMethodSubtypes(true)
// imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
// both explicitly and implicitly enabled input method subtype.
// (The current IME should be LatinIME.)
return subtypes.count { it.mode == Constants.Subtype.KEYBOARD_MODE } > 1
}
private fun checkInitialized() {
if (!isInitializedInternal) {
throw RuntimeException("$TAG is used before initialization")
}
}
private fun initInternal(ctx: Context) {
if (isInitializedInternal) {
return
}
imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
context = ctx
inputMethodInfoCache = InputMethodInfoCache(imm, ctx.packageName)
// Initialize the current input method subtype and the shortcut IME.
refreshSubtypeCaches()
}
companion object {
private val TAG = RichInputMethodManager::class.java.simpleName
private const val DEBUG = false
private val instance = RichInputMethodManager()
@JvmStatic
fun getInstance(): RichInputMethodManager {
instance.checkInitialized()
return instance
}
fun init(ctx: Context) {
instance.initInternal(ctx)
}
@JvmStatic
fun isInitialized() = instance.isInitializedInternal
private var forcedSubtypeForTesting: RichInputMethodSubtype? = null
fun forceSubtype(subtype: InputMethodSubtype) {
forcedSubtypeForTesting = RichInputMethodSubtype.get(subtype)
}
fun canSwitchLanguage(): Boolean {
if (!isInitialized()) return false
if (Settings.getValues().mLanguageSwitchKeyToOtherSubtypes && instance.hasMultipleEnabledSubtypesInThisIme(false)) return true
if (Settings.getValues().mLanguageSwitchKeyToOtherImes && instance.imm.enabledInputMethodList.size > 1) return true
return false
}
}
}
private class InputMethodInfoCache(private val imm: InputMethodManager, private val imePackageName: String) {
private var cachedThisImeInfo: InputMethodInfo? = null
private val cachedSubtypeListWithImplicitlySelected = HashMap<InputMethodInfo, List<InputMethodSubtype>>()
private val cachedSubtypeListOnlyExplicitlySelected = HashMap<InputMethodInfo, List<InputMethodSubtype>>()
@get:Synchronized
val inputMethodOfThisIme: InputMethodInfo get() {
if (cachedThisImeInfo == null)
cachedThisImeInfo = imm.inputMethodList.firstOrNull { it.packageName == imePackageName }
cachedThisImeInfo?.let { return it }
throw RuntimeException("Input method id for $imePackageName not found, only found " +
imm.inputMethodList.map { it.packageName })
}
@Synchronized
fun getEnabledInputMethodSubtypeList(imi: InputMethodInfo, allowsImplicitlySelectedSubtypes: Boolean): List<InputMethodSubtype> {
val cache = if (allowsImplicitlySelectedSubtypes) cachedSubtypeListWithImplicitlySelected
else cachedSubtypeListOnlyExplicitlySelected
cache[imi]?.let { return it }
val result = if (imi == inputMethodOfThisIme) {
// allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled,
// use it to fall back to system locales or en_US to avoid returning an empty list
SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes)
} else {
imm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes)
}
cache[imi] = result
return result
}
@Synchronized
fun clear() {
cachedThisImeInfo = null
cachedSubtypeListWithImplicitlySelected.clear()
cachedSubtypeListOnlyExplicitlySelected.clear()
}
}

View file

@ -10,12 +10,15 @@ 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.SubtypeSettings
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty
import java.util.Locale
/**
@ -25,7 +28,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 +43,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 +72,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)
@ -115,11 +106,8 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val noLanguageSubtype: RichInputMethodSubtype get() {
sNoLanguageSubtype?.let { return it }
var noLanguageSubtype = sNoLanguageSubtype
val rawNoLanguageSubtype = RichInputMethodManager.getInstance()
.findSubtypeByLocaleAndKeyboardLayoutSet(
SubtypeLocaleUtils.NO_LANGUAGE.constructLocale(),
SubtypeLocaleUtils.QWERTY
)
val rawNoLanguageSubtype = SubtypeSettings.getResourceSubtypesForLocale(SubtypeLocaleUtils.NO_LANGUAGE.constructLocale())
.firstOrNull { it.mainLayoutNameOrQwerty() == SubtypeLocaleUtils.QWERTY }
if (rawNoLanguageSubtype != null) {
noLanguageSubtype = RichInputMethodSubtype(rawNoLanguageSubtype)
}

View file

@ -0,0 +1,124 @@
// 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.makedict.WordProperty
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
}
fun getWordProperty(word: String): WordProperty? = dict.getWordProperty(word, false)
// ------------ 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)
}
}
}

View file

@ -45,19 +45,20 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
mAutoCorrectionThreshold = threshold
}
// todo: remove when InputLogic is ready
interface OnGetSuggestedWordsCallback {
fun onGetSuggestedWords(suggestedWords: SuggestedWords?)
}
fun getSuggestedWords(wordComposer: WordComposer, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, isCorrectionEnabled: Boolean,
inputStyle: Int, sequenceNumber: Int, callback: OnGetSuggestedWordsCallback) {
if (wordComposer.isBatchMode) {
inputStyle: Int, sequenceNumber: Int): SuggestedWords {
return if (wordComposer.isBatchMode) {
getSuggestedWordsForBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion,
inputStyle, sequenceNumber, callback)
inputStyle, sequenceNumber)
} else {
getSuggestedWordsForNonBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion,
inputStyle, isCorrectionEnabled, sequenceNumber, callback)
inputStyle, isCorrectionEnabled, sequenceNumber)
}
}
@ -65,7 +66,7 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
// and calls the callback function with the suggestions.
private fun getSuggestedWordsForNonBatchInput(wordComposer: WordComposer, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, inputStyleIfNotPrediction: Int,
isCorrectionEnabled: Boolean, sequenceNumber: Int, callback: OnGetSuggestedWordsCallback) {
isCorrectionEnabled: Boolean, sequenceNumber: Int): SuggestedWords {
val typedWordString = wordComposer.typedWord
val resultsArePredictions = !wordComposer.isComposingWord
val suggestionResults = if (typedWordString.isEmpty())
@ -131,8 +132,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
}
}
val isTypedWordValid = firstOccurrenceOfTypedWordInSuggestions > -1 || (!resultsArePredictions && !allowsToBeAutoCorrected)
callback.onGetSuggestedWords(SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions,
typedWordInfo, isTypedWordValid, hasAutoCorrection, false, inputStyle, sequenceNumber))
return SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions,
typedWordInfo, isTypedWordValid, hasAutoCorrection, false, inputStyle, sequenceNumber)
}
// returns [allowsToBeAutoCorrected, hasAutoCorrection]
@ -252,9 +253,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
wordComposer: WordComposer,
ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion,
inputStyle: Int, sequenceNumber: Int,
callback: OnGetSuggestedWordsCallback
) {
inputStyle: Int, sequenceNumber: Int
): SuggestedWords {
val suggestionResults = mDictionaryFacilitator.getSuggestionResults(
wordComposer.composedDataSnapshot, ngramContext, keyboard,
settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle
@ -312,10 +312,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
} else {
suggestionsContainer
}
callback.onGetSuggestedWords(
SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, pseudoTypedWordInfo, true,
return SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, pseudoTypedWordInfo, true,
false, false, inputStyle, sequenceNumber)
)
}
/** reduces score of the first suggestion if next one is close and has more than a single letter */

View file

@ -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);
}

View file

@ -10,8 +10,6 @@ import androidx.annotation.NonNull;
import helium314.keyboard.event.CombinerChain;
import helium314.keyboard.event.Event;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
@ -73,7 +71,7 @@ public final class WordComposer {
private boolean mIsOnlyFirstCharCapitalized;
public WordComposer() {
mCombinerChain = new CombinerChain("");
mCombinerChain = new CombinerChain("", "");
mEvents = new ArrayList<>();
mAutoCorrection = null;
mIsResumed = false;
@ -81,11 +79,6 @@ public final class WordComposer {
mCursorPositionWithinWord = 0;
mRejectedBatchModeSuggestion = null;
refreshTypedWordCache();
final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
if (keyboard != null)
// initializing with the right state is important for the spell checker,
// which creates a new WordComposer when receiving suggestions
mCombinerChain.setHangul(keyboard.mId.mSubtype.getLocale().getLanguage().equals("ko"));
}
public ComposedData getComposedDataSnapshot() {
@ -99,14 +92,11 @@ public final class WordComposer {
public void restartCombining(final String combiningSpec) {
final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString());
mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString(), nonNullCombiningSpec);
mCombiningSpec = nonNullCombiningSpec;
}
}
/** Forwards the state to CombinerChain, which disables or enables the Hangul combiner */
public void setHangul(final boolean enabled) { mCombinerChain.setHangul(enabled); }
/**
* Clear out the keys registered so far.
*/

View file

@ -278,7 +278,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
TOOL_BAR_EXPAND_KEY_BACKGROUND -> if (!isNight) accent else doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON, EMOJI_KEY_TEXT, KEY_PREVIEW_TEXT, POPUP_KEY_TEXT,
KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
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
@ -476,7 +476,7 @@ class DefaultColors (
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON, EMOJI_KEY_TEXT,
POPUP_KEY_TEXT, KEY_PREVIEW_TEXT -> keyText
POPUP_KEY_TEXT, POPUP_KEY_ICON, KEY_PREVIEW_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@ -637,6 +637,7 @@ enum class ColorType {
MORE_SUGGESTIONS_WORD_BACKGROUND,
POPUP_KEYS_BACKGROUND,
POPUP_KEY_TEXT,
POPUP_KEY_ICON,
NAVIGATION_BAR,
SHIFT_KEY_ICON,
SPACE_BAR_BACKGROUND,

View file

@ -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 */);
}
}

View file

@ -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
}
}
}

View file

@ -77,7 +77,7 @@ public final class Constants {
/** Indicates that this subtype is an additional subtype that the user defined. This extra value is private to LatinIME. */
public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
/** The subtype extra value used to specify the combining rules (currently not used). */
/** The subtype extra value used to specify the combining rules. */
public static final String COMBINING_RULES = "CombiningRules";
/** Overrides the general popup order setting */
@ -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 = " ";

View file

@ -12,3 +12,5 @@ object Links {
const val CUSTOM_LAYOUTS = "$GITHUB/discussions/categories/custom-layout"
const val CUSTOM_COLORS = "$GITHUB/discussions/categories/custom-colors"
}
val combiningRange = 0x300..0x35b

View file

@ -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)
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
}
if (overrideResId != 0) {
return if (displayLocale == null) resources.getString(overrideResId)
else runInLocale(resources, displayLocale) { it.getString(overrideResId) }
}
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 localeDisplayName = locale.getDisplayName(displayLocale)
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
}

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