diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..17e32163fd
--- /dev/null
+++ b/.editorconfig
@@ -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
diff --git a/.github/DISCUSSION_TEMPLATE/custom-colors.yml b/.github/DISCUSSION_TEMPLATE/custom-colors.yml
new file mode 100644
index 0000000000..68d57be2a3
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/custom-colors.yml
@@ -0,0 +1,18 @@
+body:
+- type: input
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ placeholder: |
+ "You can add some description and screenshots here if you want."
+ validations:
+ required: false
+ - type: textarea
+ id: colors
+ attributes:
+ label: Colors
+ placeholder: |
+ "Please paste the colors (obtained via the _copy_ button) here, and surround the text with ``` to make sure it's displayed in a way that can be copied easily."
+ validations:
+ required: true
diff --git a/.github/DISCUSSION_TEMPLATE/custom-layout.yml b/.github/DISCUSSION_TEMPLATE/custom-layout.yml
new file mode 100644
index 0000000000..03f8832ab7
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/custom-layout.yml
@@ -0,0 +1,18 @@
+body:
+- type: input
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ placeholder: |
+ "You can add some description and screenshots here if you want."
+ validations:
+ required: false
+ - type: textarea
+ id: layout
+ attributes:
+ label: Layout
+ placeholder: |
+ "Please paste the layout here, and surround it with ``` to make sure it's displayed in a way that can be copied easily."
+ validations:
+ required: true
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000000..4dc2c8057b
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+
+liberapay: Helium314
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 5c3c12385c..f7191c8b0e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,31 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
-title: ''
labels: bug
-assignees: ''
-
---
+Please see the appropriate readme section for issue reporting guidelines: https://github.com/Helium314/HeliBoard?tab=readme-ov-file#reporting-issues
+tl;dr:
+* search for duplicates, also in closed issues
+* a single issue per topic
+* reduce screenshot size
+
+
+
**Describe the bug**
-A clear and concise description of what the bug is.
**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
+If possible, provide all the necessary steps to reproduce your problem, including the involved apps or settings if relevant.
+In case you cannot reproduce the bug, say so and provide information about when the bug may occur for you. Settings and the app you're writing in are usually important, please don't omit them.
**Expected behavior**
-A clear and concise description of what you expected to happen.
+If it's not obvious (e.g. not crash), describe how you think the app should behave.
**Screenshots**
-If applicable, add screenshots to help explain your problem.
+ONLY add screenshots when they add real value.
+If you add screenshots, reduce the size or use thumbnails to keep the issue nicely readable.
-**Smartphone (please complete the following information):**
- - Device: [e.g. Samsung Galaxy S9]
- - OS: [e.g. Android 10]
+**App version**
+Please provide the explicit version (not just "latest"), or if you build the app yourself specify the latest commit.
-**Additional context**
-Add any other context about the problem here.
+**Device:**
+ - Model: [e.g. Samsung Galaxy S9]
+ - OS: [e.g. Android 10] (please also mention whether you are using the manufacturer's OS or a custom ROM)
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..80a8e648d2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Dictionary request
+ url: https://codeberg.org/Helium314/aosp-dictionaries
+ about: Requests for dictionaries (used for suggestions / autocompletion) will be handled in the linked dictionary repository. You can check whether a dictionary for the language you want already exists there.
+ - name: Discussion
+ url: https://github.com/Helium314/HeliBoard/discussions
+ about: For discussions and feedback about this app, asking questions or talking about ideas which are not yet actionable (i.e. not suitable for an issue).
+ - name: Question
+ url: https://github.com/Helium314/HeliBoard/discussions/new?category=q-a
+ about: For questions please use the discussions section. You may also want to search your question in the FAQ (see readme).
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 11fc491ef1..eb9f3ae62b 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,20 +1,32 @@
---
name: Feature request
about: Suggest an idea for this project
-title: ''
labels: enhancement
-assignees: ''
-
---
+Please see the appropriate readme section for issue reporting guidelines: https://github.com/Helium314/HeliBoard?tab=readme-ov-file#reporting-issues
+tl;dr:
+* search for duplicates, also in closed issues
+* check FAQ / hidden features
+* a single issue per topic
+* ONLY add screenshots when necessary, and reduce their size
+
+
+
+
+
**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
+Please provide a description of what you would like to have. The clearer it is described, the better it can be implemented the way you want it.
+
+**Use case**
+Provide a clear and concise description of *your use case* and what you thus think is missing, and why.
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+**Describe alternatives you've considered (if any)**
-**Additional context**
-Add any other context or screenshots about the feature request here.
+**App version**
+Please provide the explicit version, you're using.
diff --git a/.github/ISSUE_TEMPLATE/new-keyboard-layout-request.md b/.github/ISSUE_TEMPLATE/new-keyboard-layout-request.md
deleted file mode 100644
index 3e6bd59183..0000000000
--- a/.github/ISSUE_TEMPLATE/new-keyboard-layout-request.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: New keyboard layout request
-about: Use this to propose a new layout
-title: "*insert here layout name* layout"
-labels: layout
-assignees: ''
-
----
-
-Add here references to the layout (links or screenshots).
diff --git a/.github/ISSUE_TEMPLATE/new-language-request.md b/.github/ISSUE_TEMPLATE/new-language-request.md
deleted file mode 100644
index 145375bea0..0000000000
--- a/.github/ISSUE_TEMPLATE/new-language-request.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: New language request
-about: Use this to request autocompletion support for a missing language
-title: "*insert here language name* language"
-labels: dictionaries
-assignees: ''
-
----
-
-
diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md
new file mode 100644
index 0000000000..b3fb0668a4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/other.md
@@ -0,0 +1,12 @@
+---
+name: Other
+about: Anything that does not fit into the other categories. Please don't use this for questions, discussions, or anything that fits into one of the other issue categories.
+---
+
+Please see the appropriate readme section for issue reporting guidelines: https://github.com/Helium314/HeliBoard?tab=readme-ov-file#reporting-issues
+tl;dr:
+* search for duplicates, also in closed issues
+* a single issue per topic
+* ONLY add screenshots when necessary, and reduce their size
+
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..31f81344f8
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,3 @@
+Please make sure you are at least reasonably close to the contribution guidelines: https://github.com/Helium314/HeliBoard/blob/main/CONTRIBUTING.md#guidelines
+Due to maintainer availability, your PR may take quite some time to be addressed
+
diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
deleted file mode 100644
index 60c3a5ab3d..0000000000
--- a/.github/workflows/android-build.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: Build
-
-on:
- push:
- branches: [ master ]
- pull_request:
- branches: [ master ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Set up JDK
- uses: actions/setup-java@v2
- with:
- java-version: '11'
- distribution: 'temurin'
- cache: gradle
-
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
-
- - name: Build with Gradle
- run: ./gradlew assembleDebug
-
- - name: Upload APK
- uses: actions/upload-artifact@v2.2.0
- with:
- name: APK
- path: app/build/outputs/apk/debug/app-debug.apk
-
- - name: Upload lint report
- uses: actions/upload-artifact@v2.2.0
- with:
- name: Lint report
- path: app/build/reports/lint-results-debug.html
diff --git a/.github/workflows/build-debug-apk.yml b/.github/workflows/build-debug-apk.yml
new file mode 100644
index 0000000000..5ae9394457
--- /dev/null
+++ b/.github/workflows/build-debug-apk.yml
@@ -0,0 +1,39 @@
+name: Build debug APK
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - uses: gradle/actions/setup-gradle@v3
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle
+ run: ./gradlew assembleDebug
+
+ - name: Upload APK
+ uses: actions/upload-artifact@v4
+ with:
+ name: HeliBoard-debug
+ path: app/build/outputs/apk/debug/*-debug*.apk
+
+ - name: Archive reports for failed job
+ uses: actions/upload-artifact@v4
+ with:
+ name: reports
+ path: '*/build/reports'
+ if: ${{ failure() }}
diff --git a/.github/workflows/build-test-auto.yml b/.github/workflows/build-test-auto.yml
new file mode 100644
index 0000000000..a887aa5f8f
--- /dev/null
+++ b/.github/workflows/build-test-auto.yml
@@ -0,0 +1,43 @@
+name: Test build
+# builds only for a single abi and does not produce an APK
+
+on:
+# disabled on push: when I push to non-main, I do a PR anyway
+# push:
+ # don't run on main. I noticed I often don't push commits to avoid unnecessary workflow runs
+# branches-ignore: [ main ]
+# paths:
+# - 'app/**'
+ pull_request:
+ paths:
+ - 'app/src/main/java**'
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - uses: gradle/actions/setup-gradle@v3
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle
+ run: ./gradlew testRunTestsUnitTest
+
+ - name: Archive reports for failed job
+ uses: actions/upload-artifact@v4
+ with:
+ name: reports
+ path: '*/build/reports'
+ if: ${{ failure() }}
diff --git a/.gitignore b/.gitignore
index 99f9ce5b7e..955e1c4887 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.iml
.idea
.gradle
+.kotlin
local.properties
.DS_Store
Gemfile
@@ -8,4 +9,6 @@ build
app/build
app/release
app/.cxx
+app/.attach_*
fastlane/Appfile
+tools/*.txt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..60c26f2689
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,78 @@
+# Getting Started
+
+HeliBoard project is based on Gradle and Android Gradle Plugin. To get started, you can install [Android Studio](https://developer.android.com/studio), and import project 'from Version Control / Git / Github' by providing this git repository [URL](https://github.com/Helium314/HeliBoard) (or git SSH [URL](git@github.com:Helium314/heliboard.git)).
+Of course you can also use any other compatible IDE, or work with text editor and command line.
+Once everything is up correctly, you're ready to go!
+
+If you have difficulties implementing some functionality, you're welcome to ask for help. No one will write the code for you, but often other contributors can give you very useful hints.
+
+# About the Code
+
+HeliBoard is based on AOSP keyboard, and in many places still contains mostly the original code. There are some extensions, and some parts have been replaced completely.
+When working on this app, you will likely notice its rather large size, and quite different code styles and often ancient comments and _TODO_s, where the latter are typically untouched since AOSP times.
+Unfortunately a lot of the old code is hard to read or to fully understand with all of its intended (and unintended) consequences.
+
+Some hints for finding what you're looking for:
+* Layouts: stored in `layouts` folder in assets, interpreted by `KeyboardParser` and `TextKeyData`
+ * Popups: either on layouts, or in `locale_key_texts` (mostly letter variations for specific languages that are not dependent on layout)
+* Touch and swipe input handling: `PointerTracker`
+* Handling of key inputs: `InputLogic`
+* Suggestions: `DictionaryFacilitatorImpl`, `Suggest`, `InputLogic`, and `SuggestionStripView` (in order from creation to display)
+* Forwarding entered text / keys to the app / text field: `RichInputConnection`
+* Receiving events and information from the app / text field: `LatinIME`
+* Settings are in `SettingsValues`, with some functionality in `Settings` and the default values in `Default`
+
+# Guidelines
+
+## Recommended
+
+If you want to contribute, it's a good idea to make sure your idea is actually wanted in HeliBoard.
+Best check related issues before you start working on a PR. If the issue has the [labels](https://github.com/Helium314/HeliBoard/labels) [_PR_](https://github.com/Helium314/HeliBoard/labels/PR) or [_contributor needed_](https://github.com/Helium314/HeliBoard/issues?q=label%3A%22contributor%20needed%22) (even closed ones), contributions are wanted. If you don't find a related issue, it's recommended to open one, but ultimately it's your choice.
+Asking before starting a PR may help you for getting pointers to potentially relevant code, and deciding how to implement your desired changes.
+
+HeliBoard is a complex application and used by users with a large variety of opinions on how things should be.
+When contributing to the app, please:
+* Be careful when modifying core components, as it's easy to trigger unintended consequences
+* When introducing a feature or change that might not be wanted by everyone, make it optional
+* Keep code simple where possible. Complex code is harder to review and to maintain, so the complexity should also add a clear benefit
+* Avoid noticeable performance impact. Some parts of the code are executed very frequently, and the keyboard should stay responsive even on older devices.
+* Try making use of in-place mechanisms instead of re-inventing the wheel. Your contribution should only add as much complexity as necessary, the code is overly complicated already 😶.
+* Keep your changes to few places, as opposed to sprinkling them over many parts of the code. This helps with keeping down complexity during review, and with maintainability of the app.
+* Make a draft PR when you intend to still work on it. Submitting an unfinished PR can be a good idea when you're not sure how to best continue and would like some comments.
+
+Further things to consider (though irrelevant for most PRs):
+* APK size:
+ * Large increases should be discussed first, and will only be added when it's considered worth the increase for a majority of users. It might be possible to avoid size increase by importing optional parts, like it's done for dictionaries.
+ * Small increases like when adding code or layouts are never an issue
+* Do not add proprietary code or binary blobs. If it turns out to be necessary for a feature you want to add, it might be acceptable when the user opts in and imports those parts, like it's done for glide typing.
+* Privacy: Only relevant when adding some form of communication with other apps. Internet permission will not be added.
+* If your contribution contains code that is not your own, provide a link to the source
+ * This is especially relevant to be sure the code's license is compatible to HeliBoard's GPL3
+
+## Necessary
+
+Some parts of the guidelines are necessary to fulfill for facilitating code review. It doesn't need to be perfect from the start, but consider it for your future PRs when you're reminded of these guidelines. Note that the larger / more complex your PR is, the more relevant these guidelines are.
+Your PR should:
+- **Be only about a single thing**. Mixing unrelated or semi-related contributions into a single PR is hard to review and can get messy. As a general rule: if one part doesn't need the other one(s), it should be separate PRs. If one feature builds on top of another one, but the base is usable on its own, do a PR for the base and then a follow-up once it's merged.
+- **Have a proper description**. A good description helps _a lot_ for understanding what you intend to achieve with the changes, and for understanding the code. This is relevant for separating wanted from unintended changes in behavior during review.
+- **No translations**. Translations should be done using [Weblate](https://translate.codeberg.org/projects/heliboard/). Exception is when you add new resource strings, those can be added right away.
+
+Please leave dependency upgrades to the maintainers, unless you state a good reason why they should be done now.
+
+# Adding / Adjusting Layouts
+
+See [layouts.md](layouts.md#adding-new-layouts--languages) for how to add new layouts to the app. Please stay in line with other layouts regarding the popup keys.
+
+When editing existing layouts, please consider that people should should still get what they're used to. In case of doubt it might be better to add a new layout instead of overhauling existing layouts.
+`locale_key_texts` files should only contain letters that are actually part of the language, with exception of the optional `more_popups_<...>.txt` files.
+
+# Update Emojis
+
+See make-emoji-keys tool [README](tools/make-emoji-keys/README.md).
+
+# Translations
+Translations can be added using [Weblate](https://translate.codeberg.org/projects/heliboard/). You will need an account to update translations and add languages. Add the language you want to translate to in Languages -> Manage translated languages in the top menu bar.
+Updating translations in a PR will not be accepted, as it may cause conflicts with Weblate translations.
+
+# Dictionaries
+No new dictionaries will be added to this app. Please submit dictionaries and the wordlist to the [dictionaries repository](https://codeberg.org/Helium314/aosp-dictionaries)
diff --git a/LICENSE-Apache-2.0 b/LICENSE-Apache-2.0
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/LICENSE-Apache-2.0
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/LICENSE-CC-BY-SA-4.0 b/LICENSE-CC-BY-SA-4.0
new file mode 100644
index 0000000000..2d58298e6e
--- /dev/null
+++ b/LICENSE-CC-BY-SA-4.0
@@ -0,0 +1,428 @@
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. BY-SA Compatible License means a license listed at
+ creativecommons.org/compatiblelicenses, approved by Creative
+ Commons as essentially the equivalent of this Public License.
+
+ d. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ e. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ g. License Elements means the license attributes listed in the name
+ of a Creative Commons Public License. The License Elements of this
+ Public License are Attribution and ShareAlike.
+
+ h. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ i. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ j. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ k. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ l. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ m. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. Additional offer from the Licensor -- Adapted Material.
+ Every recipient of Adapted Material from You
+ automatically receives an offer from the Licensor to
+ exercise the Licensed Rights in the Adapted Material
+ under the conditions of the Adapter's License You apply.
+
+ c. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ b. ShareAlike.
+
+ In addition to the conditions in Section 3(a), if You Share
+ Adapted Material You produce, the following conditions also apply.
+
+ 1. The Adapter's License You apply must be a Creative Commons
+ license with the same License Elements, this version or
+ later, or a BY-SA Compatible License.
+
+ 2. You must include the text of, or the URI or hyperlink to, the
+ Adapter's License You apply. You may satisfy this condition
+ in any reasonable manner based on the medium, means, and
+ context in which You Share Adapted Material.
+
+ 3. You may not offer or impose any additional or different terms
+ or conditions on, or apply any Effective Technological
+ Measures to, Adapted Material that restrict exercise of the
+ rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material,
+ including for purposes of Section 3(b); and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
+
diff --git a/README.md b/README.md
index 9d737dd831..74a8a21a91 100644
--- a/README.md
+++ b/README.md
@@ -1,79 +1,110 @@
-# OpenBoard
-
-[](https://github.com/openboard-team/openboard/actions/workflows/android-build.yml)
-[](https://github.com/openboard-team/openboard/releases)
-[](https://github.com/openboard-team/openboard/commits/master)
-[](https://hosted.weblate.org/engage/openboard/)
-
-
-
-
-
-
-100% FOSS keyboard, based on AOSP.
-
-## Community [matrix] channel
-
-
-Join [here](https://matrix.to/#/#openboard:matrix.org?via=matrix.org)
-
-## Common issues
-- Cannot open settings in MIUI: See [issue #46](https://github.com/dslul/openboard/issues/46).
-
-## Contribute
-
-### Translation
-You can help in translating OpenBoard in your language through our [Weblate project](https://hosted.weblate.org/engage/openboard/).
-[](https://hosted.weblate.org/engage/openboard/)
-
-### How to create a dictionary
-You can use [this tool](https://github.com/remi0s/aosp-dictionary-tools) to create a dictionary. You need a wordlist, as described [here](dictionaries/sample.combined). The output .dict file must be put in [res/raw](app/src/main/res/raw).
-
-### How to edit keyboard texts
-Make your modifications in [tools/make-keyboard-text/src/main/resources](tools/make-keyboard-text/src/main/resources)/values-YOUR LOCALE.
-
-Generate the new version of [KeyboardTextsTable.java](app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/KeyboardTextsTable.java):
-```sh
-./gradlew tools:make-keyboard-text:makeText
-```
-
-
-
-### APK Development
-
-#### Linux
-
-Install java:
-```sh
-sudo pacman -S jdk11-openjdk jre11-openjdk jre11-openjdk-headless
-```
-
-Install Android SDK:
-```sh
-sudo pacman -S snapd
-sudo snap install androidsdk
-```
-
-Configure your SDK location in your `~/.bash_profile` or `~/.bashrc`:
-```bash
-export ANDROID_SDK_ROOT=~/snap/androidsdk/current/AndroidSDK/
-```
-
-Compile the project. This will install all dependencies, make sure to accept
-licenses when prompted.
-
-```sh
-./gradlew assembleDebug
-```
-
-Connect your phone and install the debug APK
-```sh
-adb install ./app/build/outputs/apk/debug/app-debug.apk
-```
-## Credits
-- icon by [Marco TLS](https://www.marcotls.eu)
-
+# HeliBoard
+HeliBoard is a privacy-conscious and customizable open-source keyboard, based on AOSP / OpenBoard.
+Does not use internet permission, and thus is 100% offline.
+
+[
](https://f-droid.org/packages/helium314.keyboard/)
+[
](https://github.com/Helium314/HeliBoard/releases/latest)
+[
](https://apt.izzysoft.de/fdroid/index/apk/helium314.keyboard)
+
+## Table of Contents
+
+- [Features](#features)
+- [Contributing](#contributing-)
+ * [Reporting Issues](#reporting-issues)
+ * [Translations](#translations)
+ * [To Community Creation](#to-community)
+ * [Code Contribution](CONTRIBUTING.md)
+- [License](#license)
+- [Credits](#credits)
+
+# Features
+
+ - Add dictionaries for suggestions and spell check
+
+ - build your own, or get them here, or in the experimental section (quality may vary)
+ - additional dictionaries for emojis or scientific symbols can be used to provide suggestions (similar to "emoji search")
+ - note that for Korean layouts, suggestions only work using this dictionary, the tools in the dictionary repository are not able to create working dictionaries
+
+ - Customize keyboard themes (style, colors and background image)
+
+ - can follow the system's day/night setting on Android 10+ (and on some versions of Android 9)
+ - can follow dynamic colors for Android 12+
+
+ - Customize keyboard layouts (only available when disabling use system languages)
+ - Customize special layouts, like symbols, number, or functional key layout
+ - Multilingual typing
+ - Glide typing (only with closed source library ☹️)
+
+ - library not included in the app, as there is no compatible open source library available
+ - can be extracted from GApps packages ("swypelibs"), or downloaded here (click on the file and then "raw" or the tiny download button)
+
+ - Clipboard history
+ - One-handed mode
+ - Split keyboard
+ - Number pad
+ - Backup and restore your settings and learned word / history data
+
+
+For [FAQ](https://github.com/Helium314/HeliBoard/wiki/FAQ), [hidden features](https://github.com/Helium314/HeliBoard/wiki/9.-Hidden-features) and more information about the app and features, please visit the [wiki](https://github.com/Helium314/HeliBoard/wiki)
+
+# Contributing ❤
+
+## Reporting Issues
+
+Whether you encountered a bug, or want to see a new feature in HeliBoard, you can contribute to the project by opening a new issue [here](https://github.com/Helium314/HeliBoard/issues). Your help is always welcome!
+
+Before opening a new issue, be sure to check the following:
+ - **Does the issue already exist?** Make sure a similar issue has not been reported by browsing [existing issues](https://github.com/Helium314/HeliBoard/issues?q=). Please search open and closed issues. In case of feature requests you could also check the [FAQ](https://github.com/Helium314/HeliBoard/wiki/FAQ) and [hidden features](https://github.com/Helium314/HeliBoard/wiki/9.-Hidden-features).
+ - **Is the issue still relevant?** Make sure your issue is not already fixed in the latest version of HeliBoard.
+ - **Is it a single topic?** If you want to suggest multiple things, open multiple issues.
+ - **Did you use the issue template?** It is important to make life of our kind contributors easier by avoiding issues that miss key information to their resolution.
+Note that issues that that ignore part of the issue template will likely get treated with very low priority, as often they are needlessly hard to read or understand (e.g. huge screenshots, not providing a proper description, or addressing multiple topics). Blatant violation of the guidelines may result in the issue getting closed.
+
+If you're interested, you can read the following useful text about effective bug reporting (a bit longer read): https://www.chiark.greenend.org.uk/~sgtatham/bugs.html
+
+## Translations
+Translations can be added using [Weblate](https://translate.codeberg.org/projects/heliboard/). You will need an account to update translations and add languages. Add the language you want to translate to in Languages -> Manage translated languages in the top menu bar.
+Updating translations in a PR will not be accepted, as it may cause conflicts with Weblate translations.
+
+Some notes on translations
+* when translating metadata, translating the changelogs is rather useless. It's available as it was requested by translators.
+* the `hidden_features_message` is horrible to translate with Weblate, and serves little benefit as it's just a copy of what's already in the wiki: https://github.com/Helium314/HeliBoard/wiki/9.-Hidden-features. It's been made available in the app on user request/contribution.
+
+## To Community
+There is the [discussions on GitHub](https://github.com/Helium314/HeliBoard/discussions), or if you prefer a more open network there is [Lemmy](https://lemmy.world/c/Heliboard).
+You can share your themes, layouts and dictionaries with other people:
+* Themes can be saved and loaded using the menu on top-right in the _adjust colors_ screen
+ * You can share custom colors in a separate [discussion section](https://github.com/Helium314/HeliBoard/discussions/categories/custom-colors)
+* Custom keyboard layouts are text files whose content you can edit, copy and share
+ * this applies to main keyboard layouts and to special layouts adjustable in advanced settings
+ * see [layouts.md](layouts.md) for details
+ * You can share custom layouts in a separate [discussion section](https://github.com/Helium314/HeliBoard/discussions/categories/custom-layout)
+* Creating dictionaries is a little more work
+ * first you will need a wordlist, as described [here](https://codeberg.org/Helium314/aosp-dictionaries/src/branch/main/wordlists/sample.combined) and in the repository readme
+ * the you need to compile the dictionary using [external tools](https://github.com/remi0s/aosp-dictionary-tools)
+ * the resulting file (and ideally the wordlist too) can be shared with other users
+ * note that there will not be any further dictionaries added to this app, but you can add dictionaries to the [dictionaries repository](https://codeberg.org/Helium314/aosp-dictionaries)
+
+## Code Contribution
+See [Contribution Guidelines](CONTRIBUTING.md)
+
+# License
+
+HeliBoard (as a fork of OpenBoard) is licensed under GNU General Public License v3.0.
+
+ > Permissions of this strong copyleft license are conditioned on making available complete source code of licensed works and modifications, which include larger works using a licensed work, under the same license. Copyright and license notices must be preserved. Contributors provide an express grant of patent rights.
+
+See repo's [LICENSE](/LICENSE) file.
+
+Since the app is based on Apache 2.0 licensed AOSP Keyboard, an [Apache 2.0](LICENSE-Apache-2.0) license file is provided.
+The icon is licensed under [Creative Commons BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). A [license file](LICENSE-CC-BY-SA-4.0) is also included.
+
+# Credits
+- Icon by [Fabian OvrWrt](https://github.com/FabianOvrWrt) with contributions from [The Eclectic Dyslexic](https://github.com/the-eclectic-dyslexic)
+- [OpenBoard](https://github.com/openboard-team/openboard)
- [AOSP Keyboard](https://android.googlesource.com/platform/packages/inputmethods/LatinIME/)
- [LineageOS](https://review.lineageos.org/admin/repos/LineageOS/android_packages_inputmethods_LatinIME)
- [Simple Keyboard](https://github.com/rkkr/simple-keyboard)
- [Indic Keyboard](https://gitlab.com/indicproject/indic-keyboard)
+- [FlorisBoard](https://github.com/florisboard/florisboard/)
+- Our [contributors](https://github.com/Helium314/HeliBoard/graphs/contributors)
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100755
index b9ca2978de..0000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,53 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-
-android {
- compileSdkVersion 31
-
- defaultConfig {
- applicationId "org.dslul.openboard.inputmethod.latin"
- minSdkVersion 19
- targetSdkVersion 31
- versionCode 18
- versionName '1.4.4'
- }
-
- buildTypes {
- release {
- minifyEnabled false
- debuggable false
- jniDebuggable false
- renderscriptDebuggable false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-
-
- externalNativeBuild {
- ndkBuild {
- path 'src/main/jni/Android.mk'
- }
- }
-
- lintOptions {
- abortOnError false
- }
-
- ndkVersion '21.3.6528147'
- androidResources {
- noCompress 'dict'
- }
-}
-
-dependencies {
- implementation 'com.google.code.findbugs:jsr305:3.0.2'
- implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.recyclerview:recyclerview:1.2.1' // Replaces recyclerview:1.0.0 included by above dependency
- implementation 'androidx.core:core-ktx:1.7.0'
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'androidx.viewpager2:viewpager2:1.0.0'
-}
-repositories {
- mavenCentral()
-}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100755
index 0000000000..40740ab888
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,146 @@
+import com.android.build.api.variant.ApplicationVariant
+
+plugins {
+ id("com.android.application")
+ kotlin("android")
+ kotlin("plugin.serialization") version "2.2.21"
+ kotlin("plugin.compose") version "2.2.21"
+}
+
+android {
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "helium314.keyboard"
+ minSdk = 21
+ targetSdk = 35
+ versionCode = 3603
+ versionName = "3.6"
+ ndk {
+ abiFilters.clear()
+ abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
+ }
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = false
+ isDebuggable = false
+ isJniDebuggable = false
+ }
+ create("nouserlib") { // same as release, but does not allow the user to provide a library
+ isMinifyEnabled = true
+ isShrinkResources = false
+ isDebuggable = false
+ isJniDebuggable = false
+ }
+ debug {
+ // "normal" debug has minify for smaller APK to fit the GitHub 25 MB limit when zipped
+ // and for better performance in case users want to install a debug APK
+ isMinifyEnabled = true
+ isJniDebuggable = false
+ applicationIdSuffix = ".debug"
+ }
+ create("runTests") { // build variant for running tests on CI that skips tests known to fail
+ isMinifyEnabled = false
+ isJniDebuggable = false
+ }
+ create("debugNoMinify") { // for faster builds in IDE
+ isDebuggable = true
+ isMinifyEnabled = false
+ isJniDebuggable = false
+ signingConfig = signingConfigs.getByName("debug")
+ applicationIdSuffix = ".debug"
+ }
+ base.archivesBaseName = "HeliBoard_" + defaultConfig.versionName
+ // got a little too big for GitHub after some dependency upgrades, so we remove the largest dictionary
+ androidComponents.onVariants { variant: ApplicationVariant ->
+ if (variant.buildType == "debug") {
+ variant.androidResources.ignoreAssetsPatterns = listOf("main_ro.dict")
+ variant.proguardFiles = emptyList()
+ //noinspection ProguardAndroidTxtUsage we intentionally use the "normal" file here
+ variant.proguardFiles.add(project.layout.buildDirectory.file(getDefaultProguardFile("proguard-android.txt").absolutePath))
+ variant.proguardFiles.add(project.layout.buildDirectory.file(project.buildFile.parent + "/proguard-rules.pro"))
+ }
+ }
+ }
+
+ buildFeatures {
+ viewBinding = true
+ buildConfig = true
+ compose = true
+ }
+
+ externalNativeBuild {
+ cmake {
+ path = File("src/main/jni/CMakeLists.txt")
+ }
+ }
+ ndkVersion = "28.0.13004108"
+
+ packaging {
+ jniLibs {
+ // shrinks APK by 3 MB, zipped size unchanged
+ useLegacyPackaging = true
+ }
+ }
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ // see https://github.com/Helium314/HeliBoard/issues/477
+ dependenciesInfo {
+ includeInApk = false
+ includeInBundle = false
+ }
+
+ namespace = "helium314.keyboard.latin"
+ lint {
+ abortOnError = true
+ }
+}
+
+dependencies {
+ // androidx
+ implementation("androidx.core:core-ktx:1.16.0") // 1.17 requires SDK 36
+ implementation("androidx.recyclerview:recyclerview:1.4.0")
+ implementation("androidx.autofill:autofill:1.3.0")
+ implementation("androidx.viewpager2:viewpager2:1.1.0")
+
+ // kotlin
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
+
+ // compose
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
+ // newer than 2025.11.01 contains androidx.compose.material:material-android:1.10.0, which requires minSdk 23
+ // maybe it's possible to use tools:overrideLibrary="androidx.compose.material" as it's not used explicitly, but probably this is just going to crash
+ implementation(platform("androidx.compose:compose-bom:2025.11.01"))
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ implementation("androidx.navigation:navigation-compose:2.9.6")
+ implementation("sh.calvin.reorderable:reorderable:2.4.3") // for easier re-ordering, todo: check 3.0.0
+ implementation("com.github.skydoves:colorpicker-compose:1.1.3") // for user-defined colors
+
+ // test
+ testImplementation(kotlin("test"))
+ testImplementation("junit:junit:4.13.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")
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f7bfc889b6..5af7eb4706 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,17 +1,14 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /home/iwo/android-sdk/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
+# Keep native methods
+-keepclassmembers class * {
+ native ;
+}
-# Add any project specific keep options here:
+# Keep classes that are used as a parameter type of methods that are also marked as keep
+# to preserve changing those methods' signature.
+-keep class helium314.keyboard.latin.dictionary.Dictionary
+-keep class helium314.keyboard.latin.NgramContext
+-keep class helium314.keyboard.latin.makedict.ProbabilityInfo
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+# after upgrading to gradle 8, stack traces contain "unknown source"
+-keepattributes SourceFile,LineNumberTable
+-dontobfuscate
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 0000000000..e8755c20a2
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+ HeliBoard debug
+ HeliBoard debug Spell Checker
+ HeliBoard debug Settings
+
+
diff --git a/app/src/debugNoMinify/res/values/strings.xml b/app/src/debugNoMinify/res/values/strings.xml
new file mode 100644
index 0000000000..73f1a95de9
--- /dev/null
+++ b/app/src/debugNoMinify/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+
+ HeliBoard debug
+ HeliBoard debug Spell Checker
+ HeliBoard debug Settings
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 654bb78f26..5d9b3eb13c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,52 +1,29 @@
-
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
+
+ tools:remove="android:appComponentFactory"
+ tools:targetApi="p">
-
+
-
@@ -85,46 +60,30 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
+
+
+
-
-
-
@@ -138,17 +97,24 @@
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/dictionaries_in_dict_repo.csv b/app/src/main/assets/dictionaries_in_dict_repo.csv
new file mode 100644
index 0000000000..18a6ae7e2a
--- /dev/null
+++ b/app/src/main/assets/dictionaries_in_dict_repo.csv
@@ -0,0 +1,267 @@
+emoji,af,cldr
+main,af,exp
+emoji,ak,cldr
+emoji,sq,cldr
+emoji,am,cldr
+emoji,blo,cldr
+emoji,ar,cldr
+main,ar,
+main,ar,exp
+emoji,hy,cldr
+main,hy,
+main,hy_EAST,exp
+emoji,as,cldr
+main,as,
+emoji,ast,cldr
+emoji,az,cldr
+main,bn_BD,exp
+emoji,bn,cldr
+main,bn,
+main,bn,exp
+emoji,eu,cldr
+main,eu,
+emoji,be,cldr
+main,be,
+emoji,bew,cldr
+emoji,bs,cldr
+main,bs,
+emoji,br,cldr
+emoji,bg,cldr
+main,bg,
+main,bg,exp
+emoji,my,cldr
+emoji,yue,cldr
+emoji,ca,cldr
+main,ca,
+main,ca,exp
+emoji,ceb,cldr
+main,ceb,exp
+emoji,ccp,cldr
+emoji,chr,cldr
+emoji,zh_HANT,cldr
+emoji,zh,cldr
+emoji,cv,cldr
+emoji,hr,cldr
+main,hr,
+main,hr,exp
+emoji,cs,cldr
+main,cs,
+main,cs,exp
+emoji,da,cldr
+main,da,
+main,da,exp
+emoji,nl,cldr
+main,nl,
+main,nl,exp
+emoji,en_AU,cldr
+main,en_AU,
+emoji,en_CA,cldr
+main,en_CA,exp
+emoji,en_GB,cldr
+main,en_GB,
+main,en_GB,exp
+main,en_US,
+main,en_US,exp
+symbols,en,exp
+emoji,en,
+emoji,en,cldr
+emoji,eo,cldr
+main,eo,
+main,eo,exp
+emoji,et,cldr
+main,et,exp
+emoji,fo,cldr
+emoji,fil,cldr
+emoji,tl,cldr
+main,tl,exp
+emoji,fi,cldr
+main,fi,
+main,fi,exp
+emoji,fr_CA,cldr
+emoji,fr,
+symbols,fr,exp
+emoji,fr,cldr
+main,fr,
+main,fr,exp
+emoji,gl,cldr
+main,gl,
+main,gl,exp
+emoji,ka,cldr
+main,ka,
+main,de_AT,exp
+emoji,de_CH,cldr
+main,de_CH,
+emoji,de,cldr
+main,de,
+main,de,exp
+main,gom,
+emoji,el,cldr
+main,el,
+emoji,gu,cldr
+main,gu,
+emoji,ha,cldr
+emoji,he,cldr
+main,he,
+main,iw,
+main,he,exp
+emoji,hi_ZZ,cldr
+emoji,hi,cldr
+main,hi,
+main,hi_ZZ,
+emoji,hu,cldr
+main,hu,
+main,hu,exp
+emoji,is,cldr
+main,is,exp
+emoji,ig,cldr
+emoji,id,cldr
+main,id,exp
+emoji,ia,cldr
+emoji,ga,cldr
+emoji,it,cldr
+main,it,
+main,it,exp
+emoji,ja,cldr
+emoji,jv,cldr
+emoji,kab,cldr
+main,kab,exp
+emoji,kl,cldr
+emoji,kn,cldr
+main,kn,
+main,ks,
+emoji,kk,cldr
+main,kk,exp
+emoji,km,cldr
+main,km,
+emoji,rw,cldr
+emoji,kok,cldr
+emoji,ko,cldr
+emoji,ku,cldr
+emoji,ky,cldr
+emoji,quc,cldr
+emoji,lo,cldr
+main,la,
+emoji,lv,cldr
+main,lv,
+main,lv,exp
+emoji,lij,cldr
+emoji,lt,cldr
+main,lt,
+main,lt,exp
+emoji,dsb,cldr
+emoji,lb,cldr
+main,lb,
+emoji,mk,cldr
+main,mai,
+emoji,ms,cldr
+addon,ml_ZZ,exp
+emoji,ml,cldr
+main,ml,
+emoji,mt,cldr
+emoji,mni,cldr
+emoji,mr,cldr
+main,mr,
+main,mwl,
+emoji,mn,cldr
+emoji,mi,cldr
+emoji,ne,cldr
+main,ne,exp
+emoji,pcm,cldr
+emoji,frr,cldr
+emoji,nso,cldr
+emoji,nb,cldr
+main,nb,
+main,nb,exp
+emoji,nn,cldr
+emoji,no,cldr
+emoji,oc,cldr
+emoji,or,cldr
+main,or,
+emoji,om,cldr
+emoji,pap,cldr
+emoji,ps,cldr
+main,fa_IR,exp
+emoji,fa,cldr
+main,pms,exp
+emoji,pl,cldr
+main,pl,
+main,pl,exp
+main,pt_BR,
+emoji,pt_PT,cldr
+main,pt_PT,
+main,pt_PT,exp
+emoji,pt,cldr
+emoji,pa,cldr
+main,pa,
+emoji,qu,cldr
+emoji,rhg,cldr
+emoji,ro,cldr
+main,ro,
+main,ro,exp
+emoji,rm,cldr
+emoji,ru,
+emoji,ru,cldr
+main,ru,
+main,ru,exp
+main,sa,
+emoji,sat,cldr
+main,sat,
+emoji,sc,cldr
+emoji,gd,cldr
+main,sr_ZZ,
+emoji,sr,cldr
+main,sr,
+emoji,sd,cldr
+main,sd,
+emoji,si,cldr
+emoji,sk,cldr
+main,sk,exp
+emoji,sl,cldr
+main,sl,
+main,sl,exp
+emoji,so,cldr
+emoji,es_419,cldr
+emoji,es_MX,cldr
+emoji,es,cldr
+main,es,
+main,es,exp
+main,zgh_ZZ,
+main,zgh,
+emoji,sw,cldr
+emoji,sv,cldr
+main,sv,
+main,sv,exp
+emoji,tg,cldr
+emoji,ta,cldr
+main,ta,
+emoji,te,cldr
+main,te,
+emoji,th,cldr
+emoji,ti,cldr
+main,tok,
+emoji,to,cldr
+emoji,tn,cldr
+main,tcy,
+emoji,tr,cldr
+main,tr,
+main,tr,exp
+emoji,tk,cldr
+emoji,uk,
+emoji,uk,cldr
+main,uk,
+main,uk,exp
+emoji,hsb,cldr
+emoji,ur,cldr
+main,ur,
+emoji,ug,cldr
+emoji,uz,cldr
+emoji,vec,cldr
+emoji,vi,cldr
+main,vi,exp
+emoji,cy,cldr
+emoji,bgn,cldr
+emoji,fy,cldr
+emoji,wo,cldr
+emoji,xh,cldr
+emoji,yo,cldr
+emoji,zu,cldr
diff --git a/app/src/main/res/raw/main_bg.dict b/app/src/main/assets/dicts/main_bg.dict
similarity index 100%
rename from app/src/main/res/raw/main_bg.dict
rename to app/src/main/assets/dicts/main_bg.dict
diff --git a/app/src/main/res/raw/main_bn.dict b/app/src/main/assets/dicts/main_bn.dict
similarity index 100%
rename from app/src/main/res/raw/main_bn.dict
rename to app/src/main/assets/dicts/main_bn.dict
diff --git a/app/src/main/res/raw/main_de.dict b/app/src/main/assets/dicts/main_de.dict
similarity index 100%
rename from app/src/main/res/raw/main_de.dict
rename to app/src/main/assets/dicts/main_de.dict
diff --git a/app/src/main/res/raw/main_el.dict b/app/src/main/assets/dicts/main_el.dict
similarity index 100%
rename from app/src/main/res/raw/main_el.dict
rename to app/src/main/assets/dicts/main_el.dict
diff --git a/app/src/main/assets/dicts/main_en-GB.dict b/app/src/main/assets/dicts/main_en-GB.dict
new file mode 100644
index 0000000000..77145c7d45
Binary files /dev/null and b/app/src/main/assets/dicts/main_en-GB.dict differ
diff --git a/app/src/main/res/raw/main_en.dict b/app/src/main/assets/dicts/main_en-US.dict
similarity index 100%
rename from app/src/main/res/raw/main_en.dict
rename to app/src/main/assets/dicts/main_en-US.dict
diff --git a/app/src/main/res/raw/main_es.dict b/app/src/main/assets/dicts/main_es.dict
similarity index 100%
rename from app/src/main/res/raw/main_es.dict
rename to app/src/main/assets/dicts/main_es.dict
diff --git a/app/src/main/res/raw/main_fr.dict b/app/src/main/assets/dicts/main_fr.dict
similarity index 100%
rename from app/src/main/res/raw/main_fr.dict
rename to app/src/main/assets/dicts/main_fr.dict
diff --git a/app/src/main/res/raw/main_hu.dict b/app/src/main/assets/dicts/main_hu.dict
similarity index 100%
rename from app/src/main/res/raw/main_hu.dict
rename to app/src/main/assets/dicts/main_hu.dict
diff --git a/app/src/main/res/raw/main_it.dict b/app/src/main/assets/dicts/main_it.dict
similarity index 100%
rename from app/src/main/res/raw/main_it.dict
rename to app/src/main/assets/dicts/main_it.dict
diff --git a/app/src/main/res/raw/main_nl.dict b/app/src/main/assets/dicts/main_nl.dict
similarity index 100%
rename from app/src/main/res/raw/main_nl.dict
rename to app/src/main/assets/dicts/main_nl.dict
diff --git a/app/src/main/res/raw/main_pl.dict b/app/src/main/assets/dicts/main_pl.dict
similarity index 100%
rename from app/src/main/res/raw/main_pl.dict
rename to app/src/main/assets/dicts/main_pl.dict
diff --git a/app/src/main/res/raw/main_pt_br.dict b/app/src/main/assets/dicts/main_pt-BR.dict
similarity index 100%
rename from app/src/main/res/raw/main_pt_br.dict
rename to app/src/main/assets/dicts/main_pt-BR.dict
diff --git a/app/src/main/res/raw/main_pt_pt.dict b/app/src/main/assets/dicts/main_pt-PT.dict
similarity index 100%
rename from app/src/main/res/raw/main_pt_pt.dict
rename to app/src/main/assets/dicts/main_pt-PT.dict
diff --git a/app/src/main/res/raw/main_ro.dict b/app/src/main/assets/dicts/main_ro.dict
similarity index 100%
rename from app/src/main/res/raw/main_ro.dict
rename to app/src/main/assets/dicts/main_ro.dict
diff --git a/app/src/main/res/raw/main_ru.dict b/app/src/main/assets/dicts/main_ru.dict
similarity index 100%
rename from app/src/main/res/raw/main_ru.dict
rename to app/src/main/assets/dicts/main_ru.dict
diff --git a/app/src/main/res/raw/main_sv.dict b/app/src/main/assets/dicts/main_sv.dict
similarity index 100%
rename from app/src/main/res/raw/main_sv.dict
rename to app/src/main/assets/dicts/main_sv.dict
diff --git a/app/src/main/res/raw/main_tr.dict b/app/src/main/assets/dicts/main_tr.dict
similarity index 100%
rename from app/src/main/res/raw/main_tr.dict
rename to app/src/main/assets/dicts/main_tr.dict
diff --git a/app/src/main/assets/emoji/ACTIVITIES.txt b/app/src/main/assets/emoji/ACTIVITIES.txt
new file mode 100644
index 0000000000..416ac152ea
--- /dev/null
+++ b/app/src/main/assets/emoji/ACTIVITIES.txt
@@ -0,0 +1,85 @@
+🎃
+🎄
+🎆
+🎇
+🧨
+✨
+🎈
+🎉
+🎊
+🎋
+🎍
+🎎
+🎏
+🎐
+🎑
+🧧
+🎀
+🎁
+🎗️
+🎟️
+🎫
+🎖️
+🏆
+🏅
+🥇
+🥈
+🥉
+⚽
+⚾
+🥎
+🏀
+🏐
+🏈
+🏉
+🎾
+🥏
+🎳
+🏏
+🏑
+🏒
+🥍
+🏓
+🏸
+🥊
+🥋
+🥅
+⛳
+⛸️
+🎣
+🤿
+🎽
+🎿
+🛷
+🥌
+🎯
+🪀
+🪁
+🔫
+🎱
+🔮
+🪄
+🎮
+🕹️
+🎰
+🎲
+🧩
+🧸
+🪅
+🪩
+🪆
+♠️
+♥️
+♦️
+♣️
+♟️
+🃏
+🀄
+🎴
+🎭
+🖼️
+🎨
+🧵
+🪡
+🧶
+🪢
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/ANIMALS_AND_NATURE.txt b/app/src/main/assets/emoji/ANIMALS_AND_NATURE.txt
new file mode 100644
index 0000000000..04b7145207
--- /dev/null
+++ b/app/src/main/assets/emoji/ANIMALS_AND_NATURE.txt
@@ -0,0 +1,159 @@
+🐵
+🐒
+🦍
+🦧
+🐶
+🐕
+🦮
+🐕🦺
+🐩
+🐺
+🦊
+🦝
+🐱
+🐈
+🐈⬛
+🦁
+🐯
+🐅
+🐆
+🐴
+🫎
+🫏
+🐎
+🦄
+🦓
+🦌
+🦬
+🐮
+🐂
+🐃
+🐄
+🐷
+🐖
+🐗
+🐽
+🐏
+🐑
+🐐
+🐪
+🐫
+🦙
+🦒
+🐘
+🦣
+🦏
+🦛
+🐭
+🐁
+🐀
+🐹
+🐰
+🐇
+🐿️
+🦫
+🦔
+🦇
+🐻
+🐻❄️
+🐨
+🐼
+🦥
+🦦
+🦨
+🦘
+🦡
+🐾
+🦃
+🐔
+🐓
+🐣
+🐤
+🐥
+🐦
+🐧
+🕊️
+🦅
+🦆
+🦢
+🦉
+🦤
+🪶
+🦩
+🦚
+🦜
+🪽
+🐦⬛
+🪿
+🐦🔥
+🐸
+🐊
+🐢
+🦎
+🐍
+🐲
+🐉
+🦕
+🦖
+🐳
+🐋
+🐬
+🦭
+🐟
+🐠
+🐡
+🦈
+🐙
+🐚
+🪸
+🪼
+🦀
+🦞
+🦐
+🦑
+🦪
+🐌
+🦋
+🐛
+🐜
+🐝
+🪲
+🐞
+🦗
+🪳
+🕷️
+🕸️
+🦂
+🦟
+🪰
+🪱
+🦠
+💐
+🌸
+💮
+🪷
+🏵️
+🌹
+🥀
+🌺
+🌻
+🌼
+🌷
+🪻
+🌱
+🪴
+🌲
+🌳
+🌴
+🌵
+🌾
+🌿
+☘️
+🍀
+🍁
+🍂
+🍃
+🪹
+🪺
+🍄
+
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/EMOTICONS.txt b/app/src/main/assets/emoji/EMOTICONS.txt
new file mode 100644
index 0000000000..c2b3d9ddd9
--- /dev/null
+++ b/app/src/main/assets/emoji/EMOTICONS.txt
@@ -0,0 +1,25 @@
+:-)
+;-)
+:-(
+:-!
+:-$
+B-)
+=-O
+:-P
+:O
+:-*
+:-D
+:\'(
+:-\\
+O:-)
+:-[
+(╯°
+□°)
+╯︵
+┻━┻
+¯\\_
+(ツ)
+_/¯
+┬─┬
+︵ /(
+.□.\\
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/FLAGS.txt b/app/src/main/assets/emoji/FLAGS.txt
new file mode 100644
index 0000000000..c4bd834609
--- /dev/null
+++ b/app/src/main/assets/emoji/FLAGS.txt
@@ -0,0 +1,270 @@
+🏁
+🚩
+🎌
+🏴
+🏳️
+🏳️🌈
+🏳️⚧️
+🏴☠️
+🇦🇨
+🇦🇩
+🇦🇪
+🇦🇫
+🇦🇬
+🇦🇮
+🇦🇱
+🇦🇲
+🇦🇴
+🇦🇶
+🇦🇷
+🇦🇸
+🇦🇹
+🇦🇺
+🇦🇼
+🇦🇽
+🇦🇿
+🇧🇦
+🇧🇧
+🇧🇩
+🇧🇪
+🇧🇫
+🇧🇬
+🇧🇭
+🇧🇮
+🇧🇯
+🇧🇱
+🇧🇲
+🇧🇳
+🇧🇴
+🇧🇶
+🇧🇷
+🇧🇸
+🇧🇹
+🇧🇻
+🇧🇼
+🇧🇾
+🇧🇿
+🇨🇦
+🇨🇨
+🇨🇩
+🇨🇫
+🇨🇬
+🇨🇭
+🇨🇮
+🇨🇰
+🇨🇱
+🇨🇲
+🇨🇳
+🇨🇴
+🇨🇵
+🇨🇶
+🇨🇷
+🇨🇺
+🇨🇻
+🇨🇼
+🇨🇽
+🇨🇾
+🇨🇿
+🇩🇪
+🇩🇬
+🇩🇯
+🇩🇰
+🇩🇲
+🇩🇴
+🇩🇿
+🇪🇦
+🇪🇨
+🇪🇪
+🇪🇬
+🇪🇭
+🇪🇷
+🇪🇸
+🇪🇹
+🇪🇺
+🇫🇮
+🇫🇯
+🇫🇰
+🇫🇲
+🇫🇴
+🇫🇷
+🇬🇦
+🇬🇧
+🇬🇩
+🇬🇪
+🇬🇫
+🇬🇬
+🇬🇭
+🇬🇮
+🇬🇱
+🇬🇲
+🇬🇳
+🇬🇵
+🇬🇶
+🇬🇷
+🇬🇸
+🇬🇹
+🇬🇺
+🇬🇼
+🇬🇾
+🇭🇰
+🇭🇲
+🇭🇳
+🇭🇷
+🇭🇹
+🇭🇺
+🇮🇨
+🇮🇩
+🇮🇪
+🇮🇱
+🇮🇲
+🇮🇳
+🇮🇴
+🇮🇶
+🇮🇷
+🇮🇸
+🇮🇹
+🇯🇪
+🇯🇲
+🇯🇴
+🇯🇵
+🇰🇪
+🇰🇬
+🇰🇭
+🇰🇮
+🇰🇲
+🇰🇳
+🇰🇵
+🇰🇷
+🇰🇼
+🇰🇾
+🇰🇿
+🇱🇦
+🇱🇧
+🇱🇨
+🇱🇮
+🇱🇰
+🇱🇷
+🇱🇸
+🇱🇹
+🇱🇺
+🇱🇻
+🇱🇾
+🇲🇦
+🇲🇨
+🇲🇩
+🇲🇪
+🇲🇫
+🇲🇬
+🇲🇭
+🇲🇰
+🇲🇱
+🇲🇲
+🇲🇳
+🇲🇴
+🇲🇵
+🇲🇶
+🇲🇷
+🇲🇸
+🇲🇹
+🇲🇺
+🇲🇻
+🇲🇼
+🇲🇽
+🇲🇾
+🇲🇿
+🇳🇦
+🇳🇨
+🇳🇪
+🇳🇫
+🇳🇬
+🇳🇮
+🇳🇱
+🇳🇴
+🇳🇵
+🇳🇷
+🇳🇺
+🇳🇿
+🇴🇲
+🇵🇦
+🇵🇪
+🇵🇫
+🇵🇬
+🇵🇭
+🇵🇰
+🇵🇱
+🇵🇲
+🇵🇳
+🇵🇷
+🇵🇸
+🇵🇹
+🇵🇼
+🇵🇾
+🇶🇦
+🇷🇪
+🇷🇴
+🇷🇸
+🇷🇺
+🇷🇼
+🇸🇦
+🇸🇧
+🇸🇨
+🇸🇩
+🇸🇪
+🇸🇬
+🇸🇭
+🇸🇮
+🇸🇯
+🇸🇰
+🇸🇱
+🇸🇲
+🇸🇳
+🇸🇴
+🇸🇷
+🇸🇸
+🇸🇹
+🇸🇻
+🇸🇽
+🇸🇾
+🇸🇿
+🇹🇦
+🇹🇨
+🇹🇩
+🇹🇫
+🇹🇬
+🇹🇭
+🇹🇯
+🇹🇰
+🇹🇱
+🇹🇲
+🇹🇳
+🇹🇴
+🇹🇷
+🇹🇹
+🇹🇻
+🇹🇼
+🇹🇿
+🇺🇦
+🇺🇬
+🇺🇲
+🇺🇳
+🇺🇸
+🇺🇾
+🇺🇿
+🇻🇦
+🇻🇨
+🇻🇪
+🇻🇬
+🇻🇮
+🇻🇳
+🇻🇺
+🇼🇫
+🇼🇸
+🇽🇰
+🇾🇪
+🇾🇹
+🇿🇦
+🇿🇲
+🇿🇼
+🏴
+🏴
+🏴
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/FOOD_AND_DRINK.txt b/app/src/main/assets/emoji/FOOD_AND_DRINK.txt
new file mode 100644
index 0000000000..cb9b5dd54d
--- /dev/null
+++ b/app/src/main/assets/emoji/FOOD_AND_DRINK.txt
@@ -0,0 +1,131 @@
+🍇
+🍈
+🍉
+🍊
+🍋
+🍋🟩
+🍌
+🍍
+🥭
+🍎
+🍏
+🍐
+🍑
+🍒
+🍓
+🫐
+🥝
+🍅
+🫒
+🥥
+🥑
+🍆
+🥔
+🥕
+🌽
+🌶️
+🫑
+🥒
+🥬
+🥦
+🧄
+🧅
+🥜
+🫘
+🌰
+🫚
+🫛
+🍄🟫
+
+🍞
+🥐
+🥖
+🫓
+🥨
+🥯
+🥞
+🧇
+🧀
+🍖
+🍗
+🥩
+🥓
+🍔
+🍟
+🍕
+🌭
+🥪
+🌮
+🌯
+🫔
+🥙
+🧆
+🥚
+🍳
+🥘
+🍲
+🫕
+🥣
+🥗
+🍿
+🧈
+🧂
+🥫
+🍱
+🍘
+🍙
+🍚
+🍛
+🍜
+🍝
+🍠
+🍢
+🍣
+🍤
+🍥
+🥮
+🍡
+🥟
+🥠
+🥡
+🍦
+🍧
+🍨
+🍩
+🍪
+🎂
+🍰
+🧁
+🥧
+🍫
+🍬
+🍭
+🍮
+🍯
+🍼
+🥛
+☕
+🫖
+🍵
+🍶
+🍾
+🍷
+🍸
+🍹
+🍺
+🍻
+🥂
+🥃
+🫗
+🥤
+🧋
+🧃
+🧉
+🧊
+🥢
+🍽️
+🍴
+🥄
+🔪
+🫙
+🏺
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/OBJECTS.txt b/app/src/main/assets/emoji/OBJECTS.txt
new file mode 100644
index 0000000000..38b94b0b7b
--- /dev/null
+++ b/app/src/main/assets/emoji/OBJECTS.txt
@@ -0,0 +1,264 @@
+👓
+🕶️
+🥽
+🥼
+🦺
+👔
+👕
+👖
+🧣
+🧤
+🧥
+🧦
+👗
+👘
+🥻
+🩱
+🩲
+🩳
+👙
+👚
+🪭
+👛
+👜
+👝
+🛍️
+🎒
+🩴
+👞
+👟
+🥾
+🥿
+👠
+👡
+🩰
+👢
+🪮
+👑
+👒
+🎩
+🎓
+🧢
+🪖
+⛑️
+📿
+💄
+💍
+💎
+🔇
+🔈
+🔉
+🔊
+📢
+📣
+📯
+🔔
+🔕
+🎼
+🎵
+🎶
+🎙️
+🎚️
+🎛️
+🎤
+🎧
+📻
+🎷
+🪗
+🎸
+🎹
+🎺
+🎻
+🪕
+🥁
+🪘
+🪇
+🪈
+
+📱
+📲
+☎️
+📞
+📟
+📠
+🔋
+🪫
+🔌
+💻
+🖥️
+🖨️
+⌨️
+🖱️
+🖲️
+💽
+💾
+💿
+📀
+🧮
+🎥
+🎞️
+📽️
+🎬
+📺
+📷
+📸
+📹
+📼
+🔍
+🔎
+🕯️
+💡
+🔦
+🏮
+🪔
+📔
+📕
+📖
+📗
+📘
+📙
+📚
+📓
+📒
+📃
+📜
+📄
+📰
+🗞️
+📑
+🔖
+🏷️
+💰
+🪙
+💴
+💵
+💶
+💷
+💸
+💳
+🧾
+💹
+✉️
+📧
+📨
+📩
+📤
+📥
+📦
+📫
+📪
+📬
+📭
+📮
+🗳️
+✏️
+✒️
+🖋️
+🖊️
+🖌️
+🖍️
+📝
+💼
+📁
+📂
+🗂️
+📅
+📆
+🗒️
+🗓️
+📇
+📈
+📉
+📊
+📋
+📌
+📍
+📎
+🖇️
+📏
+📐
+✂️
+🗃️
+🗄️
+🗑️
+🔒
+🔓
+🔏
+🔐
+🔑
+🗝️
+🔨
+🪓
+⛏️
+⚒️
+🛠️
+🗡️
+⚔️
+💣
+🪃
+🏹
+🛡️
+🪚
+🔧
+🪛
+🔩
+⚙️
+🗜️
+⚖️
+🦯
+🔗
+⛓️💥
+⛓️
+🪝
+🧰
+🧲
+🪜
+
+⚗️
+🧪
+🧫
+🧬
+🔬
+🔭
+📡
+💉
+🩸
+💊
+🩹
+🩼
+🩺
+🩻
+🚪
+🛗
+🪞
+🪟
+🛏️
+🛋️
+🪑
+🚽
+🪠
+🚿
+🛁
+🪤
+🪒
+🧴
+🧷
+🧹
+🧺
+🧻
+🪣
+🧼
+🫧
+🪥
+🧽
+🧯
+🛒
+🚬
+⚰️
+🪦
+⚱️
+🧿
+🪬
+🗿
+🪧
+🪪
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/PEOPLE_AND_BODY.txt b/app/src/main/assets/emoji/PEOPLE_AND_BODY.txt
new file mode 100644
index 0000000000..d7ceb5bb8e
--- /dev/null
+++ b/app/src/main/assets/emoji/PEOPLE_AND_BODY.txt
@@ -0,0 +1,386 @@
+👋 👋🏻 👋🏼 👋🏽 👋🏾 👋🏿
+🤚 🤚🏻 🤚🏼 🤚🏽 🤚🏾 🤚🏿
+🖐️ 🖐🏻 🖐🏼 🖐🏽 🖐🏾 🖐🏿
+✋ ✋🏻 ✋🏼 ✋🏽 ✋🏾 ✋🏿
+🖖 🖖🏻 🖖🏼 🖖🏽 🖖🏾 🖖🏿
+🫱 🫱🏻 🫱🏼 🫱🏽 🫱🏾 🫱🏿
+🫲 🫲🏻 🫲🏼 🫲🏽 🫲🏾 🫲🏿
+🫳 🫳🏻 🫳🏼 🫳🏽 🫳🏾 🫳🏿
+🫴 🫴🏻 🫴🏼 🫴🏽 🫴🏾 🫴🏿
+🫷 🫷🏻 🫷🏼 🫷🏽 🫷🏾 🫷🏿
+🫸 🫸🏻 🫸🏼 🫸🏽 🫸🏾 🫸🏿
+👌 👌🏻 👌🏼 👌🏽 👌🏾 👌🏿
+🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿
+🤏 🤏🏻 🤏🏼 🤏🏽 🤏🏾 🤏🏿
+✌️ ✌🏻 ✌🏼 ✌🏽 ✌🏾 ✌🏿
+🤞 🤞🏻 🤞🏼 🤞🏽 🤞🏾 🤞🏿
+🫰 🫰🏻 🫰🏼 🫰🏽 🫰🏾 🫰🏿
+🤟 🤟🏻 🤟🏼 🤟🏽 🤟🏾 🤟🏿
+🤘 🤘🏻 🤘🏼 🤘🏽 🤘🏾 🤘🏿
+🤙 🤙🏻 🤙🏼 🤙🏽 🤙🏾 🤙🏿
+👈 👈🏻 👈🏼 👈🏽 👈🏾 👈🏿
+👉 👉🏻 👉🏼 👉🏽 👉🏾 👉🏿
+👆 👆🏻 👆🏼 👆🏽 👆🏾 👆🏿
+🖕 🖕🏻 🖕🏼 🖕🏽 🖕🏾 🖕🏿
+👇 👇🏻 👇🏼 👇🏽 👇🏾 👇🏿
+☝️ ☝🏻 ☝🏼 ☝🏽 ☝🏾 ☝🏿
+🫵 🫵🏻 🫵🏼 🫵🏽 🫵🏾 🫵🏿
+👍 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿
+👎 👎🏻 👎🏼 👎🏽 👎🏾 👎🏿
+✊ ✊🏻 ✊🏼 ✊🏽 ✊🏾 ✊🏿
+👊 👊🏻 👊🏼 👊🏽 👊🏾 👊🏿
+🤛 🤛🏻 🤛🏼 🤛🏽 🤛🏾 🤛🏿
+🤜 🤜🏻 🤜🏼 🤜🏽 🤜🏾 🤜🏿
+👏 👏🏻 👏🏼 👏🏽 👏🏾 👏🏿
+🙌 🙌🏻 🙌🏼 🙌🏽 🙌🏾 🙌🏿
+🫶 🫶🏻 🫶🏼 🫶🏽 🫶🏾 🫶🏿
+👐 👐🏻 👐🏼 👐🏽 👐🏾 👐🏿
+🤲 🤲🏻 🤲🏼 🤲🏽 🤲🏾 🤲🏿
+🤝 🤝🏻 🤝🏼 🤝🏽 🤝🏾 🤝🏿
+🙏 🙏🏻 🙏🏼 🙏🏽 🙏🏾 🙏🏿
+✍️ ✍🏻 ✍🏼 ✍🏽 ✍🏾 ✍🏿
+💅 💅🏻 💅🏼 💅🏽 💅🏾 💅🏿
+🤳 🤳🏻 🤳🏼 🤳🏽 🤳🏾 🤳🏿
+💪 💪🏻 💪🏼 💪🏽 💪🏾 💪🏿
+🦾
+🦿
+🦵 🦵🏻 🦵🏼 🦵🏽 🦵🏾 🦵🏿
+🦶 🦶🏻 🦶🏼 🦶🏽 🦶🏾 🦶🏿
+👂 👂🏻 👂🏼 👂🏽 👂🏾 👂🏿
+🦻 🦻🏻 🦻🏼 🦻🏽 🦻🏾 🦻🏿
+👃 👃🏻 👃🏼 👃🏽 👃🏾 👃🏿
+🧠
+🫀
+🫁
+🦷
+🦴
+👀
+👁️
+👅
+👄
+🫦
+👶 👶🏻 👶🏼 👶🏽 👶🏾 👶🏿
+🧒 🧒🏻 🧒🏼 🧒🏽 🧒🏾 🧒🏿
+👦 👦🏻 👦🏼 👦🏽 👦🏾 👦🏿
+👧 👧🏻 👧🏼 👧🏽 👧🏾 👧🏿
+🧑 🧑🏻 🧑🏼 🧑🏽 🧑🏾 🧑🏿
+👱 👱🏻 👱🏼 👱🏽 👱🏾 👱🏿
+👨 👨🏻 👨🏼 👨🏽 👨🏾 👨🏿
+🧔 🧔🏻 🧔🏼 🧔🏽 🧔🏾 🧔🏿
+🧔♂️ 🧔🏻♂️ 🧔🏼♂️ 🧔🏽♂️ 🧔🏾♂️ 🧔🏿♂️
+🧔♀️ 🧔🏻♀️ 🧔🏼♀️ 🧔🏽♀️ 🧔🏾♀️ 🧔🏿♀️
+👨🦰 👨🏻🦰 👨🏼🦰 👨🏽🦰 👨🏾🦰 👨🏿🦰
+👨🦱 👨🏻🦱 👨🏼🦱 👨🏽🦱 👨🏾🦱 👨🏿🦱
+👨🦳 👨🏻🦳 👨🏼🦳 👨🏽🦳 👨🏾🦳 👨🏿🦳
+👨🦲 👨🏻🦲 👨🏼🦲 👨🏽🦲 👨🏾🦲 👨🏿🦲
+👩 👩🏻 👩🏼 👩🏽 👩🏾 👩🏿
+👩🦰 👩🏻🦰 👩🏼🦰 👩🏽🦰 👩🏾🦰 👩🏿🦰
+🧑🦰 🧑🏻🦰 🧑🏼🦰 🧑🏽🦰 🧑🏾🦰 🧑🏿🦰
+👩🦱 👩🏻🦱 👩🏼🦱 👩🏽🦱 👩🏾🦱 👩🏿🦱
+🧑🦱 🧑🏻🦱 🧑🏼🦱 🧑🏽🦱 🧑🏾🦱 🧑🏿🦱
+👩🦳 👩🏻🦳 👩🏼🦳 👩🏽🦳 👩🏾🦳 👩🏿🦳
+🧑🦳 🧑🏻🦳 🧑🏼🦳 🧑🏽🦳 🧑🏾🦳 🧑🏿🦳
+👩🦲 👩🏻🦲 👩🏼🦲 👩🏽🦲 👩🏾🦲 👩🏿🦲
+🧑🦲 🧑🏻🦲 🧑🏼🦲 🧑🏽🦲 🧑🏾🦲 🧑🏿🦲
+👱♀️ 👱🏻♀️ 👱🏼♀️ 👱🏽♀️ 👱🏾♀️ 👱🏿♀️
+👱♂️ 👱🏻♂️ 👱🏼♂️ 👱🏽♂️ 👱🏾♂️ 👱🏿♂️
+🧓 🧓🏻 🧓🏼 🧓🏽 🧓🏾 🧓🏿
+👴 👴🏻 👴🏼 👴🏽 👴🏾 👴🏿
+👵 👵🏻 👵🏼 👵🏽 👵🏾 👵🏿
+🙍 🙍🏻 🙍🏼 🙍🏽 🙍🏾 🙍🏿
+🙍♂️ 🙍🏻♂️ 🙍🏼♂️ 🙍🏽♂️ 🙍🏾♂️ 🙍🏿♂️
+🙍♀️ 🙍🏻♀️ 🙍🏼♀️ 🙍🏽♀️ 🙍🏾♀️ 🙍🏿♀️
+🙎 🙎🏻 🙎🏼 🙎🏽 🙎🏾 🙎🏿
+🙎♂️ 🙎🏻♂️ 🙎🏼♂️ 🙎🏽♂️ 🙎🏾♂️ 🙎🏿♂️
+🙎♀️ 🙎🏻♀️ 🙎🏼♀️ 🙎🏽♀️ 🙎🏾♀️ 🙎🏿♀️
+🙅 🙅🏻 🙅🏼 🙅🏽 🙅🏾 🙅🏿
+🙅♂️ 🙅🏻♂️ 🙅🏼♂️ 🙅🏽♂️ 🙅🏾♂️ 🙅🏿♂️
+🙅♀️ 🙅🏻♀️ 🙅🏼♀️ 🙅🏽♀️ 🙅🏾♀️ 🙅🏿♀️
+🙆 🙆🏻 🙆🏼 🙆🏽 🙆🏾 🙆🏿
+🙆♂️ 🙆🏻♂️ 🙆🏼♂️ 🙆🏽♂️ 🙆🏾♂️ 🙆🏿♂️
+🙆♀️ 🙆🏻♀️ 🙆🏼♀️ 🙆🏽♀️ 🙆🏾♀️ 🙆🏿♀️
+💁 💁🏻 💁🏼 💁🏽 💁🏾 💁🏿
+💁♂️ 💁🏻♂️ 💁🏼♂️ 💁🏽♂️ 💁🏾♂️ 💁🏿♂️
+💁♀️ 💁🏻♀️ 💁🏼♀️ 💁🏽♀️ 💁🏾♀️ 💁🏿♀️
+🙋 🙋🏻 🙋🏼 🙋🏽 🙋🏾 🙋🏿
+🙋♂️ 🙋🏻♂️ 🙋🏼♂️ 🙋🏽♂️ 🙋🏾♂️ 🙋🏿♂️
+🙋♀️ 🙋🏻♀️ 🙋🏼♀️ 🙋🏽♀️ 🙋🏾♀️ 🙋🏿♀️
+🧏 🧏🏻 🧏🏼 🧏🏽 🧏🏾 🧏🏿
+🧏♂️ 🧏🏻♂️ 🧏🏼♂️ 🧏🏽♂️ 🧏🏾♂️ 🧏🏿♂️
+🧏♀️ 🧏🏻♀️ 🧏🏼♀️ 🧏🏽♀️ 🧏🏾♀️ 🧏🏿♀️
+🙇 🙇🏻 🙇🏼 🙇🏽 🙇🏾 🙇🏿
+🙇♂️ 🙇🏻♂️ 🙇🏼♂️ 🙇🏽♂️ 🙇🏾♂️ 🙇🏿♂️
+🙇♀️ 🙇🏻♀️ 🙇🏼♀️ 🙇🏽♀️ 🙇🏾♀️ 🙇🏿♀️
+🤦 🤦🏻 🤦🏼 🤦🏽 🤦🏾 🤦🏿
+🤦♂️ 🤦🏻♂️ 🤦🏼♂️ 🤦🏽♂️ 🤦🏾♂️ 🤦🏿♂️
+🤦♀️ 🤦🏻♀️ 🤦🏼♀️ 🤦🏽♀️ 🤦🏾♀️ 🤦🏿♀️
+🤷 🤷🏻 🤷🏼 🤷🏽 🤷🏾 🤷🏿
+🤷♂️ 🤷🏻♂️ 🤷🏼♂️ 🤷🏽♂️ 🤷🏾♂️ 🤷🏿♂️
+🤷♀️ 🤷🏻♀️ 🤷🏼♀️ 🤷🏽♀️ 🤷🏾♀️ 🤷🏿♀️
+🧑⚕️ 🧑🏻⚕️ 🧑🏼⚕️ 🧑🏽⚕️ 🧑🏾⚕️ 🧑🏿⚕️
+👨⚕️ 👨🏻⚕️ 👨🏼⚕️ 👨🏽⚕️ 👨🏾⚕️ 👨🏿⚕️
+👩⚕️ 👩🏻⚕️ 👩🏼⚕️ 👩🏽⚕️ 👩🏾⚕️ 👩🏿⚕️
+🧑🎓 🧑🏻🎓 🧑🏼🎓 🧑🏽🎓 🧑🏾🎓 🧑🏿🎓
+👨🎓 👨🏻🎓 👨🏼🎓 👨🏽🎓 👨🏾🎓 👨🏿🎓
+👩🎓 👩🏻🎓 👩🏼🎓 👩🏽🎓 👩🏾🎓 👩🏿🎓
+🧑🏫 🧑🏻🏫 🧑🏼🏫 🧑🏽🏫 🧑🏾🏫 🧑🏿🏫
+👨🏫 👨🏻🏫 👨🏼🏫 👨🏽🏫 👨🏾🏫 👨🏿🏫
+👩🏫 👩🏻🏫 👩🏼🏫 👩🏽🏫 👩🏾🏫 👩🏿🏫
+🧑⚖️ 🧑🏻⚖️ 🧑🏼⚖️ 🧑🏽⚖️ 🧑🏾⚖️ 🧑🏿⚖️
+👨⚖️ 👨🏻⚖️ 👨🏼⚖️ 👨🏽⚖️ 👨🏾⚖️ 👨🏿⚖️
+👩⚖️ 👩🏻⚖️ 👩🏼⚖️ 👩🏽⚖️ 👩🏾⚖️ 👩🏿⚖️
+🧑🌾 🧑🏻🌾 🧑🏼🌾 🧑🏽🌾 🧑🏾🌾 🧑🏿🌾
+👨🌾 👨🏻🌾 👨🏼🌾 👨🏽🌾 👨🏾🌾 👨🏿🌾
+👩🌾 👩🏻🌾 👩🏼🌾 👩🏽🌾 👩🏾🌾 👩🏿🌾
+🧑🍳 🧑🏻🍳 🧑🏼🍳 🧑🏽🍳 🧑🏾🍳 🧑🏿🍳
+👨🍳 👨🏻🍳 👨🏼🍳 👨🏽🍳 👨🏾🍳 👨🏿🍳
+👩🍳 👩🏻🍳 👩🏼🍳 👩🏽🍳 👩🏾🍳 👩🏿🍳
+🧑🔧 🧑🏻🔧 🧑🏼🔧 🧑🏽🔧 🧑🏾🔧 🧑🏿🔧
+👨🔧 👨🏻🔧 👨🏼🔧 👨🏽🔧 👨🏾🔧 👨🏿🔧
+👩🔧 👩🏻🔧 👩🏼🔧 👩🏽🔧 👩🏾🔧 👩🏿🔧
+🧑🏭 🧑🏻🏭 🧑🏼🏭 🧑🏽🏭 🧑🏾🏭 🧑🏿🏭
+👨🏭 👨🏻🏭 👨🏼🏭 👨🏽🏭 👨🏾🏭 👨🏿🏭
+👩🏭 👩🏻🏭 👩🏼🏭 👩🏽🏭 👩🏾🏭 👩🏿🏭
+🧑💼 🧑🏻💼 🧑🏼💼 🧑🏽💼 🧑🏾💼 🧑🏿💼
+👨💼 👨🏻💼 👨🏼💼 👨🏽💼 👨🏾💼 👨🏿💼
+👩💼 👩🏻💼 👩🏼💼 👩🏽💼 👩🏾💼 👩🏿💼
+🧑🔬 🧑🏻🔬 🧑🏼🔬 🧑🏽🔬 🧑🏾🔬 🧑🏿🔬
+👨🔬 👨🏻🔬 👨🏼🔬 👨🏽🔬 👨🏾🔬 👨🏿🔬
+👩🔬 👩🏻🔬 👩🏼🔬 👩🏽🔬 👩🏾🔬 👩🏿🔬
+🧑💻 🧑🏻💻 🧑🏼💻 🧑🏽💻 🧑🏾💻 🧑🏿💻
+👨💻 👨🏻💻 👨🏼💻 👨🏽💻 👨🏾💻 👨🏿💻
+👩💻 👩🏻💻 👩🏼💻 👩🏽💻 👩🏾💻 👩🏿💻
+🧑🎤 🧑🏻🎤 🧑🏼🎤 🧑🏽🎤 🧑🏾🎤 🧑🏿🎤
+👨🎤 👨🏻🎤 👨🏼🎤 👨🏽🎤 👨🏾🎤 👨🏿🎤
+👩🎤 👩🏻🎤 👩🏼🎤 👩🏽🎤 👩🏾🎤 👩🏿🎤
+🧑🎨 🧑🏻🎨 🧑🏼🎨 🧑🏽🎨 🧑🏾🎨 🧑🏿🎨
+👨🎨 👨🏻🎨 👨🏼🎨 👨🏽🎨 👨🏾🎨 👨🏿🎨
+👩🎨 👩🏻🎨 👩🏼🎨 👩🏽🎨 👩🏾🎨 👩🏿🎨
+🧑✈️ 🧑🏻✈️ 🧑🏼✈️ 🧑🏽✈️ 🧑🏾✈️ 🧑🏿✈️
+👨✈️ 👨🏻✈️ 👨🏼✈️ 👨🏽✈️ 👨🏾✈️ 👨🏿✈️
+👩✈️ 👩🏻✈️ 👩🏼✈️ 👩🏽✈️ 👩🏾✈️ 👩🏿✈️
+🧑🚀 🧑🏻🚀 🧑🏼🚀 🧑🏽🚀 🧑🏾🚀 🧑🏿🚀
+👨🚀 👨🏻🚀 👨🏼🚀 👨🏽🚀 👨🏾🚀 👨🏿🚀
+👩🚀 👩🏻🚀 👩🏼🚀 👩🏽🚀 👩🏾🚀 👩🏿🚀
+🧑🚒 🧑🏻🚒 🧑🏼🚒 🧑🏽🚒 🧑🏾🚒 🧑🏿🚒
+👨🚒 👨🏻🚒 👨🏼🚒 👨🏽🚒 👨🏾🚒 👨🏿🚒
+👩🚒 👩🏻🚒 👩🏼🚒 👩🏽🚒 👩🏾🚒 👩🏿🚒
+👮 👮🏻 👮🏼 👮🏽 👮🏾 👮🏿
+👮♂️ 👮🏻♂️ 👮🏼♂️ 👮🏽♂️ 👮🏾♂️ 👮🏿♂️
+👮♀️ 👮🏻♀️ 👮🏼♀️ 👮🏽♀️ 👮🏾♀️ 👮🏿♀️
+🕵️ 🕵🏻 🕵🏼 🕵🏽 🕵🏾 🕵🏿
+🕵️♂️ 🕵🏻♂️ 🕵🏼♂️ 🕵🏽♂️ 🕵🏾♂️ 🕵🏿♂️
+🕵️♀️ 🕵🏻♀️ 🕵🏼♀️ 🕵🏽♀️ 🕵🏾♀️ 🕵🏿♀️
+💂 💂🏻 💂🏼 💂🏽 💂🏾 💂🏿
+💂♂️ 💂🏻♂️ 💂🏼♂️ 💂🏽♂️ 💂🏾♂️ 💂🏿♂️
+💂♀️ 💂🏻♀️ 💂🏼♀️ 💂🏽♀️ 💂🏾♀️ 💂🏿♀️
+🥷 🥷🏻 🥷🏼 🥷🏽 🥷🏾 🥷🏿
+👷 👷🏻 👷🏼 👷🏽 👷🏾 👷🏿
+👷♂️ 👷🏻♂️ 👷🏼♂️ 👷🏽♂️ 👷🏾♂️ 👷🏿♂️
+👷♀️ 👷🏻♀️ 👷🏼♀️ 👷🏽♀️ 👷🏾♀️ 👷🏿♀️
+🫅 🫅🏻 🫅🏼 🫅🏽 🫅🏾 🫅🏿
+🤴 🤴🏻 🤴🏼 🤴🏽 🤴🏾 🤴🏿
+👸 👸🏻 👸🏼 👸🏽 👸🏾 👸🏿
+👳 👳🏻 👳🏼 👳🏽 👳🏾 👳🏿
+👳♂️ 👳🏻♂️ 👳🏼♂️ 👳🏽♂️ 👳🏾♂️ 👳🏿♂️
+👳♀️ 👳🏻♀️ 👳🏼♀️ 👳🏽♀️ 👳🏾♀️ 👳🏿♀️
+👲 👲🏻 👲🏼 👲🏽 👲🏾 👲🏿
+🧕 🧕🏻 🧕🏼 🧕🏽 🧕🏾 🧕🏿
+🤵 🤵🏻 🤵🏼 🤵🏽 🤵🏾 🤵🏿
+🤵♂️ 🤵🏻♂️ 🤵🏼♂️ 🤵🏽♂️ 🤵🏾♂️ 🤵🏿♂️
+🤵♀️ 🤵🏻♀️ 🤵🏼♀️ 🤵🏽♀️ 🤵🏾♀️ 🤵🏿♀️
+👰 👰🏻 👰🏼 👰🏽 👰🏾 👰🏿
+👰♂️ 👰🏻♂️ 👰🏼♂️ 👰🏽♂️ 👰🏾♂️ 👰🏿♂️
+👰♀️ 👰🏻♀️ 👰🏼♀️ 👰🏽♀️ 👰🏾♀️ 👰🏿♀️
+🤰 🤰🏻 🤰🏼 🤰🏽 🤰🏾 🤰🏿
+🫃 🫃🏻 🫃🏼 🫃🏽 🫃🏾 🫃🏿
+🫄 🫄🏻 🫄🏼 🫄🏽 🫄🏾 🫄🏿
+🤱 🤱🏻 🤱🏼 🤱🏽 🤱🏾 🤱🏿
+👩🍼 👩🏻🍼 👩🏼🍼 👩🏽🍼 👩🏾🍼 👩🏿🍼
+👨🍼 👨🏻🍼 👨🏼🍼 👨🏽🍼 👨🏾🍼 👨🏿🍼
+🧑🍼 🧑🏻🍼 🧑🏼🍼 🧑🏽🍼 🧑🏾🍼 🧑🏿🍼
+👼 👼🏻 👼🏼 👼🏽 👼🏾 👼🏿
+🎅 🎅🏻 🎅🏼 🎅🏽 🎅🏾 🎅🏿
+🤶 🤶🏻 🤶🏼 🤶🏽 🤶🏾 🤶🏿
+🧑🎄 🧑🏻🎄 🧑🏼🎄 🧑🏽🎄 🧑🏾🎄 🧑🏿🎄
+🦸 🦸🏻 🦸🏼 🦸🏽 🦸🏾 🦸🏿
+🦸♂️ 🦸🏻♂️ 🦸🏼♂️ 🦸🏽♂️ 🦸🏾♂️ 🦸🏿♂️
+🦸♀️ 🦸🏻♀️ 🦸🏼♀️ 🦸🏽♀️ 🦸🏾♀️ 🦸🏿♀️
+🦹 🦹🏻 🦹🏼 🦹🏽 🦹🏾 🦹🏿
+🦹♂️ 🦹🏻♂️ 🦹🏼♂️ 🦹🏽♂️ 🦹🏾♂️ 🦹🏿♂️
+🦹♀️ 🦹🏻♀️ 🦹🏼♀️ 🦹🏽♀️ 🦹🏾♀️ 🦹🏿♀️
+🧙 🧙🏻 🧙🏼 🧙🏽 🧙🏾 🧙🏿
+🧙♂️ 🧙🏻♂️ 🧙🏼♂️ 🧙🏽♂️ 🧙🏾♂️ 🧙🏿♂️
+🧙♀️ 🧙🏻♀️ 🧙🏼♀️ 🧙🏽♀️ 🧙🏾♀️ 🧙🏿♀️
+🧚 🧚🏻 🧚🏼 🧚🏽 🧚🏾 🧚🏿
+🧚♂️ 🧚🏻♂️ 🧚🏼♂️ 🧚🏽♂️ 🧚🏾♂️ 🧚🏿♂️
+🧚♀️ 🧚🏻♀️ 🧚🏼♀️ 🧚🏽♀️ 🧚🏾♀️ 🧚🏿♀️
+🧛 🧛🏻 🧛🏼 🧛🏽 🧛🏾 🧛🏿
+🧛♂️ 🧛🏻♂️ 🧛🏼♂️ 🧛🏽♂️ 🧛🏾♂️ 🧛🏿♂️
+🧛♀️ 🧛🏻♀️ 🧛🏼♀️ 🧛🏽♀️ 🧛🏾♀️ 🧛🏿♀️
+🧜 🧜🏻 🧜🏼 🧜🏽 🧜🏾 🧜🏿
+🧜♂️ 🧜🏻♂️ 🧜🏼♂️ 🧜🏽♂️ 🧜🏾♂️ 🧜🏿♂️
+🧜♀️ 🧜🏻♀️ 🧜🏼♀️ 🧜🏽♀️ 🧜🏾♀️ 🧜🏿♀️
+🧝 🧝🏻 🧝🏼 🧝🏽 🧝🏾 🧝🏿
+🧝♂️ 🧝🏻♂️ 🧝🏼♂️ 🧝🏽♂️ 🧝🏾♂️ 🧝🏿♂️
+🧝♀️ 🧝🏻♀️ 🧝🏼♀️ 🧝🏽♀️ 🧝🏾♀️ 🧝🏿♀️
+🧞
+🧞♂️
+🧞♀️
+🧟
+🧟♂️
+🧟♀️
+🧌
+💆 💆🏻 💆🏼 💆🏽 💆🏾 💆🏿
+💆♂️ 💆🏻♂️ 💆🏼♂️ 💆🏽♂️ 💆🏾♂️ 💆🏿♂️
+💆♀️ 💆🏻♀️ 💆🏼♀️ 💆🏽♀️ 💆🏾♀️ 💆🏿♀️
+💇 💇🏻 💇🏼 💇🏽 💇🏾 💇🏿
+💇♂️ 💇🏻♂️ 💇🏼♂️ 💇🏽♂️ 💇🏾♂️ 💇🏿♂️
+💇♀️ 💇🏻♀️ 💇🏼♀️ 💇🏽♀️ 💇🏾♀️ 💇🏿♀️
+🚶 🚶🏻 🚶🏼 🚶🏽 🚶🏾 🚶🏿
+🚶♂️ 🚶🏻♂️ 🚶🏼♂️ 🚶🏽♂️ 🚶🏾♂️ 🚶🏿♂️
+🚶♀️ 🚶🏻♀️ 🚶🏼♀️ 🚶🏽♀️ 🚶🏾♀️ 🚶🏿♀️
+🚶➡️ 🚶🏻➡️ 🚶🏼➡️ 🚶🏽➡️ 🚶🏾➡️ 🚶🏿➡️
+🚶♀️➡️ 🚶🏻♀️➡️ 🚶🏼♀️➡️ 🚶🏽♀️➡️ 🚶🏾♀️➡️ 🚶🏿♀️➡️
+🚶♂️➡️ 🚶🏻♂️➡️ 🚶🏼♂️➡️ 🚶🏽♂️➡️ 🚶🏾♂️➡️ 🚶🏿♂️➡️
+🧍 🧍🏻 🧍🏼 🧍🏽 🧍🏾 🧍🏿
+🧍♂️ 🧍🏻♂️ 🧍🏼♂️ 🧍🏽♂️ 🧍🏾♂️ 🧍🏿♂️
+🧍♀️ 🧍🏻♀️ 🧍🏼♀️ 🧍🏽♀️ 🧍🏾♀️ 🧍🏿♀️
+🧎 🧎🏻 🧎🏼 🧎🏽 🧎🏾 🧎🏿
+🧎♂️ 🧎🏻♂️ 🧎🏼♂️ 🧎🏽♂️ 🧎🏾♂️ 🧎🏿♂️
+🧎♀️ 🧎🏻♀️ 🧎🏼♀️ 🧎🏽♀️ 🧎🏾♀️ 🧎🏿♀️
+🧎➡️ 🧎🏻➡️ 🧎🏼➡️ 🧎🏽➡️ 🧎🏾➡️ 🧎🏿➡️
+🧎♀️➡️ 🧎🏻♀️➡️ 🧎🏼♀️➡️ 🧎🏽♀️➡️ 🧎🏾♀️➡️ 🧎🏿♀️➡️
+🧎♂️➡️ 🧎🏻♂️➡️ 🧎🏼♂️➡️ 🧎🏽♂️➡️ 🧎🏾♂️➡️ 🧎🏿♂️➡️
+🧑🦯 🧑🏻🦯 🧑🏼🦯 🧑🏽🦯 🧑🏾🦯 🧑🏿🦯
+🧑🦯➡️ 🧑🏻🦯➡️ 🧑🏼🦯➡️ 🧑🏽🦯➡️ 🧑🏾🦯➡️ 🧑🏿🦯➡️
+👨🦯 👨🏻🦯 👨🏼🦯 👨🏽🦯 👨🏾🦯 👨🏿🦯
+👨🦯➡️ 👨🏻🦯➡️ 👨🏼🦯➡️ 👨🏽🦯➡️ 👨🏾🦯➡️ 👨🏿🦯➡️
+👩🦯 👩🏻🦯 👩🏼🦯 👩🏽🦯 👩🏾🦯 👩🏿🦯
+👩🦯➡️ 👩🏻🦯➡️ 👩🏼🦯➡️ 👩🏽🦯➡️ 👩🏾🦯➡️ 👩🏿🦯➡️
+🧑🦼 🧑🏻🦼 🧑🏼🦼 🧑🏽🦼 🧑🏾🦼 🧑🏿🦼
+🧑🦼➡️ 🧑🏻🦼➡️ 🧑🏼🦼➡️ 🧑🏽🦼➡️ 🧑🏾🦼➡️ 🧑🏿🦼➡️
+👨🦼 👨🏻🦼 👨🏼🦼 👨🏽🦼 👨🏾🦼 👨🏿🦼
+👨🦼➡️ 👨🏻🦼➡️ 👨🏼🦼➡️ 👨🏽🦼➡️ 👨🏾🦼➡️ 👨🏿🦼➡️
+👩🦼 👩🏻🦼 👩🏼🦼 👩🏽🦼 👩🏾🦼 👩🏿🦼
+👩🦼➡️ 👩🏻🦼➡️ 👩🏼🦼➡️ 👩🏽🦼➡️ 👩🏾🦼➡️ 👩🏿🦼➡️
+🧑🦽 🧑🏻🦽 🧑🏼🦽 🧑🏽🦽 🧑🏾🦽 🧑🏿🦽
+🧑🦽➡️ 🧑🏻🦽➡️ 🧑🏼🦽➡️ 🧑🏽🦽➡️ 🧑🏾🦽➡️ 🧑🏿🦽➡️
+👨🦽 👨🏻🦽 👨🏼🦽 👨🏽🦽 👨🏾🦽 👨🏿🦽
+👨🦽➡️ 👨🏻🦽➡️ 👨🏼🦽➡️ 👨🏽🦽➡️ 👨🏾🦽➡️ 👨🏿🦽➡️
+👩🦽 👩🏻🦽 👩🏼🦽 👩🏽🦽 👩🏾🦽 👩🏿🦽
+👩🦽➡️ 👩🏻🦽➡️ 👩🏼🦽➡️ 👩🏽🦽➡️ 👩🏾🦽➡️ 👩🏿🦽➡️
+🏃 🏃🏻 🏃🏼 🏃🏽 🏃🏾 🏃🏿
+🏃♂️ 🏃🏻♂️ 🏃🏼♂️ 🏃🏽♂️ 🏃🏾♂️ 🏃🏿♂️
+🏃♀️ 🏃🏻♀️ 🏃🏼♀️ 🏃🏽♀️ 🏃🏾♀️ 🏃🏿♀️
+🏃➡️ 🏃🏻➡️ 🏃🏼➡️ 🏃🏽➡️ 🏃🏾➡️ 🏃🏿➡️
+🏃♀️➡️ 🏃🏻♀️➡️ 🏃🏼♀️➡️ 🏃🏽♀️➡️ 🏃🏾♀️➡️ 🏃🏿♀️➡️
+🏃♂️➡️ 🏃🏻♂️➡️ 🏃🏼♂️➡️ 🏃🏽♂️➡️ 🏃🏾♂️➡️ 🏃🏿♂️➡️
+💃 💃🏻 💃🏼 💃🏽 💃🏾 💃🏿
+🕺 🕺🏻 🕺🏼 🕺🏽 🕺🏾 🕺🏿
+🕴️ 🕴🏻 🕴🏼 🕴🏽 🕴🏾 🕴🏿
+👯
+👯♂️
+👯♀️
+🧖 🧖🏻 🧖🏼 🧖🏽 🧖🏾 🧖🏿
+🧖♂️ 🧖🏻♂️ 🧖🏼♂️ 🧖🏽♂️ 🧖🏾♂️ 🧖🏿♂️
+🧖♀️ 🧖🏻♀️ 🧖🏼♀️ 🧖🏽♀️ 🧖🏾♀️ 🧖🏿♀️
+🧗 🧗🏻 🧗🏼 🧗🏽 🧗🏾 🧗🏿
+🧗♂️ 🧗🏻♂️ 🧗🏼♂️ 🧗🏽♂️ 🧗🏾♂️ 🧗🏿♂️
+🧗♀️ 🧗🏻♀️ 🧗🏼♀️ 🧗🏽♀️ 🧗🏾♀️ 🧗🏿♀️
+🤺
+🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿
+⛷️
+🏂 🏂🏻 🏂🏼 🏂🏽 🏂🏾 🏂🏿
+🏌️ 🏌🏻 🏌🏼 🏌🏽 🏌🏾 🏌🏿
+🏌️♂️ 🏌🏻♂️ 🏌🏼♂️ 🏌🏽♂️ 🏌🏾♂️ 🏌🏿♂️
+🏌️♀️ 🏌🏻♀️ 🏌🏼♀️ 🏌🏽♀️ 🏌🏾♀️ 🏌🏿♀️
+🏄 🏄🏻 🏄🏼 🏄🏽 🏄🏾 🏄🏿
+🏄♂️ 🏄🏻♂️ 🏄🏼♂️ 🏄🏽♂️ 🏄🏾♂️ 🏄🏿♂️
+🏄♀️ 🏄🏻♀️ 🏄🏼♀️ 🏄🏽♀️ 🏄🏾♀️ 🏄🏿♀️
+🚣 🚣🏻 🚣🏼 🚣🏽 🚣🏾 🚣🏿
+🚣♂️ 🚣🏻♂️ 🚣🏼♂️ 🚣🏽♂️ 🚣🏾♂️ 🚣🏿♂️
+🚣♀️ 🚣🏻♀️ 🚣🏼♀️ 🚣🏽♀️ 🚣🏾♀️ 🚣🏿♀️
+🏊 🏊🏻 🏊🏼 🏊🏽 🏊🏾 🏊🏿
+🏊♂️ 🏊🏻♂️ 🏊🏼♂️ 🏊🏽♂️ 🏊🏾♂️ 🏊🏿♂️
+🏊♀️ 🏊🏻♀️ 🏊🏼♀️ 🏊🏽♀️ 🏊🏾♀️ 🏊🏿♀️
+⛹️ ⛹🏻 ⛹🏼 ⛹🏽 ⛹🏾 ⛹🏿
+⛹️♂️ ⛹🏻♂️ ⛹🏼♂️ ⛹🏽♂️ ⛹🏾♂️ ⛹🏿♂️
+⛹️♀️ ⛹🏻♀️ ⛹🏼♀️ ⛹🏽♀️ ⛹🏾♀️ ⛹🏿♀️
+🏋️ 🏋🏻 🏋🏼 🏋🏽 🏋🏾 🏋🏿
+🏋️♂️ 🏋🏻♂️ 🏋🏼♂️ 🏋🏽♂️ 🏋🏾♂️ 🏋🏿♂️
+🏋️♀️ 🏋🏻♀️ 🏋🏼♀️ 🏋🏽♀️ 🏋🏾♀️ 🏋🏿♀️
+🚴 🚴🏻 🚴🏼 🚴🏽 🚴🏾 🚴🏿
+🚴♂️ 🚴🏻♂️ 🚴🏼♂️ 🚴🏽♂️ 🚴🏾♂️ 🚴🏿♂️
+🚴♀️ 🚴🏻♀️ 🚴🏼♀️ 🚴🏽♀️ 🚴🏾♀️ 🚴🏿♀️
+🚵 🚵🏻 🚵🏼 🚵🏽 🚵🏾 🚵🏿
+🚵♂️ 🚵🏻♂️ 🚵🏼♂️ 🚵🏽♂️ 🚵🏾♂️ 🚵🏿♂️
+🚵♀️ 🚵🏻♀️ 🚵🏼♀️ 🚵🏽♀️ 🚵🏾♀️ 🚵🏿♀️
+🤸 🤸🏻 🤸🏼 🤸🏽 🤸🏾 🤸🏿
+🤸♂️ 🤸🏻♂️ 🤸🏼♂️ 🤸🏽♂️ 🤸🏾♂️ 🤸🏿♂️
+🤸♀️ 🤸🏻♀️ 🤸🏼♀️ 🤸🏽♀️ 🤸🏾♀️ 🤸🏿♀️
+🤼
+🤼♂️
+🤼♀️
+🤽 🤽🏻 🤽🏼 🤽🏽 🤽🏾 🤽🏿
+🤽♂️ 🤽🏻♂️ 🤽🏼♂️ 🤽🏽♂️ 🤽🏾♂️ 🤽🏿♂️
+🤽♀️ 🤽🏻♀️ 🤽🏼♀️ 🤽🏽♀️ 🤽🏾♀️ 🤽🏿♀️
+🤾 🤾🏻 🤾🏼 🤾🏽 🤾🏾 🤾🏿
+🤾♂️ 🤾🏻♂️ 🤾🏼♂️ 🤾🏽♂️ 🤾🏾♂️ 🤾🏿♂️
+🤾♀️ 🤾🏻♀️ 🤾🏼♀️ 🤾🏽♀️ 🤾🏾♀️ 🤾🏿♀️
+🤹 🤹🏻 🤹🏼 🤹🏽 🤹🏾 🤹🏿
+🤹♂️ 🤹🏻♂️ 🤹🏼♂️ 🤹🏽♂️ 🤹🏾♂️ 🤹🏿♂️
+🤹♀️ 🤹🏻♀️ 🤹🏼♀️ 🤹🏽♀️ 🤹🏾♀️ 🤹🏿♀️
+🧘 🧘🏻 🧘🏼 🧘🏽 🧘🏾 🧘🏿
+🧘♂️ 🧘🏻♂️ 🧘🏼♂️ 🧘🏽♂️ 🧘🏾♂️ 🧘🏿♂️
+🧘♀️ 🧘🏻♀️ 🧘🏼♀️ 🧘🏽♀️ 🧘🏾♀️ 🧘🏿♀️
+🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿
+🛌 🛌🏻 🛌🏼 🛌🏽 🛌🏾 🛌🏿
+🧑🤝🧑 🧑🏻🤝🧑🏻 🧑🏻🤝🧑🏼 🧑🏻🤝🧑🏽 🧑🏻🤝🧑🏾 🧑🏻🤝🧑🏿 🧑🏼🤝🧑🏻 🧑🏼🤝🧑🏼 🧑🏼🤝🧑🏽 🧑🏼🤝🧑🏾 🧑🏼🤝🧑🏿 🧑🏽🤝🧑🏻 🧑🏽🤝🧑🏼 🧑🏽🤝🧑🏽 🧑🏽🤝🧑🏾 🧑🏽🤝🧑🏿 🧑🏾🤝🧑🏻 🧑🏾🤝🧑🏼 🧑🏾🤝🧑🏽 🧑🏾🤝🧑🏾 🧑🏾🤝🧑🏿 🧑🏿🤝🧑🏻 🧑🏿🤝🧑🏼 🧑🏿🤝🧑🏽 🧑🏿🤝🧑🏾 🧑🏿🤝🧑🏿
+👭 👭🏻 👭🏼 👭🏽 👭🏾 👭🏿
+👫 👫🏻 👫🏼 👫🏽 👫🏾 👫🏿
+👬 👬🏻 👬🏼 👬🏽 👬🏾 👬🏿
+💏 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿
+👩❤️💋👨 👩🏻❤️💋👨🏻 👩🏻❤️💋👨🏼 👩🏻❤️💋👨🏽 👩🏻❤️💋👨🏾 👩🏻❤️💋👨🏿 👩🏼❤️💋👨🏻 👩🏼❤️💋👨🏼 👩🏼❤️💋👨🏽 👩🏼❤️💋👨🏾 👩🏼❤️💋👨🏿 👩🏽❤️💋👨🏻 👩🏽❤️💋👨🏼 👩🏽❤️💋👨🏽 👩🏽❤️💋👨🏾 👩🏽❤️💋👨🏿 👩🏾❤️💋👨🏻 👩🏾❤️💋👨🏼 👩🏾❤️💋👨🏽 👩🏾❤️💋👨🏾 👩🏾❤️💋👨🏿 👩🏿❤️💋👨🏻 👩🏿❤️💋👨🏼 👩🏿❤️💋👨🏽 👩🏿❤️💋👨🏾 👩🏿❤️💋👨🏿
+👨❤️💋👨 👨🏻❤️💋👨🏻 👨🏻❤️💋👨🏼 👨🏻❤️💋👨🏽 👨🏻❤️💋👨🏾 👨🏻❤️💋👨🏿 👨🏼❤️💋👨🏻 👨🏼❤️💋👨🏼 👨🏼❤️💋👨🏽 👨🏼❤️💋👨🏾 👨🏼❤️💋👨🏿 👨🏽❤️💋👨🏻 👨🏽❤️💋👨🏼 👨🏽❤️💋👨🏽 👨🏽❤️💋👨🏾 👨🏽❤️💋👨🏿 👨🏾❤️💋👨🏻 👨🏾❤️💋👨🏼 👨🏾❤️💋👨🏽 👨🏾❤️💋👨🏾 👨🏾❤️💋👨🏿 👨🏿❤️💋👨🏻 👨🏿❤️💋👨🏼 👨🏿❤️💋👨🏽 👨🏿❤️💋👨🏾 👨🏿❤️💋👨🏿
+👩❤️💋👩 👩🏻❤️💋👩🏻 👩🏻❤️💋👩🏼 👩🏻❤️💋👩🏽 👩🏻❤️💋👩🏾 👩🏻❤️💋👩🏿 👩🏼❤️💋👩🏻 👩🏼❤️💋👩🏼 👩🏼❤️💋👩🏽 👩🏼❤️💋👩🏾 👩🏼❤️💋👩🏿 👩🏽❤️💋👩🏻 👩🏽❤️💋👩🏼 👩🏽❤️💋👩🏽 👩🏽❤️💋👩🏾 👩🏽❤️💋👩🏿 👩🏾❤️💋👩🏻 👩🏾❤️💋👩🏼 👩🏾❤️💋👩🏽 👩🏾❤️💋👩🏾 👩🏾❤️💋👩🏿 👩🏿❤️💋👩🏻 👩🏿❤️💋👩🏼 👩🏿❤️💋👩🏽 👩🏿❤️💋👩🏾 👩🏿❤️💋👩🏿
+💑 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿
+👩❤️👨 👩🏻❤️👨🏻 👩🏻❤️👨🏼 👩🏻❤️👨🏽 👩🏻❤️👨🏾 👩🏻❤️👨🏿 👩🏼❤️👨🏻 👩🏼❤️👨🏼 👩🏼❤️👨🏽 👩🏼❤️👨🏾 👩🏼❤️👨🏿 👩🏽❤️👨🏻 👩🏽❤️👨🏼 👩🏽❤️👨🏽 👩🏽❤️👨🏾 👩🏽❤️👨🏿 👩🏾❤️👨🏻 👩🏾❤️👨🏼 👩🏾❤️👨🏽 👩🏾❤️👨🏾 👩🏾❤️👨🏿 👩🏿❤️👨🏻 👩🏿❤️👨🏼 👩🏿❤️👨🏽 👩🏿❤️👨🏾 👩🏿❤️👨🏿
+👨❤️👨 👨🏻❤️👨🏻 👨🏻❤️👨🏼 👨🏻❤️👨🏽 👨🏻❤️👨🏾 👨🏻❤️👨🏿 👨🏼❤️👨🏻 👨🏼❤️👨🏼 👨🏼❤️👨🏽 👨🏼❤️👨🏾 👨🏼❤️👨🏿 👨🏽❤️👨🏻 👨🏽❤️👨🏼 👨🏽❤️👨🏽 👨🏽❤️👨🏾 👨🏽❤️👨🏿 👨🏾❤️👨🏻 👨🏾❤️👨🏼 👨🏾❤️👨🏽 👨🏾❤️👨🏾 👨🏾❤️👨🏿 👨🏿❤️👨🏻 👨🏿❤️👨🏼 👨🏿❤️👨🏽 👨🏿❤️👨🏾 👨🏿❤️👨🏿
+👩❤️👩 👩🏻❤️👩🏻 👩🏻❤️👩🏼 👩🏻❤️👩🏽 👩🏻❤️👩🏾 👩🏻❤️👩🏿 👩🏼❤️👩🏻 👩🏼❤️👩🏼 👩🏼❤️👩🏽 👩🏼❤️👩🏾 👩🏼❤️👩🏿 👩🏽❤️👩🏻 👩🏽❤️👩🏼 👩🏽❤️👩🏽 👩🏽❤️👩🏾 👩🏽❤️👩🏿 👩🏾❤️👩🏻 👩🏾❤️👩🏼 👩🏾❤️👩🏽 👩🏾❤️👩🏾 👩🏾❤️👩🏿 👩🏿❤️👩🏻 👩🏿❤️👩🏼 👩🏿❤️👩🏽 👩🏿❤️👩🏾 👩🏿❤️👩🏿
+👨👩👦
+👨👩👧
+👨👩👧👦
+👨👩👦👦
+👨👩👧👧
+👨👨👦
+👨👨👧
+👨👨👧👦
+👨👨👦👦
+👨👨👧👧
+👩👩👦
+👩👩👧
+👩👩👧👦
+👩👩👦👦
+👩👩👧👧
+👨👦
+👨👦👦
+👨👧
+👨👧👦
+👨👧👧
+👩👦
+👩👦👦
+👩👧
+👩👧👦
+👩👧👧
+🗣️
+👤
+👥
+🫂
+👪
+🧑🧑🧒
+🧑🧑🧒🧒
+🧑🧒
+🧑🧒🧒
+👣
+
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/SMILEYS_AND_EMOTION.txt b/app/src/main/assets/emoji/SMILEYS_AND_EMOTION.txt
new file mode 100644
index 0000000000..73218d6fea
--- /dev/null
+++ b/app/src/main/assets/emoji/SMILEYS_AND_EMOTION.txt
@@ -0,0 +1,169 @@
+😀
+😃
+😄
+😁
+😆
+😅
+🤣
+😂
+🙂
+🙃
+🫠
+😉
+😊
+😇
+🥰
+😍
+🤩
+😘
+😗
+☺️
+😚
+😙
+🥲
+😋
+😛
+😜
+🤪
+😝
+🤑
+🤗
+🤭
+🫢
+🫣
+🤫
+🤔
+🫡
+🤐
+🤨
+😐
+😑
+😶
+🫥
+😶🌫️
+😏
+😒
+🙄
+😬
+😮💨
+🤥
+🫨
+🙂↔️
+🙂↕️
+😌
+😔
+😪
+🤤
+😴
+
+😷
+🤒
+🤕
+🤢
+🤮
+🤧
+🥵
+🥶
+🥴
+😵
+😵💫
+🤯
+🤠
+🥳
+🥸
+😎
+🤓
+🧐
+😕
+🫤
+😟
+🙁
+☹️
+😮
+😯
+😲
+😳
+🥺
+🥹
+😦
+😧
+😨
+😰
+😥
+😢
+😭
+😱
+😖
+😣
+😞
+😓
+😩
+😫
+🥱
+😤
+😡
+😠
+🤬
+😈
+👿
+💀
+☠️
+💩
+🤡
+👹
+👺
+👻
+👽
+👾
+🤖
+😺
+😸
+😹
+😻
+😼
+😽
+🙀
+😿
+😾
+🙈
+🙉
+🙊
+💌
+💘
+💝
+💖
+💗
+💓
+💞
+💕
+💟
+❣️
+💔
+❤️🔥
+❤️🩹
+❤️
+🩷
+🧡
+💛
+💚
+💙
+🩵
+💜
+🤎
+🖤
+🩶
+🤍
+💋
+💯
+💢
+💥
+💫
+💦
+💨
+🕳️
+💬
+👁️🗨️
+🗨️
+🗯️
+💭
+💤
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/SYMBOLS.txt b/app/src/main/assets/emoji/SYMBOLS.txt
new file mode 100644
index 0000000000..02effebc72
--- /dev/null
+++ b/app/src/main/assets/emoji/SYMBOLS.txt
@@ -0,0 +1,224 @@
+🏧
+🚮
+🚰
+♿
+🚹
+🚺
+🚻
+🚼
+🚾
+🛂
+🛃
+🛄
+🛅
+⚠️
+🚸
+⛔
+🚫
+🚳
+🚭
+🚯
+🚱
+🚷
+📵
+🔞
+☢️
+☣️
+⬆️
+↗️
+➡️
+↘️
+⬇️
+↙️
+⬅️
+↖️
+↕️
+↔️
+↩️
+↪️
+⤴️
+⤵️
+🔃
+🔄
+🔙
+🔚
+🔛
+🔜
+🔝
+🛐
+⚛️
+🕉️
+✡️
+☸️
+☯️
+✝️
+☦️
+☪️
+☮️
+🕎
+🔯
+🪯
+♈
+♉
+♊
+♋
+♌
+♍
+♎
+♏
+♐
+♑
+♒
+♓
+⛎
+🔀
+🔁
+🔂
+▶️
+⏩
+⏭️
+⏯️
+◀️
+⏪
+⏮️
+🔼
+⏫
+🔽
+⏬
+⏸️
+⏹️
+⏺️
+⏏️
+🎦
+🔅
+🔆
+📶
+🛜
+📳
+📴
+♀️
+♂️
+⚧️
+✖️
+➕
+➖
+➗
+🟰
+♾️
+‼️
+⁉️
+❓
+❔
+❕
+❗
+〰️
+💱
+💲
+⚕️
+♻️
+⚜️
+🔱
+📛
+🔰
+⭕
+✅
+☑️
+✔️
+❌
+❎
+➰
+➿
+〽️
+✳️
+✴️
+❇️
+©️
+®️
+™️
+
+#️⃣
+*️⃣
+0️⃣
+1️⃣
+2️⃣
+3️⃣
+4️⃣
+5️⃣
+6️⃣
+7️⃣
+8️⃣
+9️⃣
+🔟
+🔠
+🔡
+🔢
+🔣
+🔤
+🅰️
+🆎
+🅱️
+🆑
+🆒
+🆓
+ℹ️
+🆔
+Ⓜ️
+🆕
+🆖
+🅾️
+🆗
+🅿️
+🆘
+🆙
+🆚
+🈁
+🈂️
+🈷️
+🈶
+🈯
+🉐
+🈹
+🈚
+🈲
+🉑
+🈸
+🈴
+🈳
+㊗️
+㊙️
+🈺
+🈵
+🔴
+🟠
+🟡
+🟢
+🔵
+🟣
+🟤
+⚫
+⚪
+🟥
+🟧
+🟨
+🟩
+🟦
+🟪
+🟫
+⬛
+⬜
+◼️
+◻️
+◾
+◽
+▪️
+▫️
+🔶
+🔷
+🔸
+🔹
+🔺
+🔻
+💠
+🔘
+🔳
+🔲
diff --git a/app/src/main/assets/emoji/TRAVEL_AND_PLACES.txt b/app/src/main/assets/emoji/TRAVEL_AND_PLACES.txt
new file mode 100644
index 0000000000..ff1f0ad4a4
--- /dev/null
+++ b/app/src/main/assets/emoji/TRAVEL_AND_PLACES.txt
@@ -0,0 +1,218 @@
+🌍
+🌎
+🌏
+🌐
+🗺️
+🗾
+🧭
+🏔️
+⛰️
+🌋
+🗻
+🏕️
+🏖️
+🏜️
+🏝️
+🏞️
+🏟️
+🏛️
+🏗️
+🧱
+🪨
+🪵
+🛖
+🏘️
+🏚️
+🏠
+🏡
+🏢
+🏣
+🏤
+🏥
+🏦
+🏨
+🏩
+🏪
+🏫
+🏬
+🏭
+🏯
+🏰
+💒
+🗼
+🗽
+⛪
+🕌
+🛕
+🕍
+⛩️
+🕋
+⛲
+⛺
+🌁
+🌃
+🏙️
+🌄
+🌅
+🌆
+🌇
+🌉
+♨️
+🎠
+🛝
+🎡
+🎢
+💈
+🎪
+🚂
+🚃
+🚄
+🚅
+🚆
+🚇
+🚈
+🚉
+🚊
+🚝
+🚞
+🚋
+🚌
+🚍
+🚎
+🚐
+🚑
+🚒
+🚓
+🚔
+🚕
+🚖
+🚗
+🚘
+🚙
+🛻
+🚚
+🚛
+🚜
+🏎️
+🏍️
+🛵
+🦽
+🦼
+🛺
+🚲
+🛴
+🛹
+🛼
+🚏
+🛣️
+🛤️
+🛢️
+⛽
+🛞
+🚨
+🚥
+🚦
+🛑
+🚧
+⚓
+🛟
+⛵
+🛶
+🚤
+🛳️
+⛴️
+🛥️
+🚢
+✈️
+🛩️
+🛫
+🛬
+🪂
+💺
+🚁
+🚟
+🚠
+🚡
+🛰️
+🚀
+🛸
+🛎️
+🧳
+⌛
+⏳
+⌚
+⏰
+⏱️
+⏲️
+🕰️
+🕛
+🕧
+🕐
+🕜
+🕑
+🕝
+🕒
+🕞
+🕓
+🕟
+🕔
+🕠
+🕕
+🕡
+🕖
+🕢
+🕗
+🕣
+🕘
+🕤
+🕙
+🕥
+🕚
+🕦
+🌑
+🌒
+🌓
+🌔
+🌕
+🌖
+🌗
+🌘
+🌙
+🌚
+🌛
+🌜
+🌡️
+☀️
+🌝
+🌞
+🪐
+⭐
+🌟
+🌠
+🌌
+☁️
+⛅
+⛈️
+🌤️
+🌥️
+🌦️
+🌧️
+🌨️
+🌩️
+🌪️
+🌫️
+🌬️
+🌀
+🌈
+🌂
+☂️
+☔
+⛱️
+⚡
+❄️
+☃️
+⛄
+☄️
+🔥
+💧
+🌊
\ No newline at end of file
diff --git a/app/src/main/assets/emoji/minApi.txt b/app/src/main/assets/emoji/minApi.txt
new file mode 100644
index 0000000000..842da2d6be
--- /dev/null
+++ b/app/src/main/assets/emoji/minApi.txt
@@ -0,0 +1,12 @@
+23 🙂 🙃 🤑 🤗 🤔 🤐 🙄 🤒 🤕 🤓 🙁 ☹️ ☠️ 🤖 ❣️ 🕳️ 👁️🗨️ 🗨️ 🗯️ 🖐️ 🖖 🤘 🖕 ✍️ 👁️ 🕵️ 🕴️ ⛷️ 🏌️ ⛹️ 🏋️ 🛌 👨❤️💋👨 👩❤️💋👩 👨❤️👨 👩❤️👩 👨👩👧 👨👩👧👦 👨👩👦👦 👨👩👧👧 👨👨👦 👨👨👧 👨👨👧👦 👨👨👦👦 👨👨👧👧 👩👩👦 👩👩👧 👩👩👧👦 👩👩👦👦 👩👩👧👧 🗣️ 🦁 🦄 🐿️ 🦃 🕊️ 🦀 🕷️ 🕸️ 🦂 🏵️ ☘️ 🌶️ 🧀 🌭 🌮 🌯 🍿 🍾 🍽️ 🏺 🗺️ 🏔️ ⛰️ 🏕️ 🏖️ 🏜️ 🏝️ 🏞️ 🏟️ 🏛️ 🏗️ 🏘️ 🏚️ 🕌 🕍 ⛩️ 🕋 🏙️ 🏎️ 🏍️ 🛣️ 🛤️ 🛢️ 🛳️ ⛴️ 🛥️ 🛩️ 🛫 🛬 🛰️ 🛎️ ⏱️ ⏲️ 🕰️ 🌡️ ⛈️ 🌤️ 🌥️ 🌦️ 🌧️ 🌨️ 🌩️ 🌪️ 🌫️ 🌬️ ☂️ ⛱️ ☃️ ☄️ 🎗️ 🎟️ 🎖️ 🏅 🏐 🏏 🏑 🏒 🏓 🏸 ⛸️ 🕹️ 🖼️ 🕶️ 🛍️ ⛑️ 📿 🎙️ 🎚️ 🎛️ 🖥️ 🖨️ ⌨️ 🖱️ 🖲️ 🎞️ 📽️ 📸 🕯️ 🗞️ 🏷️ 🗳️ 🖋️ 🖊️ 🖌️ 🖍️ 🗂️ 🗒️ 🗓️ 🖇️ 🗃️ 🗄️ 🗑️ 🗝️ ⛏️ ⚒️ 🛠️ 🗡️ ⚔️ 🏹 🛡️ ⚙️ 🗜️ ⚖️ ⛓️ ⚗️ 🛏️ 🛋️ ⚰️ ⚱️ ☢️ ☣️ 🛐 ⚛️ 🕉️ ✡️ ☸️ ☯️ ✝️ ☦️ ☪️ ☮️ 🕎 ⏭️ ⏯️ ⏮️ ⏸️ ⏹️ ⏺️ ⏏️ ⚜️ *️⃣ 🏴 🏳️
+24 🤣 🤥 🤤 🤢 🤧 🤠 🤡 🖤 👋🏻 👋🏼 👋🏽 👋🏾 👋🏿 🤚 🤚🏻 🤚🏼 🤚🏽 🤚🏾 🤚🏿 🖐🏻 🖐🏼 🖐🏽 🖐🏾 🖐🏿 ✋🏻 ✋🏼 ✋🏽 ✋🏾 ✋🏿 🖖🏻 🖖🏼 🖖🏽 🖖🏾 🖖🏿 👌🏻 👌🏼 👌🏽 👌🏾 👌🏿 ✌🏻 ✌🏼 ✌🏽 ✌🏾 ✌🏿 🤞 🤞🏻 🤞🏼 🤞🏽 🤞🏾 🤞🏿 🤘🏻 🤘🏼 🤘🏽 🤘🏾 🤘🏿 🤙 🤙🏻 🤙🏼 🤙🏽 🤙🏾 🤙🏿 👈🏻 👈🏼 👈🏽 👈🏾 👈🏿 👉🏻 👉🏼 👉🏽 👉🏾 👉🏿 👆🏻 👆🏼 👆🏽 👆🏾 👆🏿 🖕🏻 🖕🏼 🖕🏽 🖕🏾 🖕🏿 👇🏻 👇🏼 👇🏽 👇🏾 👇🏿 ☝🏻 ☝🏼 ☝🏽 ☝🏾 ☝🏿 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿 👎🏻 👎🏼 👎🏽 👎🏾 👎🏿 ✊🏻 ✊🏼 ✊🏽 ✊🏾 ✊🏿 👊🏻 👊🏼 👊🏽 👊🏾 👊🏿 🤛 🤛🏻 🤛🏼 🤛🏽 🤛🏾 🤛🏿 🤜 🤜🏻 🤜🏼 🤜🏽 🤜🏾 🤜🏿 👏🏻 👏🏼 👏🏽 👏🏾 👏🏿 🙌🏻 🙌🏼 🙌🏽 🙌🏾 🙌🏿 👐🏻 👐🏼 👐🏽 👐🏾 👐🏿 🤝 🙏🏻 🙏🏼 🙏🏽 🙏🏾 🙏🏿 ✍🏻 ✍🏼 ✍🏽 ✍🏾 ✍🏿 💅🏻 💅🏼 💅🏽 💅🏾 💅🏿 🤳 🤳🏻 🤳🏼 🤳🏽 🤳🏾 🤳🏿 💪🏻 💪🏼 💪🏽 💪🏾 💪🏿 👂🏻 👂🏼 👂🏽 👂🏾 👂🏿 👃🏻 👃🏼 👃🏽 👃🏾 👃🏿 👶🏻 👶🏼 👶🏽 👶🏾 👶🏿 👦🏻 👦🏼 👦🏽 👦🏾 👦🏿 👧🏻 👧🏼 👧🏽 👧🏾 👧🏿 👱🏻 👱🏼 👱🏽 👱🏾 👱🏿 👨🏻 👨🏼 👨🏽 👨🏾 👨🏿 👩🏻 👩🏼 👩🏽 👩🏾 👩🏿 👴🏻 👴🏼 👴🏽 👴🏾 👴🏿 👵🏻 👵🏼 👵🏽 👵🏾 👵🏿 🙍🏻 🙍🏼 🙍🏽 🙍🏾 🙍🏿 🙎🏻 🙎🏼 🙎🏽 🙎🏾 🙎🏿 🙅🏻 🙅🏼 🙅🏽 🙅🏾 🙅🏿 🙆🏻 🙆🏼 🙆🏽 🙆🏾 🙆🏿 💁🏻 💁🏼 💁🏽 💁🏾 💁🏿 🙋🏻 🙋🏼 🙋🏽 🙋🏾 🙋🏿 🙇🏻 🙇🏼 🙇🏽 🙇🏾 🙇🏿 🤦 🤦🏻 🤦🏼 🤦🏽 🤦🏾 🤦🏿 🤷 🤷🏻 🤷🏼 🤷🏽 🤷🏾 🤷🏿 👮🏻 👮🏼 👮🏽 👮🏾 👮🏿 🕵🏻 🕵🏼 🕵🏽 🕵🏾 🕵🏿 💂🏻 💂🏼 💂🏽 💂🏾 💂🏿 👷🏻 👷🏼 👷🏽 👷🏾 👷🏿 🤴 🤴🏻 🤴🏼 🤴🏽 🤴🏾 🤴🏿 👸🏻 👸🏼 👸🏽 👸🏾 👸🏿 👳🏻 👳🏼 👳🏽 👳🏾 👳🏿 👲🏻 👲🏼 👲🏽 👲🏾 👲🏿 🤵 🤵🏻 🤵🏼 🤵🏽 🤵🏾 🤵🏿 👰🏻 👰🏼 👰🏽 👰🏾 👰🏿 🤰 🤰🏻 🤰🏼 🤰🏽 🤰🏾 🤰🏿 👼🏻 👼🏼 👼🏽 👼🏾 👼🏿 🎅🏻 🎅🏼 🎅🏽 🎅🏾 🎅🏿 🤶 🤶🏻 🤶🏼 🤶🏽 🤶🏾 🤶🏿 💆🏻 💆🏼 💆🏽 💆🏾 💆🏿 💇🏻 💇🏼 💇🏽 💇🏾 💇🏿 🚶🏻 🚶🏼 🚶🏽 🚶🏾 🚶🏿 🏃🏻 🏃🏼 🏃🏽 🏃🏾 🏃🏿 💃🏻 💃🏼 💃🏽 💃🏾 💃🏿 🕺 🕺🏻 🕺🏼 🕺🏽 🕺🏾 🕺🏿 🤺 🏄🏻 🏄🏼 🏄🏽 🏄🏾 🏄🏿 🚣🏻 🚣🏼 🚣🏽 🚣🏾 🚣🏿 🏊🏻 🏊🏼 🏊🏽 🏊🏾 🏊🏿 ⛹🏻 ⛹🏼 ⛹🏽 ⛹🏾 ⛹🏿 🏋🏻 🏋🏼 🏋🏽 🏋🏾 🏋🏿 🚴🏻 🚴🏼 🚴🏽 🚴🏾 🚴🏿 🚵🏻 🚵🏼 🚵🏽 🚵🏾 🚵🏿 🤸 🤸🏻 🤸🏼 🤸🏽 🤸🏾 🤸🏿 🤼 🤽 🤽🏻 🤽🏼 🤽🏽 🤽🏾 🤽🏿 🤾 🤾🏻 🤾🏼 🤾🏽 🤾🏾 🤾🏿 🤹 🤹🏻 🤹🏼 🤹🏽 🤹🏾 🤹🏿 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿 🦍 🦊 🦌 🦏 🦇 🦅 🦆 🦉 🦎 🦈 🦐 🦑 🦋 🥀 🥝 🥑 🥔 🥕 🥒 🥜 🥐 🥖 🥞 🥓 🥙 🥚 🥘 🥗 🥛 🥂 🥃 🥄 🛵 🛴 🛑 🛶 🥇 🥈 🥉 🥊 🥋 🥅 🥁 🛒 🇦🇨 🇦🇶 🇧🇻 🇨🇵 🇭🇲 🇮🇨 🇸🇭 🇸🇯 🇹🇦 🇺🇲
+25 🤝🏻 🤝🏼 🤝🏽 🤝🏾 🤝🏿 👱♀️ 👱🏻♀️ 👱🏼♀️ 👱🏽♀️ 👱🏾♀️ 👱🏿♀️ 👱♂️ 👱🏻♂️ 👱🏼♂️ 👱🏽♂️ 👱🏾♂️ 👱🏿♂️ 🙍♂️ 🙍🏻♂️ 🙍🏼♂️ 🙍🏽♂️ 🙍🏾♂️ 🙍🏿♂️ 🙍♀️ 🙍🏻♀️ 🙍🏼♀️ 🙍🏽♀️ 🙍🏾♀️ 🙍🏿♀️ 🙎♂️ 🙎🏻♂️ 🙎🏼♂️ 🙎🏽♂️ 🙎🏾♂️ 🙎🏿♂️ 🙎♀️ 🙎🏻♀️ 🙎🏼♀️ 🙎🏽♀️ 🙎🏾♀️ 🙎🏿♀️ 🙅♂️ 🙅🏻♂️ 🙅🏼♂️ 🙅🏽♂️ 🙅🏾♂️ 🙅🏿♂️ 🙅♀️ 🙅🏻♀️ 🙅🏼♀️ 🙅🏽♀️ 🙅🏾♀️ 🙅🏿♀️ 🙆♂️ 🙆🏻♂️ 🙆🏼♂️ 🙆🏽♂️ 🙆🏾♂️ 🙆🏿♂️ 🙆♀️ 🙆🏻♀️ 🙆🏼♀️ 🙆🏽♀️ 🙆🏾♀️ 🙆🏿♀️ 💁♂️ 💁🏻♂️ 💁🏼♂️ 💁🏽♂️ 💁🏾♂️ 💁🏿♂️ 💁♀️ 💁🏻♀️ 💁🏼♀️ 💁🏽♀️ 💁🏾♀️ 💁🏿♀️ 🙋♂️ 🙋🏻♂️ 🙋🏼♂️ 🙋🏽♂️ 🙋🏾♂️ 🙋🏿♂️ 🙋♀️ 🙋🏻♀️ 🙋🏼♀️ 🙋🏽♀️ 🙋🏾♀️ 🙋🏿♀️ 🙇♂️ 🙇🏻♂️ 🙇🏼♂️ 🙇🏽♂️ 🙇🏾♂️ 🙇🏿♂️ 🙇♀️ 🙇🏻♀️ 🙇🏼♀️ 🙇🏽♀️ 🙇🏾♀️ 🙇🏿♀️ 🤦♂️ 🤦🏻♂️ 🤦🏼♂️ 🤦🏽♂️ 🤦🏾♂️ 🤦🏿♂️ 🤦♀️ 🤦🏻♀️ 🤦🏼♀️ 🤦🏽♀️ 🤦🏾♀️ 🤦🏿♀️ 🤷♂️ 🤷🏻♂️ 🤷🏼♂️ 🤷🏽♂️ 🤷🏾♂️ 🤷🏿♂️ 🤷♀️ 🤷🏻♀️ 🤷🏼♀️ 🤷🏽♀️ 🤷🏾♀️ 🤷🏿♀️ 👨⚕️ 👨🏻⚕️ 👨🏼⚕️ 👨🏽⚕️ 👨🏾⚕️ 👨🏿⚕️ 👩⚕️ 👩🏻⚕️ 👩🏼⚕️ 👩🏽⚕️ 👩🏾⚕️ 👩🏿⚕️ 👨🎓 👨🏻🎓 👨🏼🎓 👨🏽🎓 👨🏾🎓 👨🏿🎓 👩🎓 👩🏻🎓 👩🏼🎓 👩🏽🎓 👩🏾🎓 👩🏿🎓 👨🏫 👨🏻🏫 👨🏼🏫 👨🏽🏫 👨🏾🏫 👨🏿🏫 👩🏫 👩🏻🏫 👩🏼🏫 👩🏽🏫 👩🏾🏫 👩🏿🏫 👨⚖️ 👨🏻⚖️ 👨🏼⚖️ 👨🏽⚖️ 👨🏾⚖️ 👨🏿⚖️ 👩⚖️ 👩🏻⚖️ 👩🏼⚖️ 👩🏽⚖️ 👩🏾⚖️ 👩🏿⚖️ 👨🌾 👨🏻🌾 👨🏼🌾 👨🏽🌾 👨🏾🌾 👨🏿🌾 👩🌾 👩🏻🌾 👩🏼🌾 👩🏽🌾 👩🏾🌾 👩🏿🌾 👨🍳 👨🏻🍳 👨🏼🍳 👨🏽🍳 👨🏾🍳 👨🏿🍳 👩🍳 👩🏻🍳 👩🏼🍳 👩🏽🍳 👩🏾🍳 👩🏿🍳 👨🔧 👨🏻🔧 👨🏼🔧 👨🏽🔧 👨🏾🔧 👨🏿🔧 👩🔧 👩🏻🔧 👩🏼🔧 👩🏽🔧 👩🏾🔧 👩🏿🔧 👨🏭 👨🏻🏭 👨🏼🏭 👨🏽🏭 👨🏾🏭 👨🏿🏭 👩🏭 👩🏻🏭 👩🏼🏭 👩🏽🏭 👩🏾🏭 👩🏿🏭 👨💼 👨🏻💼 👨🏼💼 👨🏽💼 👨🏾💼 👨🏿💼 👩💼 👩🏻💼 👩🏼💼 👩🏽💼 👩🏾💼 👩🏿💼 👨🔬 👨🏻🔬 👨🏼🔬 👨🏽🔬 👨🏾🔬 👨🏿🔬 👩🔬 👩🏻🔬 👩🏼🔬 👩🏽🔬 👩🏾🔬 👩🏿🔬 👨💻 👨🏻💻 👨🏼💻 👨🏽💻 👨🏾💻 👨🏿💻 👩💻 👩🏻💻 👩🏼💻 👩🏽💻 👩🏾💻 👩🏿💻 👨🎤 👨🏻🎤 👨🏼🎤 👨🏽🎤 👨🏾🎤 👨🏿🎤 👩🎤 👩🏻🎤 👩🏼🎤 👩🏽🎤 👩🏾🎤 👩🏿🎤 👨🎨 👨🏻🎨 👨🏼🎨 👨🏽🎨 👨🏾🎨 👨🏿🎨 👩🎨 👩🏻🎨 👩🏼🎨 👩🏽🎨 👩🏾🎨 👩🏿🎨 👨✈️ 👨🏻✈️ 👨🏼✈️ 👨🏽✈️ 👨🏾✈️ 👨🏿✈️ 👩✈️ 👩🏻✈️ 👩🏼✈️ 👩🏽✈️ 👩🏾✈️ 👩🏿✈️ 👨🚀 👨🏻🚀 👨🏼🚀 👨🏽🚀 👨🏾🚀 👨🏿🚀 👩🚀 👩🏻🚀 👩🏼🚀 👩🏽🚀 👩🏾🚀 👩🏿🚀 👨🚒 👨🏻🚒 👨🏼🚒 👨🏽🚒 👨🏾🚒 👨🏿🚒 👩🚒 👩🏻🚒 👩🏼🚒 👩🏽🚒 👩🏾🚒 👩🏿🚒 👮♂️ 👮🏻♂️ 👮🏼♂️ 👮🏽♂️ 👮🏾♂️ 👮🏿♂️ 👮♀️ 👮🏻♀️ 👮🏼♀️ 👮🏽♀️ 👮🏾♀️ 👮🏿♀️ 🕵️♂️ 🕵🏻♂️ 🕵🏼♂️ 🕵🏽♂️ 🕵🏾♂️ 🕵🏿♂️ 🕵️♀️ 🕵🏻♀️ 🕵🏼♀️ 🕵🏽♀️ 🕵🏾♀️ 🕵🏿♀️ 💂♂️ 💂🏻♂️ 💂🏼♂️ 💂🏽♂️ 💂🏾♂️ 💂🏿♂️ 💂♀️ 💂🏻♀️ 💂🏼♀️ 💂🏽♀️ 💂🏾♀️ 💂🏿♀️ 👷♂️ 👷🏻♂️ 👷🏼♂️ 👷🏽♂️ 👷🏾♂️ 👷🏿♂️ 👷♀️ 👷🏻♀️ 👷🏼♀️ 👷🏽♀️ 👷🏾♀️ 👷🏿♀️ 👳♂️ 👳🏻♂️ 👳🏼♂️ 👳🏽♂️ 👳🏾♂️ 👳🏿♂️ 👳♀️ 👳🏻♀️ 👳🏼♀️ 👳🏽♀️ 👳🏾♀️ 👳🏿♀️ 💆♂️ 💆🏻♂️ 💆🏼♂️ 💆🏽♂️ 💆🏾♂️ 💆🏿♂️ 💆♀️ 💆🏻♀️ 💆🏼♀️ 💆🏽♀️ 💆🏾♀️ 💆🏿♀️ 💇♂️ 💇🏻♂️ 💇🏼♂️ 💇🏽♂️ 💇🏾♂️ 💇🏿♂️ 💇♀️ 💇🏻♀️ 💇🏼♀️ 💇🏽♀️ 💇🏾♀️ 💇🏿♀️ 🚶♂️ 🚶🏻♂️ 🚶🏼♂️ 🚶🏽♂️ 🚶🏾♂️ 🚶🏿♂️ 🚶♀️ 🚶🏻♀️ 🚶🏼♀️ 🚶🏽♀️ 🚶🏾♀️ 🚶🏿♀️ 🏃♂️ 🏃🏻♂️ 🏃🏼♂️ 🏃🏽♂️ 🏃🏾♂️ 🏃🏿♂️ 🏃♀️ 🏃🏻♀️ 🏃🏼♀️ 🏃🏽♀️ 🏃🏾♀️ 🏃🏿♀️ 👯♂️ 👯♀️ 🏌️♂️ 🏌️♀️ 🏄♂️ 🏄🏻♂️ 🏄🏼♂️ 🏄🏽♂️ 🏄🏾♂️ 🏄🏿♂️ 🏄♀️ 🏄🏻♀️ 🏄🏼♀️ 🏄🏽♀️ 🏄🏾♀️ 🏄🏿♀️ 🚣♂️ 🚣🏻♂️ 🚣🏼♂️ 🚣🏽♂️ 🚣🏾♂️ 🚣🏿♂️ 🚣♀️ 🚣🏻♀️ 🚣🏼♀️ 🚣🏽♀️ 🚣🏾♀️ 🚣🏿♀️ 🏊♂️ 🏊🏻♂️ 🏊🏼♂️ 🏊🏽♂️ 🏊🏾♂️ 🏊🏿♂️ 🏊♀️ 🏊🏻♀️ 🏊🏼♀️ 🏊🏽♀️ 🏊🏾♀️ 🏊🏿♀️ ⛹️♂️ ⛹🏻♂️ ⛹🏼♂️ ⛹🏽♂️ ⛹🏾♂️ ⛹🏿♂️ ⛹️♀️ ⛹🏻♀️ ⛹🏼♀️ ⛹🏽♀️ ⛹🏾♀️ ⛹🏿♀️ 🏋️♂️ 🏋🏻♂️ 🏋🏼♂️ 🏋🏽♂️ 🏋🏾♂️ 🏋🏿♂️ 🏋️♀️ 🏋🏻♀️ 🏋🏼♀️ 🏋🏽♀️ 🏋🏾♀️ 🏋🏿♀️ 🚴♂️ 🚴🏻♂️ 🚴🏼♂️ 🚴🏽♂️ 🚴🏾♂️ 🚴🏿♂️ 🚴♀️ 🚴🏻♀️ 🚴🏼♀️ 🚴🏽♀️ 🚴🏾♀️ 🚴🏿♀️ 🚵♂️ 🚵🏻♂️ 🚵🏼♂️ 🚵🏽♂️ 🚵🏾♂️ 🚵🏿♂️ 🚵♀️ 🚵🏻♀️ 🚵🏼♀️ 🚵🏽♀️ 🚵🏾♀️ 🚵🏿♀️ 🤸♂️ 🤸🏻♂️ 🤸🏼♂️ 🤸🏽♂️ 🤸🏾♂️ 🤸🏿♂️ 🤸♀️ 🤸🏻♀️ 🤸🏼♀️ 🤸🏽♀️ 🤸🏾♀️ 🤸🏿♀️ 🤼♂️ 🤼♀️ 🤽♂️ 🤽🏻♂️ 🤽🏼♂️ 🤽🏽♂️ 🤽🏾♂️ 🤽🏿♂️ 🤽♀️ 🤽🏻♀️ 🤽🏼♀️ 🤽🏽♀️ 🤽🏾♀️ 🤽🏿♀️ 🤾♂️ 🤾🏻♂️ 🤾🏼♂️ 🤾🏽♂️ 🤾🏾♂️ 🤾🏿♂️ 🤾♀️ 🤾🏻♀️ 🤾🏼♀️ 🤾🏽♀️ 🤾🏾♀️ 🤾🏿♀️ 🤹♂️ 🤹🏻♂️ 🤹🏼♂️ 🤹🏽♂️ 🤹🏾♂️ 🤹🏿♂️ 🤹♀️ 🤹🏻♀️ 🤹🏼♀️ 🤹🏽♀️ 🤹🏾♀️ 🤹🏿♀️ 👨👩👦 👨👦 👨👦👦 👨👧 👨👧👦 👨👧👧 👩👦 👩👦👦 👩👧 👩👧👦 👩👧👧 ♀️ ♂️ ⚕️ 🏳️🌈
+26 🤩 🤪 🤭 🤫 🤨 🤮 🤯 🧐 🤬 🧡 🤟 🤟🏻 🤟🏼 🤟🏽 🤟🏾 🤟🏿 🤲 🤲🏻 🤲🏼 🤲🏽 🤲🏾 🤲🏿 🧠 🧒 🧒🏻 🧒🏼 🧒🏽 🧒🏾 🧒🏿 🧑 🧑🏻 🧑🏼 🧑🏽 🧑🏾 🧑🏿 🧔 🧔🏻 🧔🏼 🧔🏽 🧔🏾 🧔🏿 🧓 🧓🏻 🧓🏼 🧓🏽 🧓🏾 🧓🏿 🧕 🧕🏻 🧕🏼 🧕🏽 🧕🏾 🧕🏿 🤱 🤱🏻 🤱🏼 🤱🏽 🤱🏾 🤱🏿 🧙 🧙🏻 🧙🏼 🧙🏽 🧙🏾 🧙🏿 🧙♂️ 🧙🏻♂️ 🧙🏼♂️ 🧙🏽♂️ 🧙🏾♂️ 🧙🏿♂️ 🧙♀️ 🧙🏻♀️ 🧙🏼♀️ 🧙🏽♀️ 🧙🏾♀️ 🧙🏿♀️ 🧚 🧚🏻 🧚🏼 🧚🏽 🧚🏾 🧚🏿 🧚♂️ 🧚🏻♂️ 🧚🏼♂️ 🧚🏽♂️ 🧚🏾♂️ 🧚🏿♂️ 🧚♀️ 🧚🏻♀️ 🧚🏼♀️ 🧚🏽♀️ 🧚🏾♀️ 🧚🏿♀️ 🧛 🧛🏻 🧛🏼 🧛🏽 🧛🏾 🧛🏿 🧛♂️ 🧛🏻♂️ 🧛🏼♂️ 🧛🏽♂️ 🧛🏾♂️ 🧛🏿♂️ 🧛♀️ 🧛🏻♀️ 🧛🏼♀️ 🧛🏽♀️ 🧛🏾♀️ 🧛🏿♀️ 🧜 🧜🏻 🧜🏼 🧜🏽 🧜🏾 🧜🏿 🧜♂️ 🧜🏻♂️ 🧜🏼♂️ 🧜🏽♂️ 🧜🏾♂️ 🧜🏿♂️ 🧜♀️ 🧜🏻♀️ 🧜🏼♀️ 🧜🏽♀️ 🧜🏾♀️ 🧜🏿♀️ 🧝 🧝🏻 🧝🏼 🧝🏽 🧝🏾 🧝🏿 🧝♂️ 🧝🏻♂️ 🧝🏼♂️ 🧝🏽♂️ 🧝🏾♂️ 🧝🏿♂️ 🧝♀️ 🧝🏻♀️ 🧝🏼♀️ 🧝🏽♀️ 🧝🏾♀️ 🧝🏿♀️ 🧞 🧞♂️ 🧞♀️ 🧟 🧟♂️ 🧟♀️ 🕴🏻 🕴🏼 🕴🏽 🕴🏾 🕴🏿 🧖 🧖🏻 🧖🏼 🧖🏽 🧖🏾 🧖🏿 🧖♂️ 🧖🏻♂️ 🧖🏼♂️ 🧖🏽♂️ 🧖🏾♂️ 🧖🏿♂️ 🧖♀️ 🧖🏻♀️ 🧖🏼♀️ 🧖🏽♀️ 🧖🏾♀️ 🧖🏿♀️ 🧗 🧗🏻 🧗🏼 🧗🏽 🧗🏾 🧗🏿 🧗♂️ 🧗🏻♂️ 🧗🏼♂️ 🧗🏽♂️ 🧗🏾♂️ 🧗🏿♂️ 🧗♀️ 🧗🏻♀️ 🧗🏼♀️ 🧗🏽♀️ 🧗🏾♀️ 🧗🏿♀️ 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿 🏂🏻 🏂🏼 🏂🏽 🏂🏾 🏂🏿 🏌🏻 🏌🏼 🏌🏽 🏌🏾 🏌🏿 🏌🏻♂️ 🏌🏼♂️ 🏌🏽♂️ 🏌🏾♂️ 🏌🏿♂️ 🏌🏻♀️ 🏌🏼♀️ 🏌🏽♀️ 🏌🏾♀️ 🏌🏿♀️ 🧘 🧘🏻 🧘🏼 🧘🏽 🧘🏾 🧘🏿 🧘♂️ 🧘🏻♂️ 🧘🏼♂️ 🧘🏽♂️ 🧘🏾♂️ 🧘🏿♂️ 🧘♀️ 🧘🏻♀️ 🧘🏼♀️ 🧘🏽♀️ 🧘🏾♀️ 🧘🏿♀️ 🛌🏻 🛌🏼 🛌🏽 🛌🏾 🛌🏿 👩❤️💋👨 👩❤️👨 🦓 🦒 🦔 🦕 🦖 🦗 🥥 🥦 🥨 🥩 🥪 🥣 🥫 🥟 🥠 🥡 🥧 🥤 🥢 🛸 🛷 🥌 🧣 🧤 🧥 🧦 🧢 🇺🇳 🏴 🏴 🏴
+28 🥰 🥵 🥶 🥴 🥳 🥺 🦵 🦵🏻 🦵🏼 🦵🏽 🦵🏾 🦵🏿 🦶 🦶🏻 🦶🏼 🦶🏽 🦶🏾 🦶🏿 🦷 🦴 👨🦰 👨🏻🦰 👨🏼🦰 👨🏽🦰 👨🏾🦰 👨🏿🦰 👨🦱 👨🏻🦱 👨🏼🦱 👨🏽🦱 👨🏾🦱 👨🏿🦱 👨🦳 👨🏻🦳 👨🏼🦳 👨🏽🦳 👨🏾🦳 👨🏿🦳 👨🦲 👨🏻🦲 👨🏼🦲 👨🏽🦲 👨🏾🦲 👨🏿🦲 👩🦰 👩🏻🦰 👩🏼🦰 👩🏽🦰 👩🏾🦰 👩🏿🦰 👩🦱 👩🏻🦱 👩🏼🦱 👩🏽🦱 👩🏾🦱 👩🏿🦱 👩🦳 👩🏻🦳 👩🏼🦳 👩🏽🦳 👩🏾🦳 👩🏿🦳 👩🦲 👩🏻🦲 👩🏼🦲 👩🏽🦲 👩🏾🦲 👩🏿🦲 🦸 🦸🏻 🦸🏼 🦸🏽 🦸🏾 🦸🏿 🦸♂️ 🦸🏻♂️ 🦸🏼♂️ 🦸🏽♂️ 🦸🏾♂️ 🦸🏿♂️ 🦸♀️ 🦸🏻♀️ 🦸🏼♀️ 🦸🏽♀️ 🦸🏾♀️ 🦸🏿♀️ 🦹 🦹🏻 🦹🏼 🦹🏽 🦹🏾 🦹🏿 🦹♂️ 🦹🏻♂️ 🦹🏼♂️ 🦹🏽♂️ 🦹🏾♂️ 🦹🏿♂️ 🦹♀️ 🦹🏻♀️ 🦹🏼♀️ 🦹🏽♀️ 🦹🏾♀️ 🦹🏿♀️ 🦝 🦙 🦛 🦘 🦡 🦢 🦚 🦜 🦞 🦟 🦠 🥭 🥬 🥯 🧂 🥮 🧁 🧭 🧱 🛹 🧳 🧨 🧧 🥎 🥏 🥍 🧩 🧸 ♟️ 🧵 🧶 🥽 🥼 🥾 🥿 🧮 🧾 🧰 🧲 🧪 🧫 🧬 🧴 🧷 🧹 🧺 🧻 🧼 🧽 🧯 🧿 ♾️ 🏴☠️
+29 🥱 🤎 🤍 🤏 🤏🏻 🤏🏼 🤏🏽 🤏🏾 🤏🏿 🦾 🦿 🦻 🦻🏻 🦻🏼 🦻🏽 🦻🏾 🦻🏿 🧏 🧏🏻 🧏🏼 🧏🏽 🧏🏾 🧏🏿 🧏♂️ 🧏🏻♂️ 🧏🏼♂️ 🧏🏽♂️ 🧏🏾♂️ 🧏🏿♂️ 🧏♀️ 🧏🏻♀️ 🧏🏼♀️ 🧏🏽♀️ 🧏🏾♀️ 🧏🏿♀️ 🧍 🧍🏻 🧍🏼 🧍🏽 🧍🏾 🧍🏿 🧍♂️ 🧍🏻♂️ 🧍🏼♂️ 🧍🏽♂️ 🧍🏾♂️ 🧍🏿♂️ 🧍♀️ 🧍🏻♀️ 🧍🏼♀️ 🧍🏽♀️ 🧍🏾♀️ 🧍🏿♀️ 🧎 🧎🏻 🧎🏼 🧎🏽 🧎🏾 🧎🏿 🧎♂️ 🧎🏻♂️ 🧎🏼♂️ 🧎🏽♂️ 🧎🏾♂️ 🧎🏿♂️ 🧎♀️ 🧎🏻♀️ 🧎🏼♀️ 🧎🏽♀️ 🧎🏾♀️ 🧎🏿♀️ 👨🦯 👨🏻🦯 👨🏼🦯 👨🏽🦯 👨🏾🦯 👨🏿🦯 👩🦯 👩🏻🦯 👩🏼🦯 👩🏽🦯 👩🏾🦯 👩🏿🦯 👨🦼 👨🏻🦼 👨🏼🦼 👨🏽🦼 👨🏾🦼 👨🏿🦼 👩🦼 👩🏻🦼 👩🏼🦼 👩🏽🦼 👩🏾🦼 👩🏿🦼 👨🦽 👨🏻🦽 👨🏼🦽 👨🏽🦽 👨🏾🦽 👨🏿🦽 👩🦽 👩🏻🦽 👩🏼🦽 👩🏽🦽 👩🏾🦽 👩🏿🦽 🧑🤝🧑 🧑🏻🤝🧑🏻 🧑🏼🤝🧑🏻 🧑🏼🤝🧑🏼 🧑🏽🤝🧑🏻 🧑🏽🤝🧑🏼 🧑🏽🤝🧑🏽 🧑🏾🤝🧑🏻 🧑🏾🤝🧑🏼 🧑🏾🤝🧑🏽 🧑🏾🤝🧑🏾 🧑🏿🤝🧑🏻 🧑🏿🤝🧑🏼 🧑🏿🤝🧑🏽 🧑🏿🤝🧑🏾 🧑🏿🤝🧑🏿 👭🏻 👭🏼 👭🏽 👭🏾 👭🏿 👫🏻 👫🏼 👫🏽 👫🏾 👫🏿 👬🏻 👬🏼 👬🏽 👬🏾 👬🏿 🦧 🦮 🐕🦺 🦥 🦦 🦨 🦩 🦪 🧄 🧅 🧇 🧆 🧈 🧃 🧉 🧊 🛕 🦽 🦼 🛺 🪂 🪐 🤿 🪀 🪁 🦺 🥻 🩱 🩲 🩳 🩰 🪕 🪔 🪓 🦯 🩸 🩹 🩺 🪑 🪒 🟠 🟡 🟢 🟣 🟤 🟥 🟧 🟨 🟩 🟦 🟪 🟫 🇧🇱 🇧🇶 🇲🇶 🇷🇪 🇹🇫 🇽🇰
+30 🥲 🥸 🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿 🫀 🫁 🧑🦰 🧑🏻🦰 🧑🏼🦰 🧑🏽🦰 🧑🏾🦰 🧑🏿🦰 🧑🦱 🧑🏻🦱 🧑🏼🦱 🧑🏽🦱 🧑🏾🦱 🧑🏿🦱 🧑🦳 🧑🏻🦳 🧑🏼🦳 🧑🏽🦳 🧑🏾🦳 🧑🏿🦳 🧑🦲 🧑🏻🦲 🧑🏼🦲 🧑🏽🦲 🧑🏾🦲 🧑🏿🦲 🧑⚕️ 🧑🏻⚕️ 🧑🏼⚕️ 🧑🏽⚕️ 🧑🏾⚕️ 🧑🏿⚕️ 🧑🎓 🧑🏻🎓 🧑🏼🎓 🧑🏽🎓 🧑🏾🎓 🧑🏿🎓 🧑🏫 🧑🏻🏫 🧑🏼🏫 🧑🏽🏫 🧑🏾🏫 🧑🏿🏫 🧑⚖️ 🧑🏻⚖️ 🧑🏼⚖️ 🧑🏽⚖️ 🧑🏾⚖️ 🧑🏿⚖️ 🧑🌾 🧑🏻🌾 🧑🏼🌾 🧑🏽🌾 🧑🏾🌾 🧑🏿🌾 🧑🍳 🧑🏻🍳 🧑🏼🍳 🧑🏽🍳 🧑🏾🍳 🧑🏿🍳 🧑🔧 🧑🏻🔧 🧑🏼🔧 🧑🏽🔧 🧑🏾🔧 🧑🏿🔧 🧑🏭 🧑🏻🏭 🧑🏼🏭 🧑🏽🏭 🧑🏾🏭 🧑🏿🏭 🧑💼 🧑🏻💼 🧑🏼💼 🧑🏽💼 🧑🏾💼 🧑🏿💼 🧑🔬 🧑🏻🔬 🧑🏼🔬 🧑🏽🔬 🧑🏾🔬 🧑🏿🔬 🧑💻 🧑🏻💻 🧑🏼💻 🧑🏽💻 🧑🏾💻 🧑🏿💻 🧑🎤 🧑🏻🎤 🧑🏼🎤 🧑🏽🎤 🧑🏾🎤 🧑🏿🎤 🧑🎨 🧑🏻🎨 🧑🏼🎨 🧑🏽🎨 🧑🏾🎨 🧑🏿🎨 🧑✈️ 🧑🏻✈️ 🧑🏼✈️ 🧑🏽✈️ 🧑🏾✈️ 🧑🏿✈️ 🧑🚀 🧑🏻🚀 🧑🏼🚀 🧑🏽🚀 🧑🏾🚀 🧑🏿🚀 🧑🚒 🧑🏻🚒 🧑🏼🚒 🧑🏽🚒 🧑🏾🚒 🧑🏿🚒 🥷 🥷🏻 🥷🏼 🥷🏽 🥷🏾 🥷🏿 🤵♂️ 🤵🏻♂️ 🤵🏼♂️ 🤵🏽♂️ 🤵🏾♂️ 🤵🏿♂️ 🤵♀️ 🤵🏻♀️ 🤵🏼♀️ 🤵🏽♀️ 🤵🏾♀️ 🤵🏿♀️ 👰♂️ 👰🏻♂️ 👰🏼♂️ 👰🏽♂️ 👰🏾♂️ 👰🏿♂️ 👰♀️ 👰🏻♀️ 👰🏼♀️ 👰🏽♀️ 👰🏾♀️ 👰🏿♀️ 👩🍼 👩🏻🍼 👩🏼🍼 👩🏽🍼 👩🏾🍼 👩🏿🍼 👨🍼 👨🏻🍼 👨🏼🍼 👨🏽🍼 👨🏾🍼 👨🏿🍼 🧑🍼 🧑🏻🍼 🧑🏼🍼 🧑🏽🍼 🧑🏾🍼 🧑🏿🍼 🧑🎄 🧑🏻🎄 🧑🏼🎄 🧑🏽🎄 🧑🏾🎄 🧑🏿🎄 🧑🦯 🧑🏻🦯 🧑🏼🦯 🧑🏽🦯 🧑🏾🦯 🧑🏿🦯 🧑🦼 🧑🏻🦼 🧑🏼🦼 🧑🏽🦼 🧑🏾🦼 🧑🏿🦼 🧑🦽 🧑🏻🦽 🧑🏼🦽 🧑🏽🦽 🧑🏾🦽 🧑🏿🦽 🧑🏻🤝🧑🏼 🧑🏻🤝🧑🏽 🧑🏻🤝🧑🏾 🧑🏻🤝🧑🏿 🧑🏼🤝🧑🏽 🧑🏼🤝🧑🏾 🧑🏼🤝🧑🏿 🧑🏽🤝🧑🏾 🧑🏽🤝🧑🏿 🧑🏾🤝🧑🏿 🫂 🐈⬛ 🦬 🦣 🦫 🐻❄️ 🦤 🪶 🦭 🪲 🪳 🪰 🪱 🪴 🫐 🫒 🫑 🫓 🫔 🫕 🫖 🧋 🪨 🪵 🛖 🛻 🛼 🪄 🪅 🪆 🪡 🪢 🩴 🪖 🪗 🪘 🪙 🪃 🪚 🪛 🪝 🪜 🛗 🪞 🪟 🪠 🪤 🪣 🪥 🪦 🪧 ⚧️ 🏳️⚧️ 🇩🇬 🇪🇦 🇪🇭 🇫🇰 🇬🇫 🇬🇵 🇬🇸 🇲🇫 🇳🇨 🇵🇲 🇼🇫 🇾🇹
+31 😶🌫️ 😮💨 😵💫 ❤️🔥 ❤️🩹 🧔♂️ 🧔🏻♂️ 🧔🏼♂️ 🧔🏽♂️ 🧔🏾♂️ 🧔🏿♂️ 🧔♀️ 🧔🏻♀️ 🧔🏼♀️ 🧔🏽♀️ 🧔🏾♀️ 🧔🏿♀️ 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿 👩🏻❤️💋👨🏻 👩🏻❤️💋👨🏼 👩🏻❤️💋👨🏽 👩🏻❤️💋👨🏾 👩🏻❤️💋👨🏿 👩🏼❤️💋👨🏻 👩🏼❤️💋👨🏼 👩🏼❤️💋👨🏽 👩🏼❤️💋👨🏾 👩🏼❤️💋👨🏿 👩🏽❤️💋👨🏻 👩🏽❤️💋👨🏼 👩🏽❤️💋👨🏽 👩🏽❤️💋👨🏾 👩🏽❤️💋👨🏿 👩🏾❤️💋👨🏻 👩🏾❤️💋👨🏼 👩🏾❤️💋👨🏽 👩🏾❤️💋👨🏾 👩🏾❤️💋👨🏿 👩🏿❤️💋👨🏻 👩🏿❤️💋👨🏼 👩🏿❤️💋👨🏽 👩🏿❤️💋👨🏾 👩🏿❤️💋👨🏿 👨🏻❤️💋👨🏻 👨🏻❤️💋👨🏼 👨🏻❤️💋👨🏽 👨🏻❤️💋👨🏾 👨🏻❤️💋👨🏿 👨🏼❤️💋👨🏻 👨🏼❤️💋👨🏼 👨🏼❤️💋👨🏽 👨🏼❤️💋👨🏾 👨🏼❤️💋👨🏿 👨🏽❤️💋👨🏻 👨🏽❤️💋👨🏼 👨🏽❤️💋👨🏽 👨🏽❤️💋👨🏾 👨🏽❤️💋👨🏿 👨🏾❤️💋👨🏻 👨🏾❤️💋👨🏼 👨🏾❤️💋👨🏽 👨🏾❤️💋👨🏾 👨🏾❤️💋👨🏿 👨🏿❤️💋👨🏻 👨🏿❤️💋👨🏼 👨🏿❤️💋👨🏽 👨🏿❤️💋👨🏾 👨🏿❤️💋👨🏿 👩🏻❤️💋👩🏻 👩🏻❤️💋👩🏼 👩🏻❤️💋👩🏽 👩🏻❤️💋👩🏾 👩🏻❤️💋👩🏿 👩🏼❤️💋👩🏻 👩🏼❤️💋👩🏼 👩🏼❤️💋👩🏽 👩🏼❤️💋👩🏾 👩🏼❤️💋👩🏿 👩🏽❤️💋👩🏻 👩🏽❤️💋👩🏼 👩🏽❤️💋👩🏽 👩🏽❤️💋👩🏾 👩🏽❤️💋👩🏿 👩🏾❤️💋👩🏻 👩🏾❤️💋👩🏼 👩🏾❤️💋👩🏽 👩🏾❤️💋👩🏾 👩🏾❤️💋👩🏿 👩🏿❤️💋👩🏻 👩🏿❤️💋👩🏼 👩🏿❤️💋👩🏽 👩🏿❤️💋👩🏾 👩🏿❤️💋👩🏿 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿 👩🏻❤️👨🏻 👩🏻❤️👨🏼 👩🏻❤️👨🏽 👩🏻❤️👨🏾 👩🏻❤️👨🏿 👩🏼❤️👨🏻 👩🏼❤️👨🏼 👩🏼❤️👨🏽 👩🏼❤️👨🏾 👩🏼❤️👨🏿 👩🏽❤️👨🏻 👩🏽❤️👨🏼 👩🏽❤️👨🏽 👩🏽❤️👨🏾 👩🏽❤️👨🏿 👩🏾❤️👨🏻 👩🏾❤️👨🏼 👩🏾❤️👨🏽 👩🏾❤️👨🏾 👩🏾❤️👨🏿 👩🏿❤️👨🏻 👩🏿❤️👨🏼 👩🏿❤️👨🏽 👩🏿❤️👨🏾 👩🏿❤️👨🏿 👨🏻❤️👨🏻 👨🏻❤️👨🏼 👨🏻❤️👨🏽 👨🏻❤️👨🏾 👨🏻❤️👨🏿 👨🏼❤️👨🏻 👨🏼❤️👨🏼 👨🏼❤️👨🏽 👨🏼❤️👨🏾 👨🏼❤️👨🏿 👨🏽❤️👨🏻 👨🏽❤️👨🏼 👨🏽❤️👨🏽 👨🏽❤️👨🏾 👨🏽❤️👨🏿 👨🏾❤️👨🏻 👨🏾❤️👨🏼 👨🏾❤️👨🏽 👨🏾❤️👨🏾 👨🏾❤️👨🏿 👨🏿❤️👨🏻 👨🏿❤️👨🏼 👨🏿❤️👨🏽 👨🏿❤️👨🏾 👨🏿❤️👨🏿 👩🏻❤️👩🏻 👩🏻❤️👩🏼 👩🏻❤️👩🏽 👩🏻❤️👩🏾 👩🏻❤️👩🏿 👩🏼❤️👩🏻 👩🏼❤️👩🏼 👩🏼❤️👩🏽 👩🏼❤️👩🏾 👩🏼❤️👩🏿 👩🏽❤️👩🏻 👩🏽❤️👩🏼 👩🏽❤️👩🏽 👩🏽❤️👩🏾 👩🏽❤️👩🏿 👩🏾❤️👩🏻 👩🏾❤️👩🏼 👩🏾❤️👩🏽 👩🏾❤️👩🏾 👩🏾❤️👩🏿 👩🏿❤️👩🏻 👩🏿❤️👩🏼 👩🏿❤️👩🏽 👩🏿❤️👩🏾 👩🏿❤️👩🏿
+32 🫠 🫢 🫣 🫡 🫥 🫤 🥹 🫱 🫱🏻 🫱🏼 🫱🏽 🫱🏾 🫱🏿 🫲 🫲🏻 🫲🏼 🫲🏽 🫲🏾 🫲🏿 🫳 🫳🏻 🫳🏼 🫳🏽 🫳🏾 🫳🏿 🫴 🫴🏻 🫴🏼 🫴🏽 🫴🏾 🫴🏿 🫰 🫰🏻 🫰🏼 🫰🏽 🫰🏾 🫰🏿 🫵 🫵🏻 🫵🏼 🫵🏽 🫵🏾 🫵🏿 🫶 🫶🏻 🫶🏼 🫶🏽 🫶🏾 🫶🏿 🫦 🫅 🫅🏻 🫅🏼 🫅🏽 🫅🏾 🫅🏿 🫃 🫃🏻 🫃🏼 🫃🏽 🫃🏾 🫃🏿 🫄 🫄🏻 🫄🏼 🫄🏽 🫄🏾 🫄🏿 🧌 🪸 🪷 🪹 🪺 🫘 🫗 🫙 🛝 🛞 🛟 🪩 🪫 🩼 🩻 🫧 🪬 🪪 🟰
+33 🫨 🩷 🩵 🩶 🫷 🫷🏻 🫷🏼 🫷🏽 🫷🏾 🫷🏿 🫸 🫸🏻 🫸🏼 🫸🏽 🫸🏾 🫸🏿 🫎 🫏 🪽 🐦⬛ 🪿 🪼 🪻 🫚 🫛 🪭 🪮 🪇 🪈 🪯 🛜
+34 🙂↔️ 🙂↕️ 🚶➡️ 🚶🏻➡️ 🚶🏼➡️ 🚶🏽➡️ 🚶🏾➡️ 🚶🏿➡️ 🚶♀️➡️ 🚶🏻♀️➡️ 🚶🏼♀️➡️ 🚶🏽♀️➡️ 🚶🏾♀️➡️ 🚶🏿♀️➡️ 🚶♂️➡️ 🚶🏻♂️➡️ 🚶🏼♂️➡️ 🚶🏽♂️➡️ 🚶🏾♂️➡️ 🚶🏿♂️➡️ 🧎➡️ 🧎🏻➡️ 🧎🏼➡️ 🧎🏽➡️ 🧎🏾➡️ 🧎🏿➡️ 🧎♀️➡️ 🧎🏻♀️➡️ 🧎🏼♀️➡️ 🧎🏽♀️➡️ 🧎🏾♀️➡️ 🧎🏿♀️➡️ 🧎♂️➡️ 🧎🏻♂️➡️ 🧎🏼♂️➡️ 🧎🏽♂️➡️ 🧎🏾♂️➡️ 🧎🏿♂️➡️ 🧑🦯➡️ 🧑🏻🦯➡️ 🧑🏼🦯➡️ 🧑🏽🦯➡️ 🧑🏾🦯➡️ 🧑🏿🦯➡️ 👨🦯➡️ 👨🏻🦯➡️ 👨🏼🦯➡️ 👨🏽🦯➡️ 👨🏾🦯➡️ 👨🏿🦯➡️ 👩🦯➡️ 👩🏻🦯➡️ 👩🏼🦯➡️ 👩🏽🦯➡️ 👩🏾🦯➡️ 👩🏿🦯➡️ 🧑🦼➡️ 🧑🏻🦼➡️ 🧑🏼🦼➡️ 🧑🏽🦼➡️ 🧑🏾🦼➡️ 🧑🏿🦼➡️ 👨🦼➡️ 👨🏻🦼➡️ 👨🏼🦼➡️ 👨🏽🦼➡️ 👨🏾🦼➡️ 👨🏿🦼➡️ 👩🦼➡️ 👩🏻🦼➡️ 👩🏼🦼➡️ 👩🏽🦼➡️ 👩🏾🦼➡️ 👩🏿🦼➡️ 🧑🦽➡️ 🧑🏻🦽➡️ 🧑🏼🦽➡️ 🧑🏽🦽➡️ 🧑🏾🦽➡️ 🧑🏿🦽➡️ 👨🦽➡️ 👨🏻🦽➡️ 👨🏼🦽➡️ 👨🏽🦽➡️ 👨🏾🦽➡️ 👨🏿🦽➡️ 👩🦽➡️ 👩🏻🦽➡️ 👩🏼🦽➡️ 👩🏽🦽➡️ 👩🏾🦽➡️ 👩🏿🦽➡️ 🏃➡️ 🏃🏻➡️ 🏃🏼➡️ 🏃🏽➡️ 🏃🏾➡️ 🏃🏿➡️ 🏃♀️➡️ 🏃🏻♀️➡️ 🏃🏼♀️➡️ 🏃🏽♀️➡️ 🏃🏾♀️➡️ 🏃🏿♀️➡️ 🏃♂️➡️ 🏃🏻♂️➡️ 🏃🏼♂️➡️ 🏃🏽♂️➡️ 🏃🏾♂️➡️ 🏃🏿♂️➡️ 🧑🧑🧒 🧑🧑🧒🧒 🧑🧒 🧑🧒🧒 🐦🔥 🍋🟩 🍄🟫 ⛓️💥
+35 🇨🇶
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/clipboard_bottom/clip_bottom_row.json b/app/src/main/assets/layouts/clipboard_bottom/clip_bottom_row.json
new file mode 100644
index 0000000000..2a7c8f6b17
--- /dev/null
+++ b/app/src/main/assets/layouts/clipboard_bottom/clip_bottom_row.json
@@ -0,0 +1,7 @@
+[
+ [
+ { "label": "alpha", "width": 0.15 },
+ { "label": "space", "width": -1 },
+ { "label": "delete", "width": 0.15 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/clipboard_bottom/clip_bottom_row_with_action.json b/app/src/main/assets/layouts/clipboard_bottom/clip_bottom_row_with_action.json
new file mode 100644
index 0000000000..4827b365d4
--- /dev/null
+++ b/app/src/main/assets/layouts/clipboard_bottom/clip_bottom_row_with_action.json
@@ -0,0 +1,8 @@
+[
+ [
+ { "label": "alpha", "width": 0.15 },
+ { "label": "space", "width": -1 },
+ { "label": "delete", "width": 0.15 },
+ { "label": "action", "width": 0.15 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json
new file mode 100644
index 0000000000..2a7c8f6b17
--- /dev/null
+++ b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row.json
@@ -0,0 +1,7 @@
+[
+ [
+ { "label": "alpha", "width": 0.15 },
+ { "label": "space", "width": -1 },
+ { "label": "delete", "width": 0.15 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json
new file mode 100644
index 0000000000..4827b365d4
--- /dev/null
+++ b/app/src/main/assets/layouts/emoji_bottom/emoji_bottom_row_with_action.json
@@ -0,0 +1,8 @@
+[
+ [
+ { "label": "alpha", "width": 0.15 },
+ { "label": "space", "width": -1 },
+ { "label": "delete", "width": 0.15 },
+ { "label": "action", "width": 0.15 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/functional/functional_keys.json b/app/src/main/assets/layouts/functional/functional_keys.json
new file mode 100644
index 0000000000..6d28dd24fc
--- /dev/null
+++ b/app/src/main/assets/layouts/functional/functional_keys.json
@@ -0,0 +1,21 @@
+[
+ [
+ { "label": "shift", "width": 0.15 },
+ { "type": "placeholder" },
+ { "label": "delete", "width": 0.15 }
+ ],
+ [
+ { "label": "symbol_alpha", "width": 0.15 },
+ { "$": "variation_selector",
+ "default": { "label": "comma" },
+ "email": { "label": "@", "groupId": 1, "type": "function" },
+ "uri": { "label": "/", "groupId": 1, "type": "function" }
+ },
+ { "$": "keyboard_state_selector", "languageKeyEnabled": { "$": "keyboard_state_selector", "alphabet": { "label": "language_switch" }}},
+ { "$": "keyboard_state_selector", "emojiKeyEnabled": { "$": "keyboard_state_selector", "alphabet": { "label": "emoji" }}},
+ { "$": "keyboard_state_selector", "symbols": { "label": "numpad" }},
+ { "label": "space" },
+ { "label": "period", "labelFlags": 1073741824 },
+ { "label": "action", "width": 0.15 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/functional/functional_keys_tablet.json b/app/src/main/assets/layouts/functional/functional_keys_tablet.json
new file mode 100644
index 0000000000..606f64eaeb
--- /dev/null
+++ b/app/src/main/assets/layouts/functional/functional_keys_tablet.json
@@ -0,0 +1,35 @@
+[
+ [
+ { "type": "placeholder" },
+ { "label": "delete", "width": 0.1 }
+ ],
+ [
+ { "type": "placeholder" }
+ ],
+ [
+ { "type": "placeholder" },
+ { "label": "action", "width": 0.1 }
+ ],
+ [
+ { "label": "shift", "width": 0.1 },
+ { "type": "placeholder" },
+ { "label": "shift" }
+ ],
+ [
+ { "label": "symbol_alpha" },
+ { "$": "variation_selector",
+ "default": { "label": "comma" },
+ "email": { "label": "@", "groupId": 1, "type": "function" },
+ "uri": { "label": "/", "groupId": 1, "type": "function" }
+ },
+ { "$": "keyboard_state_selector", "languageKeyEnabled": { "$": "keyboard_state_selector", "alphabet": { "label": "language_switch" }}},
+ { "$": "keyboard_state_selector", "symbols": { "label": "numpad" }},
+ { "label": "space" },
+ { "label": "period" },
+ { "$": "variation_selector",
+ "default": { "label": "emoji" },
+ "email": { "label": "com" },
+ "uri": { "label": "com" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/akan.txt b/app/src/main/assets/layouts/main/akan.txt
new file mode 100644
index 0000000000..1f139b582f
--- /dev/null
+++ b/app/src/main/assets/layouts/main/akan.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/arabic.txt b/app/src/main/assets/layouts/main/arabic.txt
new file mode 100644
index 0000000000..0bbfbb1170
--- /dev/null
+++ b/app/src/main/assets/layouts/main/arabic.txt
@@ -0,0 +1,34 @@
+ض
+ص
+ث
+ق
+ف
+غ
+ع
+ه
+خ
+ح
+ج
+
+ش
+س
+ي
+ب
+ل
+ا
+ت
+ن
+م
+ك
+ط
+
+ذ
+ء
+ؤ
+ر
+ى
+ة
+و
+ز
+ظ
+د
diff --git a/app/src/main/assets/layouts/main/arabic_hijai.txt b/app/src/main/assets/layouts/main/arabic_hijai.txt
new file mode 100644
index 0000000000..acc3a007d8
--- /dev/null
+++ b/app/src/main/assets/layouts/main/arabic_hijai.txt
@@ -0,0 +1,34 @@
+ز
+ر
+ذ
+د
+خ
+ح
+ج
+ث
+ت
+ب
+ا
+
+ك
+ق
+ف
+غ
+ع
+ظ
+ط
+ض
+ص
+ش
+س
+
+ء
+ى
+ي
+ؤ
+و
+ة
+ﻩ
+ن
+م
+ل
diff --git a/app/src/main/assets/layouts/main/arabic_pc.txt b/app/src/main/assets/layouts/main/arabic_pc.txt
new file mode 100644
index 0000000000..c2d9f9d25c
--- /dev/null
+++ b/app/src/main/assets/layouts/main/arabic_pc.txt
@@ -0,0 +1,31 @@
+ض
+ص
+ق
+ف
+غ
+ع
+ه
+خ
+ح
+ج
+
+ش
+س
+ي
+ب
+ل
+ا
+ت
+ن
+م
+ك
+
+ظ
+ط
+ذ
+د
+ز
+ر
+و
+ة
+ث
diff --git a/app/src/main/assets/layouts/main/armenian_phonetic.txt b/app/src/main/assets/layouts/main/armenian_phonetic.txt
new file mode 100644
index 0000000000..d59ce9d4d5
--- /dev/null
+++ b/app/src/main/assets/layouts/main/armenian_phonetic.txt
@@ -0,0 +1,41 @@
+է
+թ
+փ
+ձ
+ջ
+ր
+չ
+ճ
+ժ
+ծ
+
+ք
+ո
+ե և
+ռ
+տ
+ը
+ւ
+ի
+օ
+պ
+
+ա
+ս
+դ $$$
+ֆ
+գ
+հ
+յ
+կ
+լ
+խ
+
+զ
+ղ
+ց
+վ
+բ
+ն
+մ
+շ
diff --git a/app/src/main/assets/layouts/main/azerty.json b/app/src/main/assets/layouts/main/azerty.json
new file mode 100644
index 0000000000..92b5a95b23
--- /dev/null
+++ b/app/src/main/assets/layouts/main/azerty.json
@@ -0,0 +1,38 @@
+[
+ [
+ { "label": "a" },
+ { "label": "z" },
+ { "label": "e" },
+ { "label": "r" },
+ { "label": "t" },
+ { "label": "y" },
+ { "label": "u" },
+ { "label": "i" },
+ { "label": "o" },
+ { "label": "p" }
+ ],
+ [
+ { "label": "q" },
+ { "label": "s" },
+ { "label": "d" },
+ { "label": "f" },
+ { "label": "g" },
+ { "label": "h" },
+ { "label": "j" },
+ { "label": "k" },
+ { "label": "l" },
+ { "label": "m" }
+ ],
+ [
+ { "label": "w" },
+ { "label": "x" },
+ { "label": "c" },
+ { "label": "v" },
+ { "label": "b" },
+ { "label": "n" },
+ { "$": "shift_state_selector",
+ "shiftedManual": { "label": "?" },
+ "default": { "label": "'" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/belarusian.txt b/app/src/main/assets/layouts/main/belarusian.txt
new file mode 100644
index 0000000000..44408c5605
--- /dev/null
+++ b/app/src/main/assets/layouts/main/belarusian.txt
@@ -0,0 +1,33 @@
+й
+ц
+у
+к
+е
+н
+г
+ш
+ў
+з
+х
+
+ф
+ы
+в
+а
+п
+р
+о
+л
+д
+ж
+э
+
+я
+ч
+с
+м
+і
+т
+ь
+б <
+ю >
diff --git a/app/src/main/assets/layouts/main/bemba.txt b/app/src/main/assets/layouts/main/bemba.txt
new file mode 100644
index 0000000000..5253b2885a
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bemba.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/bengali_akkhor.json b/app/src/main/assets/layouts/main/bengali_akkhor.json
new file mode 100644
index 0000000000..4b6a6f07d9
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bengali_akkhor.json
@@ -0,0 +1,132 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ" },
+ "default": { "label": "ধ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঠ" },
+ "default": { "label": "থ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৈ" },
+ "default": { "label": "ে" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ড়" },
+ "default": { "label": "র" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ট" },
+ "default": { "label": "ত" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঞ" },
+ "default": { "label": "য়" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ূ" },
+ "default": { "label": "ু" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ী" },
+ "default": { "label": "ি" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৌ" },
+ "default": { "label": "ো" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ফ" },
+ "default": { "label": "প" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঋ" },
+ "default": { "label": "আ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "অ" },
+ "default": { "label": "া" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "শ" },
+ "default": { "label": "স" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ড" },
+ "default": { "label": "দ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ়" },
+ "default": { "label": "ৃ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঘ" },
+ "default": { "label": "গ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "হ" },
+ "default": { "label": "্" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঝ" },
+ "default": { "label": "জ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "খ" },
+ "default": { "label": "ক" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৎ" },
+ "default": { "label": "ল" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঈ" },
+ "default": { "label": "ই" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঊ" },
+ "default": { "label": "উ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "।", "popup": { "main": { "label": "॥" } }, "labelFlags": 1073741824 },
+ "default": { "label": "য" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঙ" },
+ "default": { "label": "ষ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ছ" },
+ "default": { "label": "চ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঃ" },
+ "default": { "label": "ভ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঁ" },
+ "default": { "label": "ব" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ণ" },
+ "default": { "label": "ন" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ং" },
+ "default": { "label": "ম" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঐ" },
+ "default": { "label": "এ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঔ" },
+ "default": { "label": "ও" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/bengali_baishakhi.json b/app/src/main/assets/layouts/main/bengali_baishakhi.json
new file mode 100644
index 0000000000..6b6ee44872
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bengali_baishakhi.json
@@ -0,0 +1,127 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ", "labelFlags": 1073741824 },
+ "default": { "label": "ড", "popup": { "relevant": [{ "label": "ঢ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ূ", "labelFlags": 1073741824 },
+ "default": { "label": "ী", "popup": { "relevant": [{ "label": "ূ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "এ", "labelFlags": 1073741824 },
+ "default": { "label": "ে", "popup": { "relevant": [{ "label": "ঐ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৃ", "labelFlags": 1073741824 },
+ "default": { "label": "র", "popup": { "main": { "label": "ঋ" }, "relevant": [{ "label": "র্য" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঠ", "labelFlags": 1073741824 },
+ "default": { "label": "ট", "popup": { "relevant": [{ "label": "ঠ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "য়", "labelFlags": 1073741824 },
+ "default": { "label": "য", "popup": { "relevant": [{ "label": "য়" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "উ", "labelFlags": 1073741824 },
+ "default": { "label": "ু", "popup": { "relevant": [{ "label": "ঊ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ই", "labelFlags": 1073741824 },
+ "default": { "label": "ি", "popup": { "relevant": [{ "label": "ঈ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ও", "labelFlags": 1073741824 },
+ "default": { "label": "ো", "popup": { "relevant": [{ "label": "ঔ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ফ", "labelFlags": 1073741824 },
+ "default": { "label": "প", "popup": { "relevant": [{ "label": "ফ" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "অ", "labelFlags": 1073741824 },
+ "default": { "label": "া", "popup": { "relevant": [{ "label": "আ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "শ", "labelFlags": 1073741824 },
+ "default": { "label": "স", "popup": { "relevant": [{ "label": "ষ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ধ", "labelFlags": 1073741824 },
+ "default": { "label": "দ", "popup": { "relevant": [{ "label": "ধ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "থ", "labelFlags": 1073741824 },
+ "default": { "label": "ত", "popup": { "main": { "label": "থ" }, "relevant": [{ "label": "ৎ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঘ", "labelFlags": 1073741824 },
+ "default": { "label": "গ", "popup": { "relevant": [{ "label": "ঘ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "হ", "labelFlags": 1073741824 },
+ "default": { "label": "্", "popup": { "relevant": [{ "label": "ঃ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঝ", "labelFlags": 1073741824 },
+ "default": { "label": "জ", "popup": { "relevant": [{ "label": "ঝ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "খ", "labelFlags": 1073741824 },
+ "default": { "label": "ক", "popup": { "relevant": [{ "label": "খ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ং", "labelFlags": 1073741824 },
+ "default": { "label": "ল", "popup": { "relevant": [{ "label": "ং" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৌ", "labelFlags": 1073741824 },
+ "default": { "label": "ৈ", "popup": { "relevant": [{ "label": "ৌ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ়", "labelFlags": 1073741824 },
+ "default": { "label": "ড়", "popup": { "relevant": [{ "label": "ঢ়" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ছ", "labelFlags": 1073741824 },
+ "default": { "label": "চ", "popup": { "relevant": [{ "label": "ছ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঁ", "labelFlags": 1073741824, "popup": { "relevant": [
+ { "label": "!autoColumnOrder!6" },
+ { "label": "়" },
+ { "label": "ৄ" },
+ { "label": "ঽ" },
+ { "label": "ৢ" },
+ { "label": "ৱ" },
+ { "label": "ৣ" },
+ { "label": "ৗ" },
+ { "label": "ৠ" },
+ { "label": "৺" },
+ { "label": "ঌ" },
+ { "label": "ৰ" },
+ { "label": "ৡ"}
+ ]}},
+ "default": { "label": "ঞ", "popup": { "relevant": [{ "label": "ঁ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ভ", "labelFlags": 1073741824 },
+ "default": { "label": "ব", "popup": { "relevant": [{ "label": "ভ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ণ", "labelFlags": 1073741824 },
+ "default": { "label": "ন", "popup": { "relevant": [{ "label": "ণ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঙ", "labelFlags": 1073741824 },
+ "default": { "label": "ম", "popup": { "relevant": [{ "label": "ঁ" }]}}
+ }
+ ]
+ ]
+
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/bengali_inscript.json b/app/src/main/assets/layouts/main/bengali_inscript.json
new file mode 100644
index 0000000000..3e03525e80
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bengali_inscript.json
@@ -0,0 +1,142 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঔ", "labelFlags": 1073741824 },
+ "default": { "label": "ৌ", "popup": { "relevant": [{"label": "ঔ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঐ", "labelFlags": 1073741824 },
+ "default": { "label": "ৈ", "popup": { "relevant": [{"label": "ঐ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "আ", "labelFlags": 1073741824 },
+ "default": { "label": "া", "popup": { "relevant": [{"label": "আ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঈ", "labelFlags": 1073741824 },
+ "default": { "label": "ী", "popup": { "relevant": [{"label": "ঈ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঊ", "labelFlags": 1073741824 },
+ "default": { "label": "ূ", "popup": { "relevant": [{"label": "ঊ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ভ", "labelFlags": 1073741824 },
+ "default": { "label": "ব", "popup": { "relevant": [{"label": "ভ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঙ", "labelFlags": 1073741824 },
+ "default": { "label": "হ", "popup": { "relevant": [{"label": "ঙ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঘ", "labelFlags": 1073741824 },
+ "default": { "label": "গ", "popup": { "relevant": [{"label": "ঘ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ধ", "labelFlags": 1073741824 },
+ "default": { "label": "দ", "popup": { "relevant": [{"label": "ধ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঝ", "labelFlags": 1073741824 },
+ "default": { "label": "জ", "popup": { "relevant": [{"label": "ঝ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ", "labelFlags": 1073741824 },
+ "default": { "label": "ড", "popup": { "relevant": [{"label": "ঢ" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ও", "labelFlags": 1073741824 },
+ "default": { "label": "ো", "popup": { "relevant": [{"label": "ও" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "এ", "labelFlags": 1073741824 },
+ "default": { "label": "ে", "popup": { "relevant": [{"label": "এ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "অ", "labelFlags": 1073741824 },
+ "default": { "label": "্", "popup": { "relevant": [{"label": "অ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ই", "labelFlags": 1073741824 },
+ "default": { "label": "ি", "popup": { "relevant": [{"label": "ই" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "উ", "labelFlags": 1073741824 },
+ "default": { "label": "ু", "popup": { "relevant": [{"label": "উ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ফ", "labelFlags": 1073741824 },
+ "default": { "label": "প", "popup": { "relevant": [{"label": "ফ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ড়", "labelFlags": 1073741824 },
+ "default": { "label": "র", "popup": { "main": { "label": "ড়" }, "relevant": [{ "label": "র্য" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "খ", "labelFlags": 1073741824 },
+ "default": { "label": "ক", "popup": { "relevant": [{"label": "খ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "থ", "labelFlags": 1073741824 },
+ "default": { "label": "ত", "popup": { "main": { "label": "থ" }, "relevant": [{ "label": "ৎ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ছ", "labelFlags": 1073741824 },
+ "default": { "label": "চ", "popup": { "relevant": [{"label": "ছ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঠ", "labelFlags": 1073741824 },
+ "default": { "label": "ট", "popup": { "relevant": [{"label": "ঠ" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঋ", "labelFlags": 1073741824 },
+ "default": { "label": "ৃ", "popup": { "relevant": [{"label": "ঋ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঁ", "labelFlags": 1073741824, "popup": { "relevant": [
+ {"label": "!autoColumnOrder!6" },
+ { "label": "়" },
+ { "label": "ৄ" },
+ { "label": "ঽ" },
+ { "label": "ৢ" },
+ { "label": "ৱ" },
+ { "label": "ৣ" },
+ { "label": "ৗ" },
+ { "label": "ৠ" },
+ { "label": "৺" },
+ { "label": "ঌ" },
+ { "label": "ৰ" },
+ { "label": "ৡ" }
+ ]}},
+ "default": { "label": "ং", "popup": { "main": { "label": "ঁ" }, "relevant": [{ "label": "ঃ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ণ", "labelFlags": 1073741824 },
+ "default": { "label": "ম", "popup": { "relevant": [{"label": "ণ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঞ", "labelFlags": 1073741824 },
+ "default": { "label": "ন", "popup": { "relevant": [{"label": "ঞ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ়", "labelFlags": 1073741824 },
+ "default": { "label": "ব", "popup": { "relevant": [{"label": "ঢ়" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ষ", "labelFlags": 1073741824 },
+ "default": { "label": "ল", "popup": { "relevant": [{"label": "ষ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "শ", "labelFlags": 1073741824 },
+ "default": { "label": "স", "popup": { "relevant": [{"label": "শ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "য", "labelFlags": 1073741824 },
+ "default": { "label": "য়", "popup": { "relevant": [{"label": "য" }]}}
+ }
+ ]
+]
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/bengali_probhat.json b/app/src/main/assets/layouts/main/bengali_probhat.json
new file mode 100644
index 0000000000..fc147f0e68
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bengali_probhat.json
@@ -0,0 +1,141 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ধ", "labelFlags": 1073741824 },
+ "default": { "label": "দ", "popup": { "relevant": [{ "label": "ধ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঊ", "labelFlags": 1073741824 },
+ "default": { "label": "ূ", "popup": { "relevant": [{ "label": "ঊ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঈ", "labelFlags": 1073741824 },
+ "default": { "label": "ী", "popup": { "relevant": [{ "label": "ঈ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ড়", "labelFlags": 1073741824 },
+ "default": { "label": "র", "popup": { "main": { "label": "ড়" }, "relevant": [{ "label": "র্য" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঠ", "labelFlags": 1073741824 },
+ "default": { "label": "ট", "popup": { "relevant": [{ "label": "ঠ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঐ", "labelFlags": 1073741824 },
+ "default": { "label": "এ", "popup": { "relevant": [{ "label": "ঐ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "উ", "labelFlags": 1073741824 },
+ "default": { "label": "ু", "popup": { "relevant": [{ "label": "উ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ই", "labelFlags": 1073741824 },
+ "default": { "label": "ি", "popup": { "relevant": [{ "label": "ই" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঔ", "labelFlags": 1073741824 },
+ "default": { "label": "ও", "popup": { "relevant": [{ "label": "ঔ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ফ", "labelFlags": 1073741824 },
+ "default": { "label": "প", "popup": { "relevant": [{ "label": "ফ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৈ", "labelFlags": 1073741824 },
+ "default": { "label": "ে", "popup": { "relevant": [{ "label": "ৈ" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "অ", "labelFlags": 1073741824 },
+ "default": { "label": "া", "popup": { "relevant": [{ "label": "অ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ষ", "labelFlags": 1073741824 },
+ "default": { "label": "স", "popup": { "relevant": [{ "label": "ষ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ", "labelFlags": 1073741824 },
+ "default": { "label": "ড", "popup": { "relevant": [{ "label": "ঢ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "থ", "labelFlags": 1073741824 },
+ "default": { "label": "ত", "popup": { "main": { "label": "থ" }, "relevant": [{ "label": "ৎ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঘ", "labelFlags": 1073741824 },
+ "default": { "label": "গ", "popup": { "relevant": [{ "label": "ঘ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঃ", "labelFlags": 1073741824 },
+ "default": { "label": "হ", "popup": { "relevant": [{ "label": "ঃ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঝ", "labelFlags": 1073741824 },
+ "default": { "label": "জ", "popup": { "relevant": [{ "label": "ঝ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "খ", "labelFlags": 1073741824 },
+ "default": { "label": "ক", "popup": { "relevant": [{ "label": "খ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ং", "labelFlags": 1073741824 },
+ "default": { "label": "ল", "popup": { "relevant": [{ "label": "ং" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৌ", "labelFlags": 1073741824 },
+ "default": { "label": "ো", "popup": { "relevant": [{ "label": "ৌ" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "য", "labelFlags": 1073741824 },
+ "default": { "label": "য়", "popup": { "main": { "label": "য" }, "relevant": [{ "label": "্য" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ়", "labelFlags": 1073741824 },
+ "default": { "label": "শ", "popup": { "relevant": [{ "label": "ঢ়" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ছ", "labelFlags": 1073741824 },
+ "default": { "label": "চ", "popup": { "relevant": [{ "label": "ছ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঋ", "labelFlags": 1073741824 },
+ "default": { "label": "আ", "popup": { "relevant": [{ "label": "ঋ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ভ", "labelFlags": 1073741824 },
+ "default": { "label": "ব", "popup": { "relevant": [{ "label": "ভ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ণ", "labelFlags": 1073741824 },
+ "default": { "label": "ন", "popup": { "relevant": [{ "label": "ণ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঙ", "labelFlags": 1073741824 },
+ "default": { "label": "ম", "popup": { "relevant": [{ "label": "ঙ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৃ", "labelFlags": 1073741824 },
+ "default": { "label": "ঞ", "popup": { "relevant": [{ "label": "ৃ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঁ", "popup": { "relevant": [
+ { "label": "!autoColumnOrder!6"},
+ { "label": "়" },
+ { "label": "ৄ"},
+ { "label": "ঽ"},
+ { "label": "ৢ"},
+ { "label": "ৱ"},
+ { "label": "ৣ"},
+ { "label": "ৗ"},
+ { "label": "ৠ"},
+ { "label": "৺"},
+ { "label": "ঌ"},
+ { "label": "ৰ"},
+ { "label": "ৡ" }]}},
+ "default": { "label": "্", "popup": { "relevant": [{ "label": "ঁ" }]}}
+ }
+ ]
+]
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/bengali_unijoy.json b/app/src/main/assets/layouts/main/bengali_unijoy.json
new file mode 100644
index 0000000000..929f5d93e3
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bengali_unijoy.json
@@ -0,0 +1,126 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ং", "labelFlags": 1073741824 },
+ "default": { "label": "ঙ", "popup": { "relevant": [{ "label": "ং" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "য়", "labelFlags": 1073741824 },
+ "default": { "label": "য", "popup": { "relevant": [{ "label": "য়" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ", "labelFlags": 1073741824 },
+ "default": { "label": "ড", "popup": { "relevant": [{ "label": "ঢ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ফ", "labelFlags": 1073741824 },
+ "default": { "label": "প", "popup": { "relevant": [{ "label": "ফ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঠ", "labelFlags": 1073741824 },
+ "default": { "label": "ট", "popup": { "relevant": [{ "label": "ঠ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ছ", "labelFlags": 1073741824 },
+ "default": { "label": "চ", "popup": { "relevant": [{ "label": "ছ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঝ", "labelFlags": 1073741824 },
+ "default": { "label": "জ", "popup": { "relevant": [{ "label": "ঝ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঞ", "labelFlags": 1073741824 },
+ "default": { "label": "হ", "popup": { "relevant": [{ "label": "ঞ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঘ", "labelFlags": 1073741824 },
+ "default": { "label": "গ", "popup": { "relevant": [{ "label": "ঘ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঢ়", "labelFlags": 1073741824 },
+ "default": { "label": "ড়", "popup": { "relevant": [{ "label": "ঢ়" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঃ", "labelFlags": 1073741824 },
+ "default": { "label": "ৃ", "popup": { "relevant": [{ "label": "ঋ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ূ", "popup": { "relevant": [{ "label": "ঊ" }]}},
+ "default": { "label": "ু", "popup": { "relevant": [{ "label": "উ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ী", "popup": { "relevant": [{ "label": "ঈ"}]}},
+ "default": { "label": "ি", "popup": { "relevant": [{ "label": "ই" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "অ", "labelFlags": 1073741824 },
+ "default": { "label": "া", "popup": { "main": { "label": "আ" }, "relevant": [{ "label": "অ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ঁ", "labelFlags": 1073741824, "popup": { "relevant": [
+ { "label": "!autoColumnOrder!6" },
+ { "label": "়" },
+ { "label": "ৄ" },
+ { "label": "ঽ" },
+ { "label": "ৢ" },
+ { "label": "ৱ" },
+ { "label": "ৣ" },
+ { "label": "ৗ" },
+ { "label": "ৠ" },
+ { "label": "৺" },
+ { "label": "ঌ" },
+ { "label": "ৰ" },
+ { "label": "ৡ"}
+ ]}},
+ "default": { "label": "্", "popup": { "relevant": [{ "label": "ঁ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ভ", "labelFlags": 1073741824 },
+ "default": { "label": "ব", "popup": { "relevant": [{ "label": "ভ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "খ", "labelFlags": 1073741824 },
+ "default": { "label": "ক", "popup": { "relevant": [{ "label": "খ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "থ", "labelFlags": 1073741824 },
+ "default": { "label": "ত", "popup": { "main": { "label": "থ" }, "relevant": [{ "label": "ৎ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ধ", "labelFlags": 1073741824 },
+ "default": { "label": "দ", "popup": { "relevant": [{ "label": "ধ" }]}}
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "্য", "labelFlags": 1073741824 },
+ "default": { "label": "্র", "popup": { "relevant": [{ "label": "্য" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৌ", "popup": { "relevant": [{ "label": "ঔ" }]}},
+ "default": { "label": "ো", "popup": { "relevant": [{ "label": "ও" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ৈ", "popup": { "relevant": [{ "label": "ঐ" }]}},
+ "default": { "label": "ে", "popup": { "relevant": [{ "label": "এ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ল", "labelFlags": 1073741824 },
+ "default": { "label": "র", "popup": { "main": { "label": "ল" }, "relevant": [{ "label": "র্য" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ণ", "labelFlags": 1073741824 },
+ "default": { "label": "ন", "popup": { "relevant": [{ "label": "ণ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ষ", "labelFlags": 1073741824 },
+ "default": { "label": "স", "popup": { "relevant": [{ "label": "ষ" }]}}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "শ", "labelFlags": 1073741824 },
+ "default": { "label": "ম", "popup": { "relevant": [{ "label": "শ" }]}}
+ }
+ ]
+]
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/bepo.txt b/app/src/main/assets/layouts/main/bepo.txt
new file mode 100644
index 0000000000..de5b027f59
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bepo.txt
@@ -0,0 +1,29 @@
+b
+é è
+p
+o
+v
+d
+l
+j
+z
+w
+
+a
+u
+i
+e
+c
+t
+s
+r
+n
+m
+
+y
+x
+k
+q
+g
+h
+f
diff --git a/app/src/main/assets/layouts/main/bulgarian.txt b/app/src/main/assets/layouts/main/bulgarian.txt
new file mode 100644
index 0000000000..b7486cd852
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bulgarian.txt
@@ -0,0 +1,32 @@
+я
+в
+е
+р
+т
+ъ
+у
+и ѝ
+о
+п
+ч
+
+а
+с
+д
+ф
+г
+х
+й
+к
+л
+ш
+щ
+
+з
+ь
+ц
+ж
+б
+н
+м
+ю
diff --git a/app/src/main/assets/layouts/main/bulgarian_bds.txt b/app/src/main/assets/layouts/main/bulgarian_bds.txt
new file mode 100644
index 0000000000..88af6f47ce
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bulgarian_bds.txt
@@ -0,0 +1,33 @@
+у
+е
+и ѝ
+ш
+щ
+к
+с
+д
+з
+ц
+б
+
+ь
+я
+а
+о
+ж
+г
+т
+н
+в
+м
+ч
+
+ю
+й
+ъ
+э
+ф
+х
+п
+р
+л
diff --git a/app/src/main/assets/layouts/main/bulgarian_bekl.txt b/app/src/main/assets/layouts/main/bulgarian_bekl.txt
new file mode 100644
index 0000000000..885fc533fd
--- /dev/null
+++ b/app/src/main/assets/layouts/main/bulgarian_bekl.txt
@@ -0,0 +1,33 @@
+у
+е
+и ѝ
+ш
+щ
+к
+с
+д
+з
+ц
+б
+
+ь
+я
+а
+о
+ж
+г
+т
+н
+в
+м
+ч
+
+ю
+й ѭ
+ъ ѫ
+ѣ
+ф
+х
+п
+р
+л
diff --git a/app/src/main/assets/layouts/main/central_kurdish.txt b/app/src/main/assets/layouts/main/central_kurdish.txt
new file mode 100644
index 0000000000..eec1593e8d
--- /dev/null
+++ b/app/src/main/assets/layouts/main/central_kurdish.txt
@@ -0,0 +1,31 @@
+ق
+و
+ە
+ر
+ت
+ی
+ێ
+ئ
+ۆ
+پ
+
+ا
+س
+ش
+د
+ف
+ھ|ه
+ژ
+ل
+ک
+گ
+
+ز
+ع
+ح
+ج
+چ
+خ
+ب
+ن
+م
diff --git a/app/src/main/assets/layouts/main/chuvash.txt b/app/src/main/assets/layouts/main/chuvash.txt
new file mode 100644
index 0000000000..c94eef9c2f
--- /dev/null
+++ b/app/src/main/assets/layouts/main/chuvash.txt
@@ -0,0 +1,44 @@
+ё
+ӑ
+ӗ
+ҫ
+ӳ
+ъ
+-
+!
+?
+"
+
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х
+
+ф
+ы
+в
+а
+п
+р
+о
+л
+д
+ж
+э
+
+я
+ч
+с
+м
+и
+т
+ь
+б
+ю
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/colemak.json b/app/src/main/assets/layouts/main/colemak.json
new file mode 100644
index 0000000000..46c8f2f59e
--- /dev/null
+++ b/app/src/main/assets/layouts/main/colemak.json
@@ -0,0 +1,38 @@
+[
+ [
+ { "label": "q" },
+ { "label": "w" },
+ { "label": "f" },
+ { "label": "p" },
+ { "label": "g" },
+ { "label": "j" },
+ { "label": "l" },
+ { "label": "u" },
+ { "label": "y" },
+ { "$": "shift_state_selector",
+ "shiftedManual": { "label": ":" },
+ "default": { "label": ";", "popup": { "main": { "label": ":" } } }
+ }
+ ],
+ [
+ { "label": "a" },
+ { "label": "r" },
+ { "label": "s" },
+ { "label": "t" },
+ { "label": "d" },
+ { "label": "h" },
+ { "label": "n" },
+ { "label": "e" },
+ { "label": "i" },
+ { "label": "o", "popup": { "main": { "label": "…" } } }
+ ],
+ [
+ { "label": "z" },
+ { "label": "x" },
+ { "label": "c" },
+ { "label": "v" },
+ { "label": "b" },
+ { "label": "k" },
+ { "label": "m" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/colemak_dh.json b/app/src/main/assets/layouts/main/colemak_dh.json
new file mode 100644
index 0000000000..7579a5d602
--- /dev/null
+++ b/app/src/main/assets/layouts/main/colemak_dh.json
@@ -0,0 +1,38 @@
+[
+ [
+ { "label": "q" },
+ { "label": "w" },
+ { "label": "f" },
+ { "label": "p" },
+ { "label": "b" },
+ { "label": "j" },
+ { "label": "l" },
+ { "label": "u" },
+ { "label": "y" },
+ { "$": "shift_state_selector",
+ "shiftedManual": { "label": ":" },
+ "default": { "label": ";", "popup": { "main": { "label": ":" } } }
+ }
+ ],
+ [
+ { "label": "a" },
+ { "label": "r" },
+ { "label": "s" },
+ { "label": "t" },
+ { "label": "g" },
+ { "label": "m" },
+ { "label": "n" },
+ { "label": "e" },
+ { "label": "i" },
+ { "label": "o", "popup": { "main": { "label": "…" } } }
+ ],
+ [
+ { "label": "z" },
+ { "label": "x" },
+ { "label": "c" },
+ { "label": "d" },
+ { "label": "v" },
+ { "label": "k" },
+ { "label": "h" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/dagbani.txt b/app/src/main/assets/layouts/main/dagbani.txt
new file mode 100644
index 0000000000..588ff05132
--- /dev/null
+++ b/app/src/main/assets/layouts/main/dagbani.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/dargwa_urakhi.txt b/app/src/main/assets/layouts/main/dargwa_urakhi.txt
new file mode 100644
index 0000000000..44da4e9101
--- /dev/null
+++ b/app/src/main/assets/layouts/main/dargwa_urakhi.txt
@@ -0,0 +1,35 @@
+й
+ц
+у ӯ ӱ ý ӱ́
+к ҟ ҝ ҡ
+е ē ë е́ ë́
+н
+г ґ ғ ꚕ
+ш
+щ
+з ҙ
+х ҳ ẋ
+ъ
+
+ф
+ы
+в w
+а ā ӓ á ӓ́
+п ԥ
+р ҏ
+о о̄ ӧ ó ӧ́
+л
+д
+ж җ
+э э̄ э́
+Ӏ
+
+я я̄ я́ ǽ æ ǣ
+ч ҹ
+с
+м
+и ӣ и́
+т ԏ
+ь
+б ҕ
+ю ю́
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/dvorak.json b/app/src/main/assets/layouts/main/dvorak.json
new file mode 100644
index 0000000000..b4b318a717
--- /dev/null
+++ b/app/src/main/assets/layouts/main/dvorak.json
@@ -0,0 +1,55 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "shifted": { "label": "\"" },
+ "default": { "$": "variation_selector",
+ "uri": { "label": "/" },
+ "email": { "label": "@" },
+ "default": { "label": "'", "popup": { "relevant": [
+ { "label": "!" },
+ { "label": "\"" }
+ ] } }
+ }
+ },
+ { "$": "shift_state_selector",
+ "shifted": { "label": "<" },
+ "default": { "label": "," }
+ },
+ { "$": "shift_state_selector",
+ "shifted": { "label": ">" },
+ "default": { "label": "." }
+ },
+ { "label": "p" },
+ { "label": "y" },
+ { "label": "f" },
+ { "label": "g" },
+ { "label": "c" },
+ { "label": "r" },
+ { "label": "l" }
+ ],
+ [
+ { "label": "a" },
+ { "label": "o" },
+ { "label": "e" },
+ { "label": "u" },
+ { "label": "i" },
+ { "label": "d" },
+ { "label": "h" },
+ { "label": "t" },
+ { "label": "n" },
+ { "label": "s" }
+ ],
+ [
+ { "label": "j" },
+ { "label": "k" },
+ { "label": "x" },
+ { "label": "b" },
+ { "label": "m" },
+ { "label": "w" },
+ { "label": "v" }
+ ],
+ [
+ { "label": "q" },
+ { "label": "z" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/esperanto.txt b/app/src/main/assets/layouts/main/esperanto.txt
new file mode 100644
index 0000000000..a725a22bbf
--- /dev/null
+++ b/app/src/main/assets/layouts/main/esperanto.txt
@@ -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 w
+b
+n
+m
diff --git a/app/src/main/assets/layouts/main/ewe.txt b/app/src/main/assets/layouts/main/ewe.txt
new file mode 100644
index 0000000000..bb307f1a3c
--- /dev/null
+++ b/app/src/main/assets/layouts/main/ewe.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/farsi.txt b/app/src/main/assets/layouts/main/farsi.txt
new file mode 100644
index 0000000000..2bb3154098
--- /dev/null
+++ b/app/src/main/assets/layouts/main/farsi.txt
@@ -0,0 +1,34 @@
+ض
+ص
+ث
+ق
+ف
+غ
+ع
+ه
+خ
+ح
+ج
+
+ش
+س
+ی
+ب
+ل
+ا
+ت
+ن
+م
+ک
+گ
+
+ظ
+ط
+ژ
+ز
+ر
+ذ
+د
+پ
+و
+چ
diff --git a/app/src/main/assets/layouts/main/ga.txt b/app/src/main/assets/layouts/main/ga.txt
new file mode 100644
index 0000000000..03d1fe66e0
--- /dev/null
+++ b/app/src/main/assets/layouts/main/ga.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/georgian.json b/app/src/main/assets/layouts/main/georgian.json
new file mode 100644
index 0000000000..e04af6b698
--- /dev/null
+++ b/app/src/main/assets/layouts/main/georgian.json
@@ -0,0 +1,112 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "Q" },
+ "default": { "label": "ქ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ჭ" },
+ "default": { "label": "წ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "E" },
+ "default": { "label": "ე" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ღ" },
+ "default": { "label": "რ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "თ" },
+ "default": { "label": "ტ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "Y" },
+ "default": { "label": "ყ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "U" },
+ "default": { "label": "უ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "I" },
+ "default": { "label": "ი" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "O" },
+ "default": { "label": "ო" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "P" },
+ "default": { "label": "პ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "A" },
+ "default": { "label": "ა" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "შ" },
+ "default": { "label": "ს" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "D" },
+ "default": { "label": "დ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "F" },
+ "default": { "label": "ფ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "G" },
+ "default": { "label": "გ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "H" },
+ "default": { "label": "ჰ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ჟ" },
+ "default": { "label": "ჯ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "K" },
+ "default": { "label": "კ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "L" },
+ "default": { "label": "ლ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ძ" },
+ "default": { "label": "ზ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "X" },
+ "default": { "label": "ხ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ჩ" },
+ "default": { "label": "ც" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "V" },
+ "default": { "label": "ვ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "B" },
+ "default": { "label": "ბ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "N" },
+ "default": { "label": "ნ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "M" },
+ "default": { "label": "მ" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/greek.json b/app/src/main/assets/layouts/main/greek.json
new file mode 100644
index 0000000000..2510bc5343
--- /dev/null
+++ b/app/src/main/assets/layouts/main/greek.json
@@ -0,0 +1,41 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "shiftedManual": { "label": ":", "popup": { "main": { "label": ";" } } },
+ "default": { "label": ";", "popup": { "main": { "label": ":" } } }
+ },
+ { "label": "ς", "labelFlags": 65536 },
+ { "label": "ε" },
+ { "label": "ρ" },
+ { "label": "τ" },
+ { "label": "υ" },
+ { "label": "θ" },
+ { "label": "ι" },
+ { "label": "ο" },
+ { "label": "π" }
+ ],
+ [
+ { "label": "α" },
+ { "label": "σ" },
+ { "label": "δ" },
+ { "label": "φ" },
+ { "label": "γ" },
+ { "label": "η" },
+ { "label": "ξ" },
+ { "label": "κ" },
+ { "label": "λ" },
+ { "$": "shift_state_selector",
+ "shiftedManual": { "code": 776, "label": "¨" },
+ "default": { "code": 769, "label": "´" }
+ }
+ ],
+ [
+ { "label": "ζ" },
+ { "label": "χ" },
+ { "label": "ψ" },
+ { "label": "ω" },
+ { "label": "β" },
+ { "label": "ν" },
+ { "label": "μ" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/gujarati.json b/app/src/main/assets/layouts/main/gujarati.json
new file mode 100644
index 0000000000..119854dd98
--- /dev/null
+++ b/app/src/main/assets/layouts/main/gujarati.json
@@ -0,0 +1,129 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ધ" },
+ "default": { "label": "અ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ન" },
+ "default": { "label": "ા" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "પ" },
+ "default": { "label": "િ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ફ" },
+ "default": { "label": "ી" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "બ" },
+ "default": { "label": "ુ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ભ" },
+ "default": { "label": "ૂ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "મ" },
+ "default": { "label": "ે" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ય" },
+ "default": { "label": "ૈ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ર" },
+ "default": { "label": "ો" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "લ" },
+ "default": { "label": "ૌ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "વ" },
+ "default": { "label": "ં" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "શ" },
+ "default": { "label": "ઃ", "popup": { "main": { "label": "ઍ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ષ" },
+ "default": { "label": "ક", "popup": { "main": { "label": "ઑ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "સ" },
+ "default": { "label": "ખ", "popup": { "main": { "label": "ૅ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "હ" },
+ "default": { "label": "ગ", "popup": { "main": { "label": "ૉ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ળ" },
+ "default": { "label": "ઘ", "popup": { "main": { "label": "ૃ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ક્ષ", "labelFlags": 128 },
+ "default": { "label": "ઙ", "popup": { "main": { "label": "ઋ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "જ્ઞ", "labelFlags": 128 },
+ "default": { "label": "ચ", "popup": { "main": { "label": "ત્ર" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "આ" },
+ "default": { "label": "છ", "popup": { "main": { "label": "ત્ત" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઇ" },
+ "default": { "label": "જ", "popup": { "main": { "label": "દ્વ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઈ" },
+ "default": { "label": "ઝ", "popup": { "main": { "label": "દ્ધ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઉ" },
+ "default": { "label": "ઞ", "popup": { "main": { "label": "દ્ર" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઊ" },
+ "default": { "label": "ટ"}
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "એ" },
+ "default": { "label": "ઠ", "popup": { "main": { "label": "શ્ર" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઐ" },
+ "default": { "label": "ડ", "popup": { "main": { "label": "શ્વ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઓ" },
+ "default": { "label": "ઢ", "popup": { "main": { "label": "દ્દ" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ઔ" },
+ "default": { "label": "ણ", "popup": { "main": { "label": "હ્ય" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "અં", "labelFlags": 128 },
+ "default": { "label": "ત", "popup": { "main": { "label": "꠰" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "અઃ", "labelFlags": 128 },
+ "default": { "label": "થ", "popup": { "main": { "label": "꠱" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ૐ" },
+ "default": { "label": "દ", "popup": { "main": { "label": "꠲" } } }
+ },
+ { "label": "્", "popup": { "main": { "label": "૱" } } }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/halmak.txt b/app/src/main/assets/layouts/main/halmak.txt
new file mode 100644
index 0000000000..2b1d7007b1
--- /dev/null
+++ b/app/src/main/assets/layouts/main/halmak.txt
@@ -0,0 +1,32 @@
+w
+l
+r
+b
+z
+;
+q
+u
+d
+j
+
+s
+h
+n
+t
+,
+.
+a
+e
+o
+i
+
+m
+v
+c
+g
+p
+x
+k
+
+f
+y
diff --git a/app/src/main/assets/layouts/main/hausa.txt b/app/src/main/assets/layouts/main/hausa.txt
new file mode 100644
index 0000000000..3138eb9910
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hausa.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/hebrew.json b/app/src/main/assets/layouts/main/hebrew.json
new file mode 100644
index 0000000000..99cdaa34b3
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hebrew.json
@@ -0,0 +1,89 @@
+[
+ [
+ { "$": "variation_selector",
+ "email": { "label": "-" },
+ "uri": { "label": "-" },
+ "default": { "label": "'", "popup": { "relevant": [{ "label": "׳" }, { "label": "״" }, { "label": "\"" }] } }
+ },
+ { "$": "variation_selector",
+ "email": { "label": "_" },
+ "uri": { "label": "_" },
+ "default": { "label": "-", "popup": { "relevant": [{ "label": "־" }, { "label": "_" }] } }
+ },
+ { "label": "ק", "popup": {
+ "relevant": [
+ { "label": "\u05b8" },
+ { "label": "\u05b3" },
+ { "label": "\u05bb" }
+ ]
+ } },
+ { "label": "ר", "popup": {
+ "relevant": [
+ { "label": "\u05bf" }
+ ]
+ } },
+ { "label": "א" },
+ { "label": "ט" },
+ { "label": "ו", "popup": {
+ "relevant": [
+ { "label": "ו\u05b9" },
+ { "label": "ו\u05bc" }
+ ]
+ } },
+ { "label": "ן" },
+ { "label": "ם" },
+ { "label": "פ", "popup": {
+ "relevant": [
+ { "label": "\u05b7" },
+ { "label": "\u05b2" }
+ ]
+ } }
+ ],
+ [
+ { "label": "ש", "popup": {
+ "relevant": [
+ { "label": "\u05b0" },
+ { "label": "ש\u05c2" },
+ { "label": "ש\u05c1" }
+ ]
+ } },
+ { "label": "ד", "popup": {
+ "relevant": [
+ { "label": "\u05bc" }
+ ]
+ } },
+ { "label": "ג" },
+ { "label": "כ" },
+ { "label": "ע" },
+ { "label": "י" },
+ { "label": "ח", "popup": {
+ "relevant": [
+ { "label": "\u05b4" },
+ { "label": "\u05b9" }
+ ]
+ } },
+ { "label": "ל" },
+ { "label": "ך" },
+ { "label": "ף" }
+ ],
+ [
+ { "label": "ז" },
+ { "label": "ס", "popup": {
+ "relevant": [
+ { "label": "\u05b6" },
+ { "label": "\u05b1" }
+ ]
+ } },
+ { "label": "ב" },
+ { "label": "ה" },
+ { "label": "נ" },
+ { "label": "מ" },
+ { "label": "צ", "popup": {
+ "relevant": [
+ { "label": "\u05b5" }
+ ]
+ } },
+ { "label": "ת" },
+ { "label": "ץ" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/hebrew_1452_2.json b/app/src/main/assets/layouts/main/hebrew_1452_2.json
new file mode 100644
index 0000000000..137a5d5107
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hebrew_1452_2.json
@@ -0,0 +1,85 @@
+[
+ [
+ { "label": "ץ", "popup": {
+ "relevant": [
+ { "label": "ש\u05c2" }
+ ]
+ } },
+ { "label": "ן", "popup": {
+ "relevant": [
+ { "label": "ש\u05c1" }
+ ]
+ } },
+ { "label": "ק", "popup": {
+ "relevant": [
+ { "label": "\u05b8" },
+ { "label": "\u05bb" }
+ ]
+ } },
+ { "label": "ר", "popup": {
+ "relevant": [
+ { "label": "\u05b3" }
+ ]
+ } },
+ { "label": "א" },
+ { "label": "ט" },
+ { "label": "ו", "popup": {
+ "relevant": [
+ { "label": "\u05b9" }
+ ]
+ } },
+ { "label": "ת" },
+ { "label": "ם" },
+ { "label": "פ", "popup": {
+ "relevant": [
+ { "label": "\u05b2" },
+ { "label": "\u05b7" }
+ ]
+ } }
+ ],
+ [
+ { "label": "ש", "popup": {
+ "relevant": [
+ { "label": "\u05b0" }
+ ]
+ } },
+ { "label": "ד", "popup": {
+ "relevant": [
+ { "label": "\u05bc" }
+ ]
+ } },
+ { "label": "ג" },
+ { "label": "כ" },
+ { "label": "ע" },
+ { "label": "י" },
+ { "label": "ח", "popup": {
+ "relevant": [
+ { "label": "\u05b4" }
+ ]
+ } },
+ { "label": "ל" },
+ { "label": "ך" },
+ { "label": "ף" }
+ ],
+ [
+ { "label": "ז" },
+ { "label": "ס", "popup": {
+ "relevant": [
+ { "label": "\u05b6" }
+ ]
+ } },
+ { "label": "ב", "popup": {
+ "relevant": [
+ { "label": "\u05b1" }
+ ]
+ } },
+ { "label": "ה" },
+ { "label": "נ" },
+ { "label": "מ" },
+ { "label": "צ", "popup": {
+ "relevant": [
+ { "label": "\u05b5" }
+ ]
+ } }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/hindi.json b/app/src/main/assets/layouts/main/hindi.json
new file mode 100644
index 0000000000..6cbf926538
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hindi.json
@@ -0,0 +1,132 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "औ" },
+ "default": { "label": "ौ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऐ" },
+ "default": { "label": "ै" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "आ" },
+ "default": { "label": "ा" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ई" },
+ "default": { "label": "ी" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऊ" },
+ "default": { "label": "ू" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "भ" },
+ "default": { "label": "ब" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ः" },
+ "default": { "label": "ह" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "घ" },
+ "default": { "label": "ग" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ध" },
+ "default": { "label": "द" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "झ" },
+ "default": { "label": "ज" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ढ" },
+ "default": { "label": "ड" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ओ" },
+ "default": { "label": "ो" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ए" },
+ "default": { "label": "े" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "अ" },
+ "default": { "label": "्" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "इ" },
+ "default": { "label": "ि" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "उ" },
+ "default": { "label": "ु" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "फ" },
+ "default": { "label": "प" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऱ" },
+ "default": { "label": "र" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ख" },
+ "default": { "label": "क" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "थ" },
+ "default": { "label": "त" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "छ" },
+ "default": { "label": "च" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ठ" },
+ "default": { "label": "ट" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऑ" },
+ "default": { "label": "ॉ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ँ" },
+ "default": { "label": "ं" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ण" },
+ "default": { "label": "म" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऩ" },
+ "default": { "label": "न" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ळ" },
+ "default": { "label": "व" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "श" },
+ "default": { "label": "ल" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ष" },
+ "default": { "label": "स" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ृ" },
+ "default": { "label": "य" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ञ" },
+ "default": { "label": "़" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/hindi_compact.json b/app/src/main/assets/layouts/main/hindi_compact.json
new file mode 100644
index 0000000000..bb659ab7b1
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hindi_compact.json
@@ -0,0 +1,40 @@
+[
+ [
+ { "label": "औ" },
+ { "label": "ऐ" },
+ { "label": "आ" },
+ { "label": "ई" },
+ { "label": "ऊ" },
+ { "label": "ब" },
+ { "label": "ह" },
+ { "label": "ग" },
+ { "label": "द" },
+ { "label": "ज" },
+ { "label": "ड" }
+ ],
+ [
+ { "label": "ओ" },
+ { "label": "ए" },
+ { "label": "अ" },
+ { "label": "इ" },
+ { "label": "उ" },
+ { "label": "प" },
+ { "label": "र" },
+ { "label": "क" },
+ { "label": "त" },
+ { "label": "च" },
+ { "label": "ट" }
+ ],
+ [
+ { "label": "ऑ" },
+ { "label": "्" },
+ { "label": "ं" },
+ { "label": "म" },
+ { "label": "न" },
+ { "label": "व" },
+ { "label": "ल" },
+ { "label": "स" },
+ { "label": "य" },
+ { "label": "क्ष", "labelFlags": 128 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/hindi_phonetic.json b/app/src/main/assets/layouts/main/hindi_phonetic.json
new file mode 100644
index 0000000000..0a9193299d
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hindi_phonetic.json
@@ -0,0 +1,554 @@
+[
+ [
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ठ"
+ },
+ "default": {
+ "label": "ट"
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ढ"
+ },
+ "default": {
+ "label": "ड",
+ "popup": {
+ "main": {
+ "label": "ड़"
+ },
+ "relevant": [
+ {
+ "label": "ढ़"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ै"
+ },
+ "default": {
+ "label": "े",
+ "popup": {
+ "main": {
+ "label": "ए"
+ },
+ "relevant": [
+ {
+ "label": "ऍ"
+ },
+ {
+ "label": "ऐ"
+ },
+ {
+ "code": 2374,
+ "label": " ॆ"
+ },
+ {
+ "code": 2389,
+ "label": " ॕ"
+ },
+ {
+ "code": 2382,
+ "label": " ॎ"
+ },
+ {
+ "label": "ऎ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ृ"
+ },
+ "default": {
+ "label": "र",
+ "popup": {
+ "main": {
+ "label": "ऋ"
+ },
+ "relevant": [
+ {
+ "label": "ॠ"
+ },
+ {
+ "label": "ॄ"
+ },
+ {
+ "label": "ऱ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "थ"
+ },
+ "default": {
+ "label": "त",
+ "popup": {
+ "main": {
+ "label": "त्र"
+ }
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "य़"
+ },
+ "default": {
+ "label": "य",
+ "popup": {
+ "main": {
+ "label": "ॺ"
+ }
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ू"
+ },
+ "default": {
+ "label": "ु",
+ "popup": {
+ "main": {
+ "label": "उ"
+ },
+ "relevant": [
+ {
+ "label": "ऊ"
+ },
+ {
+ "label": "ॷ"
+ },
+ {
+ "code": 2390,
+ "label": " ॖ"
+ },
+ {
+ "label": "ॶ"
+ },
+ {
+ "code": 2391,
+ "label": " ॗ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ी"
+ },
+ "default": {
+ "label": "ि",
+ "popup": {
+ "main": {
+ "label": "इ"
+ },
+ "relevant": [
+ {
+ "label": "ई"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ौ"
+ },
+ "default": {
+ "label": "ो",
+ "popup": {
+ "main": {
+ "label": "ओ"
+ },
+ "relevant": [
+ {
+ "label": "औ"
+ },
+ {
+ "label": "ऑ"
+ },
+ {
+ "code": 2383,
+ "label": " ॏ"
+ },
+ {
+ "label": "ॵ"
+ },
+ {
+ "label": "ॐ"
+ },
+ {
+ "label": "ॉ"
+ },
+ {
+ "label": "ॳ"
+ },
+ {
+ "label": "ॴ"
+ },
+ {
+ "code": 2362,
+ "label": " ऺ"
+ },
+ {
+ "code": 2363,
+ "label": " ऻ"
+ },
+ {
+ "label": "ऒ"
+ },
+ {
+ "code": 2378,
+ "label": " ॊ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "फ़"
+ },
+ "default": {
+ "label": "प"
+ }
+ }
+ ],
+ [
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "अ"
+ },
+ "default": {
+ "label": "ा",
+ "popup": {
+ "main": {
+ "label": "आ"
+ },
+ "relevant": [
+ {
+ "label": "ॅ"
+ },
+ {
+ "label": "ॲ"
+ },
+ {
+ "label": "ऄ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "श"
+ },
+ "default": {
+ "label": "स",
+ "popup": {
+ "main": {
+ "label": "श्र"
+ },
+ "relevant": [
+ {
+ "label": "ष"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ध"
+ },
+ "default": {
+ "label": "द",
+ "popup": {
+ "main": {
+ "label": "ड़"
+ },
+ "relevant": [
+ {
+ "label": "ॾ"
+ },
+ {
+ "label": "ढ़"
+ },
+ {
+ "label": "ॸ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "code": 2364,
+ "label": " ़"
+ },
+ "default": {
+ "label": "फ",
+ "popup": {
+ "main": {
+ "label": "फ़"
+ }
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "घ"
+ },
+ "default": {
+ "label": "ग",
+ "popup": {
+ "main": {
+ "label": "ग़"
+ },
+ "relevant": [
+ {
+ "label": "ॻ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ः"
+ },
+ "default": {
+ "label": "ह"
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "झ"
+ },
+ "default": {
+ "label": "ज",
+ "popup": {
+ "main": {
+ "label": "ज़"
+ },
+ "relevant": [
+ {
+ "label": "ॼ"
+ },
+ {
+ "label": "ॹ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ख"
+ },
+ "default": {
+ "label": "क",
+ "popup": {
+ "main": {
+ "label": "क़"
+ },
+ "relevant": [
+ {
+ "label": "ख़"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ळ"
+ },
+ "default": {
+ "label": "ल",
+ "popup": {
+ "relevant": [
+ {
+ "label": "ऴ"
+ },
+ {
+ "label": "ॣ"
+ },
+ {
+ "label": "ऌ"
+ },
+ {
+ "label": "ॡ"
+ },
+ {
+ "label": "ॢ"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ [
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ज़",
+ "labelFlags": 128
+ },
+ "default": {
+ "label": "ज्ञ",
+ "labelFlags": 128
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ष"
+ },
+ "default": {
+ "label": "क्ष",
+ "labelFlags": 128
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "छ"
+ },
+ "default": {
+ "label": "च",
+ "popup": {
+ "relevant": [
+ {
+ "$": "auto_text_key",
+ "code": 2385,
+ "label": " ॑"
+ },
+ {
+ "$": "auto_text_key",
+ "code": 2386,
+ "label": " ॒"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "्"
+ },
+ "default": {
+ "label": "व",
+ "popup": {
+ "relevant": [
+ {
+ "$": "auto_text_key",
+ "code": 2387,
+ "label": " ॓"
+ },
+ {
+ "$": "auto_text_key",
+ "code": 2388,
+ "label": " ॔"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "भ"
+ },
+ "default": {
+ "label": "ब",
+ "popup": {
+ "relevant": [
+ {
+ "label": "ॿ"
+ },
+ {
+ "label": "ऽ"
+ },
+ {
+ "label": "॰"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ण"
+ },
+ "default": {
+ "label": "न",
+ "popup": {
+ "main": {
+ "label": "ङ"
+ },
+ "relevant": [
+ {
+ "label": "ऩ"
+ },
+ {
+ "label": "ञ"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$": "shift_state_selector",
+ "manualOrLocked": {
+ "label": "ं"
+ },
+ "default": {
+ "label": "म",
+ "popup": {
+ "main": {
+ "label": "ँ"
+ },
+ "relevant": [
+ {
+ "label": "ऀ"
+ }
+ ]
+ }
+ }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/hungarian_extended_qwertz.txt b/app/src/main/assets/layouts/main/hungarian_extended_qwertz.txt
new file mode 100644
index 0000000000..d38e22ef87
--- /dev/null
+++ b/app/src/main/assets/layouts/main/hungarian_extended_qwertz.txt
@@ -0,0 +1,39 @@
+á
+é
+í
+ó
+ö
+ő
+ú
+ü
+ű
+'
+
+q
+w
+e
+r
+t
+z
+u
+i
+o
+p
+
+a
+s
+d
+f
+g
+h
+j
+k
+l
+
+y
+x
+c
+v
+b
+n
+m
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/igbo.txt b/app/src/main/assets/layouts/main/igbo.txt
new file mode 100644
index 0000000000..9cf50d7802
--- /dev/null
+++ b/app/src/main/assets/layouts/main/igbo.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/kabyle.json b/app/src/main/assets/layouts/main/kabyle.json
new file mode 100644
index 0000000000..ccaa5a0b26
--- /dev/null
+++ b/app/src/main/assets/layouts/main/kabyle.json
@@ -0,0 +1,38 @@
+[
+ [
+ { "label": "a" },
+ { "label": "z" },
+ { "label": "e" },
+ { "label": "r" },
+ { "label": "t" },
+ { "label": "y" },
+ { "label": "u" },
+ { "label": "i" },
+ { "label": "ɛ" },
+ { "label": "ɣ" }
+ ],
+ [
+ { "label": "q" },
+ { "label": "s" },
+ { "label": "d" },
+ { "label": "f" },
+ { "label": "g" },
+ { "label": "h" },
+ { "label": "j" },
+ { "label": "k" },
+ { "label": "l" },
+ { "label": "m" }
+ ],
+ [
+ { "label": "w" },
+ { "label": "x" },
+ { "label": "c" },
+ { "label": "v" },
+ { "label": "b" },
+ { "label": "n" },
+ { "$": "shift_state_selector",
+ "shiftedManual": { "label": "?" },
+ "default": { "label": "'" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/kaitag.txt b/app/src/main/assets/layouts/main/kaitag.txt
new file mode 100644
index 0000000000..a79cd8c598
--- /dev/null
+++ b/app/src/main/assets/layouts/main/kaitag.txt
@@ -0,0 +1,32 @@
+й
+ц
+у
+к
+е
+н
+г
+ш
+ҡ
+з
+х №
+
+ҳ
+ғ
+в
+а
+п
+р
+о
+л
+д
+ж
+ъ ~
+
+я
+ч
+с
+м
+и
+т
+ь
+б < >
diff --git a/app/src/main/assets/layouts/main/kannada.txt b/app/src/main/assets/layouts/main/kannada.txt
new file mode 100644
index 0000000000..4df75750c3
--- /dev/null
+++ b/app/src/main/assets/layouts/main/kannada.txt
@@ -0,0 +1,34 @@
+ೌ ಔ %
+ೈ ಐ %
+ಾ ಆ %
+ೀ ಈ %
+ೂ ಊ %
+ಬ ಭ %
+ಹ ಙ %
+ಗ ಘ %
+ದ ಧ %
+ಜ ಝ %
+ಡ ಢ
+
+ೋ ಓ
+ೇ ಏ
+್ ಅ
+ಿ ಇ
+ು ಉ
+ಪ ಫ
+ರ ಱ ೃ
+ಕ ಖ
+ತ ಥ
+ಚ ಛ
+ಟ ಠ
+
+ೆ ಒ
+ಂ ಎ
+ಮ
+ನ ಣ
+ವ
+ಲ ಳ
+ಸ ಶ
+ಋ ್ರ
+ಷ ಕ್ಷ
+ಯ ಜ್ಞ
diff --git a/app/src/main/assets/layouts/main/kannada_extended.txt b/app/src/main/assets/layouts/main/kannada_extended.txt
new file mode 100644
index 0000000000..c094afa842
--- /dev/null
+++ b/app/src/main/assets/layouts/main/kannada_extended.txt
@@ -0,0 +1,53 @@
+ಅ
+ಆ
+ಇ
+ಈ
+ಉ
+ಊ
+ಋ
+ಎ
+ಏ
+ಐ
+
+ಒ
+ಓ
+ಔ
+ಂ
+ಕ
+ಖ
+ಗ
+ಘ
+ಙ
+ಚ
+
+ಛ
+ಜ
+ಝ
+ಞ
+ಟ
+ಠ
+ಡ
+ಢ
+ಣ
+ತ
+
+ಥ
+ದ
+ಧ
+ನ
+ಪ
+ಫ
+ಬ
+ಭ
+ಮ
+ಯ
+
+್
+ರ
+ಲ
+ವ
+ಶ
+ಷ
+ಸ
+ಹ
+ಳ
diff --git a/app/src/main/assets/layouts/main/khmer.json b/app/src/main/assets/layouts/main/khmer.json
new file mode 100644
index 0000000000..ad1c9642b8
--- /dev/null
+++ b/app/src/main/assets/layouts/main/khmer.json
@@ -0,0 +1,194 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "!", "labelFlags": 48 },
+ "default": { "label": "១" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ៗ" },
+ "default": { "label": "២" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\"", "labelFlags": 48 },
+ "default": { "label": "៣" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៛" },
+ "default": { "label": "៤" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "%", "labelFlags": 48 },
+ "default": { "label": "៥" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៍" },
+ "default": { "label": "៦" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "័" },
+ "default": { "label": "៧" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៏" },
+ "default": { "label": "៨" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "(", "labelFlags": 48 },
+ "default": { "label": "៩" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ")", "labelFlags": 48 },
+ "default": { "label": "០" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៌" },
+ "default": { "label": "ឥ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៎" },
+ "default": { "label": "ឲ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឈ" },
+ "default": { "label": "ឆ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឺ" },
+ "default": { "label": "ឹ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ែ" },
+ "default": { "label": "េ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឬ" },
+ "default": { "label": "រ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ទ" },
+ "default": { "label": "ត" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ួ" },
+ "default": { "label": "យ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ូ" },
+ "default": { "label": "ុ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ី" },
+ "default": { "label": "ិ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ៅ" },
+ "default": { "label": "ោ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ភ" },
+ "default": { "label": "ផ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឿ" },
+ "default": { "label": "ៀ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឰ" },
+ "default": { "label": "ឪ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ាំ", "labelFlags": 128 },
+ "default": { "label": "ា" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ៃ" },
+ "default": { "label": "ស" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឌ" },
+ "default": { "label": "ដ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ធ" },
+ "default": { "label": "ថ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "អ" },
+ "default": { "label": "ង" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ះ" },
+ "default": { "label": "ហ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ញ" },
+ "default": { "label": "្" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "គ" },
+ "default": { "label": "ក" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឡ" },
+ "default": { "label": "ល" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ោះ", "labelFlags": 49280 },
+ "default": { "label": "ើ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៉" },
+ "default": { "label": "់" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឯ" },
+ "default": { "label": "ឮ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឍ" },
+ "default": { "label": "ឋ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ឃ" },
+ "default": { "label": "ខ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ជ" },
+ "default": { "label": "ច" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "េះ", "labelFlags": 128 },
+ "default": { "label": "វ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ព" },
+ "default": { "label": "ប" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ណ" },
+ "default": { "label": "ន" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ំ" },
+ "default": { "label": "ម" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ុះ", "labelFlags": 128 },
+ "default": { "label": "ុំ", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "៕" },
+ "default": { "label": "។" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\?", "labelFlags": 48 },
+ "default": { "label": "៊" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/kikuyu.txt b/app/src/main/assets/layouts/main/kikuyu.txt
new file mode 100644
index 0000000000..0fdbdd8d21
--- /dev/null
+++ b/app/src/main/assets/layouts/main/kikuyu.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/korean.json b/app/src/main/assets/layouts/main/korean.json
new file mode 100644
index 0000000000..f843fe0441
--- /dev/null
+++ b/app/src/main/assets/layouts/main/korean.json
@@ -0,0 +1,55 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3143" },
+ "default": { "label": "\u3142", "popup": { "main": { "label": "\u3143" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3149" },
+ "default": { "label": "\u3148", "popup": { "main": { "label": "\u3149" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3138" },
+ "default": { "label": "\u3137", "popup": { "main": { "label": "\u3138" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3132" },
+ "default": { "label": "\u3131", "popup": { "main": { "label": "\u3132" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3146" },
+ "default": { "label": "\u3145", "popup": { "main": { "label": "\u3146" } } }
+ },
+ { "label": "\u315b" },
+ { "label": "\u3155" },
+ { "label": "\u3151" },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3152" },
+ "default": { "label": "\u3150", "popup": { "main": { "label": "\u3152" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u3156" },
+ "default": { "label": "\u3154", "popup": { "main": { "label": "\u3156" } } }
+ }
+ ],
+ [
+ { "label": "\u3141" },
+ { "label": "\u3134" },
+ { "label": "\u3147" },
+ { "label": "\u3139" },
+ { "label": "\u314e" },
+ { "label": "\u3157" },
+ { "label": "\u3153" },
+ { "label": "\u314f" },
+ { "label": "\u3163" }
+ ],
+ [
+ { "label": "\u314b" },
+ { "label": "\u314c" },
+ { "label": "\u314a" },
+ { "label": "\u314d" },
+ { "label": "\u3160" },
+ { "label": "\u315c" },
+ { "label": "\u3161" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/korean_phonetic.json b/app/src/main/assets/layouts/main/korean_phonetic.json
new file mode 100644
index 0000000000..b2c2a6bb25
--- /dev/null
+++ b/app/src/main/assets/layouts/main/korean_phonetic.json
@@ -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" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/korean_sebeolsik_390.json b/app/src/main/assets/layouts/main/korean_sebeolsik_390.json
new file mode 100644
index 0000000000..1bf28269f9
--- /dev/null
+++ b/app/src/main/assets/layouts/main/korean_sebeolsik_390.json
@@ -0,0 +1,162 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11bd" },
+ "default": { "label": "\u11c2", "popup": { "main": { "label": "\u11bd" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0040" },
+ "default": { "label": "\u11bb", "popup": { "main": { "label": "\u0040" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0023" },
+ "default": { "label": "\u11b8", "popup": { "main": { "label": "\u0023" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0024" },
+ "default": { "label": "\u116d", "popup": { "main": { "label": "\u0024" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0025" },
+ "default": { "label": "\u1172", "popup": { "main": { "label": "\u0025" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u005e" },
+ "default": { "label": "\u1163", "popup": { "main": { "label": "\u005e" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0026" },
+ "default": { "label": "\u1168", "popup": { "main": { "label": "\u0026" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u002a" },
+ "default": { "label": "\u1174", "popup": { "main": { "label": "\u002a" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0028" },
+ "default": { "label": "\u116e", "popup": { "main": { "label": "\u0028" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0029" },
+ "default": { "label": "\u110f", "popup": { "main": { "label": "\u0029" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11c1" },
+ "default": { "label": "\u11ba", "popup": { "main": { "label": "\u11c1" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11c0" },
+ "default": { "label": "\u11af", "popup": { "main": { "label": "\u11c0" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11ac" },
+ "default": { "label": "\u1167", "popup": { "main": { "label": "\u11ac" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b6" },
+ "default": { "label": "\u1162", "popup": { "main": { "label": "\u11b6" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b3" },
+ "default": { "label": "\u1165", "popup": { "main": { "label": "\u11b3" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u003c" },
+ "default": { "label": "\u1105", "popup": { "main": { "label": "\u003c" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0037" },
+ "default": { "label": "\u1103", "popup": { "main": { "label": "\u0037" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0038" },
+ "default": { "label": "\u1106", "popup": { "main": { "label": "\u0038" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0039" },
+ "default": { "label": "\u110e", "popup": { "main": { "label": "\u0039" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u003e" },
+ "default": { "label": "\u1111", "popup": { "main": { "label": "\u003e" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11ae" },
+ "default": { "label": "\u11bc", "popup": { "main": { "label": "\u11ae" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11ad" },
+ "default": { "label": "\u11ab", "popup": { "main": { "label": "\u11ad" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b0" },
+ "default": { "label": "\u1175", "popup": { "main": { "label": "\u11b0" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11a9" },
+ "default": { "label": "\u1161", "popup": { "main": { "label": "\u11a9" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u002f" },
+ "default": { "label": "\u1173", "popup": { "main": { "label": "\u002f" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0027" },
+ "default": { "label": "\u1102", "popup": { "main": { "label": "\u0027" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0034" },
+ "default": { "label": "\u110b", "popup": { "main": { "label": "\u0034" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0035" },
+ "default": { "label": "\u1100", "popup": { "main": { "label": "\u0035" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0036" },
+ "default": { "label": "\u110c", "popup": { "main": { "label": "\u0036" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u003a" },
+ "default": { "label": "\u1107", "popup": { "main": { "label": "\u003a" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11be" },
+ "default": { "label": "\u11b7", "popup": { "main": { "label": "\u11be" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b9" },
+ "default": { "label": "\u11a8", "popup": { "main": { "label": "\u11b9" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b1" },
+ "default": { "label": "\u1166", "popup": { "main": { "label": "\u11b1" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b6" },
+ "default": { "label": "\u1169", "popup": { "main": { "label": "\u11b6" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0030", "popup": { "main": { "label": "\u0021" } } },
+ "default": { "label": "\u116e", "popup": { "relevant": [{ "label": "\u0030" }, { "label": "\u0021" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0031" },
+ "default": { "label": "\u1109", "popup": { "main": { "label": "\u0031" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0032" },
+ "default": { "label": "\u1112", "popup": { "main": { "label": "\u0032" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0033", "popup": { "main": { "label": "\u0022" } } },
+ "default": { "label": "\u1110", "popup": { "relevant": [{ "label": "\u0033" }, { "label": "\u0022" }] } }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/korean_sebeolsik_final.json b/app/src/main/assets/layouts/main/korean_sebeolsik_final.json
new file mode 100644
index 0000000000..f6a5e1747f
--- /dev/null
+++ b/app/src/main/assets/layouts/main/korean_sebeolsik_final.json
@@ -0,0 +1,162 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11a9" },
+ "default": { "label": "\u11c2", "popup": { "main": { "label": "\u11a9" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b0" },
+ "default": { "label": "\u11bb", "popup": { "main": { "label": "\u11b0" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11bd" },
+ "default": { "label": "\u11b8", "popup": { "main": { "label": "\u11bd" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b5" },
+ "default": { "label": "\u116d", "popup": { "main": { "label": "\u11b5" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b4" },
+ "default": { "label": "\u1172", "popup": { "main": { "label": "\u11b4" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u003d" },
+ "default": { "label": "\u1163", "popup": { "main": { "label": "\u003d" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u201c" },
+ "default": { "label": "\u1168", "popup": { "main": { "label": "\u201c" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u201d" },
+ "default": { "label": "\u1174", "popup": { "main": { "label": "\u201d" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0027" },
+ "default": { "label": "\u116e", "popup": { "main": { "label": "\u0027" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u007e" },
+ "default": { "label": "\u110f", "popup": { "main": { "label": "\u007e" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11c1" },
+ "default": { "label": "\u11ba", "popup": { "main": { "label": "\u11c1" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11c0" },
+ "default": { "label": "\u11af", "popup": { "main": { "label": "\u11c0" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11ac" },
+ "default": { "label": "\u1167", "popup": { "main": { "label": "\u11ac" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b6" },
+ "default": { "label": "\u1162", "popup": { "main": { "label": "\u11b6" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b3" },
+ "default": { "label": "\u1165", "popup": { "main": { "label": "\u11b3" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0035" },
+ "default": { "label": "\u1105", "popup": { "main": { "label": "\u0035" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0036" },
+ "default": { "label": "\u1103", "popup": { "main": { "label": "\u0036" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0037" },
+ "default": { "label": "\u1106", "popup": { "main": { "label": "\u0037" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0038" },
+ "default": { "label": "\u110e", "popup": { "main": { "label": "\u0038" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0039" },
+ "default": { "label": "\u1111", "popup": { "main": { "label": "\u0039" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11ae" },
+ "default": { "label": "\u11bc", "popup": { "main": { "label": "\u11ae" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11ad" },
+ "default": { "label": "\u11ab", "popup": { "main": { "label": "\u11ad" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b2" },
+ "default": { "label": "\u1175", "popup": { "main": { "label": "\u11b2" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b1" },
+ "default": { "label": "\u1161", "popup": { "main": { "label": "\u11b1" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u1164" },
+ "default": { "label": "\u1173", "popup": { "main": { "label": "\u1164" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0030" },
+ "default": { "label": "\u1102", "popup": { "main": { "label": "\u0030" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0031" },
+ "default": { "label": "\u110b", "popup": { "main": { "label": "\u0031" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0032" },
+ "default": { "label": "\u1100", "popup": { "main": { "label": "\u0032" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0033" },
+ "default": { "label": "\u110c", "popup": { "main": { "label": "\u0033" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0034" },
+ "default": { "label": "\u1107", "popup": { "main": { "label": "\u0034" } } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11be" },
+ "default": { "label": "\u11b7", "popup": { "main": { "label": "\u11be" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11b9" },
+ "default": { "label": "\u11a8", "popup": { "main": { "label": "\u11b9" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11bf" },
+ "default": { "label": "\u1166", "popup": { "main": { "label": "\u11bf" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u11aa" },
+ "default": { "label": "\u1169", "popup": { "main": { "label": "\u11aa" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u003f" },
+ "default": { "label": "\u116e", "popup": { "main": { "label": "\u003f" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u002d" },
+ "default": { "label": "\u1109", "popup": { "main": { "label": "\u002d" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u0022" },
+ "default": { "label": "\u1112", "popup": { "main": { "label": "\u0022" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\u00b7" },
+ "default": { "label": "\u1110", "popup": { "main": { "label": "\u00b7" } } }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/lao.json b/app/src/main/assets/layouts/main/lao.json
new file mode 100644
index 0000000000..6af00883f1
--- /dev/null
+++ b/app/src/main/assets/layouts/main/lao.json
@@ -0,0 +1,194 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໑" },
+ "default": { "label": "ຢ", "popup": { "relevant": [ { "label": "1" }, { "label": "໑" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໒" },
+ "default": { "label": "ຟ", "popup": { "relevant": [ { "label": "2" }, { "label": "໒" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໓" },
+ "default": { "label": "ໂ", "popup": { "relevant": [ { "label": "3" }, { "label": "໓" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໔" },
+ "default": { "label": "ຖ", "popup": { "relevant": [ { "label": "4" }, { "label": "໔" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໌" },
+ "default": { "label": "ຸ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຼ" },
+ "default": { "label": "ູ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໕" },
+ "default": { "label": "ຄ", "popup": { "relevant": [ { "label": "5" }, { "label": "໕" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໖" },
+ "default": { "label": "ຕ", "popup": { "relevant": [ { "label": "6" }, { "label": "໖" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໗" },
+ "default": { "label": "ຈ", "popup": { "relevant": [ { "label": "7" }, { "label": "໗" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໘" },
+ "default": { "label": "ຂ", "popup": { "relevant": [ { "label": "8" }, { "label": "໘" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໙" },
+ "default": { "label": "ຊ", "popup": { "relevant": [ { "label": "9" }, { "label": "໙" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ໍ່", "labelFlags": 128 },
+ "default": { "label": "ໍ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ົ້", "labelFlags": 128 },
+ "default": { "label": "ົ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໐" },
+ "default": { "label": "ໄ", "popup": { "relevant": [ { "label": "0" }, { "label": "໐" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຳ້", "labelFlags": 128 },
+ "default": { "label": "ຳ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "_", "labelFlags": 48 },
+ "default": { "label": "ພ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "+", "labelFlags": 48 },
+ "default": { "label": "ະ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ິ້", "labelFlags": 128 },
+ "default": { "label": "ິ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ີ້", "labelFlags": 128 },
+ "default": { "label": "ີ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຣ" },
+ "default": { "label": "ຮ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ໜ" },
+ "default": { "label": "ນ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຽ" },
+ "default": { "label": "ຍ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຫຼ", "labelFlags": 128 },
+ "default": { "label": "ບ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "”", "labelFlags": 48 },
+ "default": { "label": "ລ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ັ້", "labelFlags": 128 },
+ "default": { "label": "ັ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ";", "labelFlags": 48 },
+ "default": { "label": "ຫ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ".", "labelFlags": 48 },
+ "default": { "label": "ກ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ",", "labelFlags": 48 },
+ "default": { "label": "ດ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ":", "labelFlags": 48 },
+ "default": { "label": "ເ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໊" },
+ "default": { "label": "້" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "໋" },
+ "default": { "label": "່" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "!", "labelFlags": 48 },
+ "default": { "label": "າ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\?", "labelFlags": 48 },
+ "default": { "label": "ສ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "%", "labelFlags": 48 },
+ "default": { "label": "ວ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "=", "labelFlags": 48 },
+ "default": { "label": "ງ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "“", "labelFlags": 48 },
+ "default": { "label": "“", "labelFlags": 48 }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "₭", "labelFlags": 48 },
+ "default": { "label": "ຜ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "(", "labelFlags": 48 },
+ "default": { "label": "ປ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຯ" },
+ "default": { "label": "ແ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\@", "labelFlags": 48 },
+ "default": { "label": "ອ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ຶ້", "labelFlags": 128 },
+ "default": { "label": "ຶ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ື້", "labelFlags": 128 },
+ "default": { "label": "ື" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ໆ" },
+ "default": { "label": "ທ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ໝ" },
+ "default": { "label": "ມ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "$", "labelFlags": 48 },
+ "default": { "label": "ໃ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ")", "labelFlags": 48 },
+ "default": { "label": "ຝ" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/lingala.txt b/app/src/main/assets/layouts/main/lingala.txt
new file mode 100644
index 0000000000..830109805e
--- /dev/null
+++ b/app/src/main/assets/layouts/main/lingala.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/luganda.txt b/app/src/main/assets/layouts/main/luganda.txt
new file mode 100644
index 0000000000..5253b2885a
--- /dev/null
+++ b/app/src/main/assets/layouts/main/luganda.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/macedonian.txt b/app/src/main/assets/layouts/main/macedonian.txt
new file mode 100644
index 0000000000..1979065e64
--- /dev/null
+++ b/app/src/main/assets/layouts/main/macedonian.txt
@@ -0,0 +1,33 @@
+љ
+њ
+е
+р
+т
+ѕ
+у
+и
+о
+п
+ш
+
+а
+с
+д
+ф
+г
+х
+ј
+к
+л
+ч
+ќ
+
+з
+џ
+ц
+в
+б
+н
+м
+ѓ
+ж
diff --git a/app/src/main/assets/layouts/main/malayalam.txt b/app/src/main/assets/layouts/main/malayalam.txt
new file mode 100644
index 0000000000..3e967057f5
--- /dev/null
+++ b/app/src/main/assets/layouts/main/malayalam.txt
@@ -0,0 +1,34 @@
+്
+ാ
+ി
+ീ
+ു
+ൂ
+ൃ
+െ
+േ
+ൊ
+ോ
+
+ക
+ഗ
+ങ
+ച
+ജ
+ട
+ഡ
+ണ
+ത
+ദ
+ന
+
+പ
+ബ
+മ
+യ
+ര
+ല
+വ
+ശ
+ഹ
+ള
diff --git a/app/src/main/assets/layouts/main/mansi_north.txt b/app/src/main/assets/layouts/main/mansi_north.txt
new file mode 100644
index 0000000000..efdbd965e0
--- /dev/null
+++ b/app/src/main/assets/layouts/main/mansi_north.txt
@@ -0,0 +1,47 @@
+ё
+ы̄
+ӯ
+а̄
+е̄
+ӈ
+о̄
+я̄
+ю̄
+ӣ
+э̄
+ё̄
+
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х
+ъ
+
+ф
+ы
+в
+а
+п
+р
+о
+л
+д
+ж
+э
+
+я
+ч
+с
+м
+и
+т
+ь
+б
+ю
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/main/marathi.json b/app/src/main/assets/layouts/main/marathi.json
new file mode 100644
index 0000000000..e8c8c18acd
--- /dev/null
+++ b/app/src/main/assets/layouts/main/marathi.json
@@ -0,0 +1,40 @@
+[
+ [
+ { "label": "ौ" },
+ { "label": "ै" },
+ { "label": "ा" },
+ { "label": "ी" },
+ { "label": "ू" },
+ { "label": "ब" },
+ { "label": "ह" },
+ { "label": "ग" },
+ { "label": "द" },
+ { "label": "ज" },
+ { "label": "ड" }
+ ],
+ [
+ { "label": "ो" },
+ { "label": "े" },
+ { "label": "्" },
+ { "label": "ि" },
+ { "label": "ु" },
+ { "label": "प" },
+ { "label": "र" },
+ { "label": "क" },
+ { "label": "त" },
+ { "label": "च" },
+ { "label": "ट" }
+ ],
+ [
+ { "label": "ॉ" },
+ { "label": "ॅ" },
+ { "label": "ं" },
+ { "label": "म" },
+ { "label": "न" },
+ { "label": "व" },
+ { "label": "ल" },
+ { "label": "स" },
+ { "label": "य" },
+ { "label": "क्ष", "labelFlags": 128 }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/mari.txt b/app/src/main/assets/layouts/main/mari.txt
new file mode 100644
index 0000000000..528cd828bc
--- /dev/null
+++ b/app/src/main/assets/layouts/main/mari.txt
@@ -0,0 +1,44 @@
+ё
+ҥ
+ӧ
+ӱ
+ъ
+-
+!
+?
+"
+/
+
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х
+
+ф
+ы
+в
+а
+п
+р
+о
+л
+д
+ж
+э
+
+я
+ч
+с
+м
+и
+т
+ь
+б
+ю
diff --git a/app/src/main/assets/layouts/main/mongolian.txt b/app/src/main/assets/layouts/main/mongolian.txt
new file mode 100644
index 0000000000..6614f82e54
--- /dev/null
+++ b/app/src/main/assets/layouts/main/mongolian.txt
@@ -0,0 +1,33 @@
+ф
+ц
+у
+ж
+э
+н
+г
+ш
+ү
+з
+к
+
+й
+ы
+б
+ө
+а
+х
+р
+о
+л
+д
+п
+
+я
+ч
+ё
+с
+м
+и
+т
+ь
+в
diff --git a/app/src/main/assets/layouts/main/nepali_romanized.json b/app/src/main/assets/layouts/main/nepali_romanized.json
new file mode 100644
index 0000000000..955b7d30a5
--- /dev/null
+++ b/app/src/main/assets/layouts/main/nepali_romanized.json
@@ -0,0 +1,128 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ठ" },
+ "default": { "label": "ट" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "औ" },
+ "default": { "label": "ौ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ै" },
+ "default": { "label": "े" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ृ" },
+ "default": { "label": "र" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "थ" },
+ "default": { "label": "त" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ञ" },
+ "default": { "label": "य" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ू" },
+ "default": { "label": "ु" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ी" },
+ "default": { "label": "ि" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ओ" },
+ "default": { "label": "ो" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "फ" },
+ "default": { "label": "प" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ई" },
+ "default": { "label": "इ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "आ" },
+ "default": { "label": "ा" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "श" },
+ "default": { "label": "स" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ध" },
+ "default": { "label": "द" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऊ" },
+ "default": { "label": "उ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "घ" },
+ "default": { "label": "ग" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "अ" },
+ "default": { "label": "ह" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "झ" },
+ "default": { "label": "ज" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ख" },
+ "default": { "label": "क" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "॥" },
+ "default": { "label": "ल" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऐ" },
+ "default": { "label": "ए" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ः" },
+ "default": { "label": "ॐ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऋ" },
+ "default": { "label": "ष" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ढ" },
+ "default": { "label": "ड" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "छ" },
+ "default": { "label": "च" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ँ" },
+ "default": { "label": "व" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "भ" },
+ "default": { "label": "ब" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ण" },
+ "default": { "label": "न" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ं" },
+ "default": { "label": "म" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ङ" },
+ "default": { "label": "्" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/nepali_traditional.json b/app/src/main/assets/layouts/main/nepali_traditional.json
new file mode 100644
index 0000000000..5ba62ef194
--- /dev/null
+++ b/app/src/main/assets/layouts/main/nepali_traditional.json
@@ -0,0 +1,132 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "त्त", "labelFlags": 128 },
+ "default": { "label": "ट" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ड्ढ", "labelFlags": 128 },
+ "default": { "label": "ध" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऐ" },
+ "default": { "label": "भ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "द्व", "labelFlags": 128 },
+ "default": { "label": "च" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ट्ट", "labelFlags": 128 },
+ "default": { "label": "त" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ठ्ठ", "labelFlags": 128 },
+ "default": { "label": "थ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऊ" },
+ "default": { "label": "ग" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "क्ष", "labelFlags": 128 },
+ "default": { "label": "ष" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "इ" },
+ "default": { "label": "य" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ए" },
+ "default": { "label": "उ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ृ" },
+ "default": { "label": "इ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "आ" },
+ "default": { "label": "ब" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ङ्", "labelFlags": 128 },
+ "default": { "label": "क" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ड्ड", "labelFlags": 128 },
+ "default": { "label": "म" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ँ" },
+ "default": { "label": "ा" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "द्द", "labelFlags": 128 },
+ "default": { "label": "न" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "झ" },
+ "default": { "label": "ज" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ो" },
+ "default": { "label": "व" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "फ" },
+ "default": { "label": "प" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ी" },
+ "default": { "label": "ि" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ट्ठ", "labelFlags": 128 },
+ "default": { "label": "स" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ू" },
+ "default": { "label": "ु" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "क्", "labelFlags": 128 },
+ "default": { "label": "श" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ह्म", "labelFlags": 128 },
+ "default": { "label": "ह" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ऋ" },
+ "default": { "label": "अ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ॐ" },
+ "default": { "label": "ख" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ौ" },
+ "default": { "label": "द" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "द्य", "labelFlags": 128 },
+ "default": { "label": "ल" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ं" },
+ "default": { "label": "े" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ङ" },
+ "default": { "label": "्" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ै" },
+ "default": { "label": "र" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/pcqwerty.json b/app/src/main/assets/layouts/main/pcqwerty.json
new file mode 100644
index 0000000000..b9d6a17b57
--- /dev/null
+++ b/app/src/main/assets/layouts/main/pcqwerty.json
@@ -0,0 +1,120 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "~" },
+ "default": { "label": "`", "popup": { "main": { "label": "~" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "!" },
+ "default": { "label": "1", "popup": { "relevant": [ { "label": "!" }, { "label": "¡" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\@" },
+ "default": { "label": "2", "popup": { "main": { "label": "\\@" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\#" },
+ "default": { "label": "3", "popup": { "main": { "label": "\\#" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "$" },
+ "default": { "label": "4", "popup": { "main": { "label": "$" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\%" },
+ "default": { "label": "5", "popup": { "main": { "label": "\\%" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "^" },
+ "default": { "label": "6", "popup": { "main": { "label": "^" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "&" },
+ "default": { "label": "7", "popup": { "main": { "label": "&" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "*" },
+ "default": { "label": "8", "popup": { "main": { "label": "*" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "(" },
+ "default": { "label": "9", "popup": { "main": { "label": "(" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ")" },
+ "default": { "label": "0", "popup": { "main": { "label": ")" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "_" },
+ "default": { "label": "-", "popup": { "relevant": [ { "label": "_" }, { "label": "–" }, { "label": "—" }, { "label": "·" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "+", "popup": { "relevant": [ { "label": "×" }, { "label": "÷" }, { "label": "√" } ] } },
+ "default": { "label": "=", "popup": { "relevant": [ { "label": "+" }, { "label": "∞" }, { "label": "≠" }, { "label": "≈" } ] } }
+ }
+ ],
+ [
+ { "label": "q", "popup": { "main": { "label": "%" } } },
+ { "label": "w", "popup": { "main": { "label": "\\" } } },
+ { "label": "e", "popup": { "main": { "label": "|" } } },
+ { "label": "r", "popup": { "main": { "label": "=" } } },
+ { "label": "t", "popup": { "main": { "label": "[" } } },
+ { "label": "y", "popup": { "main": { "label": "]" } } },
+ { "label": "u", "popup": { "main": { "label": "<" } } },
+ { "label": "i", "popup": { "main": { "label": ">" } } },
+ { "label": "o", "popup": { "main": { "label": "{" } } },
+ { "label": "p", "popup": { "main": { "label": "}" } } },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "{" },
+ "default": { "label": "[", "popup": { "main": { "label": "{" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "}" },
+ "default": { "label": "]", "popup": { "main": { "label": "}" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "|" },
+ "default": { "label": "\\", "popup": { "main": { "label": "\\|" } } }
+ }
+ ],
+ [
+ { "label": "a", "popup": { "main": { "label": "@" } } },
+ { "label": "s", "popup": { "main": { "label": "#" } } },
+ { "label": "d", "popup": { "main": { "label": "$$$" } } },
+ { "label": "f", "popup": { "main": { "label": "_" } } },
+ { "label": "g", "popup": { "main": { "label": "&" } } },
+ { "label": "h", "popup": { "main": { "label": "-" } } },
+ { "label": "j", "popup": { "main": { "label": "+" } } },
+ { "label": "k", "popup": { "main": { "label": "(" } } },
+ { "label": "l", "popup": { "main": { "label": ")" } } },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ":" },
+ "default": { "label": ";", "popup": { "main": { "label": ":" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\"" },
+ "default": { "label": "'", "popup": { "main": { "label": "\"" } } }
+ }
+ ],
+ [
+ { "label": "z", "popup": { "main": { "label": "*" } } },
+ { "label": "x", "popup": { "main": { "label": "\"" } } },
+ { "label": "c", "popup": { "main": { "label": "'" } } },
+ { "label": "v", "popup": { "main": { "label": ":" } } },
+ { "label": "b", "popup": { "main": { "label": ";" } } },
+ { "label": "n", "popup": { "main": { "label": "!" } } },
+ { "label": "m", "popup": { "main": { "label": "?" } } },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "<", "popup": { "relevant": [ { "label": "‹" }, { "label": "≤" }, { "label": "«" } ] } },
+ "default": { "label": ",", "popup": { "main": { "label": "<" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ">", "popup": { "relevant": [ { "label": "›" }, { "label": "›" }, { "label": "»" } ] } },
+ "default": { "label": ".", "popup": { "main": { "label": ">" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\?" },
+ "default": { "label": "/", "popup": { "main": { "label": "\\?" }, "relevant": [ { "label": "¿" } ] } }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/qwerty.txt b/app/src/main/assets/layouts/main/qwerty.txt
new file mode 100644
index 0000000000..0ff6ed76f0
--- /dev/null
+++ b/app/src/main/assets/layouts/main/qwerty.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/qwertz.txt b/app/src/main/assets/layouts/main/qwertz.txt
new file mode 100644
index 0000000000..c0296e173d
--- /dev/null
+++ b/app/src/main/assets/layouts/main/qwertz.txt
@@ -0,0 +1,28 @@
+q
+w
+e
+r
+t
+z
+u
+i
+o
+p
+
+a
+s
+d
+f
+g
+h
+j
+k
+l
+
+y
+x
+c
+v
+b
+n
+m
diff --git a/app/src/main/assets/layouts/main/russian.txt b/app/src/main/assets/layouts/main/russian.txt
new file mode 100644
index 0000000000..7da99f64e6
--- /dev/null
+++ b/app/src/main/assets/layouts/main/russian.txt
@@ -0,0 +1,33 @@
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х ъ [ {
+
+ф
+ы
+в
+а
+п
+р
+о
+л
+д
+ж
+э э́ ] }
+
+я
+ч
+с
+м
+и
+т
+ь
+б <
+ю >
diff --git a/app/src/main/assets/layouts/main/russian_extended.txt b/app/src/main/assets/layouts/main/russian_extended.txt
new file mode 100644
index 0000000000..8b3249fee4
--- /dev/null
+++ b/app/src/main/assets/layouts/main/russian_extended.txt
@@ -0,0 +1,34 @@
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х [ {
+ъ ] }
+
+ф
+ы
+в
+а
+п
+р
+о
+л
+д
+ж
+э э́
+
+я
+ч
+с
+м
+и
+т
+ь
+б <
+ю >
diff --git a/app/src/main/assets/layouts/main/russian_student.txt b/app/src/main/assets/layouts/main/russian_student.txt
new file mode 100644
index 0000000000..d4049968ca
--- /dev/null
+++ b/app/src/main/assets/layouts/main/russian_student.txt
@@ -0,0 +1,33 @@
+я
+ш
+е
+р
+т
+ы
+у
+и
+о
+п
+э
+
+а
+с
+д
+ф
+г
+ч
+й
+к
+л
+ж
+щ
+
+з
+х
+ц
+в
+б
+н
+м
+ь
+ю
diff --git a/app/src/main/assets/layouts/main/serbian.txt b/app/src/main/assets/layouts/main/serbian.txt
new file mode 100644
index 0000000000..9e0b71efa0
--- /dev/null
+++ b/app/src/main/assets/layouts/main/serbian.txt
@@ -0,0 +1,32 @@
+љ
+њ
+е
+р
+т
+з ѕ
+у
+и
+о
+п
+ш
+
+а
+с
+д
+ф
+г
+х
+ј
+к
+л
+ч
+ћ
+
+џ
+ц
+в
+б
+н
+м
+ђ
+ж
diff --git a/app/src/main/assets/layouts/main/sesotho.txt b/app/src/main/assets/layouts/main/sesotho.txt
new file mode 100644
index 0000000000..b0796b3b5c
--- /dev/null
+++ b/app/src/main/assets/layouts/main/sesotho.txt
@@ -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
diff --git a/app/src/main/assets/layouts/main/sinhala.json b/app/src/main/assets/layouts/main/sinhala.json
new file mode 100644
index 0000000000..83a16b4a8c
--- /dev/null
+++ b/app/src/main/assets/layouts/main/sinhala.json
@@ -0,0 +1,132 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ූ" },
+ "default": { "label": "ු" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "උ" },
+ "default": { "label": "අ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ෑ" },
+ "default": { "label": "ැ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඍ" },
+ "default": { "label": "ර" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඔ" },
+ "default": { "label": "එ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ශ" },
+ "default": { "label": "හ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඹ" },
+ "default": { "label": "ම" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ෂ" },
+ "default": { "label": "ස" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ධ" },
+ "default": { "label": "ද" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඡ" },
+ "default": { "label": "ච" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඥ" },
+ "default": { "label": "ඤ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ෟ" },
+ "default": { "label": "්" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ී" },
+ "default": { "label": "ි" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ෘ" },
+ "default": { "label": "ා" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ෆ" },
+ "default": { "label": "ෙ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඨ" },
+ "default": { "label": "ට" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "්ය" },
+ "default": { "label": "ය" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ළු" },
+ "default": { "label": "ව" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ණ" },
+ "default": { "label": "න" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඛ" },
+ "default": { "label": "ක" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ථ" },
+ "default": { "label": "ත" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "්ර" },
+ "default": { "label": "ඏ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඞ" },
+ "default": { "label": "ං" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඣ" },
+ "default": { "label": "ජ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඪ" },
+ "default": { "label": "ඩ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඊ" },
+ "default": { "label": "ඉ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "භ" },
+ "default": { "label": "බ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඵ" },
+ "default": { "label": "ප" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ළ" },
+ "default": { "label": "ල" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ඝ" },
+ "default": { "label": "ග" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ර්" },
+ "default": { "label": "ෳ" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/tamil.txt b/app/src/main/assets/layouts/main/tamil.txt
new file mode 100644
index 0000000000..4020384f72
--- /dev/null
+++ b/app/src/main/assets/layouts/main/tamil.txt
@@ -0,0 +1,34 @@
+ஔ
+ஐ
+ஆ
+ஈ
+ஊ
+ம
+ன
+ந
+ங
+ண
+ஞ
+
+ஓ
+ஏ
+அ
+இ
+உ
+ற
+ப
+க
+த
+ச
+ட
+
+ஒ
+எ
+்
+ர
+வ
+ழ
+ல
+ள
+ய
+ஷ
diff --git a/app/src/main/assets/layouts/main/telugu.txt b/app/src/main/assets/layouts/main/telugu.txt
new file mode 100644
index 0000000000..64523060b1
--- /dev/null
+++ b/app/src/main/assets/layouts/main/telugu.txt
@@ -0,0 +1,34 @@
+ౌ
+ై
+ా
+ీ
+ూ
+బ
+హ
+గ
+ద
+జ
+డ
+
+ో
+ే
+్
+ి
+ు
+ప
+ర
+క
+త
+చ
+ట
+
+ొ
+ె
+మ
+న
+వ
+ల
+స
+ఋ
+ష
+య
diff --git a/app/src/main/assets/layouts/main/thai.json b/app/src/main/assets/layouts/main/thai.json
new file mode 100644
index 0000000000..eba1a89e3b
--- /dev/null
+++ b/app/src/main/assets/layouts/main/thai.json
@@ -0,0 +1,194 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "+", "labelFlags": 48 },
+ "default": { "label": "ๅ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๑" },
+ "default": { "label": "/", "labelFlags": 48, "popup": { "relevant": [ { "label": "1" }, { "label": "๑" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๒" },
+ "default": { "label": "_", "labelFlags": 48, "popup": { "relevant": [ { "label": "2" }, {"label": "๒" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๓" },
+ "default": { "label": "ภ", "popup": { "relevant": [ { "label": "3" }, { "label": "๓" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๔" },
+ "default": { "label": "ถ", "popup": { "relevant": [ { "label": "4" }, { "label": "๔" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ู|ู", "labelFlags": 128 },
+ "default": { "label": " ุ|ุ", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "฿" },
+ "default": { "label": " ึ|ึ", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๕" },
+ "default": { "label": "ค", "popup": { "relevant": [ { "label": "5" }, { "label": "๕" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๖" },
+ "default": { "label": "ต", "popup": { "relevant": [ { "label": "6" }, { "label": "๖" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๗" },
+ "default": { "label": "จ", "popup": { "relevant": [ { "label": "7" }, { "label": "๗" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๘" },
+ "default": { "label": "ข", "popup": { "relevant": [ { "label": "8" }, { "label": "๘" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๙" },
+ "default": { "label": "ช", "popup": { "relevant": [ { "label": "9" }, { "label": "๙" } ] } }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "๐" },
+ "default": { "label": "ๆ", "popup": { "relevant": [ { "label": "0" }, { "label": "๐" } ] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\"", "labelFlags": 48 },
+ "default": { "label": "ไ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฎ" },
+ "default": { "label": "ำ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฑ" },
+ "default": { "label": "พ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ธ" },
+ "default": { "label": "ะ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ํ|ํ", "labelFlags": 128 },
+ "default": { "label": " ั|ั", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ๊|๊", "labelFlags": 128 },
+ "default": { "label": " ี|ี", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ณ" },
+ "default": { "label": "ร" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฯ" },
+ "default": { "label": "น" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ญ" },
+ "default": { "label": "ย" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฐ" },
+ "default": { "label": "บ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ",", "labelFlags": 48 },
+ "default": { "label": "ล" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฤ" },
+ "default": { "label": "ฟ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฆ" },
+ "default": { "label": "ห" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฏ" },
+ "default": { "label": "ก" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "โ" },
+ "default": { "label": "ด" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฌ" },
+ "default": { "label": "เ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ็|็", "labelFlags": 128 },
+ "default": { "label": " ้|้", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ๋|๋", "labelFlags": 128 },
+ "default": { "label": " ่|่", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ษ" },
+ "default": { "label": "า" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ศ" },
+ "default": { "label": "ส" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ซ" },
+ "default": { "label": "ว" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ".", "labelFlags": 48 },
+ "default": { "label": "ง" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฅ" },
+ "default": { "label": "ฃ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "(", "labelFlags": 48 },
+ "default": { "label": "ผ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ")", "labelFlags": 48 },
+ "default": { "label": "ป" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฉ" },
+ "default": { "label": "แ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฮ" },
+ "default": { "label": "อ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ฺ|ฺ", "labelFlags": 128 },
+ "default": { "label": " ิ|ิ", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": " ์|์", "labelFlags": 128 },
+ "default": { "label": " ื|ื", "labelFlags": 128 }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "\\?", "labelFlags": 48 },
+ "default": { "label": "ท" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฒ" },
+ "default": { "label": "ม" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฬ" },
+ "default": { "label": "ใ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ฦ" },
+ "default": { "label": "ฝ" }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/turkish.txt b/app/src/main/assets/layouts/main/turkish.txt
new file mode 100644
index 0000000000..2da3d0d482
--- /dev/null
+++ b/app/src/main/assets/layouts/main/turkish.txt
@@ -0,0 +1,34 @@
+q
+w
+e
+r
+t
+y
+u
+ı
+o
+p
+ğ
+ü
+
+a
+s
+d
+f
+g
+h
+j
+k
+l
+ş
+i
+
+z
+x
+c
+v
+b
+n
+m
+ö
+ç
diff --git a/app/src/main/assets/layouts/main/ukrainian.txt b/app/src/main/assets/layouts/main/ukrainian.txt
new file mode 100644
index 0000000000..ff1ed643ab
--- /dev/null
+++ b/app/src/main/assets/layouts/main/ukrainian.txt
@@ -0,0 +1,34 @@
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х [ {
+ї ] }
+
+ф
+і
+в
+а
+п
+р
+о
+л
+д
+ж
+є ' "
+
+я
+ч
+с
+м
+и
+т
+ь
+б <
+ю > ґ
diff --git a/app/src/main/assets/layouts/main/ukrainian_extended.txt b/app/src/main/assets/layouts/main/ukrainian_extended.txt
new file mode 100644
index 0000000000..7e37e93cc4
--- /dev/null
+++ b/app/src/main/assets/layouts/main/ukrainian_extended.txt
@@ -0,0 +1,35 @@
+й
+ц
+у
+к
+е
+н
+г
+ш
+щ
+з
+х [ {
+ї ] }
+
+ф
+і
+в
+а
+п
+р
+о
+л
+д
+ж
+є ' "
+' "
+
+я
+ч
+с
+м
+и
+т
+ь
+б <
+ю > ґ
diff --git a/app/src/main/assets/layouts/main/urdu.json b/app/src/main/assets/layouts/main/urdu.json
new file mode 100644
index 0000000000..7bacf7d21b
--- /dev/null
+++ b/app/src/main/assets/layouts/main/urdu.json
@@ -0,0 +1,116 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ٔ" },
+ "default": { "label": "ق" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ْ" },
+ "default": { "label": "و" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ّ" },
+ "default": { "label": "ع" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ً" },
+ "default": { "label": "ر" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ٗ" },
+ "default": { "label": "ت" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ٖ" },
+ "default": { "label": "ے" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ٰ" },
+ "default": { "label": "ء" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ُ" },
+ "default": { "label": "ی" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ِ" },
+ "default": { "label": "ہ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "َ" },
+ "default": { "label": "پ" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "؏" },
+ "default": { "label": "ا" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "؎" },
+ "default": { "label": "س" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ؔ" },
+ "default": { "label": "د" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ؒ" },
+ "default": { "label": "ف" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ؓ" },
+ "default": { "label": "گ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ؑ" },
+ "default": { "label": "ح" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ؐ" },
+ "default": { "label": "ج" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ﷺ" },
+ "default": { "label": "ک" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ﷻ" },
+ "default": { "label": "ل" }
+ }
+ ],
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "ﷲ" },
+ "default": { "label": "َ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "" },
+ "default": { "label": "ز" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "" },
+ "default": { "label": "ش" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "" },
+ "default": { "label": "چ" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "؍" },
+ "default": { "label": "ط" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "٘" },
+ "default": { "label": "ب", "popup": { "main": { "label": "(" } } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "؟" },
+ "default": { "label": "ن" }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "٫" },
+ "default": { "label": "م", "popup": { "main": { "label": ")" } } }
+ }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/uzbek.json b/app/src/main/assets/layouts/main/uzbek.json
new file mode 100644
index 0000000000..a7bff22767
--- /dev/null
+++ b/app/src/main/assets/layouts/main/uzbek.json
@@ -0,0 +1,37 @@
+[
+ [
+ { "label": "q" },
+ { "label": "w" },
+ { "label": "e" },
+ { "label": "r" },
+ { "label": "t" },
+ { "label": "y" },
+ { "label": "u" },
+ { "label": "i" },
+ { "label": "o" },
+ { "label": "p" },
+ { "label": "oʻ", "labelFlags": 128 }
+ ],
+ [
+ { "label": "a" },
+ { "label": "s" },
+ { "label": "d" },
+ { "label": "f" },
+ { "label": "g" },
+ { "label": "h" },
+ { "label": "j" },
+ { "label": "k" },
+ { "label": "l" },
+ { "label": "gʻ", "labelFlags": 128 },
+ { "label": "ʼ" }
+ ],
+ [
+ { "label": "z" },
+ { "label": "x" },
+ { "label": "c" },
+ { "label": "v" },
+ { "label": "b" },
+ { "label": "n" },
+ { "label": "m" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/main/workman.txt b/app/src/main/assets/layouts/main/workman.txt
new file mode 100644
index 0000000000..be1d1d8e66
--- /dev/null
+++ b/app/src/main/assets/layouts/main/workman.txt
@@ -0,0 +1,29 @@
+q
+d
+r
+w
+b
+j
+f
+u
+p
+;
+
+a
+s
+h
+t
+g
+y
+n
+e
+o
+i
+
+z
+x
+m
+c
+v
+k
+l
diff --git a/app/src/main/assets/layouts/main/yoruba.txt b/app/src/main/assets/layouts/main/yoruba.txt
new file mode 100644
index 0000000000..3ee1696ddc
--- /dev/null
+++ b/app/src/main/assets/layouts/main/yoruba.txt
@@ -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
diff --git a/app/src/main/assets/layouts/more_symbols/symbols_shifted.txt b/app/src/main/assets/layouts/more_symbols/symbols_shifted.txt
new file mode 100644
index 0000000000..c707680a80
--- /dev/null
+++ b/app/src/main/assets/layouts/more_symbols/symbols_shifted.txt
@@ -0,0 +1,31 @@
+~
+`
+|
+• ♪ ♥ ♠ ♦ ♣
+√
+π Π
+÷
+×
+¶ §
+∆
+
+$$$1
+$$$2
+$$$3
+$$$4
+^ ↑ ↓ ← →
+° ′ ″
+= ≠ ⁼ ≈ ∞
+{
+}
+
+\
+©
+®
+™
+% ℅
+[
+]
+
+< !fixedColumnOrder!3 ‹ ≤ «
+> !fixedColumnOrder!3 › ≥ »
diff --git a/app/src/main/assets/layouts/number/number.json b/app/src/main/assets/layouts/number/number.json
new file mode 100644
index 0000000000..5ee29fcada
--- /dev/null
+++ b/app/src/main/assets/layouts/number/number.json
@@ -0,0 +1,49 @@
+[
+ [
+ { "label": "1", "type": "numeric" },
+ { "label": "2", "type": "numeric" },
+ { "label": "3", "type": "numeric" },
+ { "label": "-", "type": "function", "popup": { "main": { "label": "+" } }, "labelFlags": 64 }
+ ],
+ [
+ { "label": "4", "type": "numeric" },
+ { "label": "5", "type": "numeric" },
+ { "label": "6", "type": "numeric" },
+ { "label": "space", "type": "function" }
+ ],
+ [
+ { "label": "7", "type": "numeric" },
+ { "label": "8", "type": "numeric" },
+ { "label": "9", "type": "numeric" },
+ { "label": "delete" }
+ ],
+ [
+ { "$": "variation_selector",
+ "default": { "label": ",", "type": "numeric", "groupId": 1 },
+ "date": { "label": ".", "type": "numeric", "groupId": 1 },
+ "time": { "label": ".", "type": "numeric", "groupId": 1, "popup": { "relevant": [
+ { "label": "!fixedColumnOrder!2" },
+ { "label": "!hasLabels!" },
+ { "label": "AM" },
+ { "label": "PM" }
+ ] } },
+ "datetime": { "label": ".", "type": "numeric", "groupId": 1, "popup": { "relevant": [
+ { "label": "!fixedColumnOrder!2" },
+ { "label": "!hasLabels!" },
+ { "label": "AM" },
+ { "label": "PM" }
+ ] } }
+ },
+ { "label": "0", "type": "numeric" },
+ { "$": "variation_selector",
+ "default": { "label": ".", "type": "numeric" },
+ "date": { "label": "/", "type": "numeric" },
+ "time": { "label": ":", "type": "numeric" },
+ "datetime": { "label": "/ :|/", "type": "numeric", "popup": { "relevant": [
+ { "label": "!noPanelAutoPopupKey!" },
+ { "label": "," }
+ ] } }
+ },
+ { "label": "enter"}
+ ]
+]
diff --git a/app/src/main/assets/layouts/number_row/number_row.json b/app/src/main/assets/layouts/number_row/number_row.json
new file mode 100644
index 0000000000..3ad11861a3
--- /dev/null
+++ b/app/src/main/assets/layouts/number_row/number_row.json
@@ -0,0 +1,44 @@
+[
+ [
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "!" },
+ "default": { "label": "1", "popup": { "relevant": [{ "label": "¹" }, { "label": "½" }, { "label": "⅓" }, { "label": "¼" }, { "label": "⅛" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "@" },
+ "default": { "label": "2", "popup": { "relevant": [{ "label": "²" }, { "label": "⅔" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "#" },
+ "default": { "label": "3", "popup": { "relevant": [{ "label": "³" }, { "label": "¾" }, { "label": "⅜" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "$" },
+ "default": { "label": "4", "popup": { "relevant": [{ "label": "⁴" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "%" },
+ "default": { "label": "5", "popup": { "relevant": [{ "label": "⁵" }, { "label": "⅝" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "^" },
+ "default": { "label": "6", "popup": { "relevant": [{ "label": "⁶" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "&" },
+ "default": { "label": "7", "popup": { "relevant": [{ "label": "⁷" }, { "label": "⅞" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "*" },
+ "default": { "label": "8", "popup": { "relevant": [{ "label": "⁸" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": "(" },
+ "default": { "label": "9", "popup": { "relevant": [{ "label": "⁹" }] } }
+ },
+ { "$": "shift_state_selector",
+ "manualOrLocked": { "label": ")" },
+ "default": { "label": "0", "popup": { "relevant": [{ "label": "⁰" }, { "label": "ⁿ" }, { "label": "∅" }] } }
+ }
+ ]
+ ]
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/number_row/number_row_basic.txt b/app/src/main/assets/layouts/number_row/number_row_basic.txt
new file mode 100644
index 0000000000..709c30a8bb
--- /dev/null
+++ b/app/src/main/assets/layouts/number_row/number_row_basic.txt
@@ -0,0 +1,10 @@
+1 ¹ ½ ⅓ ¼ ⅛
+2 ² ⅔
+3 ³ ¾ ⅜
+4 ⁴
+5 ⁵ ⅝
+6 ⁶
+7 ⁷ ⅞
+8 ⁸
+9 ⁹
+0 ⁰ ⁿ ∅
\ No newline at end of file
diff --git a/app/src/main/assets/layouts/numpad/numpad.json b/app/src/main/assets/layouts/numpad/numpad.json
new file mode 100644
index 0000000000..d557932338
--- /dev/null
+++ b/app/src/main/assets/layouts/numpad/numpad.json
@@ -0,0 +1,50 @@
+[
+ [
+ { "label": "+", "type": "function", "popup": {
+ "relevant": [
+ { "label": "(" },
+ { "label": "<" },
+ { "label": "±" }
+ ]
+ }, "labelFlags": 512 },
+ { "label": "1", "type": "numeric" },
+ { "label": "2", "type": "numeric" },
+ { "label": "3", "type": "numeric" },
+ { "label": "%", "type": "function", "popup": { "main": { "label": "$$$"} }, "labelFlags": 512 }
+ ],
+ [
+ { "label": "-", "type": "function", "popup": {
+ "relevant": [
+ { "label": ")" },
+ { "label": ">" },
+ { "label": "~" }
+ ]
+ }, "labelFlags": 512 },
+ { "label": "4", "type": "numeric" },
+ { "label": "5", "type": "numeric" },
+ { "label": "6", "type": "numeric" },
+ { "label": "space", "type": "function" }
+ ],
+ [
+ { "label": "*", "type": "function", "popup": {
+ "relevant": [
+ { "label": "/" },
+ { "label": "×" },
+ { "label": "÷" }
+ ]
+ }, "labelFlags": 512 },
+ { "label": "7", "type": "numeric" },
+ { "label": "8", "type": "numeric" },
+ { "label": "9", "type": "numeric" },
+ { "label": "delete" }
+ ],
+ [
+ { "label": "alpha" },
+ { "label": "comma", "width": 0.1 },
+ { "label": "symbol", "width": 0.12 },
+ { "label": "0", "type": "numeric" },
+ { "label": "=", "type": "function", "width": 0.12, "popup": { "relevant": [ { "label": "≠"}, { "label": "≈"} ] } },
+ { "label": "period", "width": 0.1 },
+ { "label": "action" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/numpad_landscape/numpad_landscape.json b/app/src/main/assets/layouts/numpad_landscape/numpad_landscape.json
new file mode 100644
index 0000000000..19f3cff024
--- /dev/null
+++ b/app/src/main/assets/layouts/numpad_landscape/numpad_landscape.json
@@ -0,0 +1,46 @@
+[
+ [
+ { "label": "(", "type": "function", "popup": { "relevant": [ { "label": "[" }, { "label": "{" } ] } },
+ { "label": ")", "type": "function", "popup": { "relevant": [ { "label": "]" }, { "label": "}" } ] } },
+ { "label": ":", "type": "function" },
+ { "label": "1", "type": "numeric", "width": 0.3 },
+ { "label": "2", "type": "numeric", "width": 0.3 },
+ { "label": "3", "type": "numeric", "width": 0.3 },
+ { "label": "+", "type": "function", "popup": { "main": { "label": "±" } } },
+ { "label": "-", "type": "function", "popup": { "main": { "label": "~" } } },
+ { "label": "space", "type": "function" }
+ ],
+ [
+ { "label": "!", "type": "function" },
+ { "label": "?", "type": "function" },
+ { "label": ";", "type": "function" },
+ { "label": "4", "type": "numeric", "width": 0.3 },
+ { "label": "5", "type": "numeric", "width": 0.3 },
+ { "label": "6", "type": "numeric", "width": 0.3 },
+ { "label": "*", "type": "function", "popup": { "main": { "label": "×" } } },
+ { "label": "/", "type": "function", "popup": { "main": { "label": "÷" } } },
+ { "label": "delete" }
+ ],
+ [
+ { "label": "|", "type": "function" },
+ { "label": "$$$", "type": "function" },
+ { "label": "&", "type": "function" },
+ { "label": "7", "type": "numeric", "width": 0.3 },
+ { "label": "8", "type": "numeric", "width": 0.3 },
+ { "label": "9", "type": "numeric", "width": 0.3 },
+ { "label": "#", "type": "function" },
+ { "label": "%", "type": "function", "popup": { "main": { "label": "‰" } } },
+ { "label": "action" }
+ ],
+ [
+ { "label": "alpha" },
+ { "label": "<", "type": "function", "popup": { "main": { "label": "≤" } } },
+ { "label": ">", "type": "function", "popup": { "main": { "label": "≥" } } },
+ { "label": "comma", "type": "numeric", "width": 0.3 },
+ { "label": "0", "type": "numeric", "width": 0.3 },
+ { "label": "period", "type": "numeric", "width": 0.3 },
+ { "label": "=", "type": "function", "popup": { "relevant": [ { "label": "≠"}, { "label": "≈"} ] } },
+ { "label": "=", "type": "function", "popup": { "relevant": [ { "label": "≠"}, { "label": "≈"} ] } },
+ { "label": "symbol" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/phone/phone.json b/app/src/main/assets/layouts/phone/phone.json
new file mode 100644
index 0000000000..b7a87cc439
--- /dev/null
+++ b/app/src/main/assets/layouts/phone/phone.json
@@ -0,0 +1,26 @@
+[
+ [
+ { "label": "1", "type": "numeric" },
+ { "label": "2", "type": "numeric", "popup": { "main": { "label": "ABC" } } },
+ { "label": "3", "type": "numeric", "popup": { "main": { "label": "DEF" } } },
+ { "label": "-", "type": "function", "labelFlags": 1073742400, "popup": { "main": { "label": "+" } } }
+ ],
+ [
+ { "label": "4", "type": "numeric", "popup": { "main": { "label": "GHI" } } },
+ { "label": "5", "type": "numeric", "popup": { "main": { "label": "JKL" } } },
+ { "label": "6", "type": "numeric", "popup": { "main": { "label": "MNO" } } },
+ { "label": "space", "type": "function" }
+ ],
+ [
+ { "label": "7", "type": "numeric", "popup": { "main": { "label": "PQRS" } } },
+ { "label": "8", "type": "numeric", "popup": { "main": { "label": "TUV" } } },
+ { "label": "9", "type": "numeric", "popup": { "main": { "label": "WXYZ" } } },
+ { "label": "delete" }
+ ],
+ [
+ { "label": "*#|!code/key_switch_alpha_symbol", "type": "numeric", "labelFlags": 524432 },
+ { "label": "0 +|0", "type": "numeric", "popup": { "relevant": [ { "label": "!noPanelAutoPopupKey!" }, { "label": "+" } ] } },
+ { "label": ".", "type": "numeric", "labelFlags": 64, "groupId": 1 },
+ { "label": "action" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/phone_symbols/phone_symbols.json b/app/src/main/assets/layouts/phone_symbols/phone_symbols.json
new file mode 100644
index 0000000000..03ed559627
--- /dev/null
+++ b/app/src/main/assets/layouts/phone_symbols/phone_symbols.json
@@ -0,0 +1,26 @@
+[
+ [
+ { "label": "(", "type": "numeric" },
+ { "label": "/", "type": "numeric" },
+ { "label": ")", "type": "numeric" },
+ { "label": "-", "type": "function", "labelFlags": 1073742400, "popup": { "main": { "label": "+" } } }
+ ],
+ [
+ { "label": "N", "type": "numeric" },
+ { "label": "!string/label_pause_key", "code": 44, "type": "numeric" },
+ { "label": ",", "type": "numeric" },
+ { "label": "space", "type": "function" }
+ ],
+ [
+ { "label": "*|*", "type": "numeric" },
+ { "label": "!string/label_wait_key", "code": 59, "type": "numeric" },
+ { "label": "\\#", "type": "numeric" },
+ { "label": "delete" }
+ ],
+ [
+ { "label": "123|!code/key_switch_alpha_symbol", "type": "numeric", "labelFlags": 524432 },
+ { "label": "+", "type": "numeric" },
+ { "label": ".", "type": "numeric", "groupId": 1 },
+ { "label": "action" }
+ ]
+]
diff --git a/app/src/main/assets/layouts/symbols/symbols.txt b/app/src/main/assets/layouts/symbols/symbols.txt
new file mode 100644
index 0000000000..58f665b4e3
--- /dev/null
+++ b/app/src/main/assets/layouts/symbols/symbols.txt
@@ -0,0 +1,29 @@
+% ‰
+\
+|
+=
+[
+]
+<
+>
+{
+}
+
+@
+#
+$$$
+_ % ‰
+&
+- – ⁻ — ·
++ ± ⁺
+( ⁽ < { [
+) ⁾ > } ]
+/
+
+* † ‡ ★
+"
+'
+:
+;
+!
+?
diff --git a/app/src/main/assets/layouts/symbols/symbols_arabic.txt b/app/src/main/assets/layouts/symbols/symbols_arabic.txt
new file mode 100644
index 0000000000..43e40d2bd9
--- /dev/null
+++ b/app/src/main/assets/layouts/symbols/symbols_arabic.txt
@@ -0,0 +1,28 @@
+٪ % ‰
+\
+|
+=
+[
+]
+<
+>
+﴾ {
+﴿ {
+
+٬ @
+٫ #
+$$$
+_ ٪ % ‰
+&
+- – — ·
++ ±
+( ﴾ < { [
+) ﴿ > } ]
+
+* ٭ ★ † ‡
+«
+»
+:
+؛ ;
+!
+؟
diff --git a/app/src/main/assets/locale_key_texts/af.txt b/app/src/main/assets/locale_key_texts/af.txt
new file mode 100644
index 0000000000..556e2290be
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/af.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+a á â ä à æ ã å ā
+e é è ê ë ę ė ē
+i í ì ï î į ī ij
+o ó ô ö ò õ œ ø ō
+u ú û ü ù ū
+n ñ ń
+y ý ij
+
+[tlds]
+za
diff --git a/app/src/main/assets/locale_key_texts/ar.txt b/app/src/main/assets/locale_key_texts/ar.txt
new file mode 100644
index 0000000000..5c4b3c0073
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ar.txt
@@ -0,0 +1,29 @@
+[popup_keys]
+ق ڨ
+ف ڤ ڢ ڥ
+ه ﻫ|ه
+ج چ
+ش ڜ
+ي ئ ى
+ب پ
+ل ﻻ|لا ﻷ|لأ ﻹ|لإ ﻵ|لآ
+ا !fixedColumnOrder!5 آ ء أ إ ٱ
+ك گ ک
+ى ئ
+ز ژ
+و ؤ
+punctuation !fixedColumnOrder!7 ٕ|ٕ ٔ|ٔ ْ|ْ ٍ|ٍ ٌ|ٌ ً|ً ّ|ّ ٖ|ٖ ٰ|ٰ ٓ|ٓ ِ|ِ ُ|ُ َ|َ ـــ|ـ
+« „ “ ”
+» ‚ ‘ ’ ‹ ›
+
+[labels]
+alphabet: أبج
+symbol: ٣٢١؟
+comma: ،
+question: ؟
+
+[number_row]
+١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٠
+
+[tlds]
+sa
diff --git a/app/src/main/assets/locale_key_texts/az.txt b/app/src/main/assets/locale_key_texts/az.txt
new file mode 100644
index 0000000000..d5ca568a61
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/az.txt
@@ -0,0 +1,9 @@
+[popup_keys]
+e ə
+i ı
+ı i
+o ö
+u ü
+s ş
+g ğ
+c ç
diff --git a/app/src/main/assets/locale_key_texts/be.txt b/app/src/main/assets/locale_key_texts/be.txt
new file mode 100644
index 0000000000..7c8edcf877
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/be.txt
@@ -0,0 +1,8 @@
+[popup_keys]
+е ё
+ь ъ
+' ’ ‚ ‘
+" ” „ “
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/bg.txt b/app/src/main/assets/locale_key_texts/bg.txt
new file mode 100644
index 0000000000..40924464ce
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/bg.txt
@@ -0,0 +1,8 @@
+[popup_keys]
+и ѝ
+
+[labels]
+alphabet: АБВ
+
+[tlds]
+bg
diff --git a/app/src/main/assets/locale_key_texts/bn-BD.txt b/app/src/main/assets/locale_key_texts/bn-BD.txt
new file mode 100644
index 0000000000..fe5d56d4de
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/bn-BD.txt
@@ -0,0 +1,41 @@
+[popup_keys]
+ঙ ং
+য য়
+ড ঢ
+প ফ
+ট ঠ
+চ ছ
+জ ঝ
+হ ঞ
+গ ঘ
+ড় ঢ়
+ৃ ঋ
+ু উ
+ূ ঊ
+ি ই
+ী ঈ
+া আ অ
+্ ঁ
+ঁ !autoColumnOrder!6 ় ৄ ঽ ৢ ৱ ৣ ৗ ৠ ৺ ঌ ৰ ৡ
+ব ভ
+ক খ
+ত থ ৎ
+দ ধ
+্র ্য
+ো ও
+ৌ ঔ
+ে এ
+ৈ ঐ
+র ল র্য
+ন ণ
+স ষ
+ম শ
+punctuation !autoColumnOrder!8 \, ॥ ? ! !icon/zwnj_key| !icon/zwj_key| # @ ( ) / ; : - + \%
+
+[labels]
+alphabet: কখগ
+symbol: ?১২৩
+period: ।
+
+[number_row]
+১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ ০
diff --git a/app/src/main/assets/locale_key_texts/bn-IN.txt b/app/src/main/assets/locale_key_texts/bn-IN.txt
new file mode 100644
index 0000000000..f32ed305ff
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/bn-IN.txt
@@ -0,0 +1,41 @@
+[popup_keys]
+ৌ ঔ
+ৈ ঐ
+া আ
+ী ঈ
+ূ ঊ
+ব ভ
+হ ঙ
+গ ঘ
+দ ধ
+জ ঝ
+ড ঢ
+ো ও
+ে এ
+্ অ
+ি ই
+ু উ
+প ফ
+র ড় র্য
+ক খ
+ত থ ৎ
+চ ছ
+ট ঠ
+ৃ ঋ
+ং ঁ ঃ
+ঁ !autoColumnOrder!6 ় ৄ ঽ ৢ ৱ ৣ ৗ ৠ ৺ ঌ ৰ ৡ
+ম ণ
+ন ঞ
+ব ঢ়
+ল ষ
+স শ
+য় য
+punctuation !autoColumnOrder!8 \, ॥ ? ! !icon/zwnj_key| !icon/zwj_key| # @ ( ) / ; : - + \%
+
+[labels]
+alphabet: কখগ
+symbol: ?১২৩
+period: ।
+
+[number_row]
+১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ ০
diff --git a/app/src/main/assets/locale_key_texts/ca.txt b/app/src/main/assets/locale_key_texts/ca.txt
new file mode 100644
index 0000000000..619481bcd8
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ca.txt
@@ -0,0 +1,15 @@
+[popup_keys]
+a à
+e è é
+i í ï
+o ò ó
+u ú ü
+c ç
+l l·l
+punctuation !autoColumnOrder!9 \, ? ! · # ) ( / ; ' @ : - " + \% &
+
+[extra_keys]
+2: ç
+
+[tlds]
+cat es
diff --git a/app/src/main/assets/locale_key_texts/ckb.txt b/app/src/main/assets/locale_key_texts/ckb.txt
new file mode 100644
index 0000000000..5e13897b85
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ckb.txt
@@ -0,0 +1,39 @@
+[popup_keys]
+ق ٯ
+و وو
+ە ة ﻪ ـہ
+ر ڕ ڒ ࢪ
+ت ط
+ی ي ې ۍ
+ێ ؽ
+ئ ء ﺋ
+ۆ ؤ ۏ ۊ ۋ ۉ ۇ
+پ ث
+ا أ إ آ ٱ
+س ص
+ش ض
+د ۮ ڌ ﮆ
+ف ڤ ڡ
+ھ ھ
+ژ ━|ـ
+ل ڵ
+ک ك ڪ
+گ غ
+ز ظ
+ع ؏
+ب ى
+punctuation !autoColumnOrder!8 \؟ ! ، ٫ ؍ : ؛ ; : | - @ _ # * ٪ & ^
+« „ “ ”
+» ‚ ‘ ’ ‹ ›
+
+[labels]
+alphabet: ئپگ
+symbol: ٣٢١؟
+comma: ،
+question: ؟
+
+[number_row]
+١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٠
+
+[tlds]
+iq krd
diff --git a/app/src/main/assets/locale_key_texts/cs.txt b/app/src/main/assets/locale_key_texts/cs.txt
new file mode 100644
index 0000000000..a34abb4176
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/cs.txt
@@ -0,0 +1,19 @@
+[popup_keys]
+a á
+c č
+d ď
+e é ě
+i í
+n ň
+o ó
+r ř
+s š
+t ť
+u ú ů
+y ý
+z ž
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[tlds]
+cz
diff --git a/app/src/main/assets/locale_key_texts/cv.txt b/app/src/main/assets/locale_key_texts/cv.txt
new file mode 100644
index 0000000000..53fa6e14cb
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/cv.txt
@@ -0,0 +1,20 @@
+[popup_keys]
+" « » „ “ ”
+у у́ ү ӯ
+к қ
+е е́ ә
+н ң
+г ғ
+х ҳ
+ы ы́
+а а́
+о о́ ө
+ж җ
+э э́ є
+я я́
+ч ҷ
+и и́ і ӣ
+ю ю́
+
+[labels]
+alphabet: АБВ
\ No newline at end of file
diff --git a/app/src/main/assets/locale_key_texts/da.txt b/app/src/main/assets/locale_key_texts/da.txt
new file mode 100644
index 0000000000..17094de22a
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/da.txt
@@ -0,0 +1,14 @@
+[popup_keys]
+a å æ
+e é
+o ø
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[extra_keys]
+1: å
+2: æ ä
+2: ø ö
+
+[tlds]
+dk
diff --git a/app/src/main/assets/locale_key_texts/de-CH.txt b/app/src/main/assets/locale_key_texts/de-CH.txt
new file mode 100644
index 0000000000..6e452594d5
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/de-CH.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+a ä
+o ö
+u ü
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[extra_keys]
+1: ü è
+2: ö é
+2: ä à
diff --git a/app/src/main/assets/locale_key_texts/de-DE.txt b/app/src/main/assets/locale_key_texts/de-DE.txt
new file mode 100644
index 0000000000..c81f96333b
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/de-DE.txt
@@ -0,0 +1,13 @@
+[popup_keys]
+a ä
+o ö
+u ü
+s ß
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[extra_keys]
+1: ü
+2: ö
+2: ä
+3: ß
diff --git a/app/src/main/assets/locale_key_texts/de.txt b/app/src/main/assets/locale_key_texts/de.txt
new file mode 100644
index 0000000000..0cc11904f1
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/de.txt
@@ -0,0 +1,10 @@
+[popup_keys]
+a ä
+o ö
+u ü
+s ß
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[tlds]
+de at ch
diff --git a/app/src/main/assets/locale_key_texts/el.txt b/app/src/main/assets/locale_key_texts/el.txt
new file mode 100644
index 0000000000..0dd59534e2
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/el.txt
@@ -0,0 +1,14 @@
+[popup_keys]
+ε έ
+υ ύ ϋ ΰ
+ι ί ϊ ΐ
+ο ό
+α ά
+η ή
+ω ώ
+
+[labels]
+alphabet: ΑΒΓ
+
+[tlds]
+gr
diff --git a/app/src/main/assets/locale_key_texts/eo.txt b/app/src/main/assets/locale_key_texts/eo.txt
new file mode 100644
index 0000000000..f2fda7e40f
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/eo.txt
@@ -0,0 +1,7 @@
+[popup_keys]
+j ĵ
+u ŭ
+s ŝ
+c ĉ
+g ĝ
+h ĥ
diff --git a/app/src/main/assets/locale_key_texts/es.txt b/app/src/main/assets/locale_key_texts/es.txt
new file mode 100644
index 0000000000..bfaa35a947
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/es.txt
@@ -0,0 +1,15 @@
+[popup_keys]
+a á ª
+e é
+i í
+o ó º
+u ú ü
+n ñ
+y ý
+punctuation !autoColumnOrder!9 \, ? ! # ) ( / ; ¡ ' @ : - " + \% & ¿
+
+[extra_keys]
+2: ñ
+
+[tlds]
+es com.es
diff --git a/app/src/main/assets/locale_key_texts/et.txt b/app/src/main/assets/locale_key_texts/et.txt
new file mode 100644
index 0000000000..2ee735f5ac
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/et.txt
@@ -0,0 +1,17 @@
+[popup_keys]
+a ä
+o ö õ
+u ü
+s š
+z ž
+' ’ ‚ ‘
+" ” „ “
+
+[extra_keys]
+1: ü
+2: ö õ
+2: ä
+3: õ
+
+[tlds]
+ee
diff --git a/app/src/main/assets/locale_key_texts/eu.txt b/app/src/main/assets/locale_key_texts/eu.txt
new file mode 100644
index 0000000000..4cf01613c5
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/eu.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+a á ª
+e é
+i í
+o ó º
+u ú ü û
+n ñ
+c ç
+
+[extra_keys]
+2: ñ
diff --git a/app/src/main/assets/locale_key_texts/fa.txt b/app/src/main/assets/locale_key_texts/fa.txt
new file mode 100644
index 0000000000..3ce747ec4b
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/fa.txt
@@ -0,0 +1,22 @@
+[popup_keys]
+ه ﻫ|ه هٔ ة
+ی ئ ي ﯨ|ى
+ا !fixedColumnOrder!5 ٱ ء آ أ إ
+ت ة
+ک ك
+و ؤ
+punctuation !fixedColumnOrder!7 ٕ|ٕ ْ|ْ ّ|ّ ٌ|ٌ ٍ|ٍ ً|ً ٔ|ٔ ٖ|ٖ ٰ|ٰ ٓ|ٓ ُ|ُ ِ|ِ َ|َ ـــ|ـ
+« „ “ ”
+» ‚ ‘ ’ ‹ ›
+
+[labels]
+alphabet: ابپ
+symbol: ۳۲۱؟
+comma: ،
+question: ؟
+
+[number_row]
+۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۰
+
+[tlds]
+ir
diff --git a/app/src/main/assets/locale_key_texts/fi.txt b/app/src/main/assets/locale_key_texts/fi.txt
new file mode 100644
index 0000000000..287d329f89
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/fi.txt
@@ -0,0 +1,13 @@
+[popup_keys]
+a ä å
+o ö
+s š
+z ž
+
+[extra_keys]
+1: å
+2: ö ø
+2: ä æ
+
+[tlds]
+fi
diff --git a/app/src/main/assets/locale_key_texts/fr.txt b/app/src/main/assets/locale_key_texts/fr.txt
new file mode 100644
index 0000000000..02772665f9
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/fr.txt
@@ -0,0 +1,16 @@
+[popup_keys]
+a à â æ
+e é è ê ë
+i î ï
+o ô œ
+u ù û ü
+c ç
+y ÿ
+
+[extra_keys]
+1: è ü
+2: é ö
+2: à ä
+
+[tlds]
+fr
diff --git a/app/src/main/assets/locale_key_texts/gl.txt b/app/src/main/assets/locale_key_texts/gl.txt
new file mode 100644
index 0000000000..421b3f706c
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/gl.txt
@@ -0,0 +1,10 @@
+[popup_keys]
+a á ª
+e é
+i í
+o ó º
+u ú ü
+n ñ
+
+[extra_keys]
+2: ñ
diff --git a/app/src/main/assets/locale_key_texts/gu.txt b/app/src/main/assets/locale_key_texts/gu.txt
new file mode 100644
index 0000000000..9100cab2f1
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/gu.txt
@@ -0,0 +1,6 @@
+[number_row]
+૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯ ૦
+
+[labels]
+symbol: ?૧૨૩
+alphabet: કખગ
diff --git a/app/src/main/assets/locale_key_texts/hi-Latn.txt b/app/src/main/assets/locale_key_texts/hi-Latn.txt
new file mode 100644
index 0000000000..b77e1c8726
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/hi-Latn.txt
@@ -0,0 +1,2 @@
+[tlds]
+in
diff --git a/app/src/main/assets/locale_key_texts/hi.txt b/app/src/main/assets/locale_key_texts/hi.txt
new file mode 100644
index 0000000000..c30631dca3
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/hi.txt
@@ -0,0 +1,78 @@
+[popup_keys]
+औ ऒं
+ऐ ऐं
+आ आं आँ
+ई ईं
+ऊ ऊं ऊँ
+ध क्ष श्र
+ौ ौं
+ै ैं
+ा ां ाँ
+ी ीं
+ू ूं ूँ
+ब ब॒
+ग ज्ञ ग़ ग॒
+ज ज॒ ज्ञ ज़
+ड ड॒ ड़
+ओ ओं ऑ ऒ
+ए एं एँ ऍ ऎ
+अ अं अँ
+इ इं इँ
+उ उं उँ
+फ फ़
+ऱ ्र र्
+ख ख़
+ो ों ॉ ॊ
+े ें
+ि िं
+ु ुं ुँ
+र ऋ ऱ ॠ
+क क़
+त त्र
+ँ ॅ
+ळ ऴ
+ृ ॄ
+म ॐ
+न ञ ङ ऩ
+ल ऌ ॡ
+य य़
+़ ॽ ॰ ऽ
+punctuation !autoColumnOrder!9 \, . ॥ ? ! !icon/zwnj_key| !icon/zwj_key| # @ ( ) / ; : - + \%
+औ ौ
+ऐ ै
+आ ा
+ई ी
+ऊ ू
+ब भ
+ग घ
+द ध
+ज झ ज्ञ
+ड ढ
+ओ ो
+ए े
+अ ्
+इ ि
+उ ु
+प फ
+र ऋ ृ
+क ख
+त थ त्र
+च छ
+ट ठ
+् ॅ ऍ
+ं ः ँ ़
+म ॐ
+न ण ञ ङ
+स श ष श्र
+ऑ ॉ
+
+[labels]
+alphabet: कखग
+symbol: ?१२३
+period: ।
+
+[number_row]
+१ २ ३ ४ ५ ६ ७ ८ ९ ०
+
+[tlds]
+in
diff --git a/app/src/main/assets/locale_key_texts/hr.txt b/app/src/main/assets/locale_key_texts/hr.txt
new file mode 100644
index 0000000000..dbe04049c0
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/hr.txt
@@ -0,0 +1,10 @@
+[popup_keys]
+s š
+z ž
+c č ć
+d đ
+' ‘ ‚ ’ › ‹
+" “ „ ” » «
+
+[tlds]
+hr
diff --git a/app/src/main/assets/locale_key_texts/hu.txt b/app/src/main/assets/locale_key_texts/hu.txt
new file mode 100644
index 0000000000..423b451493
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/hu.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+a á
+e é
+i í
+o ó ö ő
+u ú ü ű
+' ‘ ‚ ’ › ‹
+" “ „ ” » «
+
+[tlds]
+hu gov.hu
diff --git a/app/src/main/assets/locale_key_texts/hy.txt b/app/src/main/assets/locale_key_texts/hy.txt
new file mode 100644
index 0000000000..66b3f3443e
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/hy.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+punctuation !autoColumnOrder!8 \, ՞ ՜ ․ … ' = / ՝ ՛ ֊ » « ― ) (
+? ՞ ¿
+! ՜ ¡
+
+[labels]
+alphabet: ԱԲԳ
+period: ։
+
+[tlds]
+am
diff --git a/app/src/main/assets/locale_key_texts/is.txt b/app/src/main/assets/locale_key_texts/is.txt
new file mode 100644
index 0000000000..4474bc1669
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/is.txt
@@ -0,0 +1,14 @@
+[popup_keys]
+a á æ
+d ð
+e é
+i í
+o ó ö
+u ú
+y ý
+t þ
+' ’ ‚ ‘
+" ” „ “
+
+[tlds]
+is
diff --git a/app/src/main/assets/locale_key_texts/it.txt b/app/src/main/assets/locale_key_texts/it.txt
new file mode 100644
index 0000000000..522981b9e7
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/it.txt
@@ -0,0 +1,9 @@
+[popup_keys]
+a à ª
+e è é ə ɜ
+i ì
+o ò ó º
+u ù
+
+[tlds]
+it gov.it edu.it
diff --git a/app/src/main/assets/locale_key_texts/iw.txt b/app/src/main/assets/locale_key_texts/iw.txt
new file mode 100644
index 0000000000..781fe76b4c
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/iw.txt
@@ -0,0 +1,15 @@
+[popup_keys]
++ ﬩
+ג ג׳
+י ײַ
+ח ח׳
+ז ז׳
+צ צ׳
+ת ת׳
+ץ ץ׳
+
+[labels]
+alphabet: אבג
+
+[tlds]
+il co.il gov.il
diff --git a/app/src/main/assets/locale_key_texts/ka.txt b/app/src/main/assets/locale_key_texts/ka.txt
new file mode 100644
index 0000000000..5503b53fab
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ka.txt
@@ -0,0 +1,17 @@
+[popup_keys]
+ე ჱ
+ყ ჸ
+ი ჲ
+ა ჺ
+ფ ჶ
+გ ჹ
+ჰ ჵ
+ჯ ჷ
+ხ ჴ
+ვ ჳ
+ნ ჼ
+' ’ ‚ ‘
+" ” „ “
+
+[labels]
+alphabet: აბგ
diff --git a/app/src/main/assets/locale_key_texts/kab.txt b/app/src/main/assets/locale_key_texts/kab.txt
new file mode 100644
index 0000000000..34df721241
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/kab.txt
@@ -0,0 +1,18 @@
+[popup_keys]
+a ɛ
+z ẓ
+r ṛ
+t ṭ
+u o
+s ṣ
+d ḍ
+g ǧ
+h ḥ
+c č
+b p
+
+[labels]
+alphabet: AƐΓ
+
+[tlds]
+dz fr
diff --git a/app/src/main/assets/locale_key_texts/kk.txt b/app/src/main/assets/locale_key_texts/kk.txt
new file mode 100644
index 0000000000..3a002d98e9
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/kk.txt
@@ -0,0 +1,14 @@
+[popup_keys]
+у ү ұ
+к қ
+е ё
+н ң
+г ғ
+а ә
+о ө
+ь ъ
+ы і
+э һ
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/km.txt b/app/src/main/assets/locale_key_texts/km.txt
new file mode 100644
index 0000000000..6ebd1f31c4
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/km.txt
@@ -0,0 +1,38 @@
+[popup_keys]
+! !icon/zwj_key|
+ៗ !icon/zwnj_key|
+" ៑
+៛ $ €
+% ៖
+៍ ៙
+័ ៚
+៏ *
+( { «
+) } »
+៌ ×
+១ ៱
+២ ៲
+៣ ៳
+៤ ៴
+៥ ៵
+៦ ៶
+៧ ៷
+៨ ៸
+៩ ៹
+០ ៰
+ឥ ឦ
+ឲ ឱ
+ឈ ៜ
+ឺ ៝
+ឬ ឫ
+ឪ ឧ ឱ ឳ ឩ ឨ
+ះ ៈ
+គ ឝ
+ឮ ឭ ឰ
+ព ឞ
+
+Process finished with exit code 0
+
+
+[labels]
+alphabet: កខគ
diff --git a/app/src/main/assets/locale_key_texts/kn.txt b/app/src/main/assets/locale_key_texts/kn.txt
new file mode 100644
index 0000000000..237a2c4ad1
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/kn.txt
@@ -0,0 +1,59 @@
+[popup_keys]
+ಅ
+ಆ ಾ
+ಇ ಿ
+ಈ ೀ
+ಉ ು
+ಊ ೂ
+ಋ ೄ ೃ ೠ
+ಎ ೆ
+ಏ ೇ
+ಐ ೖ ೈ
+ಒ ೊ
+ಓ ೋ
+ಔ ೌ
+ಂ ಽ ಼ ಃ ೕ
+ಕ ಕ್ಮ ಕ್ಗ ಕ್ಬ ಕ್ಟ್ರ ಕ್ಷ್ಯ ಕ್ಸ ಕ್ನ ಕ್ವ ಕ್ಲ ಕ್ಯ ಕ್ಷ್ಮ ಕ್ಟ ಕ್ತ ಕ್ಕ ಕ್ಷ ಕ್ರ
+ಖ ಖ್ಹ ಖ್ಖ ಖ್ಗಖ್ಯ ಖ್ವ ಖ್ತ
+ಗ ಗ್ಳ ಗ್ಲ್ಯ ಗ್ಸ ಗ್ರ್ಯ ಗ್ಬ ಗ್ಡ ಗ್ದ ಗ್ತ ಗ್ಧ ಗ್ವ ಗ್ನ ಗ್ಯ ಗ್ಗ ಗ್ರ ಗ್ಲ
+ಘ ಘ್ರ ಘ್ನ
+ಙ ಙ್ಮ
+ಚ ಚ್ಕ ಚ್ಮಚ್ಸ ಚ್ಗ ಚ್ಪ ಚ್ಛ್ರ ಚ್ಹ ಚ್ನ ಚ್ತ ಚ್ಡ ಚ್ಯ ಚ್ಛ ಚ್ಚ
+
+ಛ ಛ್ಘ
+ಜ ಜ್ಪ ಜ್ಗ ಜ್ಬ ಜ್ರ ಜ್ಕ ಜ್ಮ ಜ್ನ ಜ್ಯ ಜ್ಞ ಜ್ಜ ಜ್ವ
+ಝ ಝ್ಕ ಝ್ಡ
+ಞ ಞ್ಞ
+ಟ ಟ್ಜ ಟ್ದ ಟ್ಪ ಟ್ಮ ಟ್ರ್ಯ ಟ್ಸ್ಪ ಟ್ಸ್ಮ ಟ್ಬ ಟ್ಸ ಟ್ಲ ಟ್ರ ಟ್ಗ ಟ್ನ ಟ್ವ ಟ್ಟ ಟ್ರ ಟ್ಯ
+ಠ ಠ್ಮ ಠ್ಯ ಠ್ಠ
+ಡ ಡ್ರ್ಯ ಡ್ಶ ಡ್ಸ ಡ್ಳ ಡ್ಬ ಡ್ವ ಡ್ಮ ಡ್ದ ಡ್ಲ ಡ್ಕ ಡ್ನ ಡ್ಗ ಡ್ತ ಡ್ಡ ಡ್ರ ಡ್ಯ
+ಢ ಢ್ಯ
+ಣ ಣ್ಬ ಣ್ಕ ಣ್ನ ಣ್ಗ ಣ್ರ ಣ್ವ ಣ್ತ ಣ್ಮ ಣ್ಯ ಣ್ಣ
+ತ ತ್ಬ ತ್ಚ ತ್ದ ತ್ಗ ತ್ನ ತ್ವ ತ್ಕ ತ್ಲ ತ್ಪ್ರ ತ್ತ್ವ ತ್ರ್ಯ ತ್ಮ ತ್ಸ ತ್ಪ ತ್ಯ ತ್ರ ತ್ತ
+
+ಥ ಥ್ಯ ಥ್ರ ಥ್ಲ ಥ್ವ ಥ್ರ್ಯ
+ದ ದ್ರ್ಯ ದ್ಳ ದ್ನ ದ್ಮ ದ್ದ ದ್ರ ದ್ಗ ದ್ತ ದ್ಕ ದ್ಲ ದ್ಯ ದ್ಧ ದ್ಷ ದ್ಸ ದ್ಹ ದ್ಬ ದ್ವ
+ಧ ಧ್ಬ ಧ್ಪ ಧ್ಗ ಧ್ಭ ಧ್ಧ ಧ್ವ ಧ್ಯ ಧ್ರ
+ನ ನ್ದ ನ್ಪ ನ್ಡ ನ್ಶ ನ್ಫ ನ್ರ ನ್ಕ ನ್ಸ್ಟ ನ್ಗ ನ್ಲ ನ್ಟ ನ್ಸ್ಪ ನ್ಸ ನ್ಮ ನ್ನ ನ್ಯ ನ್ವ
+ಪ ಪ್ಣ ಪ್ಕ ಪ್ಮ ಪ್ಟ್ನ ಪ್ಗ ಪ್ವ ಪ್ಸ್ಟ ಪ್ಡ ಪ್ಸ ಪ್ಟ ಪ್ನ ಪ್ಲ್ಯ ಪ್ಯ ಪ್ತ ಪ್ರ ಪ್ಪ ಪ್ಲ
+ಫ ೞ ಫ್ಬ ಫ್ನ ಫ್ಸ ಫ್ಲ್ಯ ಫ್ಟ ಫ್ಘ ಫ್ಜ ಫ್ಲ ಫ್ಯ ಫ್ರ ಫ್ತ ಫ್ಟ್ವ
+ಬ ಬ್ತ ಬ್ರ್ಯ ಬ್ಗ ಬ್ಭ ಬ್ಜ ಬ್ನ ಬ್ವ ಬ್ಳ ಬ್ಲ್ಯ ಬ್ಲ ಬ್ಸ ಬ್ಧ ಬ್ಯ ಬ್ಬ ಬ್ರ ಬ್ದ
+ಭ ಭ್ಯ ಭ್ರ
+ಮ ಮ್ವ ಮ್ಡ ಮ್ಸ ಮ್ನ ಮ್ಮ ಮ್ಯ ಮ್ಗ ಮ್ಚ್ಯ ಮ್ತ ಮ್ಕ ಮ್ರ ಮ್ಹ ಮ್ಜ ಮ್ದ ಮ್ಚ ಮ್ಥ ಮ್ಲ
+ಯ ಯ್ಶ ಯ್ಲ್ಯ ಯ್ಜ ಯ್ಪ ಯ್ಬ ಯ್ಗ ಯ್ರ ಯ್ನ ಯ್ವ ಯ್ಡ ಯ್ಸ ಯ್ಲ ಯ್ತ ಯ್ದ ಯ್ಕ ಯ್ಯ
+
+ರ ರ್ಟ ರ್ಧ ರ್ಚ ರ್ಪ ಱ ರ್ಣ ರ್ದ ರ್ಗ ರ್ಭ ರ್ಜ ರ್ಷ ರ್ತ ರ್ಕ ರ್ನ ರ್ಶ ರ್ ರ್ಯ ರ್ಮ ರ್ಥ ರ್ವ
+ಲ ಲ್ಶ ಲ್ರ ಲ್ಜ ಲ್ಡ ಲ್ಫ ಲ್ಯ ಲ್ಲ ಲ್ಮ ಲ್ತ ಲ್ಬ ಲ್ಗ ಲ್ಪ ಲ್ಟ ಲ್ದ ಲ್ಕ ಲ್ಸ ಲ್ನ ಲ್ವ
+ವ ವ್ಮ ವ್ಡ ವ್ಶ ವ್ಚ ವ್ಕ ವ್ತ ವ್ಪ ವ್ಟ ವ್ಲ ವ್ಗ ವ್ಳ ವ್ದ ವ್ಹ ವ್ವ ವ್ಯ ವ್ರ ವ್ನ
+ಶ ಶ್ಶ ಶ್ಕ ಶ್ಚ ಶ್ನ ಶ್ಟ ಶ್ಬ ಶ್ಲ ಶ್ರ ಶ್ವ ಶ್ಮ ಶ್ಣ ಶ್ಟ್ರ ಶ್ಗ ಶ್ಯ
+ಷ ಷ್ರ ಷ್ಗ ಷ್ವ ಷ್ಬ ಷ್ಕ್ರ ಷ್ಟ್ಯ ಷ್ನ ಷ್ಪ್ರ ಷ್ಮ ಷ್ಠ ಷ್ಕ ಷ್ಪ ಷ್ಯ ಷ್ಟ ಷ್ಟ್ರ ಷ್ಣ
+ಸ ಸ್ಖ ಸ್ಗ ಸ್ಡ ಸ್ತ್ರ ಸ್ಟ್ರ ಸ್ಮ ಸ್ಲ ಸ್ರ ಸ್ನ ಸ್ಕ ಸ್ಪ ಸ್ಟ ಸ್ತ್ರ ಸ್ಸ ಸ್ವ ಸ್ಥ ಸ್ತ ಸ್ಯ
+ಹ ಹ್ಸ ಹ್ಞ ಹ್ರ ಹ್ಹ ಹ್ಲ ಹ್ತ ಹ್ಳ ಹ್ವ ಹ್ನ ಹ್ಮ ಹ್ಯ
+ಳ ಳ್ರ ಳ್ಲ ಳ್ಳ್ಯ ಳ್ನ ಳ್ದ ಳ್ಗ ಳ್ಕ ಳ್ಬ ಳ್ಮ ಳ್ಯ ಳ್ವ ಳ್ತ ಳ್ಳ
+
+[labels]
+alphabet: ಅಆಇ
+symbol: ?೧೨೩
+
+[number_row]
+೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯ ೦
diff --git a/app/src/main/assets/locale_key_texts/ko.txt b/app/src/main/assets/locale_key_texts/ko.txt
new file mode 100644
index 0000000000..9f7c4e37b9
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ko.txt
@@ -0,0 +1,5 @@
+[labels]
+alphabet: ㄱㄴㄷ
+
+[tlds]
+kr
diff --git a/app/src/main/assets/locale_key_texts/ky.txt b/app/src/main/assets/locale_key_texts/ky.txt
new file mode 100644
index 0000000000..a16a4c6128
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ky.txt
@@ -0,0 +1,9 @@
+[popup_keys]
+у ү
+е ё
+н ң
+о ө
+ь ъ
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/lo.txt b/app/src/main/assets/locale_key_texts/lo.txt
new file mode 100644
index 0000000000..c0b4f3ffce
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/lo.txt
@@ -0,0 +1,2 @@
+[labels]
+alphabet: ກຂຄ
diff --git a/app/src/main/assets/locale_key_texts/lt.txt b/app/src/main/assets/locale_key_texts/lt.txt
new file mode 100644
index 0000000000..d017e38127
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/lt.txt
@@ -0,0 +1,13 @@
+[popup_keys]
+a ą
+c č
+e ė ę
+i į
+s š
+u ū ų
+z ž
+' ’ ‚ ‘
+" ” „ “
+
+[tlds]
+lt
diff --git a/app/src/main/assets/locale_key_texts/lv.txt b/app/src/main/assets/locale_key_texts/lv.txt
new file mode 100644
index 0000000000..de243b16f8
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/lv.txt
@@ -0,0 +1,18 @@
+[popup_keys]
+a ā
+c č
+e ē
+g ģ
+i ī
+k ķ
+l ļ
+n ņ
+o ō
+s š
+u ū
+z ž
+' ’ ‚ ‘
+" ” „ “
+
+[tlds]
+lv
diff --git a/app/src/main/assets/locale_key_texts/mhr.txt b/app/src/main/assets/locale_key_texts/mhr.txt
new file mode 100644
index 0000000000..287860cb1f
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/mhr.txt
@@ -0,0 +1,8 @@
+[popup_keys]
+" « » „ “ ”
+н ҥ
+о ӧ
+у ӱ
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/mk.txt b/app/src/main/assets/locale_key_texts/mk.txt
new file mode 100644
index 0000000000..ef34576464
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/mk.txt
@@ -0,0 +1,8 @@
+[popup_keys]
+е ѐ
+и ѝ
+' ’ ‚ ‘
+" ” „ “
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/ml.txt b/app/src/main/assets/locale_key_texts/ml.txt
new file mode 100644
index 0000000000..a227c63a4f
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ml.txt
@@ -0,0 +1,36 @@
+[popup_keys]
+് അ
+ാ ആ
+ി ഇ
+ീ ഈ
+ു ഉ
+ൂ ഊ
+ൃ ഋ
+െ എ ഐ ൈ
+േ ഏ
+ൊ ഒ
+ോ ഓ ഔ ൗ
+ക ഖ
+ഗ ഘ
+ങ ഞ
+ച ഛ
+ജ ഝ
+ട ഠ
+ഡ ഢ
+ണ ൺ
+ത ഥ
+ദ ധ
+ന ൻ
+പ ഫ
+ബ ഭ
+മ ം
+യ ്യ
+ര ്ര ർ റ
+ല ൽ
+വ ്വ
+ശ ഷ സ
+ഹ ഃ
+ള ൾ ഴ
+
+[labels]
+alphabet: അ
diff --git a/app/src/main/assets/locale_key_texts/mn.txt b/app/src/main/assets/locale_key_texts/mn.txt
new file mode 100644
index 0000000000..90ef4f9caa
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/mn.txt
@@ -0,0 +1,8 @@
+[popup_keys]
+ш щ
+ё е
+ь ъ
+в ю
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/mns.txt b/app/src/main/assets/locale_key_texts/mns.txt
new file mode 100644
index 0000000000..9c36d5faae
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/mns.txt
@@ -0,0 +1,20 @@
+[popup_keys]
+ё ё̄
+у ӯ ӱ
+к қ
+е е̄
+н ӈ
+г ғ
+х ҳ
+ы ы̄
+а а̄ ӓ
+о о̄ ӧ
+ж җ
+э э̄
+я я̄
+ч ҷ
+и ӣ
+ю ю̄
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/more_popups_all.txt b/app/src/main/assets/locale_key_texts/more_popups_all.txt
new file mode 100644
index 0000000000..a66fe37fdb
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/more_popups_all.txt
@@ -0,0 +1,26 @@
+[popup_keys]
+a á â ä à ã æ å ā ą ª ă ả ạ ằ ắ ẳ ẵ ặ ầ ấ ẩ ẫ ậ
+e é è ê ë ē ė ę ě ə ẻ ẽ ẹ ề ế ể ễ ệ ĕ
+i í ì ï î ī į ı ij ĩ ỉ ị ĭ
+o ó ô ö ò õ œ ø ō º ő ỏ ọ ồ ố ổ ỗ ộ ơ ờ ớ ở ỡ ợ ŏ
+u ú ü ù ū û ů ű ų ũ µ ủ ụ ư ừ ứ ử ữ ự ŭ
+n ñ ń ň ņ ʼn ŋ
+y ý ÿ ij ŷ y þ ỳ ỷ ỹ ỵ
+s ß š ś ş ș ŝ ſ
+g ğ ģ ġ g\' ĝ
+c ç ć č ċ ĉ
+z ž ź ż
+l ł ĺ ļ ľ ŀ l·l
+punctuation !autoColumnOrder!10 \, ? ! # ) ( / ; ' @ : - " + \% & · ¡ ¿
+d ď đ ð
+r ř ŕ ŗ
+t ť ţ þ ț ŧ
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+k ķ ĸ
+v w ŵ
+h ĥ ħ
+w ŵ w
+q q
+x x
+j ĵ
diff --git a/app/src/main/assets/locale_key_texts/more_popups_main.txt b/app/src/main/assets/locale_key_texts/more_popups_main.txt
new file mode 100644
index 0000000000..a091434a5a
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/more_popups_main.txt
@@ -0,0 +1,9 @@
+[popup_keys]
+a à á â ä æ ã å ā
+e é è ê ë ē
+i í î ï ī ì
+o ó ô ö ò œ ø ō õ
+u ú û ü ù ū
+s ß
+n ñ
+c ç
diff --git a/app/src/main/assets/locale_key_texts/more_popups_more.txt b/app/src/main/assets/locale_key_texts/more_popups_more.txt
new file mode 100644
index 0000000000..c48b21ad4c
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/more_popups_more.txt
@@ -0,0 +1,22 @@
+[popup_keys]
+a á â ä à ã æ å ā ą ª ă
+e é è ê ë ē ė ę ě ə
+i í ì ï î ī į ı ij ĩ
+o ó ô ö ò õ œ ø ō º ő
+u ú ü ù ū û ů ű ų ũ
+n ñ ń ň ņ ʼn ŋ
+y ý ÿ ij ŷ
+s ß š ś ş ș
+g ğ ģ ġ
+c ç ć č ċ
+z ž ź ż
+l ł ĺ ļ ľ ŀ
+punctuation !autoColumnOrder!9 \, ? ! # ) ( / ; ' @ : - " + \% & ¡ ¿
+d ď đ ð
+r ř ŕ ŗ
+t ť ţ þ ț ŧ
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+k ķ ĸ
+h ĥ
+w ŵ
diff --git a/app/src/main/assets/locale_key_texts/mr.txt b/app/src/main/assets/locale_key_texts/mr.txt
new file mode 100644
index 0000000000..db6c8b72af
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/mr.txt
@@ -0,0 +1,35 @@
+[popup_keys]
+ौ औ
+ै ऐ
+ा आ
+ी ई
+ू ऊ
+ब भ
+ग घ
+द ध
+ज झ ज्ञ
+ड ढ
+ो ओ
+े ए
+् अ
+ि इ
+ु उ
+प फ
+र ऱ ऋ ृ
+क ख
+त थ त्र
+च छ
+ट ठ
+ॉ ऑ
+ॅ ऍ
+ं ः ँ
+न ण ञ ङ
+ल ळ
+स श ष श्र
+
+[labels]
+alphabet: कखग
+symbol: ?१२३
+
+[number_row]
+१ २ ३ ४ ५ ६ ७ ८ ९ ०
diff --git a/app/src/main/assets/locale_key_texts/mwl.txt b/app/src/main/assets/locale_key_texts/mwl.txt
new file mode 100644
index 0000000000..d7dbc881b9
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/mwl.txt
@@ -0,0 +1,7 @@
+[popup_keys]
+a á ª
+e é
+i í
+o ó º
+u ú ũ
+c ç
diff --git a/app/src/main/assets/locale_key_texts/my.txt b/app/src/main/assets/locale_key_texts/my.txt
new file mode 100644
index 0000000000..f670e805b4
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/my.txt
@@ -0,0 +1,6 @@
+[popup_keys]
+punctuation !autoColumnOrder!9 ၊ . ? ! # ) ( / ; ... ' @ : - " + \% &
+
+[labels]
+alphabet: ကခဂ
+period: ။
diff --git a/app/src/main/assets/locale_key_texts/nb.txt b/app/src/main/assets/locale_key_texts/nb.txt
new file mode 100644
index 0000000000..5b75b95c25
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/nb.txt
@@ -0,0 +1,14 @@
+[popup_keys]
+a å æ
+e é
+o ø
+' ‘ ‚ ’
+" ” „ “ « »
+
+[extra_keys]
+1: å
+2: ø ö
+2: æ ä
+
+[tlds]
+no
diff --git a/app/src/main/assets/locale_key_texts/ne.txt b/app/src/main/assets/locale_key_texts/ne.txt
new file mode 100644
index 0000000000..e89e7d14fd
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ne.txt
@@ -0,0 +1,27 @@
+[popup_keys]
+ट ़
+् ऽ
+punctuation !autoColumnOrder!9 . \, ? ! # ) ( / ; ' @ : - " + \% &
+त्त ञ ज्ञ ॥
+ड्ढ ई
+ऐ घ
+द्व द्ध
+ट्ट छ
+ठ्ठ ट
+ऊ ठ
+क्ष ड
+इ ढ
+ए ण
+ृ ओ
+इ औ
+ै श्र
+े ः ऽ
+र रु
+
+[labels]
+alphabet: कखग
+symbol: ?१२३
+period: ।
+
+[number_row]
+१ २ ३ ४ ५ ६ ७ ८ ९ ०
diff --git a/app/src/main/assets/locale_key_texts/nl.txt b/app/src/main/assets/locale_key_texts/nl.txt
new file mode 100644
index 0000000000..aa4f1a968e
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/nl.txt
@@ -0,0 +1,9 @@
+[popup_keys]
+a á ä â à
+e é ë ê è
+i í ï ì î ij
+o ó ö ô ò
+u ú ü û ù
+y ý ÿ
+' ‘ ‚ ’
+" “ „ ”
diff --git a/app/src/main/assets/locale_key_texts/pl.txt b/app/src/main/assets/locale_key_texts/pl.txt
new file mode 100644
index 0000000000..91baf0d310
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/pl.txt
@@ -0,0 +1,14 @@
+[popup_keys]
+a ą
+e ę
+o ó
+s ś
+n ń
+c ć
+z ż ź
+l ł
+' ‘ ‚ ’
+" “ „ ”
+
+[tlds]
+pl
diff --git a/app/src/main/assets/locale_key_texts/pms.txt b/app/src/main/assets/locale_key_texts/pms.txt
new file mode 100644
index 0000000000..2fa1e4a7a3
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/pms.txt
@@ -0,0 +1,6 @@
+[popup_keys]
+a à
+e é ë è
+i ì
+o ö ò
+u ü ù
diff --git a/app/src/main/assets/locale_key_texts/pt.txt b/app/src/main/assets/locale_key_texts/pt.txt
new file mode 100644
index 0000000000..ff73ef434a
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/pt.txt
@@ -0,0 +1,7 @@
+[popup_keys]
+a á ã à â ª
+e é ê
+i í
+o ó õ ô º
+u ú ü
+c ç
diff --git a/app/src/main/assets/locale_key_texts/ro.txt b/app/src/main/assets/locale_key_texts/ro.txt
new file mode 100644
index 0000000000..988644ef5f
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ro.txt
@@ -0,0 +1,10 @@
+[popup_keys]
+a ă â
+i î
+s ș
+t ț
+' ‘ ‚ ’
+" “ „ ”
+
+[tlds]
+ro
diff --git a/app/src/main/assets/locale_key_texts/ru.txt b/app/src/main/assets/locale_key_texts/ru.txt
new file mode 100644
index 0000000000..59583da42e
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ru.txt
@@ -0,0 +1,21 @@
+[popup_keys]
+е ё е́ ѣ
+ф ѳ
+ы ы́
+а а́
+о о́
+я я́
+и и́
+ь ъ ы
+ю ю́
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+і ы
+є э э́
+
+[labels]
+alphabet: АБВ
+
+[tlds]
+ru
diff --git a/app/src/main/assets/locale_key_texts/si.txt b/app/src/main/assets/locale_key_texts/si.txt
new file mode 100644
index 0000000000..a9f3d36098
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/si.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+ඥ ෴
+ද ඳ
+ඤ ෴
+ං ඃ
+ජ ඦ
+ඩ ඬ
+ග ඟ
+
+[labels]
+alphabet: අ ආ
diff --git a/app/src/main/assets/locale_key_texts/sk.txt b/app/src/main/assets/locale_key_texts/sk.txt
new file mode 100644
index 0000000000..b699ff6b11
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/sk.txt
@@ -0,0 +1,20 @@
+[popup_keys]
+a á ä
+e é
+i í
+o ô ó
+u ú
+s š
+n ň
+c č
+y ý
+d ď
+r ŕ
+t ť
+z ž
+l ľ ĺ
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[tlds]
+sk
diff --git a/app/src/main/assets/locale_key_texts/sl.txt b/app/src/main/assets/locale_key_texts/sl.txt
new file mode 100644
index 0000000000..beb8414337
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/sl.txt
@@ -0,0 +1,9 @@
+[popup_keys]
+s š
+c č ć
+z ž
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[tlds]
+si
diff --git a/app/src/main/assets/locale_key_texts/sr-Latn.txt b/app/src/main/assets/locale_key_texts/sr-Latn.txt
new file mode 100644
index 0000000000..7f0b254da4
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/sr-Latn.txt
@@ -0,0 +1,15 @@
+[popup_keys]
+s š
+z ž
+c č ć
+d đ
+
+[extra_keys]
+1: š
+2: č
+2: ć
+3: đ
+3: ž
+
+[tlds]
+rs
diff --git a/app/src/main/assets/locale_key_texts/sr.txt b/app/src/main/assets/locale_key_texts/sr.txt
new file mode 100644
index 0000000000..039e1f4812
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/sr.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+е ѐ
+и ѝ
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+[labels]
+alphabet: АБВ
+
+[tlds]
+rs
diff --git a/app/src/main/assets/locale_key_texts/sv.txt b/app/src/main/assets/locale_key_texts/sv.txt
new file mode 100644
index 0000000000..233e02c3bd
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/sv.txt
@@ -0,0 +1,13 @@
+[popup_keys]
+a ä å
+o ö
+' › ‹
+" » «
+
+[extra_keys]
+1: å
+2: ö
+2: ä
+
+[tlds]
+sv
diff --git a/app/src/main/assets/locale_key_texts/sw.txt b/app/src/main/assets/locale_key_texts/sw.txt
new file mode 100644
index 0000000000..d36014bf76
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/sw.txt
@@ -0,0 +1,2 @@
+[popup_keys]
+g g\'
diff --git a/app/src/main/assets/locale_key_texts/ta.txt b/app/src/main/assets/locale_key_texts/ta.txt
new file mode 100644
index 0000000000..5c2d9ecb24
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ta.txt
@@ -0,0 +1,19 @@
+[popup_keys]
+ஔ ௌ
+ஐ ை
+ஆ ா
+ஈ ீ
+ஊ ூ
+ஓ ோ ௐ
+ஏ ே
+அ ஃ
+இ ி
+உ ு
+க ஹ க்ஷ
+ச ஸ ஶ்ரீ
+ஒ ொ
+எ ெ
+ஷ ஜ
+
+[labels]
+alphabet: தமிழ்
diff --git a/app/src/main/assets/locale_key_texts/te.txt b/app/src/main/assets/locale_key_texts/te.txt
new file mode 100644
index 0000000000..baf480a39a
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/te.txt
@@ -0,0 +1,35 @@
+[popup_keys]
+ౌ ఔ
+ై ఐ
+ా ఆ
+ీ ఈ
+ూ ఊ
+బ భ
+హ ః
+గ ఘ
+ద ధ
+జ ఝ
+డ ఢ
+ో ఓ
+ే ఏ
+్ అ
+ి ఇ
+ు ఉ
+ప ఫ
+ర ఱ ్ర
+క ఖ
+త థ
+చ ఛ
+ట ఠ
+ొ ఒ
+ె ఎ
+మ ం ఁ
+న ణ ఙ ఞ
+ల ళ
+స శ
+ఋ ృ
+ష క్ష
+య జ్ఞ
+
+[labels]
+alphabet: అఆఇ
diff --git a/app/src/main/assets/locale_key_texts/th.txt b/app/src/main/assets/locale_key_texts/th.txt
new file mode 100644
index 0000000000..676dc6aba0
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/th.txt
@@ -0,0 +1,5 @@
+[labels]
+alphabet: กขค
+
+[number_row]
+๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๐
diff --git a/app/src/main/assets/locale_key_texts/tl.txt b/app/src/main/assets/locale_key_texts/tl.txt
new file mode 100644
index 0000000000..f111e26227
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/tl.txt
@@ -0,0 +1,5 @@
+[popup_keys]
+n ñ
+
+[extra_keys]
+2: ñ
diff --git a/app/src/main/assets/locale_key_texts/tr.txt b/app/src/main/assets/locale_key_texts/tr.txt
new file mode 100644
index 0000000000..e2f1771b1e
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/tr.txt
@@ -0,0 +1,12 @@
+[popup_keys]
+a â
+ı i î
+i ı î
+o ö
+u ü û
+s ş
+g ğ
+c ç
+
+[tlds]
+tr gov.tr edu.tr com.tr
diff --git a/app/src/main/assets/locale_key_texts/uk.txt b/app/src/main/assets/locale_key_texts/uk.txt
new file mode 100644
index 0000000000..cbd8986b7f
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/uk.txt
@@ -0,0 +1,25 @@
+[popup_keys]
+у у́
+е е́
+г ґ
+ф ѳ
+ї ї́
+і і́
+а а́
+о о́
+є є́
+я я́
+и и́ і ї
+ю ю́
+' ’ ‚ ‘ › ‹
+" ” „ “ » «
+
+ы і ї
+э є
+ј й
+
+[labels]
+alphabet: АБВ
+
+[tlds]
+ua
diff --git a/app/src/main/assets/locale_key_texts/ur.txt b/app/src/main/assets/locale_key_texts/ur.txt
new file mode 100644
index 0000000000..6a8019d05d
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/ur.txt
@@ -0,0 +1,33 @@
+[popup_keys]
+و ؤ
+ر ڑ
+ت ٹ
+ے ۓ
+ی ئ
+ہ ۂ ۃ
+ا آ أ ٰ ٖ
+س ص
+د ڈ
+گ غ
+ح ھ
+ج ض
+ک خ
+َ ُ ِ ٗ ْ ً ٌ ٍ ّ
+ز ذ
+ش ژ
+چ ث
+ط ظ
+ن ں
+punctuation ؟ ، ! . -
+« „ “ ”
+» ‚ ‘ ’ ‹ ›
+
+[labels]
+alphabet: ابپ
+comma: ،
+symbol: ۳۲۱؟
+period: ۔
+question: ؟
+
+[number_row]
+۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۰
diff --git a/app/src/main/assets/locale_key_texts/uz.txt b/app/src/main/assets/locale_key_texts/uz.txt
new file mode 100644
index 0000000000..6a3e684ddd
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/uz.txt
@@ -0,0 +1,12 @@
+[popup_keys]
+a â ä á
+e ə é
+i ı î ï ì í į ī
+o ö ô œ ò ó õ ø ō
+u ü û ù ú ū
+s ş ß ś š
+g ğ
+n ň ñ
+c ç ć č
+y ý
+z ž
diff --git a/app/src/main/assets/locale_key_texts/vi.txt b/app/src/main/assets/locale_key_texts/vi.txt
new file mode 100644
index 0000000000..83db93bb6e
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/vi.txt
@@ -0,0 +1,11 @@
+[popup_keys]
+a à á ả ã ạ ă ằ ắ ẳ ẵ ặ â ầ ấ ẩ ẫ ậ
+e è é ẻ ẽ ẹ ê ề ế ể ễ ệ
+i ì í ỉ ĩ ị
+o ò ó ỏ õ ọ ô ồ ố ổ ỗ ộ ơ ờ ớ ở ỡ ợ
+u ù ú ủ ũ ụ ư ừ ứ ử ữ ự
+y ỳ ý ỷ ỹ ỵ
+d đ
+
+[tlds]
+vn
diff --git a/app/src/main/assets/locale_key_texts/xdq.txt b/app/src/main/assets/locale_key_texts/xdq.txt
new file mode 100644
index 0000000000..697b2f937b
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/xdq.txt
@@ -0,0 +1,16 @@
+[popup_keys]
+у у́ ю
+е е́ э ё
+ш щ
+а а́
+п ф
+о о́
+и и́ ы
+ъ ӏ
+я я́
+ь ӏ
+' ’ ‚ ‘
+" ” „ “
+
+[labels]
+alphabet: АБВ
diff --git a/app/src/main/assets/locale_key_texts/zu.txt b/app/src/main/assets/locale_key_texts/zu.txt
new file mode 100644
index 0000000000..ccc06327dc
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/zu.txt
@@ -0,0 +1,2 @@
+[popup_keys]
+b ɓ
diff --git a/app/src/main/assets/locale_key_texts/zz.txt b/app/src/main/assets/locale_key_texts/zz.txt
new file mode 100644
index 0000000000..c95406a98a
--- /dev/null
+++ b/app/src/main/assets/locale_key_texts/zz.txt
@@ -0,0 +1,20 @@
+[popup_keys]
+a à á â ã ä å æ ā ă ą ª
+e è é ê ë ē ĕ ė ę ě
+i ì í î ï ĩ ī ĭ į ı ij
+o ò ó ô õ ö ø ō ŏ ő œ º
+u ù ú û ü ũ ū ŭ ů ű ų
+s ß ś ŝ ş š ſ
+n ñ ń ņ ň ʼn ŋ
+c ç ć ĉ ċ č
+y ý ŷ ÿ ij
+d ď đ ð
+r ŕ ŗ ř
+t þ ţ ť ŧ
+z ź ż ž
+k ķ ĸ
+l ĺ ļ ľ ŀ ł
+g ĝ ğ ġ ģ
+h ĥ
+j ĵ
+w ŵ
diff --git a/app/src/main/java/helium314/keyboard/accessibility/AccessibilityLongPressTimer.kt b/app/src/main/java/helium314/keyboard/accessibility/AccessibilityLongPressTimer.kt
new file mode 100644
index 0000000000..37077b574a
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/accessibility/AccessibilityLongPressTimer.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
+
+import android.content.Context
+import android.os.Handler
+import android.os.Message
+import helium314.keyboard.keyboard.Key
+import helium314.keyboard.latin.R
+
+// Handling long press timer to show a popup keys keyboard.
+internal class AccessibilityLongPressTimer(
+ private val mCallback: LongPressTimerCallback,
+ context: Context
+) : Handler(context.mainLooper) {
+ interface LongPressTimerCallback {
+ fun performLongClickOn(key: Key)
+ }
+
+ private val mConfigAccessibilityLongPressTimeout = context.resources.getInteger(R.integer.config_accessibility_long_press_key_timeout).toLong()
+ override fun handleMessage(msg: Message) {
+ when (msg.what) {
+ MSG_LONG_PRESS -> {
+ cancelLongPress()
+ mCallback.performLongClickOn(msg.obj as Key)
+ return
+ }
+ else -> {
+ super.handleMessage(msg)
+ return
+ }
+ }
+ }
+
+ fun startLongPress(key: Key?) {
+ cancelLongPress()
+ val longPressMessage = obtainMessage(MSG_LONG_PRESS, key)
+ sendMessageDelayed(longPressMessage, mConfigAccessibilityLongPressTimeout)
+ }
+
+ fun cancelLongPress() {
+ removeMessages(MSG_LONG_PRESS)
+ }
+
+ companion object {
+ private const val MSG_LONG_PRESS = 1
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/accessibility/AccessibilityUtils.kt b/app/src/main/java/helium314/keyboard/accessibility/AccessibilityUtils.kt
new file mode 100644
index 0000000000..dafbfb2bf8
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/accessibility/AccessibilityUtils.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
+
+import android.content.Context
+import android.media.AudioDeviceInfo.*
+import android.media.AudioManager
+import android.os.Build
+import android.os.SystemClock
+import android.provider.Settings
+import android.text.TextUtils
+import helium314.keyboard.latin.utils.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.view.inputmethod.EditorInfo
+import helium314.keyboard.latin.R
+import helium314.keyboard.latin.SuggestedWords
+import helium314.keyboard.latin.utils.InputTypeUtils
+
+class AccessibilityUtils private constructor() {
+ private lateinit var mContext: Context
+ private lateinit var mAccessibilityManager: AccessibilityManager
+ private lateinit var mAudioManager: AudioManager
+ /** The most recent auto-correction. */
+ private var mAutoCorrectionWord: String? = null
+ /** The most recent typed word for auto-correction. */
+ private var mTypedWord: String? = null
+
+ private fun initInternal(context: Context) {
+ mContext = context
+ mAccessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
+ mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ }
+
+ /**
+ * Returns `true` if accessibility is enabled. Currently, this means
+ * that the kill switch is off and system accessibility is turned on.
+ *
+ * @return `true` if accessibility is enabled.
+ */
+ val isAccessibilityEnabled: Boolean
+ get() = ENABLE_ACCESSIBILITY && mAccessibilityManager.isEnabled
+
+ /**
+ * Returns `true` if touch exploration is enabled. Currently, this
+ * means that the kill switch is off, the device supports touch exploration,
+ * and system accessibility is turned on.
+ *
+ * @return `true` if touch exploration is enabled.
+ */
+ val isTouchExplorationEnabled: Boolean
+ get() = isAccessibilityEnabled && mAccessibilityManager.isTouchExplorationEnabled
+
+ /**
+ * Returns whether the device should obscure typed password characters.
+ * Typically this means speaking "dot" in place of non-control characters.
+ *
+ * @return `true` if the device should obscure password characters.
+ */
+ fun shouldObscureInput(editorInfo: EditorInfo?): Boolean {
+ if (editorInfo == null) return false
+ // The user can optionally force speaking passwords.
+ if (Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD != null) {
+ val speakPassword = Settings.Secure.getInt(mContext.contentResolver,
+ Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0
+ if (speakPassword) return false
+ }
+ // Always speak if the user is listening through headphones.
+ val listeningThroughHeadphones = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ @Suppress("deprecation") // no replacement available
+ mAudioManager.isWiredHeadsetOn || mAudioManager.isBluetoothA2dpOn
+ } else {
+ // try the same as the deprecated thing above, for what we can assume to be headphones
+ mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).any {
+ when (it.type) {
+ TYPE_WIRED_HEADSET, TYPE_WIRED_HEADPHONES, TYPE_BLUETOOTH_SCO, TYPE_BLUETOOTH_A2DP, TYPE_USB_HEADSET, TYPE_HEARING_AID, TYPE_BLE_HEADSET -> true
+ else -> false
+ }
+ }
+ }
+ return if (listeningThroughHeadphones) {
+ false
+ } else InputTypeUtils.isPasswordInputType(editorInfo.inputType)
+ // Don't speak if the IME is connected to a password field.
+ }
+
+ /**
+ * Sets the current auto-correction word and typed word. These may be used
+ * to provide the user with a spoken description of what auto-correction
+ * will occur when a key is typed.
+ *
+ * @param suggestedWords the list of suggested auto-correction words
+ */
+ fun setAutoCorrection(suggestedWords: SuggestedWords) {
+ if (suggestedWords.mWillAutoCorrect) {
+ mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION)
+ val typedWordInfo = suggestedWords.mTypedWordInfo
+ mTypedWord = typedWordInfo?.mWord
+ } else {
+ mAutoCorrectionWord = null
+ mTypedWord = null
+ }
+ }
+
+ /**
+ * Obtains a description for an auto-correction key, taking into account the
+ * currently typed word and auto-correction.
+ *
+ * @param keyCodeDescription spoken description of the key that will insert
+ * an auto-correction
+ * @param shouldObscure whether the key should be obscured
+ * @return a description including a description of the auto-correction, if
+ * needed
+ */
+ fun getAutoCorrectionDescription(
+ keyCodeDescription: String?, shouldObscure: Boolean): String? {
+ if (!TextUtils.isEmpty(mAutoCorrectionWord)) {
+ if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) {
+ return if (shouldObscure) { // This should never happen, but just in case...
+ mContext.getString(R.string.spoken_auto_correct_obscured,
+ keyCodeDescription)
+ } else mContext.getString(R.string.spoken_auto_correct, keyCodeDescription,
+ mTypedWord, mAutoCorrectionWord)
+ }
+ }
+ return keyCodeDescription
+ }
+
+ /**
+ * Sends the specified text to the [AccessibilityManager] to be
+ * spoken.
+ *
+ * @param view The source view.
+ * @param text The text to speak.
+ */
+ fun announceForAccessibility(view: View, text: CharSequence?) {
+ if (!mAccessibilityManager.isEnabled) {
+ Log.e(TAG, "Attempted to speak when accessibility was disabled!")
+ return
+ }
+ // The following is a hack to avoid using the heavy-weight TextToSpeech
+ // class. Instead, we're just forcing a fake AccessibilityEvent into
+ // the screen reader to make it speak.
+ val event = obtainEvent()
+ event.packageName = PACKAGE
+ event.className = CLASS
+ event.eventTime = SystemClock.uptimeMillis()
+ event.isEnabled = true
+ event.text.add(text)
+ // Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
+ // announce events.
+ event.eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
+ val viewParent = view.parent
+ if (viewParent == null || viewParent !is ViewGroup) {
+ Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility")
+ return
+ }
+ viewParent.requestSendAccessibilityEvent(view, event)
+ }
+
+ /**
+ * Handles speaking the "connect a headset to hear passwords" notification
+ * when connecting to a password field.
+ *
+ * @param view The source view.
+ * @param editorInfo The input connection's editor info attribute.
+ * @param restarting Whether the connection is being restarted.
+ */
+ fun onStartInputViewInternal(view: View, editorInfo: EditorInfo?, restarting: Boolean) {
+ if (shouldObscureInput(editorInfo)) {
+ val text = mContext.getText(R.string.spoken_use_headphones)
+ announceForAccessibility(view, text)
+ }
+ }
+
+ /**
+ * Sends the specified [AccessibilityEvent] if accessibility is
+ * enabled. No operation if accessibility is disabled.
+ *
+ * @param event The event to send.
+ */
+ fun requestSendAccessibilityEvent(event: AccessibilityEvent?) {
+ if (mAccessibilityManager.isEnabled) {
+ mAccessibilityManager.sendAccessibilityEvent(event)
+ }
+ }
+
+ companion object {
+ private val TAG = AccessibilityUtils::class.java.simpleName
+ private val CLASS = AccessibilityUtils::class.java.name
+ private val PACKAGE = AccessibilityUtils::class.java.getPackage()!!.name
+ val instance = AccessibilityUtils()
+ /*
+ * Setting this constant to {@code false} will disable all keyboard
+ * accessibility code, regardless of whether Accessibility is turned on in
+ * the system settings. It should ONLY be used in the event of an emergency.
+ */
+ private const val ENABLE_ACCESSIBILITY = true
+
+ @JvmStatic
+ fun init(context: Context) {
+ if (!ENABLE_ACCESSIBILITY) return
+ // These only need to be initialized if the kill switch is off.
+ instance.initInternal(context)
+ }
+
+ /**
+ * Returns {@true} if the provided event is a touch exploration (e.g. hover)
+ * event. This is used to determine whether the event should be processed by
+ * the touch exploration code within the keyboard.
+ *
+ * @param event The event to check.
+ * @return {@true} is the event is a touch exploration event
+ */
+ fun isTouchExplorationEvent(event: MotionEvent): Boolean {
+ val action = event.action
+ return action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_HOVER_MOVE
+ }
+
+ fun obtainEvent(eventType: Int): AccessibilityEvent =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ AccessibilityEvent(eventType)
+ } else {
+ @Suppress("deprecation")
+ AccessibilityEvent.obtain(eventType)
+ }
+
+ fun obtainEvent(): AccessibilityEvent =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ AccessibilityEvent()
+ } else {
+ @Suppress("deprecation")
+ AccessibilityEvent.obtain()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt b/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt
new file mode 100644
index 0000000000..cd1841ce67
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/accessibility/KeyCodeDescriptionMapper.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
+
+import android.content.Context
+import android.text.TextUtils
+import helium314.keyboard.latin.utils.Log
+import android.util.SparseIntArray
+import android.view.inputmethod.EditorInfo
+import helium314.keyboard.keyboard.Key
+import helium314.keyboard.keyboard.Keyboard
+import helium314.keyboard.keyboard.KeyboardId
+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
+
+internal class KeyCodeDescriptionMapper private constructor() {
+ // Sparse array of spoken description resource IDs indexed by key codes
+ private val mKeyCodeMap = SparseIntArray().apply {
+ // Special non-character codes defined in Keyboard
+ put(Constants.CODE_SPACE, R.string.spoken_description_space)
+ put(KeyCode.DELETE, R.string.spoken_description_delete)
+ put(Constants.CODE_ENTER, R.string.spoken_description_return)
+ put(KeyCode.SETTINGS, R.string.spoken_description_settings)
+ put(KeyCode.SHIFT, R.string.spoken_description_shift)
+ put(KeyCode.VOICE_INPUT, R.string.spoken_description_mic)
+ put(KeyCode.SYMBOL_ALPHA, R.string.spoken_description_to_symbol)
+ put(Constants.CODE_TAB, R.string.spoken_description_tab)
+ put(KeyCode.LANGUAGE_SWITCH, R.string.spoken_description_language_switch)
+ put(KeyCode.ACTION_NEXT, R.string.spoken_description_action_next)
+ put(KeyCode.ACTION_PREVIOUS, R.string.spoken_description_action_previous)
+ put(KeyCode.EMOJI, R.string.spoken_description_emoji)
+ // Because the upper-case and lower-case mappings of the following letters is depending on
+ // the locale, the upper case descriptions should be defined here. The lower case
+ // descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
+ // U+0049: "I" LATIN CAPITAL LETTER I
+ // U+0069: "i" LATIN SMALL LETTER I
+ // U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ put(0x0049, R.string.spoken_letter_0049)
+ put(0x0130, R.string.spoken_letter_0130)
+ }
+
+ /**
+ * Returns the localized description of the action performed by a specified
+ * key based on the current keyboard state.
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @param key The key from which to obtain a description.
+ * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
+ * @return a character sequence describing the action performed by pressing the key
+ */
+ fun getDescriptionForKey(context: Context, keyboard: Keyboard?, key: Key, shouldObscure: Boolean): String? {
+ val code = key.code
+ if (code == KeyCode.SYMBOL_ALPHA || code == KeyCode.SYMBOL || code == KeyCode.ALPHA) {
+ val description = getDescriptionForSwitchAlphaSymbol(context, keyboard)
+ if (description != null) {
+ return description
+ }
+ }
+ if (code == KeyCode.SHIFT) {
+ return getDescriptionForShiftKey(context, keyboard)
+ }
+ if (code == Constants.CODE_ENTER) {
+ // The following function returns the correct description in all action and
+ // regular enter cases, taking care of all modes.
+ return getDescriptionForActionKey(context, keyboard, key)
+ }
+ if (code == KeyCode.MULTIPLE_CODE_POINTS) {
+ return key.outputText ?: context.getString(R.string.spoken_description_unknown)
+ }
+ // Just attempt to speak the description.
+ if (code != KeyCode.NOT_SPECIFIED) {
+ // If the key description should be obscured, now is the time to do it.
+ val isDefinedNonCtrl = (Character.isDefined(code)
+ && !Character.isISOControl(code))
+ if (shouldObscure && isDefinedNonCtrl) {
+ return context.getString(OBSCURED_KEY_RES_ID)
+ }
+ val description = getDescriptionForCodePoint(context, code)
+ if (description != null) {
+ return description
+ }
+ return if (!TextUtils.isEmpty(key.label)) {
+ key.label
+ } else context.getString(R.string.spoken_description_unknown)
+ }
+ return null
+ }
+
+ /**
+ * Returns a localized character sequence describing what will happen when
+ * the specified key is pressed based on its key code point.
+ *
+ * @param context The package's context.
+ * @param codePoint The code point from which to obtain a description.
+ * @return a character sequence describing the code point.
+ */
+ fun getDescriptionForCodePoint(context: Context, codePoint: Int): String? {
+ // If the key description should be obscured, now is the time to do it.
+ val index = mKeyCodeMap.indexOfKey(codePoint)
+ if (index >= 0) {
+ return context.getString(mKeyCodeMap.valueAt(index))
+ }
+ return if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
+ StringUtils.newSingleCodePointString(codePoint)
+ } else null
+ }
+
+ companion object {
+ private val TAG = KeyCodeDescriptionMapper::class.java.simpleName
+ // The resource ID of the string spoken for obscured keys
+ private val OBSCURED_KEY_RES_ID = R.string.spoken_description_dot
+ val instance = KeyCodeDescriptionMapper()
+
+ /**
+ * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
+ * key or `null` if there is not a description provided for the
+ * current keyboard context.
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @return a character sequence describing the action performed by pressing the key
+ */
+ private fun getDescriptionForSwitchAlphaSymbol(context: Context, keyboard: Keyboard?): String? {
+ val resId = when (val elementId = keyboard?.mId?.mElementId) {
+ KeyboardId.ELEMENT_ALPHABET, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_to_symbol
+ KeyboardId.ELEMENT_SYMBOLS, KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_to_alpha
+ KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_to_symbol
+ KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_to_numeric
+ else -> {
+ Log.e(TAG, "Missing description for keyboard element ID:$elementId")
+ return null
+ }
+ }
+ return context.getString(resId)
+ }
+
+ /**
+ * Returns a context-sensitive description of the "Shift" key.
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @return A context-sensitive description of the "Shift" key.
+ */
+ private fun getDescriptionForShiftKey(context: Context, keyboard: Keyboard?): String {
+ val resId: Int = when (keyboard?.mId?.mElementId) {
+ KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_caps_lock
+ KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> R.string.spoken_description_shift_shifted
+ KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_symbols_shift
+ KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_symbols_shift_shifted
+ else -> R.string.spoken_description_shift
+ }
+ return context.getString(resId)
+ }
+
+ /**
+ * Returns a context-sensitive description of the "Enter" action key.
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @param key The key to describe.
+ * @return Returns a context-sensitive description of the "Enter" action key.
+ */
+ private fun getDescriptionForActionKey(context: Context, keyboard: Keyboard?, key: Key): String {
+ // Always use the label, if available.
+ if (!TextUtils.isEmpty(key.label)) {
+ return key.label!!.trim { it <= ' ' }
+ }
+ val resId = when (keyboard?.mId?.imeAction()) {
+ EditorInfo.IME_ACTION_SEARCH -> R.string.label_search_key
+ EditorInfo.IME_ACTION_GO -> R.string.label_go_key
+ EditorInfo.IME_ACTION_SEND -> R.string.label_send_key
+ EditorInfo.IME_ACTION_NEXT -> R.string.label_next_key
+ EditorInfo.IME_ACTION_DONE -> R.string.label_done_key
+ EditorInfo.IME_ACTION_PREVIOUS -> R.string.label_previous_key
+ else -> R.string.spoken_description_return
+ }
+ return context.getString(resId)
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.kt b/app/src/main/java/helium314/keyboard/accessibility/KeyboardAccessibilityDelegate.kt
similarity index 77%
rename from app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.kt
rename to app/src/main/java/helium314/keyboard/accessibility/KeyboardAccessibilityDelegate.kt
index abf9338a9d..17edf65d1e 100644
--- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.kt
+++ b/app/src/main/java/helium314/keyboard/accessibility/KeyboardAccessibilityDelegate.kt
@@ -1,17 +1,23 @@
-package org.dslul.openboard.inputmethod.accessibility
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
import android.os.SystemClock
-import android.util.Log
+import helium314.keyboard.latin.utils.Log
import android.view.MotionEvent
import android.view.View
import android.view.accessibility.AccessibilityEvent
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
-import org.dslul.openboard.inputmethod.keyboard.Key
-import org.dslul.openboard.inputmethod.keyboard.KeyDetector
-import org.dslul.openboard.inputmethod.keyboard.Keyboard
-import org.dslul.openboard.inputmethod.keyboard.KeyboardView
+import helium314.keyboard.keyboard.Key
+import helium314.keyboard.keyboard.KeyDetector
+import helium314.keyboard.keyboard.Keyboard
+import helium314.keyboard.keyboard.KeyboardView
/**
* This class represents a delegate that can be registered in a class that extends
@@ -25,12 +31,14 @@ import org.dslul.openboard.inputmethod.keyboard.KeyboardView
*
* @param The keyboard view class type.
*/
-open class KeyboardAccessibilityDelegate(protected val mKeyboardView: KV, protected val mKeyDetector: KeyDetector) : AccessibilityDelegateCompat() {
+open class KeyboardAccessibilityDelegate(
+ protected val mKeyboardView: KV,
+ protected val mKeyDetector: KeyDetector
+ ) : AccessibilityDelegateCompat() {
private var mKeyboard: Keyboard? = null
private var mAccessibilityNodeProvider: KeyboardAccessibilityNodeProvider? = null
private var mLastHoverKey: Key? = null
-
protected open var lastHoverKey: Key?
get() = mLastHoverKey
set(key) {
@@ -39,20 +47,19 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
/**
* Called when the keyboard layout changes.
*
- *
* **Note:** This method will be called even if accessibility is not
* enabled.
- * @param keyboard The keyboard that is being set to the wrapping view.
+ * [keyboard]: The keyboard that is being set to the wrapping view.
*/
open var keyboard: Keyboard?
- get() = mKeyboard
- set(keyboard) {
- if (keyboard == null) {
- return
+ get() = mKeyboard
+ set(keyboard) {
+ if (keyboard == null) {
+ return
+ }
+ mAccessibilityNodeProvider?.setKeyboard(keyboard)
+ mKeyboard = keyboard
}
- mAccessibilityNodeProvider?.setKeyboard(keyboard)
- mKeyboard = keyboard
- }
/**
* Sends a window state change event with the specified string resource id.
@@ -63,7 +70,7 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
if (resId == 0) {
return
}
- val context = mKeyboardView!!.context
+ val context = mKeyboardView.context
sendWindowStateChanged(context.getString(resId))
}
@@ -73,9 +80,8 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
* @param text The text to send with the event.
*/
protected fun sendWindowStateChanged(text: String?) {
- val stateChange = AccessibilityEvent.obtain(
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
- mKeyboardView!!.onInitializeAccessibilityEvent(stateChange)
+ val stateChange = AccessibilityUtils.obtainEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
+ mKeyboardView.onInitializeAccessibilityEvent(stateChange)
stateChange.text.add(text)
stateChange.contentDescription = null
val parent = mKeyboardView.parent
@@ -92,19 +98,21 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
*/
override fun getAccessibilityNodeProvider(host: View): KeyboardAccessibilityNodeProvider {
return accessibilityNodeProvider
- }// Instantiate the provide only when requested. Since the system
-// will call this method multiple times it is a good practice to
-// cache the provider instance.
+ }
+ // Instantiate the provide only when requested. Since the system
+ // will call this method multiple times it is a good practice to
+ // cache the provider instance.
/**
* @return A lazily-instantiated node provider for this view delegate.
*/
protected val accessibilityNodeProvider: KeyboardAccessibilityNodeProvider
- get() { // Instantiate the provide only when requested. Since the system
-// will call this method multiple times it is a good practice to
-// cache the provider instance.
- return mAccessibilityNodeProvider ?: KeyboardAccessibilityNodeProvider(mKeyboardView, this)
- }
+ get() {
+ // Instantiate the provide only when requested. Since the system
+ // will call this method multiple times it is a good practice to
+ // cache the provider instance.
+ return mAccessibilityNodeProvider ?: KeyboardAccessibilityNodeProvider(mKeyboardView, this)
+ }
/**
* Get a key that a hover event is on.
@@ -178,7 +186,7 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
lastKey?.let { onHoverExitFrom(it) }
val key = getHoverKeyOf(event)
// Make sure we're not getting an EXIT event because the user slid
-// off the keyboard area, then force a key press.
+ // off the keyboard area, then force a key press.
key?.let { performClickOn(it)
onHoverExitFrom(it) }
mLastHoverKey = null
@@ -209,7 +217,7 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
val eventTime = SystemClock.uptimeMillis()
val touchEvent = MotionEvent.obtain(
eventTime, eventTime, touchAction, x.toFloat(), y.toFloat(), 0 /* metaState */)
- mKeyboardView!!.onTouchEvent(touchEvent)
+ mKeyboardView.onTouchEvent(touchEvent)
touchEvent.recycle()
}
@@ -223,7 +231,7 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
Log.d(TAG, "onHoverEnterTo: key=$key")
}
key.onPressed()
- mKeyboardView!!.invalidateKey(key)
+ mKeyboardView.invalidateKey(key)
val provider = accessibilityNodeProvider
provider.onHoverEnterTo(key)
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS)
@@ -246,7 +254,7 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
Log.d(TAG, "onHoverExitFrom: key=$key")
}
key.onReleased()
- mKeyboardView!!.invalidateKey(key)
+ mKeyboardView.invalidateKey(key)
val provider = accessibilityNodeProvider
provider.onHoverExitFrom(key)
}
@@ -256,17 +264,18 @@ open class KeyboardAccessibilityDelegate(protected val mKeyb
*
* @param key A key to be long pressed on.
*/
- open fun performLongClickOn(key: Key) { // A extended class should override this method to implement long press.
+ open fun performLongClickOn(key: Key) {
+ // A extended class should override this method to implement long press.
}
companion object {
- private val TAG = KeyboardAccessibilityDelegate::class.java.simpleName
- const val DEBUG_HOVER = false
- const val HOVER_EVENT_POINTER_ID = 0
+ private val TAG = KeyboardAccessibilityDelegate::class.java.simpleName
+ const val DEBUG_HOVER = false
+ const val HOVER_EVENT_POINTER_ID = 0
}
init {
// Ensure that the view has an accessibility delegate.
- ViewCompat.setAccessibilityDelegate(mKeyboardView!!, this)
+ ViewCompat.setAccessibilityDelegate(mKeyboardView, this) // todo: see the warning, this may be bad
}
}
diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.kt b/app/src/main/java/helium314/keyboard/accessibility/KeyboardAccessibilityNodeProvider.kt
similarity index 75%
rename from app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.kt
rename to app/src/main/java/helium314/keyboard/accessibility/KeyboardAccessibilityNodeProvider.kt
index 9154e3d3ca..15dcbb97b0 100644
--- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.kt
+++ b/app/src/main/java/helium314/keyboard/accessibility/KeyboardAccessibilityNodeProvider.kt
@@ -1,19 +1,26 @@
-package org.dslul.openboard.inputmethod.accessibility
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
import android.graphics.Rect
import android.os.Bundle
-import android.util.Log
+import helium314.keyboard.latin.utils.Log
import android.view.View
import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityRecord
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityEventCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
-import org.dslul.openboard.inputmethod.keyboard.Key
-import org.dslul.openboard.inputmethod.keyboard.Keyboard
-import org.dslul.openboard.inputmethod.keyboard.KeyboardView
-import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils
-import org.dslul.openboard.inputmethod.latin.settings.Settings
+import helium314.keyboard.keyboard.Key
+import helium314.keyboard.keyboard.Keyboard
+import helium314.keyboard.keyboard.KeyboardView
+import helium314.keyboard.latin.common.CoordinateUtils
+import helium314.keyboard.latin.settings.Settings
/**
* Exposes a virtual view sub-tree for [KeyboardView] and generates
@@ -26,10 +33,14 @@ import org.dslul.openboard.inputmethod.latin.settings.Settings
* virtual views, thus conveying their logical structure.
*
*/
-class KeyboardAccessibilityNodeProvider(keyboardView: KV,
- delegate: KeyboardAccessibilityDelegate) : AccessibilityNodeProviderCompat() {
- private val mKeyCodeDescriptionMapper: KeyCodeDescriptionMapper
- private val mAccessibilityUtils: AccessibilityUtils
+class KeyboardAccessibilityNodeProvider(
+ /** The keyboard view to provide an accessibility node info. */
+ private val mKeyboardView: KV,
+ /** The accessibility delegate. */
+ private val mDelegate: KeyboardAccessibilityDelegate
+) : AccessibilityNodeProviderCompat() {
+ private val mKeyCodeDescriptionMapper: KeyCodeDescriptionMapper = KeyCodeDescriptionMapper.instance
+ private val mAccessibilityUtils: AccessibilityUtils = AccessibilityUtils.instance
/** Temporary rect used to calculate in-screen bounds. */
private val mTempBoundsInScreen = Rect()
/** The parent view's cached on-screen location. */
@@ -38,12 +49,8 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
private var mAccessibilityFocusedView = UNDEFINED
/** The virtual view identifier for the hovering node. */
private var mHoveringNodeId = UNDEFINED
- /** The keyboard view to provide an accessibility node info. */
- private val mKeyboardView: KV
- /** The accessibility delegate. */
- private val mDelegate: KeyboardAccessibilityDelegate
/** The current keyboard. */
- private var mKeyboard: Keyboard? = null
+ private var mKeyboard: Keyboard? = mKeyboardView.keyboard
/**
* Sets the keyboard represented by this node provider.
@@ -55,10 +62,8 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
}
private fun getKeyOf(virtualViewId: Int): Key? {
- if (mKeyboard == null) {
- return null
- }
- val sortedKeys = mKeyboard!!.sortedKeys
+ val keyboard = mKeyboard ?: return null
+ val sortedKeys = keyboard.sortedKeys
// Use a virtual view id as an index of the sorted keys list.
return if (virtualViewId >= 0 && virtualViewId < sortedKeys.size) {
sortedKeys[virtualViewId]
@@ -66,10 +71,8 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
}
private fun getVirtualViewIdOf(key: Key): Int {
- if (mKeyboard == null) {
- return View.NO_ID
- }
- val sortedKeys = mKeyboard!!.sortedKeys
+ val keyboard = mKeyboard ?: return View.NO_ID
+ val sortedKeys = keyboard.sortedKeys
val size = sortedKeys.size
for (index in 0 until size) {
if (sortedKeys[index] === key) { // Use an index of the sorted keys list as a virtual view id.
@@ -91,12 +94,12 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
fun createAccessibilityEvent(key: Key, eventType: Int): AccessibilityEvent {
val virtualViewId = getVirtualViewIdOf(key)
val keyDescription = getKeyDescription(key)
- val event = AccessibilityEvent.obtain(eventType)
- event.packageName = mKeyboardView!!.context.packageName
+ val event = AccessibilityUtils.obtainEvent(eventType)
+ event.packageName = mKeyboardView.context.packageName
event.className = key.javaClass.name
event.contentDescription = keyDescription
event.isEnabled = true
- val record = AccessibilityEventCompat.asRecord(event)
+ val record: AccessibilityRecord = event
record.setSource(mKeyboardView, virtualViewId)
return event
}
@@ -107,20 +110,20 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
return
}
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
-// report the node info without click and long click actions to avoid unnecessary
-// announcements.
+ // report the node info without click and long click actions to avoid unnecessary
+ // announcements.
mHoveringNodeId = id
// Invalidate the node info of the key.
- sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
- sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER)
+ sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
+ sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
}
fun onHoverExitFrom(key: Key) {
mHoveringNodeId = UNDEFINED
// Invalidate the node info of the key to be able to revert the change we have done
-// in {@link #onHoverEnterTo(Key)}.
- sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
- sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT)
+ // in {@link #onHoverEnterTo(Key)}.
+ sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)
+ sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT)
}
/**
@@ -150,13 +153,15 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
if (virtualViewId == UNDEFINED) {
return null
}
- if (virtualViewId == View.NO_ID) { // We are requested to create an AccessibilityNodeInfo describing
-// this View, i.e. the root of the virtual sub-tree.
+ val keyboard = mKeyboard ?: return null
+ if (virtualViewId == View.NO_ID) {
+ // We are requested to create an AccessibilityNodeInfo describing
+ // this View, i.e. the root of the virtual sub-tree.
val rootInfo = AccessibilityNodeInfoCompat.obtain(mKeyboardView)
- ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView!!, rootInfo)
+ ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo)
updateParentLocation()
// Add the virtual children of the root View.
- val sortedKeys = mKeyboard!!.sortedKeys
+ val sortedKeys = keyboard.sortedKeys
val size = sortedKeys.size
for (index in 0 until size) {
val key = sortedKeys[index]
@@ -178,12 +183,11 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
val boundsInParent = key.hitBox
// Calculate the key's in-screen bounds.
mTempBoundsInScreen.set(boundsInParent)
- mTempBoundsInScreen.offset(
- CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation))
+ mTempBoundsInScreen.offset(CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation))
val boundsInScreen = mTempBoundsInScreen
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
val info = AccessibilityNodeInfoCompat.obtain()
- info.packageName = mKeyboardView!!.context.packageName
+ info.packageName = mKeyboardView.context.packageName
info.className = key.javaClass.name
info.contentDescription = keyDescription
info.setBoundsInParent(boundsInParent)
@@ -193,7 +197,7 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
info.isEnabled = key.isEnabled
info.isVisibleToUser = true
// Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
-// See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
+ // See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
if (virtualViewId != mHoveringNodeId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
if (key.isLongPressEnabled) {
@@ -225,14 +229,12 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
return when (action) {
AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS -> {
mAccessibilityFocusedView = getVirtualViewIdOf(key)
- sendAccessibilityEventForKey(
- key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
+ sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
true
}
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS -> {
mAccessibilityFocusedView = UNDEFINED
- sendAccessibilityEventForKey(
- key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)
+ sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)
true
}
AccessibilityNodeInfoCompat.ACTION_CLICK -> {
@@ -267,14 +269,13 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
* @return The context-specific description of the key.
*/
private fun getKeyDescription(key: Key): String? {
- val editorInfo = mKeyboard!!.mId.mEditorInfo
+ val editorInfo = mKeyboard?.mId?.mEditorInfo
val shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo)
- val currentSettings = Settings.getInstance().current
+ val currentSettings = Settings.getValues()
val keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
- mKeyboardView!!.context, mKeyboard, key, shouldObscure)
+ mKeyboardView.context, mKeyboard, key, shouldObscure)
return if (currentSettings.isWordSeparator(key.code)) {
- mAccessibilityUtils.getAutoCorrectionDescription(
- keyCodeDescription, shouldObscure)
+ mAccessibilityUtils.getAutoCorrectionDescription(keyCodeDescription, shouldObscure)
} else keyCodeDescription
}
@@ -282,7 +283,7 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
* Updates the parent's on-screen location.
*/
private fun updateParentLocation() {
- mKeyboardView!!.getLocationOnScreen(mParentLocation)
+ mKeyboardView.getLocationOnScreen(mParentLocation)
}
companion object {
@@ -291,13 +292,4 @@ class KeyboardAccessibilityNodeProvider(keyboardView: KV,
private const val UNDEFINED = Int.MAX_VALUE
}
- init {
- mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.Companion.instance
- mAccessibilityUtils = AccessibilityUtils.Companion.instance
- mKeyboardView = keyboardView
- mDelegate = delegate
- // Since this class is constructed lazily, we might not get a subsequent
-// call to setKeyboard() and therefore need to call it now.
- setKeyboard(keyboardView!!.keyboard)
- }
}
diff --git a/app/src/main/java/helium314/keyboard/accessibility/MainKeyboardAccessibilityDelegate.kt b/app/src/main/java/helium314/keyboard/accessibility/MainKeyboardAccessibilityDelegate.kt
new file mode 100644
index 0000000000..6a4283494e
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/accessibility/MainKeyboardAccessibilityDelegate.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
+
+import android.graphics.Rect
+import android.os.SystemClock
+import helium314.keyboard.latin.utils.Log
+import android.util.SparseIntArray
+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.displayName
+
+/**
+ * This class represents a delegate that can be registered in [MainKeyboardView] to enhance
+ * accessibility support via composition rather via inheritance.
+ */
+class MainKeyboardAccessibilityDelegate(
+ mainKeyboardView: MainKeyboardView,
+ keyDetector: KeyDetector
+) : KeyboardAccessibilityDelegate(mainKeyboardView, keyDetector), LongPressTimerCallback {
+ /** The most recently set keyboard mode. */
+ private var mLastKeyboardMode = KEYBOARD_IS_HIDDEN
+ // The rectangle region to ignore hover events.
+ private val mBoundsToIgnoreHoverEvent = Rect()
+ private val mAccessibilityLongPressTimer = AccessibilityLongPressTimer(this /* callback */, mainKeyboardView.context)
+
+ // Since this method is called even when accessibility is off, make sure
+ // to check the state before announcing anything.
+ // Announce the language name only when the language is changed.
+ // Announce the mode only when the mode is changed.
+ // Announce the keyboard type only when the type is changed.
+ /**
+ * {@inheritDoc}
+ */
+ override var keyboard: Keyboard?
+ get() = super.keyboard
+ set(keyboard) {
+ if (keyboard == null) {
+ return
+ }
+ val lastKeyboard = super.keyboard
+ super.keyboard = keyboard
+ val lastKeyboardMode = mLastKeyboardMode
+ mLastKeyboardMode = keyboard.mId.mMode
+ // Since this method is called even when accessibility is off, make sure
+ // to check the state before announcing anything.
+ if (!AccessibilityUtils.instance.isAccessibilityEnabled) {
+ return
+ }
+ // Announce the language name only when the language is changed.
+ if (lastKeyboard == null || keyboard.mId.mSubtype != lastKeyboard.mId.mSubtype) {
+ announceKeyboardLanguage(keyboard)
+ return
+ }
+ // Announce the mode only when the mode is changed.
+ if (keyboard.mId.mMode != lastKeyboardMode) {
+ announceKeyboardMode(keyboard)
+ return
+ }
+ // Announce the keyboard type only when the type is changed.
+ if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
+ announceKeyboardType(keyboard, lastKeyboard)
+ return
+ }
+ }
+
+ /**
+ * Called when the keyboard is hidden and accessibility is enabled.
+ */
+ fun onHideWindow() {
+ if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
+ announceKeyboardHidden()
+ }
+ mLastKeyboardMode = KEYBOARD_IS_HIDDEN
+ }
+
+ /**
+ * Announces which language of keyboard is being displayed.
+ *
+ * @param keyboard The new keyboard.
+ */
+ private fun announceKeyboardLanguage(keyboard: Keyboard) {
+ sendWindowStateChanged(keyboard.mId.mSubtype.rawSubtype.displayName())
+ }
+
+ /**
+ * Announces which type of keyboard is being displayed.
+ * If the keyboard type is unknown, no announcement is made.
+ *
+ * @param keyboard The new keyboard.
+ */
+ private fun announceKeyboardMode(keyboard: Keyboard) {
+ val context = mKeyboardView.context
+ val modeTextResId = KEYBOARD_MODE_RES_IDS[keyboard.mId.mMode]
+ if (modeTextResId == 0) {
+ return
+ }
+ val modeText = context.getString(modeTextResId)
+ val text = context.getString(R.string.announce_keyboard_mode, modeText)
+ sendWindowStateChanged(text)
+ }
+
+ /**
+ * Announces which type of keyboard is being displayed.
+ *
+ * @param keyboard The new keyboard.
+ * @param lastKeyboard The last keyboard.
+ */
+ private fun announceKeyboardType(keyboard: Keyboard, lastKeyboard: Keyboard) {
+ val lastElementId = lastKeyboard.mId.mElementId
+ val resId = when (keyboard.mId.mElementId) {
+ KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET -> {
+ if (lastElementId == KeyboardId.ELEMENT_ALPHABET
+ || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+ // Transition between alphabet mode and automatic shifted mode should be silently
+ // ignored because it can be determined by each key's talk back announce.
+ return
+ }
+ R.string.spoken_description_mode_alpha
+ }
+ KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> {
+ if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+ // Resetting automatic shifted mode by pressing the shift key causes the transition
+ // from automatic shifted to manual shifted that should be silently ignored.
+ return
+ }
+ R.string.spoken_description_shiftmode_on
+ }
+ KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> {
+ if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
+ // Resetting caps locked mode by pressing the shift key causes the transition
+ // from shift locked to shift lock shifted that should be silently ignored.
+ return
+ }
+ R.string.spoken_description_shiftmode_locked
+ }
+ KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_shiftmode_locked
+ KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_mode_symbol
+ KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_mode_symbol_shift
+ KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_mode_phone
+ KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_mode_phone_shift
+ else -> return
+ }
+ sendWindowStateChanged(resId)
+ }
+
+ /**
+ * Announces that the keyboard has been hidden.
+ */
+ private fun announceKeyboardHidden() {
+ sendWindowStateChanged(R.string.announce_keyboard_hidden)
+ }
+
+ override fun performClickOn(key: Key) {
+ val x = key.hitBox.centerX()
+ val y = key.hitBox.centerY()
+ if (DEBUG_HOVER) {
+ Log.d(TAG, "performClickOn: key=" + key
+ + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
+ }
+ if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
+ // This hover exit event points to the key that should be ignored.
+ // Clear the ignoring region to handle further hover events.
+ mBoundsToIgnoreHoverEvent.setEmpty()
+ return
+ }
+ super.performClickOn(key)
+ }
+
+ override fun onHoverEnterTo(key: Key) {
+ val x = key.hitBox.centerX()
+ val y = key.hitBox.centerY()
+ if (DEBUG_HOVER) {
+ Log.d(TAG, "onHoverEnterTo: key=" + key
+ + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
+ }
+ mAccessibilityLongPressTimer.cancelLongPress()
+ if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
+ return
+ }
+ // This hover enter event points to the key that isn't in the ignoring region.
+ // Further hover events should be handled.
+ mBoundsToIgnoreHoverEvent.setEmpty()
+ super.onHoverEnterTo(key)
+ if (key.isLongPressEnabled) {
+ mAccessibilityLongPressTimer.startLongPress(key)
+ }
+ }
+
+ override fun onHoverExitFrom(key: Key) {
+ val x = key.hitBox.centerX()
+ val y = key.hitBox.centerY()
+ if (DEBUG_HOVER) {
+ Log.d(TAG, "onHoverExitFrom: key=" + key
+ + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
+ }
+ mAccessibilityLongPressTimer.cancelLongPress()
+ super.onHoverExitFrom(key)
+ }
+
+ override fun performLongClickOn(key: Key) {
+ if (DEBUG_HOVER) {
+ Log.d(TAG, "performLongClickOn: key=$key")
+ }
+ val tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID)
+ val eventTime = SystemClock.uptimeMillis()
+ val x = key.hitBox.centerX()
+ val y = key.hitBox.centerY()
+ val downEvent = MotionEvent.obtain(eventTime, eventTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat(), 0)
+ // Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
+ tracker.processMotionEvent(downEvent, mKeyDetector)
+ downEvent.recycle()
+ // Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
+ tracker.onLongPressed()
+ // If {@link Key#hasNoPanelAutoPopupKeys()} is true (such as "0 +" key on the phone layout)
+ // or a key invokes IME switcher dialog, we should just ignore the next
+ // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
+ // {@link PointerTracker} is in operation or not.
+ if (tracker.isInOperation) {
+ // This long press shows a popup keys keyboard and further hover events should be
+ // handled.
+ mBoundsToIgnoreHoverEvent.setEmpty()
+ return
+ }
+ // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
+ // We should ignore further hover events on this key.
+ mBoundsToIgnoreHoverEvent.set(key.hitBox)
+ if (key.hasNoPanelAutoPopupKey()) {
+ // This long press has registered a code point without showing a popup keys keyboard.
+ // We should talk back the code point if possible.
+ val codePointOfNoPanelAutoPopupKey = key.popupKeys?.get(0)?.mCode ?: return
+ val text: String = KeyCodeDescriptionMapper.instance.getDescriptionForCodePoint(
+ mKeyboardView.context, codePointOfNoPanelAutoPopupKey) ?: return
+ sendWindowStateChanged(text)
+ }
+ }
+
+ companion object {
+ private val TAG = MainKeyboardAccessibilityDelegate::class.java.simpleName
+ /** Map of keyboard modes to resource IDs. */
+ private val KEYBOARD_MODE_RES_IDS = SparseIntArray().apply {
+ put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date)
+ put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time)
+ put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email)
+ put(KeyboardId.MODE_IM, R.string.keyboard_mode_im)
+ put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number)
+ put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone)
+ put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text)
+ put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time)
+ put(KeyboardId.MODE_URL, R.string.keyboard_mode_url)
+ }
+ private const val KEYBOARD_IS_HIDDEN = -1
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt b/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt
new file mode 100644
index 0000000000..a35133395a
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/accessibility/PopupKeysKeyboardAccessibilityDelegate.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.accessibility
+
+import android.graphics.Rect
+import helium314.keyboard.latin.utils.Log
+import android.view.MotionEvent
+import helium314.keyboard.keyboard.KeyDetector
+import helium314.keyboard.keyboard.PopupKeysKeyboardView
+import helium314.keyboard.keyboard.PointerTracker
+
+/**
+ * This class represents a delegate that can be registered in [PopupKeysKeyboardView] to
+ * enhance accessibility support via composition rather via inheritance.
+ */
+class PopupKeysKeyboardAccessibilityDelegate(
+ popupKeysKeyboardView: PopupKeysKeyboardView,
+ keyDetector: KeyDetector
+) : KeyboardAccessibilityDelegate(popupKeysKeyboardView, keyDetector) {
+ private val mPopupKeysKeyboardValidBounds = Rect()
+ private var mOpenAnnounceResId = 0
+ private var mCloseAnnounceResId = 0
+ fun setOpenAnnounce(resId: Int) {
+ mOpenAnnounceResId = resId
+ }
+
+ fun setCloseAnnounce(resId: Int) {
+ mCloseAnnounceResId = resId
+ }
+
+ fun onShowPopupKeysKeyboard() {
+ sendWindowStateChanged(mOpenAnnounceResId)
+ }
+
+ fun onDismissPopupKeysKeyboard() {
+ sendWindowStateChanged(mCloseAnnounceResId)
+ }
+
+ override fun onHoverEnter(event: MotionEvent) {
+ if (DEBUG_HOVER) {
+ Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event))
+ }
+ super.onHoverEnter(event)
+ val actionIndex = event.actionIndex
+ val x = event.getX(actionIndex).toInt()
+ val y = event.getY(actionIndex).toInt()
+ val pointerId = event.getPointerId(actionIndex)
+ val eventTime = event.eventTime
+ mKeyboardView.onDownEvent(x, y, pointerId, eventTime)
+ }
+
+ override fun onHoverMove(event: MotionEvent) {
+ super.onHoverMove(event)
+ val actionIndex = event.actionIndex
+ val x = event.getX(actionIndex).toInt()
+ val y = event.getY(actionIndex).toInt()
+ val pointerId = event.getPointerId(actionIndex)
+ val eventTime = event.eventTime
+ mKeyboardView.onMoveEvent(x, y, pointerId, eventTime)
+ }
+
+ override fun onHoverExit(event: MotionEvent) {
+ val lastKey = lastHoverKey
+ if (DEBUG_HOVER) {
+ Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey)
+ }
+ if (lastKey != null) {
+ super.onHoverExitFrom(lastKey)
+ }
+ lastHoverKey = null
+ val actionIndex = event.actionIndex
+ val x = event.getX(actionIndex).toInt()
+ val y = event.getY(actionIndex).toInt()
+ val pointerId = event.getPointerId(actionIndex)
+ val eventTime = event.eventTime
+ // A hover exit event at one pixel width or height area on the edges of popup keys keyboard
+ // are treated as closing.
+ mPopupKeysKeyboardValidBounds[0, 0, mKeyboardView.width] = mKeyboardView.height
+ mPopupKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL)
+ if (mPopupKeysKeyboardValidBounds.contains(x, y)) {
+ // Invoke {@link PopupKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
+ // exit event selects a key.
+ mKeyboardView.onUpEvent(x, y, pointerId, eventTime)
+ // TODO: Should fix this reference. This is a hack to clear the state of
+ // {@link PointerTracker}.
+ PointerTracker.dismissAllPopupKeysPanels()
+ return
+ }
+ // Close the popup keys keyboard.
+ // TODO: Should fix this reference. This is a hack to clear the state of
+ // {@link PointerTracker}.
+ PointerTracker.dismissAllPopupKeysPanels()
+ }
+
+ companion object {
+ private val TAG = PopupKeysKeyboardAccessibilityDelegate::class.java.simpleName
+ private const val CLOSING_INSET_IN_PIXEL = 1
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/compat/AppWorkarounds.kt b/app/src/main/java/helium314/keyboard/compat/AppWorkarounds.kt
new file mode 100644
index 0000000000..aa00c05cf1
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/compat/AppWorkarounds.kt
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.compat
+
+import android.text.InputType
+import android.view.inputmethod.EditorInfo
+import helium314.keyboard.latin.utils.InputTypeUtils
+
+object AppWorkarounds {
+ fun adjustInputType(inputType: Int, packageName: String?): Int {
+ return when (packageName) {
+ "org.mozilla.fennec_fdroid", "org.mozilla.fenix", "org.mozilla.firefox_beta", "org.mozilla.focus",
+ "org.mozilla.klar", "org.mozilla.firefox", "org.ironfoxoss.ironfox", "net.waterfox.android.release",
+ "io.github.forkmaintainers.iceraven", "com.zen.web.tools.browser" -> {
+ // Firefox and forks (assuming all of them) don't set these flags, so we want to force them for most text fields on websites
+ // missing TYPE_TEXT_VARIATION_WEB_EDIT_TEXT is strange, considering all text fields on web pages should set it
+ // missing TYPE_TEXT_FLAG_NO_SUGGESTIONS is horrible, because JS does not interact properly with composing region
+ if (inputType and InputType.TYPE_MASK_CLASS != InputType.TYPE_CLASS_TEXT) return inputType
+ if (inputType and InputType.TYPE_MASK_VARIATION != 0) return inputType // if any variation is specified we leave it (URL, email, password, ...)
+ // looks like most (all?) non-password text fields on websites are either IME_MULTI_LINE or IME_MULTI_LINE + AUTO_CORRECT + CAP_SENTENCES
+ if (inputType and InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE == 0) return inputType
+ // for the AUTO_CORRECT flag we assume suggestions are safe and only add WEB_EDIT_TEXT
+ if (inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT == 0) return inputType or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+ // for all others we also add NO_SUGGESTIONS to avoid JS messing with the composing text
+ inputType or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+ }
+ else -> inputType
+ }
+ }
+
+ fun adjustImeOptions(imeOptions: Int, packageName: String?): Int {
+ return when (packageName) {
+ // Looks like Google decided to set inputType multiline and imeOptions no_enter_action
+ // on their search bar in Pixel launcher, and all keyboards ignore the flags because otherwise
+ // they would actually not perform the search action on action key. See https://github.com/Helium314/HeliBoard/issues/1989
+ "com.google.android.apps.nexuslauncher" -> if (imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION != 0) imeOptions - EditorInfo.IME_FLAG_NO_ENTER_ACTION else imeOptions
+ else -> imeOptions
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/compat/ClipboardManagerCompat.java b/app/src/main/java/helium314/keyboard/compat/ClipboardManagerCompat.java
new file mode 100644
index 0000000000..325bfd73d5
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/compat/ClipboardManagerCompat.java
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+package helium314.keyboard.compat;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.os.Build;
+
+public class ClipboardManagerCompat {
+
+ public static void clearPrimaryClip(ClipboardManager cm) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ try {
+ cm.clearPrimaryClip();
+ } catch (Exception e) {
+ // workaround for system-caused crash in https://github.com/Helium314/HeliBoard/issues/203
+ cm.setPrimaryClip(ClipData.newPlainText("", ""));
+ }
+ } else {
+ cm.setPrimaryClip(ClipData.newPlainText("", ""));
+ }
+ }
+
+ public static Long getClipTimestamp(ClipData cd) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ final long timestamp = cd.getDescription().getTimestamp();
+ if (timestamp > 0) // timestamp is 0 if not set
+ return timestamp;
+ }
+ return System.currentTimeMillis();
+ }
+
+ public static Boolean getClipSensitivity(final ClipDescription cd) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ return cd != null && cd.getExtras() != null && cd.getExtras().getBoolean("android.content.extra.IS_SENSITIVE");
+ }
+ return null; // can't determine
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/compat/ConfigurationCompat.kt b/app/src/main/java/helium314/keyboard/compat/ConfigurationCompat.kt
new file mode 100644
index 0000000000..e8cc6fb7c9
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/compat/ConfigurationCompat.kt
@@ -0,0 +1,12 @@
+package helium314.keyboard.compat
+
+import android.content.res.Configuration
+import android.os.Build
+import java.util.Locale
+
+fun Configuration.locale(): Locale =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ locales[0]
+ } else {
+ @Suppress("Deprecation") locale
+ }
diff --git a/app/src/main/java/helium314/keyboard/compat/EditorInfoCompatUtils.kt b/app/src/main/java/helium314/keyboard/compat/EditorInfoCompatUtils.kt
new file mode 100644
index 0000000000..7fa04b94e3
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/compat/EditorInfoCompatUtils.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.compat
+
+import android.os.Build
+import android.text.InputType
+import android.view.inputmethod.EditorInfo
+import helium314.keyboard.latin.utils.Log
+import java.util.*
+
+object EditorInfoCompatUtils {
+
+ @JvmStatic
+ fun imeActionName(imeOptions: Int): String {
+ return when (val actionId = imeOptions and EditorInfo.IME_MASK_ACTION) {
+ EditorInfo.IME_ACTION_UNSPECIFIED -> "actionUnspecified"
+ EditorInfo.IME_ACTION_NONE -> "actionNone"
+ EditorInfo.IME_ACTION_GO -> "actionGo"
+ EditorInfo.IME_ACTION_SEARCH -> "actionSearch"
+ EditorInfo.IME_ACTION_SEND -> "actionSend"
+ EditorInfo.IME_ACTION_NEXT -> "actionNext"
+ EditorInfo.IME_ACTION_DONE -> "actionDone"
+ EditorInfo.IME_ACTION_PREVIOUS -> "actionPrevious"
+ else -> "actionUnknown($actionId)"
+ }
+ }
+
+ fun debugLog(editorInfo: EditorInfo, tag: String) {
+ val format = HexFormat {
+ upperCase = true
+ number {
+ prefix = "0x"
+ minLength = 8
+ }
+ }
+ Log.d(tag, "editorInfo: inputType: ${editorInfo.inputType.toHexString(format)}, imeOptions: ${editorInfo.imeOptions.toHexString(format)}")
+ val allCaps = (editorInfo.inputType and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0
+ val sentenceCaps = (editorInfo.inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0
+ val wordCaps = (editorInfo.inputType and InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0
+ Log.d(tag, ("All caps: $allCaps, sentence caps: $sentenceCaps, word caps: $wordCaps"))
+ }
+
+ @JvmStatic
+ fun getHintLocales(editorInfo: EditorInfo?): List {
+ if (editorInfo == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return listOf()
+ }
+ val localeList = editorInfo.hintLocales ?: return listOf()
+ val locales = ArrayList(localeList.size())
+ for (i in 0 until localeList.size()) {
+ locales.add(localeList.get(i))
+ }
+ return locales
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/compat/ImeCompat.kt b/app/src/main/java/helium314/keyboard/compat/ImeCompat.kt
new file mode 100644
index 0000000000..8dc5421f71
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/compat/ImeCompat.kt
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
+@file:Suppress("DEPRECATION")
+
+package helium314.keyboard.compat
+
+import android.inputmethodservice.InputMethodService
+import android.os.Build
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodSubtype
+import helium314.keyboard.latin.RichInputMethodManager
+import helium314.keyboard.latin.settings.Settings
+
+object ImeCompat {
+ fun InputMethodService.switchInputMethod(): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return switchToNextInputMethod(false)
+ val window = window.window ?: return false
+ val token = window.attributes.token
+ return RichInputMethodManager.getInstance().inputMethodManager.switchToNextInputMethod(token, false)
+ }
+
+ fun InputMethodService.shouldSwitchToOtherInputMethods(): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return shouldOfferSwitchingToNextInputMethod()
+ val settingsValues = Settings.getValues()
+ val window = window.window ?: return settingsValues.mLanguageSwitchKeyToOtherImes
+ val token = window.attributes.token ?: return settingsValues.mLanguageSwitchKeyToOtherImes
+ return RichInputMethodManager.getInstance().inputMethodManager.shouldOfferSwitchingToNextInputMethod(token)
+ }
+
+ fun InputMethodService.switchInputMethodAndSubtype(imi: InputMethodInfo, subtype: InputMethodSubtype) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ switchInputMethod(imi.id, subtype)
+ } else {
+ val window = window.window ?: return
+ val token = window.attributes.token
+ RichInputMethodManager.getInstance().inputMethodManager.setInputMethodAndSubtype(token, imi.id, subtype)
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/compat/IsLockedCompat.kt b/app/src/main/java/helium314/keyboard/compat/IsLockedCompat.kt
new file mode 100644
index 0000000000..e7c6117695
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/compat/IsLockedCompat.kt
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.compat
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.Build
+import android.os.UserManager
+
+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
+}
+
+fun isUserLocked(context: Context): Boolean {
+ val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ !userManager.isUserUnlocked
+ else
+ false
+}
diff --git a/app/src/main/java/helium314/keyboard/dictionarypack/DictionaryPackConstants.kt b/app/src/main/java/helium314/keyboard/dictionarypack/DictionaryPackConstants.kt
new file mode 100644
index 0000000000..2cdffa337c
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/dictionarypack/DictionaryPackConstants.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.dictionarypack
+
+/**
+ * A class to group constants for dictionary pack usage.
+ *
+ * This class only defines constants. It should not make any references to outside code as far as
+ * possible, as it's used to separate cleanly the keyboard code from the dictionary pack code; this
+ * is needed in particular to cleanly compile regression tests.
+ */
+object DictionaryPackConstants {
+ /**
+ * The root domain for the dictionary pack, upon which authorities and actions will append
+ * their own distinctive strings.
+ */
+ private const val DICTIONARY_DOMAIN = "helium314.keyboard.dictionarypack.aosp"
+ /**
+ * Authority for the ContentProvider protocol.
+ */
+ // TODO: find some way to factorize this string with the one in the resources
+ const val AUTHORITY = DICTIONARY_DOMAIN
+ /**
+ * The action of the intent for publishing that new dictionary data is available.
+ */
+ // TODO: make this different across different packages. A suggested course of action is
+ // to use the package name inside this string.
+ // NOTE: The appended string should be uppercase like all other actions, but it's not for
+ // historical reasons.
+ const val NEW_DICTIONARY_INTENT_ACTION = "$DICTIONARY_DOMAIN.newdict"
+}
diff --git a/app/src/main/java/helium314/keyboard/event/BnKhiproCombiner.kt b/app/src/main/java/helium314/keyboard/event/BnKhiproCombiner.kt
new file mode 100644
index 0000000000..18b054a95d
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/BnKhiproCombiner.kt
@@ -0,0 +1,329 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+package helium314.keyboard.event
+
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import java.util.ArrayList
+
+/**
+ * Bengali combiner implementing the Khipro state machine.
+ * Converts Latin input sequences to Bengali text using greedy longest-match algorithm.
+ *
+ * This implementation matches the m17n khipro layout with:
+ * - All core vowels (shor), consonants (byanjon), and conjuncts (juktoborno)
+ * - Minimal punctuation (।ff → ৺)
+ *
+ * Intentionally excluded:
+ * - Number mappings (ongko group)
+ * - ZWJ/ZWNJ support
+ * - Extended punctuation (currency symbols, math operators)
+ */
+class BnKhiproCombiner : Combiner {
+
+ private val composingText = StringBuilder()
+
+ enum class State {
+ INIT,
+ SHOR_STATE,
+ REPH_STATE,
+ BYANJON_STATE
+ }
+
+ companion object {
+ // Group mappings
+ private val SHOR = mapOf(
+ "o" to "অ", "oo" to "ঽ",
+ "fuf" to "ু", "fuuf" to "ূ", "fqf" to "ৃ",
+ "fa" to "া", "a" to "আ",
+ "fi" to "ি", "i" to "ই",
+ "fii" to "ী", "ii" to "ঈ",
+ "fu" to "ু", "u" to "উ",
+ "fuu" to "ূ", "uu" to "ঊ",
+ "fq" to "ৃ", "q" to "ঋ",
+ "fe" to "ে", "e" to "এ",
+ "foi" to "ৈ", "oi" to "ঐ",
+ "fw" to "ো", "w" to "ও",
+ "fou" to "ৌ", "ou" to "ঔ",
+ "fae" to "্যা", "ae" to "অ্যা",
+ "wa" to "ওয়া", "fwa" to "োয়া",
+ "wae" to "ওয়্যা",
+ "we" to "ওয়ে", "fwe" to "োয়ে",
+ "ngo" to "ঙ", "nga" to "ঙা", "ngi" to "ঙি", "ngii" to "ঙী", "ngu" to "ঙু",
+ "nguff" to "ঙু", "nguu" to "ঙূ", "nguuff" to "ঙূ", "ngq" to "ঙৃ", "nge" to "ঙে",
+ "ngoi" to "ঙৈ", "ngw" to "ঙো", "ngou" to "ঙৌ", "ngae" to "ঙ্যা"
+ )
+
+ private val BYANJON = mapOf(
+ "k" to "ক", "kh" to "খ", "g" to "গ", "gh" to "ঘ", "ngf" to "ঙ",
+ "c" to "চ", "ch" to "ছ", "j" to "জ", "jh" to "ঝ", "nff" to "ঞ",
+ "tf" to "ট", "tff" to "ঠ", "tfh" to "ঠ", "df" to "ড", "dff" to "ঢ", "dfh" to "ঢ", "nf" to "ণ",
+ "t" to "ত", "th" to "থ", "d" to "দ", "dh" to "ধ", "n" to "ন",
+ "p" to "প", "ph" to "ফ", "b" to "ব", "v" to "ভ", "m" to "ম",
+ "z" to "য", "l" to "ল", "sh" to "শ", "sf" to "ষ", "s" to "স", "h" to "হ",
+ "y" to "য়", "rf" to "ড়", "rff" to "ঢ়",
+ ",," to "়"
+ )
+
+ private val JUKTOBORNO = mapOf(
+ "rz" to "র্য",
+ "kk" to "ক্ক", "ktf" to "ক্ট", "ktfr" to "ক্ট্র", "kt" to "ক্ত", "ktr" to "ক্ত্র", "kb" to "ক্ব", "km" to "ক্ম", "kz" to "ক্য", "kr" to "ক্র", "kl" to "ক্ল",
+ "kf" to "ক্ষ", "ksf" to "ক্ষ", "kkh" to "ক্ষ", "kfnf" to "ক্ষ্ণ", "kfn" to "ক্ষ্ণ", "ksfnf" to "ক্ষ্ণ", "ksfn" to "ক্ষ্ণ", "kkhn" to "ক্ষ্ণ", "kkhnf" to "ক্ষ্ণ",
+ "kfb" to "ক্ষ্ব", "ksfb" to "ক্ষ্ব", "kkhb" to "ক্ষ্ব", "kfm" to "ক্ষ্ম", "kkhm" to "ক্ষ্ম", "ksfm" to "ক্ষ্ম", "kfz" to "ক্ষ্য", "ksfz" to "ক্ষ্য", "kkhz" to "ক্ষ্য",
+ "ks" to "ক্স",
+ "khz" to "খ্য", "khr" to "খ্র",
+ "ggg" to "গ্গ", "gnf" to "গ্ণ", "gdh" to "গ্ধ", "gdhz" to "গ্ধ্য", "gdhr" to "গ্ধ্র", "gn" to "গ্ন", "gnz" to "গ্ন্য", "gb" to "গ্ব", "gm" to "গ্ম", "gz" to "গ্য", "gr" to "গ্র", "grz" to "গ্র্য", "gl" to "গ্ল",
+ "ghn" to "ঘ্ন", "ghr" to "ঘ্র",
+ "ngk" to "ঙ্ক", "ngkt" to "ঙ্ক্ত", "ngkz" to "ঙ্ক্য", "ngkr" to "ঙ্ক্র", "ngkf" to "ঙ্ক্ষ", "ngkkh" to "ঙ্ক্ষ", "ngksf" to "ঙ্ক্ষ", "ngkh" to "ঙ্খ", "ngg" to "ঙ্গ", "nggz" to "ঙ্গ্য", "nggh" to "ঙ্ঘ", "ngghz" to "ঙ্ঘ্য", "ngghr" to "ঙ্ঘ্র", "ngm" to "ঙ্ম",
+ "ngfk" to "ঙ্ক", "ngfkt" to "ঙ্ক্ত", "ngfkz" to "ঙ্ক্য", "ngfkr" to "ঙ্ক্র", "ngfkf" to "ঙ্ক্ষ", "ngfkkh" to "ঙ্ক্ষ", "ngfksf" to "ঙ্ক্ষ", "ngfkh" to "ঙ্খ", "ngfg" to "ঙ্গ", "ngfgz" to "ঙ্গ্য", "ngfgh" to "ঙ্ঘ", "ngfghz" to "ঙ্ঘ্য", "ngfghr" to "ঙ্ঘ্র", "ngfm" to "ঙ্ম",
+ "cc" to "চ্চ", "cch" to "চ্ছ", "cchb" to "চ্ছ্ব", "cchr" to "চ্ছ্র", "cnff" to "চ্ঞ", "cb" to "চ্ব", "cz" to "চ্য",
+ "jj" to "জ্জ", "jjb" to "জ্জ্ব", "jjh" to "জ্ঝ", "jnff" to "জ্ঞ", "gg" to "জ্ঞ", "jb" to "জ্ব", "jz" to "জ্য", "jr" to "জ্র",
+ "nc" to "ঞ্চ", "nffc" to "ঞ্চ", "nj" to "ঞ্জ", "nffj" to "ঞ্জ", "njh" to "ঞ্ঝ", "nffjh" to "ঞ্ঝ", "nch" to "ঞ্ছ", "nffch" to "ঞ্ছ",
+ "ttf" to "ট্ট", "tftf" to "ট্ট", "tfb" to "ট্ব", "tfm" to "ট্ম", "tfz" to "ট্য", "tfr" to "ট্র",
+ "ddf" to "ড্ড", "dfdf" to "ড্ড", "dfb" to "ড্ব", "dfz" to "ড্য", "dfr" to "ড্র", "rfg" to "ড়্গ",
+ "dffz" to "ঢ্য", "dfhz" to "ঢ্য", "dffr" to "ঢ্র", "dfhr" to "ঢ্র",
+ "nftf" to "ণ্ট", "nftff" to "ণ্ঠ", "nftfh" to "ণ্ঠ", "nftffz" to "ণ্ঠ্য", "nftfhz" to "ণ্ঠ্য", "nfdf" to "ণ্ড", "nfdfz" to "ণ্ড্য", "nfdfr" to "ণ্ড্র", "nfdff" to "ণ্ঢ", "nfdfh" to "ণ্ঢ", "nfnf" to "ণ্ণ", "nfn" to "ণ্ণ", "nfb" to "ণ্ব", "nfm" to "ণ্ম", "nfz" to "ণ্য",
+ "tt" to "ত্ত", "ttb" to "ত্ত্ব", "ttz" to "ত্ত্য", "tth" to "ত্থ", "tn" to "ত্ন", "tb" to "ত্ব", "tm" to "ত্ম", "tmz" to "ত্ম্য", "tz" to "ত্য", "tr" to "ত্র", "trz" to "ত্র্য",
+ "thb" to "থ্ব", "thz" to "থ্য", "thr" to "থ্র",
+ "dg" to "দ্গ", "dgh" to "দ্ঘ", "dd" to "দ্দ", "ddb" to "দ্দ্ব", "ddh" to "দ্ধ", "db" to "দ্ব", "dv" to "দ্ভ", "dvr" to "দ্ভ্র", "dm" to "দ্ম", "dz" to "দ্য", "dr" to "দ্র", "drz" to "দ্র্য",
+ "dhn" to "ধ্ন", "dhb" to "ধ্ব", "dhm" to "ধ্ম", "dhz" to "ধ্য", "dhr" to "ধ্র",
+ "ntf" to "ন্ট", "ntfr" to "ন্ট্র", "ntff" to "ন্ঠ", "ntfh" to "ন্ঠ", "ndf" to "ন্ড", "ndfr" to "ন্ড্র", "nt" to "ন্ত", "ntb" to "ন্ত্ব", "ntr" to "ন্ত্র", "ntrz" to "ন্ত্র্য", "nth" to "ন্থ", "nthr" to "ন্থ্র", "nd" to "ন্দ", "ndb" to "ন্দ্ব", "ndz" to "ন্দ্য",
+ "ndr" to "ন্দ্র", "ndh" to "ন্ধ", "ndhz" to "ন্ধ্য", "ndhr" to "ন্ধ্র", "nn" to "ন্ন", "nb" to "ন্ব", "nm" to "ন্ম", "nz" to "ন্য", "ns" to "ন্স",
+ "ptf" to "প্ট", "pt" to "প্ত", "pn" to "প্ন", "pp" to "প্প", "pz" to "প্য", "pr" to "প্র", "pl" to "প্ল", "ps" to "প্স",
+ "phr" to "ফ্র", "phl" to "ফ্ল",
+ "bj" to "ব্জ", "bd" to "ব্দ", "bdh" to "ব্ধ", "bb" to "ব্ব", "bz" to "ব্য", "br" to "ব্র", "bl" to "ব্ল", "vb" to "ভ্ব", "vz" to "ভ্য", "vr" to "ভ্র", "vl" to "ভ্ল",
+ "mn" to "ম্ন", "mp" to "ম্প", "mpr" to "ম্প্র", "mph" to "ম্ফ", "mb" to "ম্ব", "mbr" to "ম্ব্র", "mv" to "ম্ভ", "mvr" to "ম্ভ্র", "mm" to "ম্ম", "mz" to "ম্য", "mr" to "ম্র", "ml" to "ম্ল",
+ "zz" to "য্য",
+ "lk" to "ল্ক", "lkz" to "ল্ক্য", "lg" to "ল্গ", "ltf" to "ল্ট", "ldf" to "ল্ড", "lp" to "ল্প", "lph" to "ল্ফ", "lb" to "ল্ব", "lv" to "ল্ভ", "lm" to "ল্ম", "lz" to "ল্য", "ll" to "ল্ল",
+ "shc" to "শ্চ", "shch" to "শ্ছ", "shn" to "শ্ন", "shb" to "শ্ব", "shm" to "শ্ম", "shz" to "শ্য", "shr" to "শ্র", "shl" to "শ্ল",
+ "sfk" to "ষ্ক", "sfkr" to "ষ্ক্র", "sftf" to "ষ্ট", "sftfz" to "ষ্ট্য", "sftfr" to "ষ্ট্র", "sftff" to "ষ্ঠ", "sftfh" to "ষ্ঠ", "sftffz" to "ষ্ঠ্য", "sftfhz" to "ষ্ঠ্য", "sfnf" to "ষ্ণ", "sfn" to "ষ্ণ",
+ "sfp" to "ষ্প", "sfpr" to "ষ্প্র", "sfph" to "ষ্ফ", "sfb" to "ষ্ব", "sfm" to "ষ্ম", "sfz" to "ষ্য",
+ "sk" to "স্ক", "skr" to "স্ক্র", "skh" to "স্খ", "stf" to "স্ট", "stfr" to "স্ট্র", "st" to "স্ত", "stb" to "স্ত্ব", "stz" to "স্ত্য", "str" to "স্ত্র", "sth" to "স্থ", "sthz" to "স্থ্য", "sn" to "স্ন",
+ "sp" to "স্প", "spr" to "স্প্র", "spl" to "স্প্ল", "sph" to "স্ফ", "sb" to "স্ব", "sm" to "স্ম", "sz" to "স্য", "sr" to "স্র", "sl" to "স্ল",
+ "hn" to "হ্ন", "hnf" to "হ্ণ", "hb" to "হ্ব", "hm" to "হ্ম", "hz" to "হ্য", "hr" to "হ্র", "hl" to "হ্ল",
+ // oshomvob juktoborno
+ "ksh" to "কশ", "nsh" to "নশ", "psh" to "পশ", "ld" to "লদ", "gd" to "গদ", "ngkk" to "ঙ্কক", "ngks" to "ঙ্কস", "cn" to "চন", "cnf" to "চণ", "jn" to "জন", "jnf" to "জণ", "tft" to "টত", "dfd" to "ডদ",
+ "nft" to "ণত", "nfd" to "ণদ", "lt" to "লত", "sft" to "ষত", "nfth" to "ণথ", "nfdh" to "ণধ", "sfth" to "ষথ",
+ "ktff" to "কঠ", "ktfh" to "কঠ", "ptff" to "পঠ", "ptfh" to "পঠ", "ltff" to "লঠ", "ltfh" to "লঠ", "stff" to "সঠ", "stfh" to "সঠ", "dfdff" to "ডঢ", "dfdfh" to "ডঢ", "ndff" to "নঢ", "ndfh" to "নঢ",
+ "ktfrf" to "ক্টড়", "ktfrff" to "ক্টঢ়", "kth" to "কথ", "ktrf" to "ক্তড়", "ktrff" to "ক্তঢ়", "krf" to "কড়", "krff" to "কঢ়", "khrf" to "খড়", "khrff" to "খঢ়", "gggh" to "জ্ঞঘ", "gdff" to "গঢ", "gdfh" to "গঢ", "gdhrf" to "গ্ধড়",
+ "gdhrff" to "গ্ধঢ়", "grf" to "গড়", "grff" to "গঢ়", "ghrf" to "ঘড়", "ghrff" to "ঘঢ়", "ngkth" to "ঙ্কথ", "ngkrf" to "ঙ্কড়", "ngkrff" to "ঙ্কঢ়", "ngghrf" to "ঙ্ঘড়", "ngghrff" to "ঙ্ঘঢ়", "cchrf" to "চ্ছড়", "cchrff" to "চ্ছঢ়",
+ "tfrf" to "টড়", "tfrff" to "টঢ়", "dfrf" to "ডড়", "dfrff" to "ডঢ়", "rfgh" to "ড়ঘ", "dffrf" to "ঢড়", "dfhrf" to "ঢড়", "dffrff" to "ঢঢ়", "dfhrff" to "ঢঢ়", "nfdfrf" to "ণ্ডড়", "nfdfrff" to "ণ্ডঢ়", "trf" to "তড়", "trff" to "তঢ়", "thrf" to "থড়", "thrff" to "থঢ়",
+ "dvrf" to "দ্ভড়", "dvrff" to "দ্ভঢ়", "drf" to "দড়", "drff" to "দঢ়", "dhrf" to "ধড়", "dhrff" to "ধঢ়", "ntfrf" to "ন্টড়", "ntfrff" to "ন্টঢ়", "ndfrf" to "ন্ডড়", "ndfrff" to "ন্ডঢ়", "ntrf" to "ন্তড়", "ntrff" to "ন্তঢ়", "nthrf" to "ন্থড়",
+ "nthrff" to "ন্থঢ়", "ndrf" to "ন্দড়", "ndrff" to "ন্দঢ়", "ndhrf" to "ন্ধড়", "ndhrff" to "ন্ধঢ়", "pth" to "পথ", "pph" to "পফ", "prf" to "পড়", "prff" to "পঢ়", "phrf" to "ফড়", "phrff" to "ফঢ়", "bjh" to "বঝ", "brf" to "বড়", "brff" to "বঢ়",
+ "vrf" to "ভড়", "vrff" to "ভঢ়", "mprf" to "ম্পড়", "mprff" to "ম্পঢ়", "mbrf" to "ম্বড়", "mbrff" to "ম্বঢ়", "mvrf" to "ম্ভড়", "mvrff" to "ম্ভঢ়", "mrf" to "মড়", "mrff" to "মঢ়", "lkh" to "লখ", "lgh" to "লঘ", "shrf" to "শড়", "shrff" to "শঢ়", "sfkh" to "ষখ",
+ "sfkrf" to "ষ্কড়", "sfkrff" to "ষ্কঢ়", "sftfrf" to "ষ্টড়", "sftfrff" to "ষ্টঢ়", "sfprf" to "ষ্পড়", "sfprff" to "ষ্পঢ়", "skrf" to "স্কড়", "skrff" to "স্কঢ়", "stfrf" to "স্টড়", "stfrff" to "স্টঢ়", "strf" to "স্তড়", "strff" to "স্তঢ়", "sprf" to "স্পড়", "sprff" to "স্পঢ়",
+ "srf" to "সড়", "srff" to "সঢ়", "hrf" to "হড়", "hrff" to "হঢ়", "ldh" to "লধ", "ngksh" to "ঙ্কশ", "tfth" to "টথ", "dfdh" to "ডধ", "lth" to "লথ",
+ "ngfkk" to "ঙ্কক", "ngfks" to "ঙ্কস", "ngfkth" to "ঙ্কথ", "ngfkrf" to "ঙ্কড়", "ngfkrff" to "ঙ্কঢ়", "ngfghrf" to "ঙ্ঘড়", "ngfghrff" to "ঙ্ঘঢ়", "ngfksh" to "ঙ্কশ",
+ "kkf" to "কক্ষ", "lkf" to "লক্ষ", "sfkf" to "ষক্ষ", "skf" to "সক্ষ", "kkkh" to "কক্ষ", "lkkh" to "লক্ষ", "sfkkh" to "ষক্ষ", "skkh" to "সক্ষ", "kksf" to "কক্ষ", "lksf" to "লক্ষ", "sfksf" to "ষক্ষ", "sksf" to "সক্ষ",
+ "yr" to "য়র"
+ )
+
+ private val REPH = mapOf(
+ "rr" to "র্",
+ "r" to "র"
+ )
+
+ private val PHOLA = mapOf(
+ "r" to "র",
+ "z" to "য"
+ )
+
+ private val KAR = mapOf(
+ "o" to "", "of" to "অ",
+ "a" to "া", "af" to "আ",
+ "i" to "ি", "if" to "ই",
+ "ii" to "ী", "iif" to "ঈ",
+ "u" to "ু", "uf" to "উ",
+ "uu" to "ূ", "uuf" to "ঊ",
+ "q" to "ৃ", "qf" to "ঋ",
+ "e" to "ে", "ef" to "এ",
+ "oi" to "ৈ", "oif" to "ই",
+ "w" to "ো", "wf" to "ও",
+ "ou" to "ৌ", "ouf" to "উ",
+ "ae" to "্যা", "aef" to "অ্যা",
+ "uff" to "ু", "uuff" to "ূ", "qff" to "ৃ",
+ "we" to "োয়ে", "wef" to "ওয়ে",
+ "waf" to "ওয়া", "wa" to "োয়া",
+ "wae" to "ওয়্যা"
+ )
+
+ private val DIACRITIC = mapOf(
+ "qq" to "্", "xx" to "্", "t/" to "ৎ", "x" to "ঃ", "ng" to "ং", "/" to "ঁ", "//" to "/"
+ )
+
+ private val BIRAM = mapOf(
+ "।ff" to "৺"
+ )
+
+ private val PRITHAYOK = mapOf(
+ ";" to "", ";;" to ";"
+ )
+
+ private val AE = mapOf(
+ "ae" to "্যা"
+ )
+
+ // Group maps
+ private val GROUP_MAPS = mapOf(
+ "shor" to SHOR,
+ "byanjon" to BYANJON,
+ "juktoborno" to JUKTOBORNO,
+ "reph" to REPH,
+ "phola" to PHOLA,
+ "kar" to KAR,
+ "diacritic" to DIACRITIC,
+ "biram" to BIRAM,
+ "prithayok" to PRITHAYOK,
+ "ae" to AE
+ )
+
+ // Group order per state (priority used when same-length matches)
+ private val STATE_GROUP_ORDER = mapOf(
+ State.INIT to listOf("diacritic", "shor", "prithayok", "biram", "reph", "byanjon", "juktoborno"),
+ State.SHOR_STATE to listOf("diacritic", "shor", "biram", "prithayok", "reph", "byanjon", "juktoborno"),
+ State.REPH_STATE to listOf("prithayok", "ae", "byanjon", "juktoborno", "kar"),
+ State.BYANJON_STATE to listOf("diacritic", "prithayok", "biram", "kar", "phola", "byanjon", "juktoborno")
+ )
+
+ // Precompute max key length per group for greedy matching
+ private val MAXLEN_PER_GROUP = GROUP_MAPS.mapValues { (_, map) ->
+ map.keys.maxOfOrNull { it.length } ?: 0
+ }
+
+ private fun findLongest(state: State, text: String, i: Int): Triple {
+ val allowed = STATE_GROUP_ORDER[state] ?: return Triple("", "", "")
+
+ // Determine the max lookahead we need
+ val maxlen = allowed.maxOfOrNull { MAXLEN_PER_GROUP[it] ?: 0 } ?: 0
+ val end = minOf(text.length, i + maxlen)
+
+ // Try lengths from longest to shortest to implement greedy matching
+ for (l in (end - i) downTo 1) {
+ val chunk = text.substring(i, i + l)
+ // Check groups by priority
+ for (g in allowed) {
+ val map = GROUP_MAPS[g]
+ if (map?.containsKey(chunk) == true) {
+ // First match at this length wins due to priority order
+ return Triple(g, chunk, map[chunk]!!)
+ }
+ }
+ }
+ return Triple("", "", "")
+ }
+
+ private fun applyTransition(state: State, group: String): State {
+ return when (state) {
+ State.INIT -> when (group) {
+ "diacritic", "shor" -> State.SHOR_STATE
+ "prithayok", "biram" -> State.INIT
+ "reph" -> State.REPH_STATE
+ "byanjon" -> State.BYANJON_STATE
+ "juktoborno" -> State.BYANJON_STATE
+ else -> state
+ }
+ State.SHOR_STATE -> when (group) {
+ "diacritic", "shor" -> State.SHOR_STATE
+ "biram", "prithayok" -> State.INIT
+ "reph" -> State.REPH_STATE
+ "byanjon" -> State.BYANJON_STATE
+ "juktoborno" -> State.BYANJON_STATE
+ else -> state
+ }
+ State.REPH_STATE -> when (group) {
+ "prithayok" -> State.INIT
+ "ae" -> State.SHOR_STATE
+ "byanjon" -> State.BYANJON_STATE
+ "juktoborno" -> State.BYANJON_STATE
+ "kar" -> State.SHOR_STATE
+ else -> state
+ }
+ State.BYANJON_STATE -> when (group) {
+ "diacritic", "kar" -> State.SHOR_STATE
+ "prithayok", "biram" -> State.INIT
+ "byanjon" -> State.BYANJON_STATE
+ "juktoborno" -> State.BYANJON_STATE
+ else -> state
+ }
+ }
+ }
+
+ /**
+ * Convert an ASCII input string to Bengali output using the bn-khipro state machine.
+ */
+ fun convert(text: String): String {
+ var i = 0
+ val n = text.length
+ var state = State.INIT
+ val out = mutableListOf()
+
+ while (i < n) {
+ val (group, key, value) = findLongest(state, text, i)
+ if (group.isEmpty()) {
+ // No mapping: pass through this char and reset to INIT
+ out.add(text[i].toString())
+ i += 1
+ state = State.INIT
+ continue
+ }
+
+ // Special handling: PHOLA in BYANJON_STATE inserts virama before mapped char
+ if (state == State.BYANJON_STATE && group == "phola") {
+ out.add("্")
+ out.add(value)
+ } else {
+ out.add(value)
+ }
+
+ i += key.length
+ state = applyTransition(state, group)
+ }
+
+ return out.joinToString("")
+ }
+ }
+
+ override fun processEvent(previousEvents: ArrayList?, event: Event): Event {
+ if (event.keyCode == KeyCode.SHIFT) return event
+
+ if (Character.isWhitespace(event.codePoint)) {
+ val text = combiningStateFeedback
+ reset()
+ return createEventChainFromSequence(text, event)
+ } else if (event.isFunctionalKeyEvent) {
+ if (event.keyCode == KeyCode.DELETE) {
+ // Always reset composing state and let keyboard handle delete natively
+ val text = combiningStateFeedback
+ reset()
+ return createEventChainFromSequence(text, event)
+ }
+ val text = combiningStateFeedback
+ reset()
+ return createEventChainFromSequence(text, event)
+ } else {
+ // Add the character to composing text
+ // Use Character.toChars() to properly handle supplementary characters (emojis)
+ composingText.append(Character.toChars(event.codePoint))
+
+ // Check if we just completed a biram sequence
+ val text = composingText.toString()
+ if (text.endsWith(".ff")) {
+ val result = combiningStateFeedback
+ reset()
+ return createEventChainFromSequence(result, event)
+ }
+
+ return Event.createConsumedEvent(event)
+ }
+ }
+
+ override val combiningStateFeedback: CharSequence
+ get() = convert(composingText.toString())
+
+ override fun reset() {
+ composingText.setLength(0)
+ }
+
+ private fun createEventChainFromSequence(text: CharSequence, originalEvent: Event): Event {
+ return Event.createSoftwareTextEvent(text, KeyCode.MULTIPLE_CODE_POINTS, originalEvent)
+ }
+}
diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/Combiner.kt b/app/src/main/java/helium314/keyboard/event/Combiner.kt
similarity index 86%
rename from app/src/main/java/org/dslul/openboard/inputmethod/event/Combiner.kt
rename to app/src/main/java/helium314/keyboard/event/Combiner.kt
index c1290dfe0f..4ea4c9ab4d 100644
--- a/app/src/main/java/org/dslul/openboard/inputmethod/event/Combiner.kt
+++ b/app/src/main/java/helium314/keyboard/event/Combiner.kt
@@ -1,4 +1,10 @@
-package org.dslul.openboard.inputmethod.event
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
import java.util.*
@@ -17,7 +23,7 @@ interface Combiner {
* @param event the event to combine with the existing state.
* @return the resulting event.
*/
- fun processEvent(previousEvents: ArrayList?, event: Event?): Event?
+ fun processEvent(previousEvents: ArrayList?, event: Event): Event
/**
* Get the feedback that should be shown to the user for the current state of this combiner.
diff --git a/app/src/main/java/helium314/keyboard/event/CombinerChain.kt b/app/src/main/java/helium314/keyboard/event/CombinerChain.kt
new file mode 100644
index 0000000000..e24b035bd5
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/CombinerChain.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
+
+import android.text.SpannableStringBuilder
+import android.text.TextUtils
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import java.util.*
+
+/**
+ * This class implements the logic chain between receiving events and generating code points.
+ *
+ * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard,
+ * or any exotic input source.
+ * This class will orchestrate the composing chain that starts with an event as its input. Each
+ * composer will be given turns one after the other.
+ * The output is composed of two sequences of code points: the first, representing the already
+ * finished combining part, will be shown normally as the composing string, while the second is
+ * feedback on the composing state and will typically be shown with different styling such as
+ * a colored background.
+ *
+ * The combiner chain takes events as inputs and outputs code points and combining state.
+ * For example, if the input language is Japanese, the combining chain will typically perform
+ * kana conversion. This takes a string for initial text, taken to be present before the
+ * cursor: we'll start after this.
+ * @param initialText The text that has already been combined so far.
+ */
+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()
+
+ init {
+ // The dead key combiner is always active, and always first
+ mCombiners.add(DeadKeyCombiner())
+ if (combiningSpec == "hangul")
+ mCombiners.add(HangulCombiner())
+ else if (combiningSpec == "bn_khipro")
+ mCombiners.add(BnKhiproCombiner())
+ }
+
+ fun reset() {
+ mCombinedText.setLength(0)
+ mStateFeedback.clear()
+ for (c in mCombiners) {
+ c.reset()
+ }
+ }
+
+ private fun updateStateFeedback() {
+ mStateFeedback.clear()
+ for (i in mCombiners.indices.reversed()) {
+ mStateFeedback.append(mCombiners[i].combiningStateFeedback)
+ }
+ }
+
+ /**
+ * Process an event through the combining chain, and return a processed event to apply.
+ * @param previousEvents the list of previous events in this composition
+ * @param newEvent the new event to process
+ * @return the processed event. It may be the same event, or a consumed event, or a completely
+ * new event. However it may never be null.
+ */
+ fun processEvent(previousEvents: ArrayList?, newEvent: Event): Event {
+ val modifiablePreviousEvents = ArrayList(previousEvents!!)
+ var event = newEvent
+ for (combiner in mCombiners) {
+ // A combiner can never return more than one event; it can return several
+ // code points, but they should be encapsulated within one event.
+ event = combiner.processEvent(modifiablePreviousEvents, event)
+ if (event.isConsumed) {
+ // If the event is consumed, then we don't pass it to subsequent combiners:
+ // they should not see it at all.
+ break
+ }
+ }
+ updateStateFeedback()
+ return event
+ }
+
+ /**
+ * Apply a processed event.
+ * @param event the event to be applied
+ */
+ fun applyProcessedEvent(event: Event?) {
+ if (null != event) { // TODO: figure out the generic way of doing this
+ if (KeyCode.DELETE == event.keyCode) {
+ val length = mCombinedText.length
+ if (length > 0) {
+ val lastCodePoint = mCombinedText.codePointBefore(length)
+ mCombinedText.delete(length - Character.charCount(lastCodePoint), length)
+ }
+ } else {
+ val textToCommit = event.textToCommit
+ if (!TextUtils.isEmpty(textToCommit)) {
+ mCombinedText.append(textToCommit)
+ }
+ }
+ }
+ updateStateFeedback()
+ }
+
+ /**
+ * Get the char sequence that should be displayed as the composing word. It may include
+ * styling spans.
+ */
+ val composingWordWithCombiningFeedback: CharSequence
+ get() {
+ val s = SpannableStringBuilder(mCombinedText)
+ return s.append(mStateFeedback)
+ }
+
+}
diff --git a/app/src/main/java/helium314/keyboard/event/DeadKeyCombiner.kt b/app/src/main/java/helium314/keyboard/event/DeadKeyCombiner.kt
new file mode 100644
index 0000000000..2c3d04dd9a
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/DeadKeyCombiner.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
+
+import android.text.TextUtils
+import android.util.SparseIntArray
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.latin.common.Constants
+import java.text.Normalizer
+import java.util.*
+
+/**
+ * A combiner that handles dead keys.
+ */
+class DeadKeyCombiner : Combiner {
+ private object Data {
+ // This class data taken from KeyCharacterMap.java.
+/* Characters used to display placeholders for dead keys. */
+ private const val ACCENT_ACUTE = '\u00B4'.code
+ private const val ACCENT_BREVE = '\u02D8'.code
+ private const val ACCENT_CARON = '\u02C7'.code
+ private const val ACCENT_CEDILLA = '\u00B8'.code
+ private const val ACCENT_CIRCUMFLEX = '\u02C6'.code
+ private const val ACCENT_COMMA_ABOVE = '\u1FBD'.code
+ private const val ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'.code
+ private const val ACCENT_DOT_ABOVE = '\u02D9'.code
+ private const val ACCENT_DOT_BELOW = Constants.CODE_PERIOD // approximate
+ private const val ACCENT_DOUBLE_ACUTE = '\u02DD'.code
+ private const val ACCENT_GRAVE = '\u02CB'.code
+ private const val ACCENT_HOOK_ABOVE = '\u02C0'.code
+ private const val ACCENT_HORN = Constants.CODE_SINGLE_QUOTE // approximate
+ private const val ACCENT_MACRON = '\u00AF'.code
+ private const val ACCENT_MACRON_BELOW = '\u02CD'.code
+ private const val ACCENT_OGONEK = '\u02DB'.code
+ private const val ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'.code
+ private const val ACCENT_RING_ABOVE = '\u02DA'.code
+ private const val ACCENT_STROKE = Constants.CODE_DASH // approximate
+ private const val ACCENT_TILDE = '\u02DC'.code
+ private const val ACCENT_TURNED_COMMA_ABOVE = '\u02BB'.code
+ private const val ACCENT_UMLAUT = '\u00A8'.code
+ private const val ACCENT_VERTICAL_LINE_ABOVE = '\u02C8'.code
+ private const val ACCENT_VERTICAL_LINE_BELOW = '\u02CC'.code
+ /* Legacy dead key display characters used in previous versions of the API (before L)
+ * We still support these characters by mapping them to their non-legacy version. */
+ private const val ACCENT_GRAVE_LEGACY = Constants.CODE_GRAVE_ACCENT
+ private const val ACCENT_CIRCUMFLEX_LEGACY = Constants.CODE_CIRCUMFLEX_ACCENT
+ private const val ACCENT_TILDE_LEGACY = Constants.CODE_TILDE
+ /**
+ * Maps Unicode combining diacritical to display-form dead key.
+ */
+ val sCombiningToAccent = SparseIntArray()
+ val sAccentToCombining = SparseIntArray()
+ private fun addCombining(combining: Int, accent: Int) {
+ sCombiningToAccent.append(combining, accent)
+ sAccentToCombining.append(accent, combining)
+ }
+
+ // Caution! This may only contain chars, not supplementary code points. It's unlikely
+ // it will ever need to, but if it does we'll have to change this
+ private val sNonstandardDeadCombinations = SparseIntArray()
+
+ private fun addNonStandardDeadCombination(deadCodePoint: Int, spacingCodePoint: Int, result: Int) {
+ val combination = deadCodePoint shl 16 or spacingCodePoint
+ sNonstandardDeadCombinations.put(combination, result)
+ }
+
+ const val NOT_A_CHAR = 0
+ const val BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION = 16
+ // Get a non-standard combination
+ fun getNonstandardCombination(deadCodePoint: Int,
+ spacingCodePoint: Int): Char {
+ val combination = spacingCodePoint or
+ (deadCodePoint shl BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION)
+ return sNonstandardDeadCombinations[combination, NOT_A_CHAR].toChar()
+ }
+
+ init { // U+0300: COMBINING GRAVE ACCENT
+ addCombining('\u0300'.code, ACCENT_GRAVE)
+ // U+0301: COMBINING ACUTE ACCENT
+ addCombining('\u0301'.code, ACCENT_ACUTE)
+ // U+0302: COMBINING CIRCUMFLEX ACCENT
+ addCombining('\u0302'.code, ACCENT_CIRCUMFLEX)
+ // U+0303: COMBINING TILDE
+ addCombining('\u0303'.code, ACCENT_TILDE)
+ // U+0304: COMBINING MACRON
+ addCombining('\u0304'.code, ACCENT_MACRON)
+ // U+0306: COMBINING BREVE
+ addCombining('\u0306'.code, ACCENT_BREVE)
+ // U+0307: COMBINING DOT ABOVE
+ addCombining('\u0307'.code, ACCENT_DOT_ABOVE)
+ // U+0308: COMBINING DIAERESIS
+ addCombining('\u0308'.code, ACCENT_UMLAUT)
+ // U+0309: COMBINING HOOK ABOVE
+ addCombining('\u0309'.code, ACCENT_HOOK_ABOVE)
+ // U+030A: COMBINING RING ABOVE
+ addCombining('\u030A'.code, ACCENT_RING_ABOVE)
+ // U+030B: COMBINING DOUBLE ACUTE ACCENT
+ addCombining('\u030B'.code, ACCENT_DOUBLE_ACUTE)
+ // U+030C: COMBINING CARON
+ addCombining('\u030C'.code, ACCENT_CARON)
+ // U+030D: COMBINING VERTICAL LINE ABOVE
+ addCombining('\u030D'.code, ACCENT_VERTICAL_LINE_ABOVE)
+ // U+030E: COMBINING DOUBLE VERTICAL LINE ABOVE
+ //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE);
+ // U+030F: COMBINING DOUBLE GRAVE ACCENT
+ //addCombining('\u030F', ACCENT_DOUBLE_GRAVE);
+ // U+0310: COMBINING CANDRABINDU
+ //addCombining('\u0310', ACCENT_CANDRABINDU);
+ // U+0311: COMBINING INVERTED BREVE
+ //addCombining('\u0311', ACCENT_INVERTED_BREVE);
+ // U+0312: COMBINING TURNED COMMA ABOVE
+ addCombining('\u0312'.code, ACCENT_TURNED_COMMA_ABOVE)
+ // U+0313: COMBINING COMMA ABOVE
+ addCombining('\u0313'.code, ACCENT_COMMA_ABOVE)
+ // U+0314: COMBINING REVERSED COMMA ABOVE
+ addCombining('\u0314'.code, ACCENT_REVERSED_COMMA_ABOVE)
+ // U+0315: COMBINING COMMA ABOVE RIGHT
+ addCombining('\u0315'.code, ACCENT_COMMA_ABOVE_RIGHT)
+ // U+031B: COMBINING HORN
+ addCombining('\u031B'.code, ACCENT_HORN)
+ // U+0323: COMBINING DOT BELOW
+ addCombining('\u0323'.code, ACCENT_DOT_BELOW)
+ // U+0326: COMBINING COMMA BELOW
+ //addCombining('\u0326', ACCENT_COMMA_BELOW);
+ // U+0327: COMBINING CEDILLA
+ addCombining('\u0327'.code, ACCENT_CEDILLA)
+ // U+0328: COMBINING OGONEK
+ addCombining('\u0328'.code, ACCENT_OGONEK)
+ // U+0329: COMBINING VERTICAL LINE BELOW
+ addCombining('\u0329'.code, ACCENT_VERTICAL_LINE_BELOW)
+ // U+0331: COMBINING MACRON BELOW
+ addCombining('\u0331'.code, ACCENT_MACRON_BELOW)
+ // U+0335: COMBINING SHORT STROKE OVERLAY
+ addCombining('\u0335'.code, ACCENT_STROKE)
+ // U+0342: COMBINING GREEK PERISPOMENI
+ //addCombining('\u0342', ACCENT_PERISPOMENI);
+ // U+0344: COMBINING GREEK DIALYTIKA TONOS
+ //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS);
+ // U+0345: COMBINING GREEK YPOGEGRAMMENI
+ //addCombining('\u0345', ACCENT_YPOGEGRAMMENI);
+ // One-way mappings to equivalent preferred accents.
+ // U+0340: COMBINING GRAVE TONE MARK
+ sCombiningToAccent.append('\u0340'.code, ACCENT_GRAVE)
+ // U+0341: COMBINING ACUTE TONE MARK
+ sCombiningToAccent.append('\u0341'.code, ACCENT_ACUTE)
+ // U+0343: COMBINING GREEK KORONIS
+ sCombiningToAccent.append('\u0343'.code, ACCENT_COMMA_ABOVE)
+ // One-way legacy mappings to preserve compatibility with older applications.
+ // U+0300: COMBINING GRAVE ACCENT
+ sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'.code)
+ // U+0302: COMBINING CIRCUMFLEX ACCENT
+ sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'.code)
+ // U+0303: COMBINING TILDE
+ sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'.code)
+ }
+
+ init { // Non-standard decompositions.
+ // Stroke modifier for Finnish multilingual keyboard and others.
+ // U+0110: LATIN CAPITAL LETTER D WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'D'.code, '\u0110'.code)
+ // U+01E4: LATIN CAPITAL LETTER G WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'G'.code, '\u01e4'.code)
+ // U+0126: LATIN CAPITAL LETTER H WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'H'.code, '\u0126'.code)
+ // U+0197: LATIN CAPITAL LETTER I WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'I'.code, '\u0197'.code)
+ // U+0141: LATIN CAPITAL LETTER L WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'L'.code, '\u0141'.code)
+ // U+00D8: LATIN CAPITAL LETTER O WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'O'.code, '\u00d8'.code)
+ // U+0166: LATIN CAPITAL LETTER T WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'T'.code, '\u0166'.code)
+ // U+0111: LATIN SMALL LETTER D WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'd'.code, '\u0111'.code)
+ // U+01E5: LATIN SMALL LETTER G WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'g'.code, '\u01e5'.code)
+ // U+0127: LATIN SMALL LETTER H WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'h'.code, '\u0127'.code)
+ // U+0268: LATIN SMALL LETTER I WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'i'.code, '\u0268'.code)
+ // U+0142: LATIN SMALL LETTER L WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'l'.code, '\u0142'.code)
+ // U+00F8: LATIN SMALL LETTER O WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 'o'.code, '\u00f8'.code)
+ // U+0167: LATIN SMALL LETTER T WITH STROKE
+ addNonStandardDeadCombination(ACCENT_STROKE, 't'.code, '\u0167'.code)
+ }
+ }
+
+ // TODO: make this a list of events instead
+ val mDeadSequence = StringBuilder()
+
+ override fun processEvent(previousEvents: ArrayList?, event: Event): Event {
+ if (TextUtils.isEmpty(mDeadSequence)) { // No dead char is currently being tracked: this is the most common case.
+ if (event.isDead) { // The event was a dead key. Start tracking it.
+ mDeadSequence.appendCodePoint(event.codePoint)
+ return Event.createConsumedEvent(event)
+ }
+ // Regular keystroke when not keeping track of a dead key. Simply said, there are
+ // no dead keys at all in the current input, so this combiner has nothing to do and
+ // simply returns the event as is. The majority of events will go through this path.
+ return event
+ }
+ if (Character.isWhitespace(event.codePoint)
+ || event.codePoint == mDeadSequence.codePointBefore(mDeadSequence.length)) { // When whitespace or twice the same dead key, we should output the dead sequence as is.
+ val resultEvent = createEventChainFromSequence(mDeadSequence.toString(), event)
+ mDeadSequence.setLength(0)
+ return resultEvent
+ }
+ if (event.isFunctionalKeyEvent) {
+ if (KeyCode.DELETE == event.keyCode) { // Remove the last code point
+ val trimIndex = mDeadSequence.length - Character.charCount(
+ mDeadSequence.codePointBefore(mDeadSequence.length))
+ mDeadSequence.setLength(trimIndex)
+ return Event.createConsumedEvent(event)
+ }
+ return event
+ }
+ if (event.isDead) {
+ mDeadSequence.appendCodePoint(event.codePoint)
+ return Event.createConsumedEvent(event)
+ }
+ // Combine normally.
+ val sb = StringBuilder()
+ sb.appendCodePoint(event.codePoint)
+ var codePointIndex = 0
+ while (codePointIndex < mDeadSequence.length) {
+ val deadCodePoint = mDeadSequence.codePointAt(codePointIndex)
+ val replacementSpacingChar = Data.getNonstandardCombination(deadCodePoint, event.codePoint)
+ if (Data.NOT_A_CHAR != replacementSpacingChar.code) {
+ sb.setCharAt(0, replacementSpacingChar)
+ } else {
+ val combining = Data.sAccentToCombining[deadCodePoint]
+ sb.appendCodePoint(if (0 == combining) deadCodePoint else combining)
+ }
+ codePointIndex += if (Character.isSupplementaryCodePoint(deadCodePoint)) 2 else 1
+ }
+ val normalizedString = Normalizer.normalize(sb, Normalizer.Form.NFC)
+ val resultEvent = createEventChainFromSequence(normalizedString, event)
+ mDeadSequence.setLength(0)
+ return resultEvent
+ }
+
+ override fun reset() {
+ mDeadSequence.setLength(0)
+ }
+
+ override val combiningStateFeedback: CharSequence
+ get() = mDeadSequence
+
+ companion object {
+ private fun createEventChainFromSequence(text: CharSequence, originalEvent: Event): Event {
+ var index = text.length
+ if (index <= 0) {
+ return originalEvent
+ }
+ var lastEvent: Event? = null
+ do {
+ val codePoint = Character.codePointBefore(text, index)
+ lastEvent = Event.createHardwareKeypressEvent(codePoint, originalEvent.keyCode, 0, lastEvent, false)
+ index -= Character.charCount(codePoint)
+ } while (index > 0)
+ // can't be null because
+ return lastEvent!!
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/event/Event.kt b/app/src/main/java/helium314/keyboard/event/Event.kt
new file mode 100644
index 0000000000..09e00bdf09
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/Event.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
+
+import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
+import helium314.keyboard.latin.common.Constants
+import helium314.keyboard.latin.common.StringUtils
+
+/**
+ * Class representing a generic input event as handled by Latin IME.
+ *
+ * This contains information about the origin of the event, but it is generalized and should
+ * represent a software keypress, hardware keypress, or d-pad move alike.
+ * Very importantly, this does not necessarily result in inputting one character, or even anything
+ * at all - it may be a dead key, it may be a partial input, it may be a special key on the
+ * keyboard, it may be a cancellation of a keypress (e.g. in a soft keyboard the finger of the
+ * user has slid out of the key), etc. It may also be a batch input from a gesture or handwriting
+ * for example.
+ * The combiner should figure out what to do with this.
+ */
+class Event private constructor(
+ // The type of event - one of the constants above
+ private val eventType: Int,
+ // If applicable, this contains the string that should be input.
+ val text: CharSequence? = null,
+ // The code point associated with the event, if relevant. This is a unicode code point, and
+ // has nothing to do with other representations of the key. It is only relevant if this event
+ // is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point
+ // associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when
+ // it's not relevant.
+ val codePoint: Int = NOT_A_CODE_POINT,
+ // The key code associated with the event, if relevant. This is relevant whenever this event
+ // has been triggered by a key press, but not for a gesture for example. This has conceptually
+ // no link to the code point, although keys that enter a straight code point may often set
+ // this to be equal to mCodePoint for convenience. If this is not a key, this must contain
+ // NOT_A_KEY_CODE.
+ val keyCode: Int = NOT_A_KEY_CODE,
+ // State of meta keys (currently ctrl, alt, fn, meta)
+ // same value as https://developer.android.com/reference/android/view/KeyEvent#getMetaState()
+ val metaState: Int = 0,
+ // Coordinates of the touch event, if relevant. If useful, we may want to replace this with
+ // a MotionEvent or something in the future. This is only relevant when the keypress is from
+ // a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the
+ // future or some other awesome sauce.
+ val x: Int = Constants.NOT_A_COORDINATE,
+ val y: Int = Constants.NOT_A_COORDINATE,
+ // If this is of type EVENT_TYPE_SUGGESTION_PICKED, this must not be null (and must be null in
+ // other cases).
+ val suggestedWordInfo: SuggestedWordInfo? = null,
+ // Some flags that can't go into the key code. It's a bit field of FLAG_*
+ private val flags: Int = FLAG_NONE,
+ // The next event, if any. Null if there is no next event yet.
+ val nextEvent: Event? = null
+ // This logic may need to be refined in the future
+) {
+ init {
+ if ((EVENT_TYPE_SUGGESTION_PICKED == eventType) != (suggestedWordInfo != null))
+ throw RuntimeException("Wrong event: SUGGESTION_PICKED event must have a non-null SuggestedWordInfo, other events may not")
+ }
+
+ // Returns whether this is a function key like backspace, ctrl, settings... as opposed to keys
+ // that result in input like letters or space.
+ val isFunctionalKeyEvent: Boolean
+ get() = NOT_A_CODE_POINT == codePoint || metaState != 0 // This logic may need to be refined in the future
+
+ // Returns whether this event is for a dead character. @see {@link #FLAG_DEAD}
+ val isDead: Boolean get() = 0 != FLAG_DEAD and flags
+
+ val isKeyRepeat: Boolean get() = 0 != FLAG_REPEAT and flags
+
+ val isConsumed: Boolean get() = 0 != FLAG_CONSUMED and flags
+
+ val isCombining: Boolean get() = 0 != FLAG_COMBINING and flags
+
+ val isGesture: Boolean get() = EVENT_TYPE_GESTURE == eventType
+
+ // Returns whether this is a fake key press from the suggestion strip. This happens with
+ // punctuation signs selected from the suggestion strip.
+ val isSuggestionStripPress: Boolean get() = EVENT_TYPE_SUGGESTION_PICKED == eventType
+
+ val isHandled: Boolean get() = EVENT_TYPE_NOT_HANDLED != eventType
+
+ // A consumed event should input no text.
+ val textToCommit: CharSequence?
+ get() {
+ if (isConsumed) {
+ return "" // A consumed event should input no text.
+ }
+ return when (eventType) {
+ EVENT_TYPE_MODE_KEY, EVENT_TYPE_NOT_HANDLED, EVENT_TYPE_TOGGLE, EVENT_TYPE_CURSOR_MOVE -> ""
+ EVENT_TYPE_INPUT_KEYPRESS -> StringUtils.newSingleCodePointString(codePoint)
+ EVENT_TYPE_GESTURE, EVENT_TYPE_SOFTWARE_GENERATED_STRING, EVENT_TYPE_SUGGESTION_PICKED -> text
+ else -> throw RuntimeException("Unknown event type: $eventType")
+ }
+ }
+
+ companion object {
+ // Should the types below be represented by separate classes instead? It would be cleaner
+ // but probably a bit too much
+ // An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard.
+ const val EVENT_TYPE_NOT_HANDLED = 0
+ // A key press that is part of input, for example pressing an alphabetic character on a
+ // hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later
+ // through combination.
+ const val EVENT_TYPE_INPUT_KEYPRESS = 1
+ // A toggle event is triggered by a key that affects the previous character. An example would
+ // be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with
+ // repeated presses.
+ const val EVENT_TYPE_TOGGLE = 2
+ // A mode event instructs the combiner to change modes. The canonical example would be the
+ // hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard
+ // if handled at the combiner level.
+ const val EVENT_TYPE_MODE_KEY = 3
+ // An event corresponding to a gesture.
+ const val EVENT_TYPE_GESTURE = 4
+ // An event corresponding to the manual pick of a suggestion.
+ const val EVENT_TYPE_SUGGESTION_PICKED = 5
+ // An event corresponding to a string generated by some software process.
+ const val EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6
+ // An event corresponding to a cursor move
+ const val EVENT_TYPE_CURSOR_MOVE = 7
+
+ // 0 is a valid code point, so we use -1 here.
+ const val NOT_A_CODE_POINT = -1
+ // -1 is a valid key code, so we use 0 here.
+ const val NOT_A_KEY_CODE = 0
+
+ private const val FLAG_NONE = 0
+ // This event is a dead character, usually input by a dead key. Examples include dead-acute or dead-abovering.
+ private const val FLAG_DEAD = 0x1
+ // This event is coming from a key repeat, software or hardware.
+ private const val FLAG_REPEAT = 0x2
+ // This event has already been consumed.
+ private const val FLAG_CONSUMED = 0x4
+ // This event is a combining character, usually a hangul input.
+ private const val FLAG_COMBINING = 0x8
+
+ @JvmStatic
+ fun createSoftwareKeypressEvent(codePoint: Int, keyCode: Int, metaState: Int, x: Int, y: Int, isKeyRepeat: Boolean) =
+ Event(
+ eventType = EVENT_TYPE_INPUT_KEYPRESS,
+ codePoint = codePoint,
+ keyCode = keyCode,
+ metaState = metaState,
+ x = x,
+ y = y,
+ flags = if (isKeyRepeat) FLAG_REPEAT else FLAG_NONE
+ )
+
+ // 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(
+ eventType = EVENT_TYPE_INPUT_KEYPRESS,
+ codePoint = codePoint,
+ keyCode = keyCode,
+ metaState = metaState,
+ x = Constants.EXTERNAL_KEYBOARD_COORDINATE,
+ y = Constants.EXTERNAL_KEYBOARD_COORDINATE,
+ flags = if (isKeyRepeat) FLAG_REPEAT else FLAG_NONE,
+ nextEvent = next
+ )
+
+ // This creates an input event for a dead character. see FLAG_DEAD
+ fun createDeadEvent(codePoint: Int, keyCode: Int, metaState: Int, next: Event?) =
+ Event(
+ eventType = EVENT_TYPE_INPUT_KEYPRESS,
+ codePoint = codePoint,
+ keyCode = keyCode,
+ metaState = metaState,
+ x = Constants.EXTERNAL_KEYBOARD_COORDINATE,
+ y = Constants.EXTERNAL_KEYBOARD_COORDINATE,
+ flags = FLAG_DEAD,
+ nextEvent = next
+ )
+
+ // This creates an input event for a dead character. see FLAG_DEAD
+ fun createSoftwareDeadEvent(codePoint: Int, keyCode: Int, metaState: Int, x: Int, y: Int, next: Event?) =
+ Event(
+ eventType = EVENT_TYPE_INPUT_KEYPRESS,
+ codePoint = codePoint,
+ keyCode = keyCode,
+ metaState = metaState,
+ x = x,
+ y = y,
+ flags = FLAG_DEAD,
+ nextEvent = next
+ )
+
+ /**
+ * Create an input event with nothing but a code point. This is the most basic possible input
+ * event; it contains no information on many things the IME requires to function correctly,
+ * so avoid using it unless really nothing is known about this input.
+ * @param codePoint the code point.
+ * @return an event for this code point.
+ */
+ @JvmStatic
+ // TODO: should we have a different type of event for this? After all, it's not a key press.
+ fun createEventForCodePointFromUnknownSource(codePoint: Int) = Event(eventType = EVENT_TYPE_INPUT_KEYPRESS, codePoint = codePoint)
+
+ /**
+ * Creates an input event with a code point and x, y coordinates. This is typically used when
+ * resuming a previously-typed word, when the coordinates are still known.
+ * @param codePoint the code point to input.
+ * @param x the X coordinate.
+ * @param y the Y coordinate.
+ * @return an event for this code point and coordinates.
+ */
+ @JvmStatic
+ // TODO: should we have a different type of event for this? After all, it's not a key press.
+ fun createEventForCodePointFromAlreadyTypedText(codePoint: Int, x: Int, y: Int) =
+ Event(eventType = EVENT_TYPE_INPUT_KEYPRESS, codePoint = codePoint, x = x, y = y)
+
+ /**
+ * Creates an input event representing the manual pick of a suggestion.
+ * @return an event for this suggestion pick.
+ */
+ @JvmStatic
+ fun createSuggestionPickedEvent(suggestedWordInfo: SuggestedWordInfo) =
+ Event(
+ eventType = EVENT_TYPE_SUGGESTION_PICKED,
+ text = suggestedWordInfo.mWord,
+ x = Constants.SUGGESTION_STRIP_COORDINATE,
+ y = Constants.SUGGESTION_STRIP_COORDINATE,
+ suggestedWordInfo = suggestedWordInfo
+ )
+
+ /**
+ * Creates an input event with a CharSequence. This is used by some software processes whose
+ * output is a string, possibly with styling. Examples include press on a multi-character key,
+ * or combination that outputs a string.
+ * @param text the CharSequence associated with this event.
+ * @param keyCode the key code, or NOT_A_KEYCODE if not applicable.
+ * @param nextEvent the next event, or null if not applicable.
+ * @return an event for this text.
+ */
+ @JvmStatic
+ fun createSoftwareTextEvent(text: CharSequence?, keyCode: Int, nextEvent: Event? = null) =
+ Event(eventType = EVENT_TYPE_SOFTWARE_GENERATED_STRING, text = text, keyCode = keyCode, nextEvent = nextEvent)
+
+ /**
+ * Creates an input event representing the manual pick of a punctuation suggestion.
+ * @return an event for this suggestion pick.
+ */
+ @JvmStatic
+ fun createPunctuationSuggestionPickedEvent(suggestedWordInfo: SuggestedWordInfo) =
+ Event(
+ eventType = EVENT_TYPE_SUGGESTION_PICKED,
+ text = suggestedWordInfo.mWord,
+ codePoint = suggestedWordInfo.mWord[0].code,
+ x = Constants.SUGGESTION_STRIP_COORDINATE,
+ y = Constants.SUGGESTION_STRIP_COORDINATE,
+ suggestedWordInfo = suggestedWordInfo
+ )
+
+ /**
+ * Creates an input event representing moving the cursor. The relative move amount is stored
+ * in mX.
+ * @param moveAmount the relative move amount.
+ * @return an event for this cursor move.
+ */
+ @JvmStatic
+ fun createCursorMovedEvent(moveAmount: Int) = Event(eventType = EVENT_TYPE_CURSOR_MOVE, x = moveAmount)
+
+ /**
+ * Creates an event identical to the passed event, but that has already been consumed.
+ * @param source the event to copy the properties of.
+ * @return an identical event marked as consumed.
+ */
+ // A consumed event should not input any text at all, so we pass the empty string as text.
+ fun createConsumedEvent(source: Event) =
+ Event(source.eventType, source.text, source.codePoint, source.keyCode, source.metaState,
+ source.x, source.y, source.suggestedWordInfo, source.flags or FLAG_CONSUMED, source.nextEvent)
+
+ fun createCombiningEvent(source: Event) =
+ Event(source.eventType, source.text, source.codePoint, source.keyCode, source.metaState,
+ source.x, source.y, source.suggestedWordInfo, source.flags or FLAG_COMBINING, source.nextEvent)
+
+ val notHandledEvent = Event(eventType = EVENT_TYPE_NOT_HANDLED)
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/event/EventDecoder.kt b/app/src/main/java/helium314/keyboard/event/EventDecoder.kt
new file mode 100644
index 0000000000..44a535f654
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/EventDecoder.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
+
+/**
+ * A generic interface for event decoders.
+ */
+interface EventDecoder
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/event/HangulCombiner.kt b/app/src/main/java/helium314/keyboard/event/HangulCombiner.kt
new file mode 100644
index 0000000000..b782e1c489
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/HangulCombiner.kt
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+package helium314.keyboard.event
+
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.latin.common.Constants
+import java.lang.StringBuilder
+import java.util.ArrayList
+
+class HangulCombiner : Combiner {
+
+ private val composingWord = StringBuilder()
+
+ val history: MutableList = mutableListOf()
+ private val syllable: HangulSyllable? get() = history.lastOrNull()
+
+ override fun processEvent(previousEvents: ArrayList?, event: Event): Event {
+ if (event.keyCode == 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.codePoint)) {
+ val text = combiningStateFeedback
+ reset()
+ return createEventChainFromSequence(text, event)
+ } else if (event.isFunctionalKeyEvent) {
+ if(event.keyCode == KeyCode.DELETE) {
+ return when {
+ history.size == 1 && composingWord.isEmpty() || history.isEmpty() && composingWord.length == 1 -> {
+ reset()
+ Event.createHardwareKeypressEvent(0x20, Constants.CODE_SPACE, 0, event, event.isKeyRepeat)
+ }
+ history.isNotEmpty() -> {
+ history.removeAt(history.lastIndex)
+ Event.createConsumedEvent(event)
+ }
+ composingWord.isNotEmpty() -> {
+ composingWord.deleteCharAt(composingWord.lastIndex)
+ Event.createConsumedEvent(event)
+ }
+ else -> event
+ }
+ }
+ val text = combiningStateFeedback
+ reset()
+ return createEventChainFromSequence(text, event)
+ } else {
+ val currentSyllable = syllable ?: HangulSyllable()
+ val jamo = HangulJamo.of(event.codePoint)
+ if (!event.isCombining || jamo is HangulJamo.NonHangul) {
+ composingWord.append(currentSyllable.string)
+ composingWord.append(jamo.string)
+ history.clear()
+ } else {
+ when (jamo) {
+ is HangulJamo.Consonant -> {
+ val initial = jamo.toInitial()
+ val final = jamo.toFinal()
+ if (currentSyllable.initial != null && currentSyllable.medial != null) {
+ if (currentSyllable.final == null) {
+ val combination = COMBINATION_TABLE_DUBEOLSIK[currentSyllable.initial.codePoint to (initial?.codePoint ?: -1)]
+ history +=
+ if (combination != null) {
+ currentSyllable.copy(initial = HangulJamo.Initial(combination))
+ } else {
+ if (final != null) {
+ currentSyllable.copy(final = final)
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ HangulSyllable(initial = initial)
+ }
+ }
+ } else {
+ val pair = currentSyllable.final.codePoint to (final?.codePoint ?: -1)
+ val combination = COMBINATION_TABLE_DUBEOLSIK[pair]
+ history += if (combination != null) {
+ currentSyllable.copy(final = HangulJamo.Final(combination, combinationPair = pair))
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ HangulSyllable(initial = initial)
+ }
+ }
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ history += HangulSyllable(initial = initial)
+ }
+ }
+ is HangulJamo.Vowel -> {
+ val medial = jamo.toMedial()
+ if (currentSyllable.final == null) {
+ history +=
+ if (currentSyllable.medial != null) {
+ val combination = COMBINATION_TABLE_DUBEOLSIK[currentSyllable.medial.codePoint to (medial?.codePoint ?: -1)]
+ if (combination != null) {
+ currentSyllable.copy(medial = HangulJamo.Medial(combination))
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ HangulSyllable(medial = medial)
+ }
+ } else {
+ currentSyllable.copy(medial = medial)
+ }
+ } else if (currentSyllable.final.combinationPair != null) {
+ val pair = currentSyllable.final.combinationPair
+
+ history.removeAt(history.lastIndex)
+ val final = HangulJamo.Final(pair.first)
+ history += currentSyllable.copy(final = final)
+ composingWord.append(syllable?.string ?: "")
+ history.clear()
+ val initial = HangulJamo.Final(pair.second).toConsonant()?.toInitial()
+ val newSyllable = HangulSyllable(initial = initial)
+ history += newSyllable
+ history += newSyllable.copy(medial = medial)
+ } else {
+ history.removeAt(history.lastIndex)
+ composingWord.append(syllable?.string ?: "")
+ history.clear()
+ val initial = currentSyllable.final.toConsonant()?.toInitial()
+ val newSyllable = HangulSyllable(initial = initial)
+ history += newSyllable
+ history += newSyllable.copy(medial = medial)
+ }
+ }
+ is HangulJamo.Initial -> {
+ history +=
+ if (currentSyllable.initial != null) {
+ val combination = COMBINATION_TABLE_SEBEOLSIK[currentSyllable.initial.codePoint to jamo.codePoint]
+ if (combination != null && currentSyllable.medial == null && currentSyllable.final == null) {
+ currentSyllable.copy(initial = HangulJamo.Initial(combination))
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ HangulSyllable(initial = jamo)
+ }
+ } else {
+ currentSyllable.copy(initial = jamo)
+ }
+ }
+ is HangulJamo.Medial -> {
+ history +=
+ if (currentSyllable.medial != null) {
+ val combination = COMBINATION_TABLE_SEBEOLSIK[currentSyllable.medial.codePoint to jamo.codePoint]
+ if (combination != null) {
+ currentSyllable.copy(medial = HangulJamo.Medial(combination))
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ HangulSyllable(medial = jamo)
+ }
+ } else {
+ currentSyllable.copy(medial = jamo)
+ }
+ }
+ is HangulJamo.Final -> {
+ history +=
+ if (currentSyllable.final != null) {
+ val combination = COMBINATION_TABLE_SEBEOLSIK[currentSyllable.final.codePoint to jamo.codePoint]
+ if (combination != null) {
+ currentSyllable.copy(final = HangulJamo.Final(combination))
+ } else {
+ composingWord.append(currentSyllable.string)
+ history.clear()
+ HangulSyllable(final = jamo)
+ }
+ } else {
+ currentSyllable.copy(final = jamo)
+ }
+ }
+ // compiler bug? when it's not added, compiler complains that it's missing
+ // but when added, linter (correctly) states it's unreachable anyway
+ is HangulJamo.NonHangul -> Unit
+ }
+ }
+ }
+
+ return Event.createConsumedEvent(event)
+ }
+
+ override val combiningStateFeedback: CharSequence
+ get() = composingWord.toString() + (syllable?.string ?: "")
+
+ override fun reset() {
+ composingWord.setLength(0)
+ history.clear()
+ }
+
+ sealed class HangulJamo {
+ abstract val codePoint: Int
+ abstract val modern: Boolean
+ val string: String get() = codePoint.toChar().toString()
+ data class NonHangul(override val codePoint: Int) : HangulJamo() {
+ override val modern: Boolean get() = false
+ }
+ data class Initial(override val codePoint: Int) : HangulJamo() {
+ override val modern: Boolean get() = codePoint in 0x1100 .. 0x1112
+ val ordinal: Int get() = codePoint - 0x1100
+ fun toConsonant(): Consonant? {
+ val codePoint = COMPAT_CONSONANTS.getOrNull(CONVERT_INITIALS.indexOf(codePoint.toChar())) ?: return null
+ if(codePoint.code == 0) return null
+ return Consonant(codePoint.code)
+ }
+ }
+ data class Medial(override val codePoint: Int) : HangulJamo() {
+ override val modern: Boolean get() = codePoint in 1161 .. 0x1175
+ val ordinal: Int get() = codePoint - 0x1161
+ fun toVowel(): Vowel? {
+ val codePoint = COMPAT_VOWELS.getOrNull(CONVERT_MEDIALS.indexOf(codePoint.toChar())) ?: return null
+ return Vowel(codePoint.code)
+ }
+ }
+ data class Final(override val codePoint: Int, val combinationPair: Pair? = null) : HangulJamo() {
+ override val modern: Boolean get() = codePoint in 0x11a8 .. 0x11c2
+ val ordinal: Int get() = codePoint - 0x11a7
+ fun toConsonant(): Consonant? {
+ val codePoint = COMPAT_CONSONANTS.getOrNull(CONVERT_FINALS.indexOf(codePoint.toChar())) ?: return null
+ if(codePoint.code == 0) return null
+ return Consonant(codePoint.code)
+ }
+ }
+ data class Consonant(override val codePoint: Int) : HangulJamo() {
+ override val modern: Boolean get() = codePoint in 0x3131 .. 0x314e
+ val ordinal: Int get() = codePoint - 0x3131
+ fun toInitial(): Initial? {
+ val codePoint = CONVERT_INITIALS.getOrNull(COMPAT_CONSONANTS.indexOf(codePoint.toChar())) ?: return null
+ if(codePoint.code == 0) return null
+ return Initial(codePoint.code)
+ }
+ fun toFinal(): Final? {
+ val codePoint = CONVERT_FINALS.getOrNull(COMPAT_CONSONANTS.indexOf(codePoint.toChar())) ?: return null
+ if(codePoint.code == 0) return null
+ return Final(codePoint.code)
+ }
+ }
+ data class Vowel(override val codePoint: Int) : HangulJamo() {
+ override val modern: Boolean get() = codePoint in 0x314f .. 0x3163
+ val ordinal: Int get() = codePoint - 0x314f1
+ fun toMedial(): Medial? {
+ val codePoint = CONVERT_MEDIALS.getOrNull(COMPAT_VOWELS.indexOf(codePoint.toChar())) ?: return null
+ if(codePoint.code == 0) return null
+ return Medial(codePoint.code)
+ }
+ }
+ companion object {
+ const val COMPAT_CONSONANTS = "ㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"
+ const val COMPAT_VOWELS = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"
+ const val CONVERT_INITIALS = "ᄀᄁ\u0000ᄂ\u0000\u0000ᄃᄄᄅ\u0000\u0000\u0000\u0000\u0000\u0000\u0000ᄆᄇᄈ\u0000ᄉᄊᄋᄌᄍᄎᄏᄐᄑᄒ"
+ const val CONVERT_MEDIALS = "ᅡᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵ"
+ const val CONVERT_FINALS = "ᆨᆩᆪᆫᆬᆭᆮ\u0000ᆯᆰᆱᆲᆳᆴᆵᆶᆷᆸ\u0000ᆹᆺᆻᆼᆽ\u0000ᆾᆿᇀᇁᇂ"
+ fun of(codePoint: Int): HangulJamo {
+ return when(codePoint) {
+ in 0x3131 .. 0x314e -> Consonant(codePoint)
+ in 0x314f .. 0x3163 -> Vowel(codePoint)
+ in 0x1100 .. 0x115f -> Initial(codePoint)
+ in 0x1160 .. 0x11a7 -> Medial(codePoint)
+ in 0x11a8 .. 0x11ff -> Final(codePoint)
+ else -> NonHangul(codePoint)
+ }
+ }
+ }
+ }
+
+ data class HangulSyllable(
+ val initial: HangulJamo.Initial? = null,
+ val medial: HangulJamo.Medial? = null,
+ val final: HangulJamo.Final? = null
+ ) {
+ val combinable: Boolean get() = (initial?.modern ?: false) && (medial?.modern ?: false) && (final?.modern ?: true)
+ val combined: String get() = (0xac00 + (initial?.ordinal ?: 0) * 21 * 28
+ + (medial?.ordinal ?: 0) * 28
+ + (final?.ordinal ?: 0)).toChar().toString()
+ val uncombined: String get() = (initial?.string ?: "") + (medial?.string ?: "") + (final?.string ?: "")
+ val uncombinedCompat: String get() = (initial?.toConsonant()?.string ?: "") +
+ (medial?.toVowel()?.string ?: "") + (final?.toConsonant()?.string ?: "")
+ val string: String get() = if (this.combinable) this.combined else this.uncombinedCompat
+ }
+
+ companion object {
+ val COMBINATION_TABLE_DUBEOLSIK = mapOf, Int>(
+ 0x1169 to 0x1161 to 0x116a,
+ 0x1169 to 0x1162 to 0x116b,
+ 0x1169 to 0x1175 to 0x116c,
+ 0x116e to 0x1165 to 0x116f,
+ 0x116e to 0x1166 to 0x1170,
+ 0x116e to 0x1175 to 0x1171,
+ 0x1173 to 0x1175 to 0x1174,
+
+ 0x11a8 to 0x11ba to 0x11aa,
+ 0x11ab to 0x11bd to 0x11ac,
+ 0x11ab to 0x11c2 to 0x11ad,
+ 0x11af to 0x11a8 to 0x11b0,
+ 0x11af to 0x11b7 to 0x11b1,
+ 0x11af to 0x11b8 to 0x11b2,
+ 0x11af to 0x11ba to 0x11b3,
+ 0x11af to 0x11c0 to 0x11b4,
+ 0x11af to 0x11c1 to 0x11b5,
+ 0x11af to 0x11c2 to 0x11b6,
+ 0x11b8 to 0x11ba to 0x11b9
+ )
+ val COMBINATION_TABLE_SEBEOLSIK = mapOf, Int>(
+ 0x1100 to 0x1100 to 0x1101, // ㄲ
+ 0x1103 to 0x1103 to 0x1104, // ㄸ
+ 0x1107 to 0x1107 to 0x1108, // ㅃ
+ 0x1109 to 0x1109 to 0x110a, // ㅆ
+ 0x110c to 0x110c to 0x110d, // ㅉ
+
+ 0x1169 to 0x1161 to 0x116a, // ㅘ
+ 0x1169 to 0x1162 to 0x116b, // ㅙ
+ 0x1169 to 0x1175 to 0x116c, // ㅚ
+ 0x116e to 0x1165 to 0x116f, // ㅝ
+ 0x116e to 0x1166 to 0x1170, // ㅞ
+ 0x116e to 0x1175 to 0x1171, // ㅟ
+ 0x1173 to 0x1175 to 0x1174, // ㅢ
+
+ 0x11a8 to 0x11a8 to 0x11a9, // ㄲ
+ 0x11a8 to 0x11ba to 0x11aa, // ㄳ
+ 0x11ab to 0x11bd to 0x11ac, // ㄵ
+ 0x11ab to 0x11c2 to 0x11ad, // ㄶ
+ 0x11af to 0x11a8 to 0x11b0, // ㄺ
+ 0x11af to 0x11b7 to 0x11b1, // ㄻ
+ 0x11af to 0x11b8 to 0x11b2, // ㄼ
+ 0x11af to 0x11ba to 0x11b3, // ㄽ
+ 0x11af to 0x11c0 to 0x11b4, // ㄾ
+ 0x11af to 0x11c1 to 0x11b5, // ㄿ
+ 0x11af to 0x11c2 to 0x11b6, // ㅀ
+ 0x11b8 to 0x11ba to 0x11b9, // ㅄ
+ 0x11ba to 0x11ba to 0x11bb // ㅆ
+ )
+ private fun createEventChainFromSequence(text: CharSequence, originalEvent: Event): Event {
+ return Event.createSoftwareTextEvent(text, KeyCode.MULTIPLE_CODE_POINTS, originalEvent)
+ }
+ }
+
+}
diff --git a/app/src/main/java/helium314/keyboard/event/HangulEventDecoder.kt b/app/src/main/java/helium314/keyboard/event/HangulEventDecoder.kt
new file mode 100644
index 0000000000..e0df5228c1
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/HangulEventDecoder.kt
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+package helium314.keyboard.event
+
+import android.view.KeyEvent
+import helium314.keyboard.latin.RichInputMethodSubtype
+
+import helium314.keyboard.event.HangulCombiner.HangulJamo
+
+object HangulEventDecoder {
+
+ @JvmStatic
+ fun decodeHardwareKeyEvent(subtype: RichInputMethodSubtype, event: KeyEvent, defaultEvent: () -> Event): Event {
+ val layout = LAYOUTS[subtype.mainLayoutName] ?: return defaultEvent()
+ val codePoint = layout[event.keyCode]?.let { if (event.isShiftPressed) it.second else it.first } ?: return defaultEvent()
+ val hardwareEvent = Event.createHardwareKeypressEvent(codePoint, event.keyCode, event.metaState, null, event.repeatCount != 0)
+ return decodeSoftwareKeyEvent(hardwareEvent)
+ }
+
+ @JvmStatic
+ fun decodeSoftwareKeyEvent(event: Event): Event {
+ if (event.isCombining) return event
+ return if (HangulJamo.of(event.codePoint) is HangulJamo.NonHangul) event
+ else Event.createCombiningEvent(event)
+ }
+
+ private val LAYOUT_DUBEOLSIK_STANDARD = mapOf(
+ 45 to (0x3142 to 0x3143),
+ 51 to (0x3148 to 0x3149),
+ 33 to (0x3137 to 0x3138),
+ 46 to (0x3131 to 0x3132),
+ 48 to (0x3145 to 0x3146),
+ 53 to (0x315b to 0x315b),
+ 49 to (0x3155 to 0x3155),
+ 37 to (0x3151 to 0x3151),
+ 43 to (0x3150 to 0x3152),
+ 44 to (0x3154 to 0x3156),
+
+ 29 to (0x3141 to 0x3141),
+ 47 to (0x3134 to 0x3134),
+ 32 to (0x3147 to 0x3147),
+ 34 to (0x3139 to 0x3139),
+ 35 to (0x314e to 0x314e),
+ 36 to (0x3157 to 0x3157),
+ 38 to (0x3153 to 0x3153),
+ 39 to (0x314f to 0x314f),
+ 40 to (0x3163 to 0x3163),
+
+ 54 to (0x314b to 0x314b),
+ 52 to (0x314c to 0x314c),
+ 31 to (0x314a to 0x314a),
+ 50 to (0x314d to 0x314d),
+ 30 to (0x3160 to 0x3160),
+ 42 to (0x315c to 0x315c),
+ 41 to (0x3161 to 0x3161)
+ )
+
+ private val LAYOUT_SEBEOLSIK_390 = mapOf(
+ 8 to (0x11c2 to 0x11bd),
+ 9 to (0x11bb to 0x0040),
+ 10 to (0x11b8 to 0x0023),
+ 11 to (0x116d to 0x0024),
+ 12 to (0x1172 to 0x0025),
+ 13 to (0x1163 to 0x005e),
+ 14 to (0x1168 to 0x0026),
+ 15 to (0x1174 to 0x002a),
+ 16 to (0x116e to 0x0028),
+ 7 to (0x110f to 0x0029),
+
+ 45 to (0x11ba to 0x11c1),
+ 51 to (0x11af to 0x11c0),
+ 33 to (0x1167 to 0x11bf),
+ 46 to (0x1162 to 0x1164),
+ 48 to (0x1165 to 0x003b),
+ 53 to (0x1105 to 0x003c),
+ 49 to (0x1103 to 0x0037),
+ 37 to (0x1106 to 0x0038),
+ 43 to (0x110e to 0x0039),
+ 44 to (0x1111 to 0x003e),
+
+ 29 to (0x11bc to 0x11ae),
+ 47 to (0x11ab to 0x11ad),
+ 32 to (0x1175 to 0x11b0),
+ 34 to (0x1161 to 0x11a9),
+ 35 to (0x1173 to 0x002f),
+ 36 to (0x1102 to 0x0027),
+ 38 to (0x110b to 0x0034),
+ 39 to (0x1100 to 0x0035),
+ 40 to (0x110c to 0x0036),
+ 74 to (0x1107 to 0x003a),
+ 75 to (0x1110 to 0x0022),
+
+ 54 to (0x11b7 to 0x11be),
+ 52 to (0x11a8 to 0x11b9),
+ 31 to (0x1166 to 0x11b1),
+ 50 to (0x1169 to 0x11b6),
+ 30 to (0x116e to 0x0021),
+ 42 to (0x1109 to 0x0030),
+ 41 to (0x1112 to 0x0031),
+ 55 to (0x002c to 0x0032),
+ 56 to (0x002e to 0x0033),
+ 76 to (0x1169 to 0x003f)
+ )
+
+ private val LAYOUT_SEBEOLSIK_FINAL = mapOf(
+ 68 to (0x002a to 0x203b),
+
+ 8 to (0x11c2 to 0x11a9),
+ 9 to (0x11bb to 0x11b0),
+ 10 to (0x11b8 to 0x11bd),
+ 11 to (0x116d to 0x11b5),
+ 12 to (0x1172 to 0x11b4),
+ 13 to (0x1163 to 0x003d),
+ 14 to (0x1168 to 0x201c),
+ 15 to (0x1174 to 0x201d),
+ 16 to (0x116e to 0x0027),
+ 7 to (0x110f to 0x007e),
+ 69 to (0x0029 to 0x003b),
+ 70 to (0x003e to 0x002b),
+
+ 45 to (0x11ba to 0x11c1),
+ 51 to (0x11af to 0x11c0),
+ 33 to (0x1167 to 0x11ac),
+ 46 to (0x1162 to 0x11b6),
+ 48 to (0x1165 to 0x11b3),
+ 53 to (0x1105 to 0x0035),
+ 49 to (0x1103 to 0x0036),
+ 37 to (0x1106 to 0x0037),
+ 43 to (0x110e to 0x0038),
+ 44 to (0x1111 to 0x0039),
+ 71 to (0x0028 to 0x0025),
+ 72 to (0x003c to 0x002f),
+ 73 to (0x003a to 0x005c),
+
+ 29 to (0x11bc to 0x11ae),
+ 47 to (0x11ab to 0x11ad),
+ 32 to (0x1175 to 0x11b2),
+ 34 to (0x1161 to 0x11b1),
+ 35 to (0x1173 to 0x1164),
+ 36 to (0x1102 to 0x0030),
+ 38 to (0x110b to 0x0031),
+ 39 to (0x1100 to 0x0032),
+ 40 to (0x110c to 0x0033),
+ 74 to (0x1107 to 0x0034),
+ 75 to (0x1110 to 0x00b7),
+
+ 54 to (0x11b7 to 0x11be),
+ 52 to (0x11a8 to 0x11b9),
+ 31 to (0x1166 to 0x11bf),
+ 50 to (0x1169 to 0x11aa),
+ 30 to (0x116e to 0x003f),
+ 42 to (0x1109 to 0x002d),
+ 41 to (0x1112 to 0x0022),
+ 55 to (0x002c to 0x002c),
+ 56 to (0x002e to 0x002e),
+ 76 to (0x1169 to 0x0021)
+ )
+
+ private val LAYOUTS = mapOf(
+ "korean" to LAYOUT_DUBEOLSIK_STANDARD,
+ "korean_sebeolsik_390" to LAYOUT_SEBEOLSIK_390,
+ "korean_sebeolsik_final" to LAYOUT_SEBEOLSIK_FINAL
+ )
+
+}
diff --git a/app/src/main/java/helium314/keyboard/event/HapticEvent.kt b/app/src/main/java/helium314/keyboard/event/HapticEvent.kt
new file mode 100644
index 0000000000..4f3e01e75b
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/HapticEvent.kt
@@ -0,0 +1,35 @@
+package helium314.keyboard.event
+
+import android.view.HapticFeedbackConstants
+
+enum class HapticEvent(@JvmField val feedbackConstant: Int, @JvmField val allowCustomDuration: Boolean) {
+ NO_HAPTICS(HapticFeedbackConstants.NO_HAPTICS, false),
+ KEY_PRESS(HapticFeedbackConstants.KEYBOARD_TAP, true),
+// KEY_RELEASE(
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+// HapticFeedbackConstants.KEYBOARD_RELEASE
+// } else {
+// HapticFeedbackConstants.?
+// },
+// ?
+// ),
+ KEY_LONG_PRESS(HapticFeedbackConstants.LONG_PRESS, true),
+// KEY_REPEAT(HapticFeedbackConstants.?, ?),
+// GESTURE_START(
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+// HapticFeedbackConstants.GESTURE_START
+// } else {
+// HapticFeedbackConstants.?
+// },
+// ?
+// ),
+ GESTURE_MOVE(HapticFeedbackConstants.CLOCK_TICK, false),
+// GESTURE_END(
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+// HapticFeedbackConstants.GESTURE_END
+// } else {
+// HapticFeedbackConstants.?
+// },
+// ?
+// )
+}
diff --git a/app/src/main/java/helium314/keyboard/event/HardwareEventDecoder.kt b/app/src/main/java/helium314/keyboard/event/HardwareEventDecoder.kt
new file mode 100644
index 0000000000..2ca286ad56
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/HardwareEventDecoder.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
+
+import android.view.KeyEvent
+
+/**
+ * An event decoder for hardware events.
+ */
+interface HardwareEventDecoder : EventDecoder {
+ fun decodeHardwareKey(keyEvent: KeyEvent): Event
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/event/HardwareKeyboardEventDecoder.kt b/app/src/main/java/helium314/keyboard/event/HardwareKeyboardEventDecoder.kt
new file mode 100644
index 0000000000..b5951077b1
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/HardwareKeyboardEventDecoder.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.event
+
+import android.view.KeyCharacterMap
+import android.view.KeyEvent
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.latin.common.Constants
+
+/**
+ * A hardware event decoder for a hardware qwerty-ish keyboard.
+ *
+ * The events are always hardware keypresses, but they can be key down or key up events, they
+ * can be dead keys, they can be meta keys like shift or ctrl... This does not deal with
+ * 10-key like keyboards; a different decoder is used for this.
+ */
+// TODO: get the layout for this hardware keyboard
+class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
+ override fun decodeHardwareKey(keyEvent: KeyEvent): Event {
+ // 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.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.
+ val keyCode = keyEvent.keyCode
+ val metaState = keyEvent.metaState
+ val isKeyRepeat = 0 != keyEvent.repeatCount
+
+ return if (KeyEvent.KEYCODE_DEL == keyCode) {
+ Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, KeyCode.DELETE, metaState, null, isKeyRepeat)
+ } else if (
+ (keyEvent.isPrintingKey && codePointAndFlags != Event.NOT_A_CODE_POINT) // can be NOT_A_CODE_POINT depending on meta state (e.g. ctrl+c)
+ || KeyEvent.KEYCODE_SPACE == keyCode
+ || KeyEvent.KEYCODE_ENTER == keyCode
+ ) {
+ if (0 != codePointAndFlags and KeyCharacterMap.COMBINING_ACCENT) { // A dead key.
+ Event.createDeadEvent(codePointAndFlags and KeyCharacterMap.COMBINING_ACCENT_MASK, keyCode, metaState, null)
+ } else if (KeyEvent.KEYCODE_ENTER == keyCode) {
+ // The Enter key. If the Shift key is not being pressed, this should send a
+ // CODE_ENTER to trigger the action if any, or a carriage return otherwise. If the
+ // Shift key is being pressed, this should send a CODE_SHIFT_ENTER and let
+ // Latin IME decide what to do with it.
+ if (keyEvent.isShiftPressed) {
+ Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, // todo: maybe remove, see also related comment in input logic
+ KeyCode.SHIFT_ENTER, 0, null, isKeyRepeat)
+ } else Event.createHardwareKeypressEvent(Constants.CODE_ENTER, keyCode, metaState, null, isKeyRepeat)
+ } 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 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
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/event/InputTransaction.kt b/app/src/main/java/helium314/keyboard/event/InputTransaction.kt
new file mode 100644
index 0000000000..b3b45fa412
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/event/InputTransaction.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+package helium314.keyboard.event
+
+import helium314.keyboard.latin.settings.SettingsValues
+
+/** An object encapsulating a single transaction for input. */
+class InputTransaction(
+ // Initial conditions
+ val settingsValues: SettingsValues,
+ val event: Event,
+ val timestamp: Long,
+ val spaceState: Int,
+ val shiftState: Int
+) {
+ /** Gets what type of shift update this transaction requires. */
+ // Outputs
+ var requiredShiftUpdate = SHIFT_NO_UPDATE
+ private set
+ private var requiresUpdateSuggestions = false
+ private var didAffectContents = false
+ private var didAutoCorrect = false
+ /**
+ * Indicate that this transaction requires some type of shift update.
+ * @param updateType What type of shift update this requires.
+ */
+ fun requireShiftUpdate(updateType: Int) {
+ requiredShiftUpdate = requiredShiftUpdate.coerceAtLeast(updateType)
+ }
+
+ /** Indicate that this transaction requires updating the suggestions.*/
+ fun setRequiresUpdateSuggestions() {
+ requiresUpdateSuggestions = true
+ }
+
+ /** Whether this transaction requires updating the suggestions. */
+ fun requiresUpdateSuggestions() = requiresUpdateSuggestions
+
+ /** Indicate that this transaction affected the contents of the editor. */
+ fun setDidAffectContents() {
+ didAffectContents = true
+ }
+
+ /** Whether this transaction affected contents of the editor. */
+ fun didAffectContents() = didAffectContents
+
+ /** Indicate that this transaction performed an auto-correction. */
+ fun setDidAutoCorrect() {
+ didAutoCorrect = true
+ }
+
+ /** Whether this transaction performed an auto-correction. */
+ fun didAutoCorrect() = didAutoCorrect
+
+ companion object {
+ // UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later,
+ // it's because something will change that we can't evaluate now, which means that even if we
+ // re-evaluate now we'll have to do it again later. The only case where that wouldn't apply
+ // would be if we needed to update now to find out the new state right away, but then we
+ // can't do it with this deferred mechanism anyway.
+ const val SHIFT_NO_UPDATE = 0
+ const val SHIFT_UPDATE_NOW = 1
+ const val SHIFT_UPDATE_LATER = 2
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/Key.java b/app/src/main/java/helium314/keyboard/keyboard/Key.java
new file mode 100644
index 0000000000..c5de708515
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/Key.java
@@ -0,0 +1,1278 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.keyboard;
+
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import helium314.keyboard.keyboard.internal.KeyDrawParams;
+import helium314.keyboard.keyboard.internal.KeySpecParser;
+import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
+import helium314.keyboard.keyboard.internal.KeyboardParams;
+import helium314.keyboard.keyboard.internal.PopupKeySpec;
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.PopupSet;
+import helium314.keyboard.latin.common.Constants;
+import helium314.keyboard.latin.common.StringUtils;
+import helium314.keyboard.latin.utils.PopupKeysUtilsKt;
+import helium314.keyboard.latin.utils.ToolbarKey;
+import helium314.keyboard.latin.utils.ToolbarUtilsKt;
+import kotlin.collections.ArraysKt;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Class for describing the position and characteristics of a single key in the keyboard.
+ */
+public class Key implements Comparable {
+ /**
+ * The key code (unicode or custom code) that this key generates.
+ */
+ private final int mCode;
+
+ /** Label to display */
+ private final String mLabel;
+ /** Hint label to display on the key in conjunction with the label */
+ private final String mHintLabel;
+ /** Flags of the label */
+ private final int mLabelFlags;
+ public static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02;
+ public static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04;
+ public static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08;
+ // Font typeface specification.
+ private static final int LABEL_FLAGS_FONT_MASK = 0x30;
+ public static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
+ public static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
+ public static final int LABEL_FLAGS_FONT_DEFAULT = 0x30;
+ // Start of key text ratio enum values
+ private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
+ public static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
+ public static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
+ public static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
+ public static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
+ // End of key text ratio mask enum values
+ public static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200;
+ public static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
+ public static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
+ // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on
+ // and autoYScale bit is off, the key label may be shrunk only for X-direction.
+ // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled.
+ public static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
+ public static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000;
+ public static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE
+ | LABEL_FLAGS_AUTO_Y_SCALE;
+ public static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000;
+ public static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
+ public static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
+ public static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
+ public static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000;
+ public static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
+ public static final int LABEL_FLAGS_DISABLE_ADDITIONAL_POPUP_KEYS = 0x80000000;
+
+ /** Icon to display instead of a label. Icon takes precedence over a label */
+ @Nullable private final String mIconName;
+
+ /** Width of the key, excluding the gap */
+ private final int mWidth;
+ /** Height of the key, excluding the gap */
+ private final int mHeight;
+ /**
+ * The combined width in pixels of the horizontal gaps belonging to this key, both to the left
+ * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key.
+ */
+ private final int mHorizontalGap;
+ /**
+ * The combined height in pixels of the vertical gaps belonging to this key, both above and
+ * below. I.e., mHeight + mVerticalGap = total height belonging to the key.
+ */
+ private final int mVerticalGap;
+ /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */
+ private final int mX;
+ /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */
+ private final int mY;
+ /** Hit bounding box of the key */
+ @NonNull
+ private final Rect mHitBox = new Rect();
+
+ /** Popup keys. It is guaranteed that this is null or an array of one or more elements */
+ @Nullable
+ private final PopupKeySpec[] mPopupKeys;
+ /** Popup keys column number and flags */
+ private final int mPopupKeysColumnAndFlags;
+ private static final int POPUP_KEYS_COLUMN_NUMBER_MASK = 0x000000ff;
+ // If this flag is specified, popup keys keyboard should have the specified number of columns.
+ // Otherwise popup keys keyboard should have less than or equal to the specified maximum number
+ // of columns.
+ private static final int POPUP_KEYS_FLAGS_FIXED_COLUMN = 0x00000100;
+ // If this flag is specified, the order of popup keys is determined by the order in the popup
+ // keys' specification. Otherwise the order of popup keys is automatically determined.
+ private static final int POPUP_KEYS_FLAGS_FIXED_ORDER = 0x00000200;
+ private static final int POPUP_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0;
+ private static final int POPUP_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER = POPUP_KEYS_FLAGS_FIXED_COLUMN;
+ private static final int POPUP_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER = (POPUP_KEYS_FLAGS_FIXED_COLUMN | POPUP_KEYS_FLAGS_FIXED_ORDER);
+ private static final int POPUP_KEYS_FLAGS_HAS_LABELS = 0x40000000;
+ private static final int POPUP_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000;
+ private static final int POPUP_KEYS_FLAGS_NO_PANEL_AUTO_POPUP_KEY = 0x10000000;
+ // TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively.
+ public static final String POPUP_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
+ public static final String POPUP_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
+ public static final String POPUP_KEYS_HAS_LABELS = "!hasLabels!";
+ private static final String POPUP_KEYS_NEEDS_DIVIDERS = "!needsDividers!";
+ private static final String POPUP_KEYS_NO_PANEL_AUTO_POPUP_KEY = "!noPanelAutoPopupKey!";
+
+ /** Background type that represents different key background visual than normal one. */
+ private final int mBackgroundType;
+ public static final int BACKGROUND_TYPE_EMPTY = 0;
+ public static final int BACKGROUND_TYPE_NORMAL = 1;
+ public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
+ public static final int BACKGROUND_TYPE_ACTION = 3;
+ public static final int BACKGROUND_TYPE_SPACEBAR = 4;
+
+ private final int mActionFlags;
+ private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
+ private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02;
+ private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
+ private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
+
+ @Nullable
+ private final KeyVisualAttributes mKeyVisualAttributes;
+ @Nullable
+ private final OptionalAttributes mOptionalAttributes;
+
+ private static final class OptionalAttributes {
+ /** Text to output when pressed. This can be multiple characters, like ".com" */
+ public final String mOutputText;
+ public final int mAltCode;
+ /** Icon for disabled state */
+ @Nullable public final String mDisabledIconName;
+ /** The visual insets */
+ public final int mVisualInsetsLeft;
+ public final int mVisualInsetsRight;
+
+ private OptionalAttributes(final String outputText, final int altCode, @Nullable final String disabledIconName,
+ final int visualInsetsLeft, final int visualInsetsRight) {
+ mOutputText = outputText;
+ mAltCode = altCode;
+ mDisabledIconName = disabledIconName;
+ mVisualInsetsLeft = visualInsetsLeft;
+ mVisualInsetsRight = visualInsetsRight;
+ }
+
+ @Nullable
+ public static OptionalAttributes newInstance(final String outputText, final int altCode,
+ @Nullable final String disabledIconName, final int visualInsetsLeft, final int visualInsetsRight) {
+ if (outputText == null && altCode == KeyCode.NOT_SPECIFIED
+ && disabledIconName == null && visualInsetsLeft == 0
+ && visualInsetsRight == 0) {
+ return null;
+ }
+ return new OptionalAttributes(outputText, altCode, disabledIconName, visualInsetsLeft,
+ visualInsetsRight);
+ }
+ }
+
+ private final int mHashCode;
+
+ /** The current pressed state of this key */
+ private boolean mPressed;
+ /** Key is enabled and responds on press */
+ private boolean mEnabled;
+ /** Key is locked (appears permanently pressed) */
+ private boolean mLocked = false;
+ /**
+ * Constructor for a key on PopupKeyKeyboard and on MoreSuggestions.
+ */
+ public Key(@Nullable final String label, @Nullable final String iconName, final int code,
+ @Nullable final String outputText, @Nullable final String hintLabel,
+ final int labelFlags, final int backgroundType, final int x, final int y,
+ final int width, final int height, final int horizontalGap, final int verticalGap) {
+ mWidth = width - horizontalGap;
+ mHeight = height - verticalGap;
+ mHorizontalGap = horizontalGap;
+ mVerticalGap = verticalGap;
+ mHintLabel = hintLabel;
+ mLabelFlags = labelFlags;
+ mBackgroundType = backgroundType;
+ // TODO: Pass keyActionFlags as an argument.
+ mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
+ mPopupKeys = null;
+ mPopupKeysColumnAndFlags = 0;
+ mLabel = label;
+ mCode = code;
+ mEnabled = (code != KeyCode.NOT_SPECIFIED);
+ mIconName = iconName;
+ mOptionalAttributes = OptionalAttributes.newInstance(outputText, KeyCode.NOT_SPECIFIED,
+ mIconName == null ? null : getDisabledIconName(mIconName), 0, 0);
+ // Horizontal gap is divided equally to both sides of the key.
+ mX = x + mHorizontalGap / 2;
+ mY = y;
+ mHitBox.set(x, y, x + width + 1, y + height);
+ mKeyVisualAttributes = null;
+
+ mHashCode = computeHashCode(this);
+ }
+
+ /**
+ * Copy constructor for DynamicGridKeyboard.GridKey.
+ *
+ * @param key the original key.
+ * @param popupKeys the popup keys that should be assigned to this key.
+ * @param labelHint the label hint that should be assigned to this key.
+ * @param backgroundType the background type that should be assigned to this key.
+ */
+ protected Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys,
+ @Nullable final String labelHint, final int backgroundType) {
+ // Final attributes.
+ mCode = key.mCode;
+ mLabel = key.mLabel;
+ mHintLabel = labelHint;
+ mLabelFlags = key.mLabelFlags;
+ mIconName = key.mIconName;
+ mWidth = key.mWidth;
+ mHeight = key.mHeight;
+ mHorizontalGap = key.mHorizontalGap;
+ mVerticalGap = key.mVerticalGap;
+ mX = key.mX;
+ mY = key.mY;
+ mHitBox.set(key.mHitBox);
+ mPopupKeys = popupKeys;
+ mPopupKeysColumnAndFlags = key.mPopupKeysColumnAndFlags;
+ mBackgroundType = backgroundType;
+ mActionFlags = key.mActionFlags;
+ mKeyVisualAttributes = key.mKeyVisualAttributes;
+ mOptionalAttributes = key.mOptionalAttributes;
+ mHashCode = key.mHashCode;
+ // Key state.
+ mPressed = key.mPressed;
+ mEnabled = key.mEnabled;
+ }
+
+ /** constructor for creating emoji recent keys when there is no keyboard to take keys from */
+ public Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys,
+ @Nullable final String labelHint, final int backgroundType, final int code, @Nullable final String outputText) {
+ // Final attributes.
+ mCode = outputText == null ? code : KeyCode.MULTIPLE_CODE_POINTS;
+ mLabel = outputText == null ? StringUtils.newSingleCodePointString(code) : outputText;
+ mHintLabel = labelHint;
+ mLabelFlags = key.mLabelFlags;
+ mIconName = key.mIconName;
+ mWidth = key.mWidth;
+ mHeight = key.mHeight;
+ mHorizontalGap = key.mHorizontalGap;
+ mVerticalGap = key.mVerticalGap;
+ mX = key.mX;
+ mY = key.mY;
+ mHitBox.set(key.mHitBox);
+ mPopupKeys = popupKeys;
+ mPopupKeysColumnAndFlags = key.mPopupKeysColumnAndFlags;
+ mBackgroundType = backgroundType;
+ mActionFlags = key.mActionFlags;
+ mKeyVisualAttributes = key.mKeyVisualAttributes;
+ mOptionalAttributes = outputText == null ? null
+ : Key.OptionalAttributes.newInstance(outputText, KeyCode.NOT_SPECIFIED, null, 0, 0);
+ mHashCode = key.mHashCode;
+ // Key state.
+ mPressed = key.mPressed;
+ mEnabled = key.mEnabled;
+ }
+
+ /** constructor from KeyParams */
+ private Key(KeyParams keyParams) {
+ // stuff to copy
+ mCode = keyParams.mCode;
+ mLabel = keyParams.mLabel;
+ mHintLabel = keyParams.mHintLabel;
+ mLabelFlags = keyParams.mLabelFlags;
+ mIconName = keyParams.mIconName;
+ mPopupKeys = keyParams.mPopupKeys;
+ mPopupKeysColumnAndFlags = keyParams.mPopupKeysColumnAndFlags;
+ mBackgroundType = keyParams.mBackgroundType;
+ mActionFlags = keyParams.mActionFlags;
+ mKeyVisualAttributes = keyParams.mKeyVisualAttributes;
+ mOptionalAttributes = keyParams.mOptionalAttributes;
+ mEnabled = keyParams.mEnabled;
+
+ // stuff to create
+
+ // get the "correct" float gap: may shift keys by one pixel, but results in more uniform gaps between keys
+ final float horizontalGapFloat = isSpacer() ? 0 : (keyParams.mKeyboardParams.mRelativeHorizontalGap * keyParams.mKeyboardParams.mOccupiedWidth);
+ mHorizontalGap = Math.round(horizontalGapFloat);
+ mVerticalGap = Math.round(keyParams.mKeyboardParams.mRelativeVerticalGap * keyParams.mKeyboardParams.mOccupiedHeight);
+ mWidth = Math.round(keyParams.mAbsoluteWidth - horizontalGapFloat);
+ // height is always rounded down, because rounding up may make the keyboard too high to fit, leading to issues
+ mHeight = (int) (keyParams.mAbsoluteHeight - keyParams.mKeyboardParams.mVerticalGap);
+ if (!isSpacer() && (mWidth == 0 || mHeight == 0)) {
+ throw new IllegalStateException("key needs positive width and height");
+ }
+ // Horizontal gap is divided equally to both sides of the key.
+ mX = Math.round(keyParams.xPos + horizontalGapFloat / 2);
+ mY = Math.round(keyParams.yPos);
+ mHitBox.set(Math.round(keyParams.xPos), Math.round(keyParams.yPos), Math.round(keyParams.xPos + keyParams.mAbsoluteWidth) + 1,
+ Math.round(keyParams.yPos + keyParams.mAbsoluteHeight));
+ mHashCode = computeHashCode(this);
+ }
+
+ private Key(@NonNull final Key key, @Nullable final PopupKeySpec[] popupKeys) {
+ // Final attributes.
+ mCode = key.mCode;
+ mLabel = key.mLabel;
+ mHintLabel = PopopUtilKt.findPopupHintLabel(popupKeys, key.mHintLabel);
+ mLabelFlags = key.mLabelFlags;
+ mIconName = key.mIconName;
+ mWidth = key.mWidth;
+ mHeight = key.mHeight;
+ mHorizontalGap = key.mHorizontalGap;
+ mVerticalGap = key.mVerticalGap;
+ mX = key.mX;
+ mY = key.mY;
+ mHitBox.set(key.mHitBox);
+ mPopupKeys = popupKeys;
+ mPopupKeysColumnAndFlags = key.mPopupKeysColumnAndFlags;
+ mBackgroundType = key.mBackgroundType;
+ if (popupKeys == null && mCode > Constants.CODE_SPACE && (key.mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0)
+ mActionFlags = key.mActionFlags - ACTION_FLAGS_ENABLE_LONG_PRESS;
+ else
+ mActionFlags = key.mActionFlags;
+ mKeyVisualAttributes = key.mKeyVisualAttributes;
+ mOptionalAttributes = key.mOptionalAttributes;
+ mHashCode = key.mHashCode;
+ // Key state.
+ mPressed = key.mPressed;
+ mEnabled = key.mEnabled;
+ }
+
+ @NonNull
+ public static Key removeRedundantPopupKeys(@NonNull final Key key,
+ @NonNull final PopupKeySpec.LettersOnBaseLayout lettersOnBaseLayout) {
+ if ((key.mPopupKeysColumnAndFlags & POPUP_KEYS_FLAGS_FIXED_COLUMN) != 0)
+ return key; // don't remove anything for fixed column popup keys
+ final PopupKeySpec[] popupKeys = key.getPopupKeys();
+ final PopupKeySpec[] filteredPopupKeys = PopupKeySpec.removeRedundantPopupKeys(
+ popupKeys, lettersOnBaseLayout);
+ return (filteredPopupKeys == popupKeys) ? key : new Key(key, filteredPopupKeys);
+ }
+
+ private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) {
+ if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
+ return switch (keyboardElementId) {
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED,
+ KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> true;
+ default -> false;
+ };
+ }
+
+ private static int computeHashCode(final Key key) {
+ return Arrays.hashCode(new Object[] {
+ key.mX,
+ key.mY,
+ key.mWidth,
+ key.mHeight,
+ key.mCode,
+ key.mLabel,
+ key.mHintLabel,
+ key.mIconName,
+ key.mBackgroundType,
+ Arrays.hashCode(key.mPopupKeys),
+ key.getOutputText(),
+ key.mActionFlags,
+ key.mLabelFlags,
+ // Key can be distinguishable without the following members.
+ // key.mOptionalAttributes.mAltCode,
+ // key.mOptionalAttributes.mDisabledIconId,
+ // key.mOptionalAttributes.mPreviewIconId,
+ // key.mHorizontalGap,
+ // key.mVerticalGap,
+ // key.mOptionalAttributes.mVisualInsetLeft,
+ // key.mOptionalAttributes.mVisualInsetRight,
+ // key.mMaxPopupKeysColumn,
+ });
+ }
+
+ private boolean equalsInternal(final Key o) {
+ if (this == o) return true;
+ return o.mX == mX
+ && o.mY == mY
+ && o.mWidth == mWidth
+ && o.mHeight == mHeight
+ && o.mCode == mCode
+ && TextUtils.equals(o.mLabel, mLabel)
+ && TextUtils.equals(o.mHintLabel, mHintLabel)
+ && TextUtils.equals(o.mIconName, mIconName)
+ && o.mBackgroundType == mBackgroundType
+ && Arrays.equals(o.mPopupKeys, mPopupKeys)
+ && TextUtils.equals(o.getOutputText(), getOutputText())
+ && o.mActionFlags == mActionFlags
+ && o.mLabelFlags == mLabelFlags;
+ }
+
+ @Override
+ public int compareTo(Key o) {
+ if (equalsInternal(o)) return 0;
+ if (mHashCode > o.mHashCode) return 1;
+ return -1;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ return o instanceof Key && equalsInternal((Key)o);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight();
+ }
+
+ public String toShortString() {
+ final int code = getCode();
+ if (code == KeyCode.MULTIPLE_CODE_POINTS) {
+ return getOutputText();
+ }
+ return Constants.printableCode(code);
+ }
+
+ public String toLongString() {
+ final String iconName = getIconName();
+ final String topVisual = (iconName != null)
+ ? KeyboardIconsSet.PREFIX_ICON + iconName : getLabel();
+ final String hintLabel = getHintLabel();
+ final String visual = (hintLabel == null) ? topVisual : topVisual + "^" + hintLabel;
+ return toString() + " " + visual + "/" + backgroundName(mBackgroundType);
+ }
+
+ private static String backgroundName(final int backgroundType) {
+ return switch (backgroundType) {
+ case BACKGROUND_TYPE_EMPTY -> "empty";
+ case BACKGROUND_TYPE_NORMAL -> "normal";
+ case BACKGROUND_TYPE_FUNCTIONAL -> "functional";
+ case BACKGROUND_TYPE_ACTION -> "action";
+ case BACKGROUND_TYPE_SPACEBAR -> "spacebar";
+ default -> null;
+ };
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ @Nullable
+ public String getLabel() {
+ return mLabel;
+ }
+
+ @Nullable
+ public String getHintLabel() {
+ return mHintLabel;
+ }
+
+ @Nullable
+ public PopupKeySpec[] getPopupKeys() {
+ return mPopupKeys;
+ }
+
+ public void markAsLeftEdge(final KeyboardParams params) {
+ mHitBox.left = params.mLeftPadding;
+ }
+
+ public void markAsRightEdge(final KeyboardParams params) {
+ mHitBox.right = params.mOccupiedWidth - params.mRightPadding;
+ }
+
+ public void markAsTopEdge(final KeyboardParams params) {
+ mHitBox.top = params.mTopPadding;
+ }
+
+ public void markAsBottomEdge(final KeyboardParams params) {
+ mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding;
+ }
+
+ public final boolean isSpacer() {
+ return this instanceof Spacer;
+ }
+
+ public final boolean hasActionKeyBackground() {
+ return mBackgroundType == BACKGROUND_TYPE_ACTION;
+ }
+
+ public final boolean isShift() {
+ return mCode == KeyCode.SHIFT;
+ }
+
+ public final boolean isModifier() {
+ return KeyCode.INSTANCE.isModifier(mCode);
+ }
+
+ public final boolean isRepeatable() {
+ return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
+ }
+
+ public final boolean hasPreview() {
+ return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) == 0;
+ }
+
+ /**
+ * altCodeWhileTyping is a weird thing.
+ * When user pressed a typing key less than ignoreAltCodeKeyTimeout (config_ignore_alt_code_key_timeout / 350 ms) ago,
+ * this code will be used instead. There is no documentation, but it appears the purpose is to avoid unintentional layout switches.
+ * Assuming this is true, the key still is used now if pressed near the center, where we assume it's less likely to be accidental.
+ * See PointerTracker.isClearlyInsideKey
+ */
+ public final boolean altCodeWhileTyping() {
+ return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
+ }
+
+ public final boolean isLongPressEnabled() {
+ // We need not start long press timer on the key which has activated shifted letter.
+ return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
+ && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
+ }
+
+ public KeyVisualAttributes getVisualAttributes() {
+ return mKeyVisualAttributes;
+ }
+
+ @NonNull
+ public final Typeface selectTypeface(final KeyDrawParams params) {
+ return switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) {
+ case LABEL_FLAGS_FONT_NORMAL -> Typeface.DEFAULT;
+ case LABEL_FLAGS_FONT_MONO_SPACE -> Typeface.MONOSPACE;
+ default -> params.mTypeface; // The type-face is specified by keyTypeface attribute.
+ };
+ }
+
+ public final int selectTextSize(final KeyDrawParams params) {
+ return switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
+ case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO -> params.mLetterSize;
+ case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO -> params.mLargeLetterSize;
+ case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO -> params.mLabelSize;
+ case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO -> params.mHintLabelSize;
+ // No follow key ratio flag specified.
+ default -> StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
+ };
+ }
+
+ public final int selectTextColor(final KeyDrawParams params) {
+ if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) {
+ return params.mFunctionalTextColor;
+ }
+ return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
+ }
+
+ public final int selectHintTextSize(final KeyDrawParams params) {
+ if (hasHintLabel()) {
+ return params.mHintLabelSize;
+ }
+ if (hasShiftedLetterHint()) {
+ return params.mShiftedLetterHintSize;
+ }
+ return params.mHintLetterSize;
+ }
+
+ public final int selectHintTextColor(final KeyDrawParams params) {
+ if (hasHintLabel()) {
+ return params.mHintLabelColor;
+ }
+ if (hasShiftedLetterHint()) {
+ return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
+ : params.mShiftedLetterHintInactivatedColor;
+ }
+ return params.mHintLetterColor;
+ }
+
+ public final int selectPopupKeyTextSize(final KeyDrawParams params) {
+ return hasLabelsInPopupKeys() ? params.mLabelSize : params.mLetterSize;
+ }
+
+ public final String getPreviewLabel() {
+ return isShiftedLetterActivated() ? mHintLabel : mLabel;
+ }
+
+ private boolean previewHasLetterSize() {
+ return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0
+ || StringUtils.codePointCount(getPreviewLabel()) == 1;
+ }
+
+ public final int selectPreviewTextSize(final KeyDrawParams params) {
+ if (previewHasLetterSize()) {
+ return params.mPreviewTextSize;
+ }
+ return params.mLetterSize;
+ }
+
+ @NonNull
+ public Typeface selectPreviewTypeface(final KeyDrawParams params) {
+ if (previewHasLetterSize()) {
+ return selectTypeface(params);
+ }
+ return Typeface.DEFAULT_BOLD;
+ }
+
+ public final boolean isAlignHintLabelToBottom(final int defaultFlags) {
+ return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0;
+ }
+
+ public final boolean isAlignIconToBottom() {
+ return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0;
+ }
+
+ public final boolean isAlignLabelOffCenter() {
+ return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0;
+ }
+
+ public final boolean hasPopupHint() {
+ return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
+ }
+
+ public final boolean hasShiftedLetterHint() {
+ return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0
+ && !TextUtils.isEmpty(mHintLabel);
+ }
+
+ public final boolean hasHintLabel() {
+ return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
+ }
+
+ public final boolean needsAutoXScale() {
+ return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
+ }
+
+ public final boolean needsAutoScale() {
+ return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE;
+ }
+
+ public final boolean needsToKeepBackgroundAspectRatio(final int defaultFlags) {
+ return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO) != 0;
+ }
+
+ public final boolean hasCustomActionLabel() {
+ return (mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0;
+ }
+
+ private boolean isShiftedLetterActivated() {
+ return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
+ && !TextUtils.isEmpty(mHintLabel);
+ }
+
+ public final int getPopupKeysColumnNumber() {
+ return mPopupKeysColumnAndFlags & POPUP_KEYS_COLUMN_NUMBER_MASK;
+ }
+
+ public final boolean isPopupKeysFixedColumn() {
+ return (mPopupKeysColumnAndFlags & POPUP_KEYS_FLAGS_FIXED_COLUMN) != 0;
+ }
+
+ public final boolean isPopupKeysFixedOrder() {
+ return (mPopupKeysColumnAndFlags & POPUP_KEYS_FLAGS_FIXED_ORDER) != 0;
+ }
+
+ public final boolean hasLabelsInPopupKeys() {
+ return (mPopupKeysColumnAndFlags & POPUP_KEYS_FLAGS_HAS_LABELS) != 0;
+ }
+
+ public final int getPopupKeyLabelFlags() {
+ final int labelSizeFlag = hasLabelsInPopupKeys()
+ ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
+ : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
+ return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE;
+ }
+
+ public final boolean needsDividersInPopupKeys() {
+ return (mPopupKeysColumnAndFlags & POPUP_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
+ }
+
+ public final boolean hasNoPanelAutoPopupKey() {
+ return (mPopupKeysColumnAndFlags & POPUP_KEYS_FLAGS_NO_PANEL_AUTO_POPUP_KEY) != 0;
+ }
+
+ @Nullable
+ public final String getOutputText() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs != null) ? attrs.mOutputText : null;
+ }
+
+ public final int getAltCode() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs != null) ? attrs.mAltCode : KeyCode.NOT_SPECIFIED;
+ }
+
+ @Nullable
+ public String getIconName() {
+ return mIconName;
+ }
+
+ @Nullable
+ public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ final String iconName = mEnabled ? getIconName() : ((attrs != null) ? attrs.mDisabledIconName : null);
+ final Drawable icon = iconSet.getIconDrawable(iconName);
+ if (icon != null) {
+ icon.setAlpha(alpha);
+ }
+ return icon;
+ }
+
+ @Nullable
+ public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
+ return iconSet.getIconDrawable(getIconName());
+ }
+
+ /**
+ * Gets the background type of this key.
+ * @return Background type.
+ * @see Key#BACKGROUND_TYPE_EMPTY
+ * @see Key#BACKGROUND_TYPE_NORMAL
+ * @see Key#BACKGROUND_TYPE_FUNCTIONAL
+ * @see Key#BACKGROUND_TYPE_ACTION
+ * @see Key#BACKGROUND_TYPE_SPACEBAR
+ */
+ public int getBackgroundType() {
+ return mBackgroundType;
+ }
+
+ /**
+ * Gets the width of the key in pixels, excluding the gap.
+ * @return The width of the key in pixels, excluding the gap.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Gets the height of the key in pixels, excluding the gap.
+ * @return The height of the key in pixels, excluding the gap.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * The combined width in pixels of the horizontal gaps belonging to this key, both above and
+ * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key.
+ * @return Horizontal gap belonging to this key.
+ */
+ public int getHorizontalGap() {
+ return mHorizontalGap;
+ }
+
+ /**
+ * The combined height in pixels of the vertical gaps belonging to this key, both above and
+ * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key.
+ * @return Vertical gap belonging to this key.
+ */
+ public int getVerticalGap() {
+ return mVerticalGap;
+ }
+
+ /**
+ * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap.
+ * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap.
+ */
+ public int getX() {
+ return mX;
+ }
+
+ /**
+ * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap.
+ * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap.
+ */
+ public int getY() {
+ return mY;
+ }
+
+ public final int getDrawX() {
+ final int x = getX();
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
+ }
+
+ public final int getDrawWidth() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs == null) ? mWidth
+ : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
+ }
+
+ /**
+ * Informs the key that it has been pressed, in case it needs to change its appearance or
+ * state.
+ * @see #onReleased()
+ */
+ public void onPressed() {
+ mPressed = true;
+ }
+
+ /**
+ * Informs the key that it has been released, in case it needs to change its appearance or
+ * state.
+ * @see #onPressed()
+ */
+ public void onReleased() {
+ mPressed = false;
+ }
+
+ public final boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setEnabled(final boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public void setLocked(final boolean locked) {
+ mLocked = locked;
+ }
+
+ @NonNull
+ public Rect getHitBox() {
+ return mHitBox;
+ }
+
+ /**
+ * Detects if a point falls on this key.
+ * @param x the x-coordinate of the point
+ * @param y the y-coordinate of the point
+ * @return whether or not the point falls on the key. If the key is attached to an edge, it
+ * will assume that all points between the key and the edge are considered to be on the key.
+ * @see #markAsLeftEdge(KeyboardParams) etc.
+ */
+ public boolean isOnKey(final int x, final int y) {
+ return mHitBox.contains(x, y);
+ }
+
+ /**
+ * Returns the square of the distance to the nearest edge of the key and the given point.
+ * @param x the x-coordinate of the point
+ * @param y the y-coordinate of the point
+ * @return the square of the distance of the point from the nearest edge of the key
+ */
+ public int squaredDistanceToEdge(final int x, final int y) {
+ final int left = getX();
+ final int right = left + mWidth;
+ final int top = getY();
+ final int bottom = top + mHeight;
+ final int edgeX = x < left ? left : Math.min(x, right);
+ final int edgeY = y < top ? top : Math.min(y, bottom);
+ final int dx = x - edgeX;
+ final int dy = y - edgeY;
+ return dx * dx + dy * dy;
+ }
+
+ static class KeyBackgroundState {
+ private final int[] mReleasedState;
+ private final int[] mPressedState;
+
+ private KeyBackgroundState(final int ... attrs) {
+ mReleasedState = attrs;
+ mPressedState = Arrays.copyOf(attrs, attrs.length + 1);
+ mPressedState[attrs.length] = android.R.attr.state_pressed;
+ }
+
+ public int[] getState(final boolean pressed) {
+ return pressed ? mPressedState : mReleasedState;
+ }
+
+ public static final KeyBackgroundState[] STATES = {
+ // 0: BACKGROUND_TYPE_EMPTY
+ new KeyBackgroundState(android.R.attr.state_empty),
+ // 1: BACKGROUND_TYPE_NORMAL
+ new KeyBackgroundState(),
+ // 2: BACKGROUND_TYPE_FUNCTIONAL
+ new KeyBackgroundState(),
+ // 3: BACKGROUND_TYPE_ACTION
+ new KeyBackgroundState(android.R.attr.state_active),
+ // 4: BACKGROUND_TYPE_SPACEBAR
+ new KeyBackgroundState(),
+ };
+ }
+
+ /**
+ * Returns the background drawable for the key, based on the current state and type of the key.
+ * @return the background drawable of the key.
+ * @see android.graphics.drawable.StateListDrawable#setState(int[])
+ */
+ @NonNull
+ public final Drawable selectBackgroundDrawable(@NonNull final Drawable keyBackground,
+ @NonNull final Drawable functionalKeyBackground,
+ @NonNull final Drawable spacebarBackground,
+ @NonNull final Drawable actionKeyBackground) {
+ final Drawable background;
+ if (hasActionKeyBackground()) {
+ background = actionKeyBackground;
+ } else if (hasFunctionalBackground()) {
+ background = functionalKeyBackground;
+ } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
+ background = spacebarBackground;
+ } else {
+ background = keyBackground;
+ }
+ final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed || mLocked);
+ background.setState(state);
+ return background;
+ }
+
+ public final boolean hasActionKeyPopups() {
+ if (!hasActionKeyBackground()) return false;
+ // only use the special action key popups for action colored keys, and only for icon popups
+ return ArraysKt.none(getPopupKeys(), (key) -> key.mIconName == null);
+ }
+
+ public boolean hasFunctionalBackground() {
+ return mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL;
+ }
+
+ @Nullable private static String getDisabledIconName(@NonNull final String iconName) {
+ if (iconName.equals(ToolbarUtilsKt.getToolbarKeyStrings().get(ToolbarKey.VOICE)))
+ return KeyboardIconsSet.NAME_SHORTCUT_KEY_DISABLED;
+ return null;
+ }
+
+ public static class Spacer extends Key {
+ private Spacer(KeyParams keyParams) {
+ super(keyParams);
+ }
+
+ /**
+ * This constructor is being used only for divider in popup keys keyboard.
+ */
+ protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
+ final int height) {
+ super(null, null, KeyCode.NOT_SPECIFIED, null,
+ null, 0, BACKGROUND_TYPE_EMPTY, x, y, width,
+ height, params.mHorizontalGap, params.mVerticalGap);
+ }
+ }
+
+ // for creating keys that might get modified later
+ public static class KeyParams {
+ // params for building
+ public boolean isSpacer;
+ private final KeyboardParams mKeyboardParams; // for reading gaps and keyboard width / height
+ public float mWidth;
+ public float mHeight; // also should allow negative values, indicating absolute height is defined
+
+ // params that may change
+ public float mAbsoluteWidth;
+ public float mAbsoluteHeight;
+ public float xPos;
+ public float yPos;
+
+ // params that remains constant
+ public final int mCode;
+ @Nullable public final String mLabel;
+ @Nullable public final String mHintLabel;
+ public final int mLabelFlags;
+ @Nullable public final String mIconName;
+ @Nullable public final PopupKeySpec[] mPopupKeys;
+ public final int mPopupKeysColumnAndFlags;
+ public final int mBackgroundType;
+ public final int mActionFlags;
+ @Nullable public final KeyVisualAttributes mKeyVisualAttributes;
+ @Nullable final OptionalAttributes mOptionalAttributes;
+ public final boolean mEnabled;
+
+ public static KeyParams newSpacer(final KeyboardParams params, final float width) {
+ final KeyParams spacer = new KeyParams(params);
+ spacer.mWidth = width;
+ spacer.mHeight = params.mDefaultRowHeight;
+ return spacer;
+ }
+
+ public Key createKey() {
+ if (isSpacer) return new Spacer(this);
+ return new Key(this);
+ }
+
+ public void setAbsoluteDimensions(final float newX, final float newY) {
+ if (mHeight == 0)
+ mHeight = mKeyboardParams.mDefaultRowHeight;
+ if (!isSpacer && mWidth == 0)
+ throw new IllegalStateException("width = 0 should have been evaluated already");
+ if (mHeight < 0)
+ // todo (later): deal with it properly when it needs to be adjusted, i.e. when changing popupKeys or moreSuggestions
+ throw new IllegalStateException("can't (yet) deal with absolute height");
+ xPos = newX;
+ yPos = newY;
+ mAbsoluteWidth = mWidth * mKeyboardParams.mBaseWidth;
+ mAbsoluteHeight = mHeight * mKeyboardParams.mBaseHeight;
+ }
+
+ private static int getPopupKeysColumnAndFlagsAndSetNullInArray(final KeyboardParams params, final String[] popupKeys) {
+ // Get maximum column order number and set a relevant mode value.
+ int popupKeysColumnAndFlags = POPUP_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER | params.mMaxPopupKeysKeyboardColumn;
+ int value;
+ if ((value = PopupKeySpec.getIntValue(popupKeys, POPUP_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
+ // Override with fixed column order number and set a relevant mode value.
+ popupKeysColumnAndFlags = POPUP_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER | (value & POPUP_KEYS_COLUMN_NUMBER_MASK);
+ }
+ if ((value = PopupKeySpec.getIntValue(popupKeys, POPUP_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
+ // Override with fixed column order number and set a relevant mode value.
+ popupKeysColumnAndFlags = POPUP_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER | (value & POPUP_KEYS_COLUMN_NUMBER_MASK);
+ }
+ if (PopupKeySpec.getBooleanValue(popupKeys, POPUP_KEYS_HAS_LABELS)) {
+ popupKeysColumnAndFlags |= POPUP_KEYS_FLAGS_HAS_LABELS;
+ }
+ if (PopupKeySpec.getBooleanValue(popupKeys, POPUP_KEYS_NEEDS_DIVIDERS)) {
+ popupKeysColumnAndFlags |= POPUP_KEYS_FLAGS_NEEDS_DIVIDERS;
+ }
+ if (PopupKeySpec.getBooleanValue(popupKeys, POPUP_KEYS_NO_PANEL_AUTO_POPUP_KEY)) {
+ popupKeysColumnAndFlags |= POPUP_KEYS_FLAGS_NO_PANEL_AUTO_POPUP_KEY;
+ }
+ return popupKeysColumnAndFlags;
+ }
+
+ public String getOutputText() {
+ return mOptionalAttributes == null ? null : mOptionalAttributes.mOutputText;
+ }
+
+ public KeyParams(
+ @NonNull final String keySpec,
+ @NonNull final KeyboardParams params,
+ final float relativeWidth,
+ final int labelFlags,
+ final int backgroundType,
+ @Nullable final PopupSet> popupSet
+ ) {
+ this(keySpec, KeySpecParser.getCode(keySpec), params, relativeWidth, labelFlags, backgroundType, popupSet);
+ }
+
+ /**
+ * constructor that does not require attrs, style or absolute key dimension / position
+ * setDimensionsFromRelativeSize needs to be called before creating the key
+ */
+ public KeyParams(
+ // todo (much later): replace keySpec? these encoded icons and codes are not really great
+ @NonNull final String keySpec, // key text or some special string for KeySpecParser, e.g. "!icon/shift_key|!code/key_shift" (avoid using !text, should be removed)
+ final int code,
+ @NonNull final KeyboardParams params,
+ final float width,
+ final int labelFlags,
+ final int backgroundType,
+ @Nullable final PopupSet> popupSet
+ ) {
+ mKeyboardParams = params;
+ mBackgroundType = backgroundType;
+ mLabelFlags = labelFlags;
+ mWidth = width;
+ mHeight = params.mDefaultRowHeight;
+ mIconName = KeySpecParser.getIconName(keySpec);
+
+ final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
+ final Locale localeForUpcasing = params.mId.getLocale();
+ int actionFlags = 0;
+ if (params.mId.isNumberLayout())
+ actionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
+
+ // label
+ String label = null;
+ if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
+ mLabel = params.mId.mCustomActionLabel;
+ } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
+ // This is a workaround to have a key that has a supplementary code point in its label.
+ // Because we can put a string in resource neither as a XML entity of a supplementary
+ // code point nor as a surrogate pair.
+ mLabel = new StringBuilder().appendCodePoint(code).toString();
+ } else {
+ label = KeySpecParser.getLabel(keySpec);
+ mLabel = needsToUpcase
+ ? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing)
+ : label;
+ }
+
+ // popupKeys
+ final String[] popupKeys = PopupKeysUtilsKt.createPopupKeysArray(popupSet, mKeyboardParams, label != null ? label : keySpec);
+ mPopupKeysColumnAndFlags = getPopupKeysColumnAndFlagsAndSetNullInArray(params, popupKeys);
+ final String[] finalPopupKeys = popupKeys == null ? null : PopupKeySpec.filterOutEmptyString(popupKeys);
+ if (finalPopupKeys != null) {
+ actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
+ mPopupKeys = new PopupKeySpec[finalPopupKeys.length];
+ for (int i = 0; i < finalPopupKeys.length; i++) {
+ mPopupKeys[i] = new PopupKeySpec(finalPopupKeys[i], needsToUpcase, localeForUpcasing);
+ }
+ } else {
+ mPopupKeys = null;
+ }
+
+ // hint label
+ if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
+ mHintLabel = null;
+ } else {
+ // maybe also always null for comma and period keys
+ final String hintLabel = PopupKeysUtilsKt.getHintLabel(popupSet, params, keySpec);
+ mHintLabel = needsToUpcase
+ ? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing)
+ : hintLabel;
+ }
+
+ String outputText = KeySpecParser.getOutputText(keySpec, code);
+ if (needsToUpcase) {
+ outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing);
+ }
+ // Choose the first letter of the label as primary code if not specified.
+ if (code == KeyCode.NOT_SPECIFIED && TextUtils.isEmpty(outputText) && !TextUtils.isEmpty(mLabel)) {
+ if (StringUtils.codePointCount(mLabel) == 1) {
+ // Use the first letter of the hint label if shiftedLetterActivated flag is
+ // specified.
+ if ((mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
+ && !TextUtils.isEmpty(mHintLabel)) {
+ mCode = mHintLabel.codePointAt(0);
+ } else {
+ mCode = mLabel.codePointAt(0);
+ }
+ } else {
+ // In some locale and case, the character might be represented by multiple code
+ // points, such as upper case Eszett of German alphabet.
+ outputText = mLabel;
+ mCode = KeyCode.MULTIPLE_CODE_POINTS;
+ }
+ } else if (code == KeyCode.NOT_SPECIFIED && outputText != null) {
+ if (StringUtils.codePointCount(outputText) == 1) {
+ mCode = outputText.codePointAt(0);
+ outputText = null;
+ } else {
+ mCode = KeyCode.MULTIPLE_CODE_POINTS;
+ }
+ } else {
+ mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing) : code;
+ }
+
+ // action flags don't need to be specified, they can be deduced from the key
+ if (mCode == Constants.CODE_SPACE
+ || mCode == KeyCode.LANGUAGE_SWITCH
+ || (mCode == KeyCode.SYMBOL_ALPHA && !params.mId.isAlphabetKeyboard())
+ )
+ actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
+ if (mCode <= Constants.CODE_SPACE && mCode != KeyCode.MULTIPLE_CODE_POINTS && mIconName == null)
+ actionFlags |= ACTION_FLAGS_NO_KEY_PREVIEW;
+ switch (mCode) {
+ case KeyCode.DELETE, KeyCode.ARROW_LEFT, KeyCode.ARROW_RIGHT, KeyCode.ARROW_UP, KeyCode.ARROW_DOWN,
+ KeyCode.WORD_LEFT, KeyCode.WORD_RIGHT, KeyCode.PAGE_UP, KeyCode.PAGE_DOWN:
+ // repeating is disabled if a key is configured with pop-ups
+ if (mPopupKeys == null)
+ actionFlags |= ACTION_FLAGS_IS_REPEATABLE;
+ // fallthrough
+ case KeyCode.SHIFT, Constants.CODE_ENTER, KeyCode.SHIFT_ENTER, KeyCode.ALPHA, Constants.CODE_SPACE, KeyCode.NUMPAD,
+ KeyCode.SYMBOL, KeyCode.SYMBOL_ALPHA, KeyCode.LANGUAGE_SWITCH, KeyCode.EMOJI, KeyCode.CLIPBOARD,
+ KeyCode.MOVE_START_OF_LINE, KeyCode.MOVE_END_OF_LINE, KeyCode.MOVE_START_OF_PAGE, KeyCode.MOVE_END_OF_PAGE:
+ actionFlags |= ACTION_FLAGS_NO_KEY_PREVIEW; // no preview even if icon!
+ }
+ if (mCode == KeyCode.SETTINGS || mCode == KeyCode.LANGUAGE_SWITCH)
+ actionFlags |= ACTION_FLAGS_ALT_CODE_WHILE_TYPING;
+ mActionFlags = actionFlags;
+
+ final int altCodeInAttr; // settings and language switch keys have alt code space, all others nothing
+ if (mCode == KeyCode.SETTINGS || mCode == KeyCode.LANGUAGE_SWITCH || mCode == KeyCode.EMOJI || mCode == KeyCode.CLIPBOARD)
+ altCodeInAttr = Constants.CODE_SPACE;
+ else
+ altCodeInAttr = KeyCode.NOT_SPECIFIED;
+ final int altCode = needsToUpcase
+ ? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing)
+ : altCodeInAttr;
+ mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
+ // disabled icon only shortcut / voice key, visual insets can be replaced with spacer
+ mIconName == null ? null : getDisabledIconName(mIconName), 0, 0);
+ // KeyVisualAttributes for a key essentially are what the theme has, but on a per-key base
+ // could be used e.g. for having a color gradient on key color
+ mKeyVisualAttributes = null;
+ mEnabled = true;
+ }
+
+ /** constructor for emoji parser */
+ public KeyParams(@Nullable final String label, final int code, @Nullable final String hintLabel,
+ @Nullable final String popupKeySpecs, final int labelFlags, final KeyboardParams params) {
+ mKeyboardParams = params;
+ mHintLabel = hintLabel;
+ mLabelFlags = labelFlags;
+ mBackgroundType = BACKGROUND_TYPE_EMPTY;
+
+ if (popupKeySpecs != null) {
+ String[] popupKeys = PopupKeySpec.splitKeySpecs(popupKeySpecs);
+ mPopupKeysColumnAndFlags = getPopupKeysColumnAndFlagsAndSetNullInArray(params, popupKeys);
+
+ popupKeys = PopupKeySpec.insertAdditionalPopupKeys(popupKeys, null);
+ int actionFlags = 0;
+ if (popupKeys != null) {
+ actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
+ mPopupKeys = new PopupKeySpec[popupKeys.length];
+ for (int i = 0; i < popupKeys.length; i++) {
+ mPopupKeys[i] = new PopupKeySpec(popupKeys[i], false, Locale.getDefault());
+ }
+ } else {
+ mPopupKeys = null;
+ }
+ mActionFlags = actionFlags;
+ } else {
+ // TODO: Pass keyActionFlags as an argument.
+ mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
+ mPopupKeys = null;
+ mPopupKeysColumnAndFlags = 0;
+ }
+
+ mLabel = label;
+ mOptionalAttributes = code == KeyCode.MULTIPLE_CODE_POINTS
+ ? OptionalAttributes.newInstance(label, KeyCode.NOT_SPECIFIED, null, 0, 0)
+ : null;
+ mCode = code;
+ mEnabled = (code != KeyCode.NOT_SPECIFIED);
+ mIconName = null;
+ mKeyVisualAttributes = null;
+ }
+
+ /** constructor for a spacer whose size MUST be determined using setDimensionsFromRelativeSize */
+ private KeyParams(final KeyboardParams params) {
+ isSpacer = true; // this is only for spacer!
+ mKeyboardParams = params;
+
+ mCode = KeyCode.NOT_SPECIFIED;
+ mLabel = null;
+ mHintLabel = null;
+ mKeyVisualAttributes = null;
+ mOptionalAttributes = null;
+ mIconName = null;
+ mBackgroundType = BACKGROUND_TYPE_NORMAL;
+ mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
+ mPopupKeys = null;
+ mPopupKeysColumnAndFlags = 0;
+ mLabelFlags = LABEL_FLAGS_FONT_NORMAL;
+ mEnabled = true;
+ }
+
+ public KeyParams(final KeyParams keyParams) {
+ xPos = keyParams.xPos;
+ yPos = keyParams.yPos;
+ mWidth = keyParams.mWidth;
+ mHeight = keyParams.mHeight;
+ isSpacer = keyParams.isSpacer;
+ mKeyboardParams = keyParams.mKeyboardParams;
+ mEnabled = keyParams.mEnabled;
+
+ mCode = keyParams.mCode;
+ mLabel = keyParams.mLabel;
+ mHintLabel = keyParams.mHintLabel;
+ mLabelFlags = keyParams.mLabelFlags;
+ mIconName = keyParams.mIconName;
+ mAbsoluteWidth = keyParams.mAbsoluteWidth;
+ mAbsoluteHeight = keyParams.mAbsoluteHeight;
+ mPopupKeys = keyParams.mPopupKeys;
+ mPopupKeysColumnAndFlags = keyParams.mPopupKeysColumnAndFlags;
+ mBackgroundType = keyParams.mBackgroundType;
+ mActionFlags = keyParams.mActionFlags;
+ mKeyVisualAttributes = keyParams.mKeyVisualAttributes;
+ mOptionalAttributes = keyParams.mOptionalAttributes;
+ }
+ }
+}
diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyDetector.java b/app/src/main/java/helium314/keyboard/keyboard/KeyDetector.java
similarity index 85%
rename from app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyDetector.java
rename to app/src/main/java/helium314/keyboard/keyboard/KeyDetector.java
index 58bd399290..400511cdad 100644
--- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyDetector.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyDetector.java
@@ -1,20 +1,10 @@
/*
* Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
-package org.dslul.openboard.inputmethod.keyboard;
+package helium314.keyboard.keyboard;
/**
* This class handles key detection.
diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Keyboard.java b/app/src/main/java/helium314/keyboard/keyboard/Keyboard.java
similarity index 75%
rename from app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Keyboard.java
rename to app/src/main/java/helium314/keyboard/keyboard/Keyboard.java
index e64794cfe9..794e10f42d 100644
--- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Keyboard.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/Keyboard.java
@@ -1,36 +1,29 @@
/*
* Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
-package org.dslul.openboard.inputmethod.keyboard;
+package helium314.keyboard.keyboard;
import android.util.SparseArray;
-import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes;
-import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet;
-import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams;
-import org.dslul.openboard.inputmethod.latin.common.Constants;
-import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import helium314.keyboard.latin.legacy.ProximityInfo;
+
+import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
+import helium314.keyboard.keyboard.internal.KeyboardParams;
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
+import helium314.keyboard.latin.common.Constants;
+import helium314.keyboard.latin.common.CoordinateUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
/**
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
* consists of rows of keys.
@@ -50,19 +43,19 @@
*
*/
public class Keyboard {
- @Nonnull
+ @NonNull
public final KeyboardId mId;
public final int mThemeId;
/** Total height of the keyboard, including the padding and keys */
public final int mOccupiedHeight;
/** Total width of the keyboard, including the padding and keys */
- public final int mOccupiedWidth;
+ public int mOccupiedWidth;
/** Base height of the keyboard, used to calculate rows' height */
public final int mBaseHeight;
/** Base width of the keyboard, used to calculate keys' width */
- public final int mBaseWidth;
+ public int mBaseWidth;
/** The padding above the keyboard */
public final int mTopPadding;
@@ -75,32 +68,30 @@ public class Keyboard {
public final int mMostCommonKeyHeight;
public final int mMostCommonKeyWidth;
- /** More keys keyboard template */
- public final int mMoreKeysTemplate;
+ /** Popup keys keyboard template */
+ public final int mPopupKeysTemplate;
- /** Maximum column for more keys keyboard */
- public final int mMaxMoreKeysKeyboardColumn;
+ /** Maximum column for popup keys keyboard */
+ public final int mMaxPopupKeysKeyboardColumn;
/** List of keys in this keyboard */
- @Nonnull
+ @NonNull
private final List mSortedKeys;
- @Nonnull
+ @NonNull
public final List mShiftKeys;
- @Nonnull
+ @NonNull
public final List mAltCodeKeysWhileTyping;
- @Nonnull
+ @NonNull
public final KeyboardIconsSet mIconsSet;
private final SparseArray mKeyCache = new SparseArray<>();
- @Nonnull
+ @NonNull
private final ProximityInfo mProximityInfo;
- @Nonnull
- private final KeyboardLayout mKeyboardLayout;
private final boolean mProximityCharsCorrectionEnabled;
- public Keyboard(@Nonnull final KeyboardParams params) {
+ public Keyboard(@NonNull final KeyboardParams params) {
mId = params.mId;
mThemeId = params.mThemeId;
mOccupiedHeight = params.mOccupiedHeight;
@@ -109,8 +100,8 @@ public Keyboard(@Nonnull final KeyboardParams params) {
mBaseWidth = params.mBaseWidth;
mMostCommonKeyHeight = params.mMostCommonKeyHeight;
mMostCommonKeyWidth = params.mMostCommonKeyWidth;
- mMoreKeysTemplate = params.mMoreKeysTemplate;
- mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn;
+ mPopupKeysTemplate = params.mPopupKeysTemplate;
+ mMaxPopupKeysKeyboardColumn = params.mMaxPopupKeysKeyboardColumn;
mKeyVisualAttributes = params.mKeyVisualAttributes;
mTopPadding = params.mTopPadding;
mVerticalGap = params.mVerticalGap;
@@ -124,11 +115,9 @@ public Keyboard(@Nonnull final KeyboardParams params) {
mOccupiedWidth, mOccupiedHeight, mMostCommonKeyWidth, mMostCommonKeyHeight,
mSortedKeys, params.mTouchPositionCorrection);
mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled;
- mKeyboardLayout = KeyboardLayout.newKeyboardLayout(mSortedKeys, mMostCommonKeyWidth,
- mMostCommonKeyHeight, mOccupiedWidth, mOccupiedHeight);
}
- protected Keyboard(@Nonnull final Keyboard keyboard) {
+ protected Keyboard(@NonNull final Keyboard keyboard) {
mId = keyboard.mId;
mThemeId = keyboard.mThemeId;
mOccupiedHeight = keyboard.mOccupiedHeight;
@@ -137,8 +126,8 @@ protected Keyboard(@Nonnull final Keyboard keyboard) {
mBaseWidth = keyboard.mBaseWidth;
mMostCommonKeyHeight = keyboard.mMostCommonKeyHeight;
mMostCommonKeyWidth = keyboard.mMostCommonKeyWidth;
- mMoreKeysTemplate = keyboard.mMoreKeysTemplate;
- mMaxMoreKeysKeyboardColumn = keyboard.mMaxMoreKeysKeyboardColumn;
+ mPopupKeysTemplate = keyboard.mPopupKeysTemplate;
+ mMaxPopupKeysKeyboardColumn = keyboard.mMaxPopupKeysKeyboardColumn;
mKeyVisualAttributes = keyboard.mKeyVisualAttributes;
mTopPadding = keyboard.mTopPadding;
mVerticalGap = keyboard.mVerticalGap;
@@ -150,7 +139,6 @@ protected Keyboard(@Nonnull final Keyboard keyboard) {
mProximityInfo = keyboard.mProximityInfo;
mProximityCharsCorrectionEnabled = keyboard.mProximityCharsCorrectionEnabled;
- mKeyboardLayout = keyboard.mKeyboardLayout;
}
public boolean hasProximityCharsCorrection(final int code) {
@@ -165,30 +153,25 @@ public boolean hasProximityCharsCorrection(final int code) {
return canAssumeNativeHasProximityCharsInfoOfAllKeys || Character.isLetter(code);
}
- @Nonnull
+ @NonNull
public ProximityInfo getProximityInfo() {
return mProximityInfo;
}
- @Nonnull
- public KeyboardLayout getKeyboardLayout() {
- return mKeyboardLayout;
- }
-
/**
* Return the sorted list of keys of this keyboard.
* The keys are sorted from top-left to bottom-right order.
* The list may contain {@link Key.Spacer} object as well.
* @return the sorted unmodifiable list of {@link Key}s of this keyboard.
*/
- @Nonnull
+ @NonNull
public List getSortedKeys() {
return mSortedKeys;
}
@Nullable
public Key getKey(final int code) {
- if (code == Constants.CODE_UNSPECIFIED) {
+ if (code == KeyCode.NOT_SPECIFIED) {
return null;
}
synchronized (mKeyCache) {
@@ -208,7 +191,7 @@ public Key getKey(final int code) {
}
}
- public boolean hasKey(@Nonnull final Key aKey) {
+ public boolean hasKey(@NonNull final Key aKey) {
if (mKeyCache.indexOfValue(aKey) >= 0) {
return true;
}
@@ -234,7 +217,7 @@ public String toString() {
* @return the list of the nearest keys to the given point. If the given
* point is out of range, then an array of size zero is returned.
*/
- @Nonnull
+ @NonNull
public List getNearestKeys(final int x, final int y) {
// Avoid dead pixels at edges of the keyboard
final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
@@ -242,8 +225,8 @@ public List getNearestKeys(final int x, final int y) {
return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
}
- @Nonnull
- public int[] getCoordinates(@Nonnull final int[] codePoints) {
+ @NonNull
+ public int[] getCoordinates(@NonNull final int[] codePoints) {
final int length = codePoints.length;
final int[] coordinates = CoordinateUtils.newCoordinateArray(length);
for (int i = 0; i < length; ++i) {
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java
new file mode 100644
index 0000000000..a71414fa30
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListener.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.keyboard;
+
+import android.view.KeyEvent;
+
+import helium314.keyboard.event.HapticEvent;
+import helium314.keyboard.latin.common.Constants;
+import helium314.keyboard.latin.common.InputPointers;
+
+public interface KeyboardActionListener {
+ /**
+ * Called when the user presses a key. This is sent before the {@link #onCodeInput} is called.
+ * For keys that repeat, this is only called once.
+ *
+ * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
+ * the value will be zero.
+ * @param repeatCount how many times the key was repeated. Zero if it is the first press.
+ * @param isSinglePointer true if pressing has occurred while no other key is being pressed.
+ * @param hapticEvent the type of haptic feedback to perform.
+ */
+ void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer, HapticEvent hapticEvent);
+
+ void onLongPressKey(int primaryCode);
+
+ /**
+ * Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
+ * For keys that repeat, this is only called once.
+ *
+ * @param primaryCode the code of the key that was released
+ * @param withSliding true if releasing has occurred because the user slid finger from the key
+ * to other key without releasing the finger.
+ */
+ 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.
+ *
+ * @param primaryCode this is the code of the key that was pressed
+ * @param x x-coordinate pixel of touched event. If onCodeInput is not called by
+ * {@link PointerTracker} or so, the value should be
+ * {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the
+ * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
+ * @param y y-coordinate pixel of touched event. If #onCodeInput is not called by
+ * {@link PointerTracker} or so, the value should be
+ * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the
+ * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
+ * @param isKeyRepeat true if this is a key repeat, false otherwise
+ */
+ // TODO: change this to send an Event object instead
+ void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat);
+
+ /**
+ * Sends a string of characters to the listener.
+ *
+ * @param text the string of characters to be registered.
+ */
+ void onTextInput(String text);
+
+ /**
+ * Called when user started batch input.
+ */
+ void onStartBatchInput();
+
+ /**
+ * Sends the ongoing batch input points data.
+ * @param batchPointers the batch input points representing the user input
+ */
+ void onUpdateBatchInput(InputPointers batchPointers);
+
+ /**
+ * Sends the final batch input points data.
+ *
+ * @param batchPointers the batch input points representing the user input
+ */
+ void onEndBatchInput(InputPointers batchPointers);
+
+ void onCancelBatchInput();
+
+ /**
+ * Called when user released a finger outside any key.
+ */
+ void onCancelInput();
+
+ /**
+ * Called when user finished sliding key input.
+ */
+ void onFinishSlidingInput();
+
+ /**
+ * Send a non-"code input" custom request to the listener.
+ * @return true if the request has been consumed, false otherwise.
+ */
+ boolean onCustomRequest(int requestCode);
+
+ /**
+ * Called when the user performs a horizontal or vertical swipe gesture
+ * on the space bar.
+ */
+ boolean onHorizontalSpaceSwipe(int steps);
+ boolean onVerticalSpaceSwipe(int steps);
+ void onEndSpaceSwipe();
+ boolean toggleNumpad(boolean withSliding, boolean forceReturnToAlpha);
+
+ void onMoveDeletePointer(int steps);
+ void onUpWithDeletePointerActive();
+ void resetMetaState();
+
+ KeyboardActionListener EMPTY_LISTENER = new Adapter();
+
+ int SWIPE_NO_ACTION = 0;
+ int SWIPE_MOVE_CURSOR = 1;
+ int SWIPE_SWITCH_LANGUAGE = 2;
+ int SWIPE_TOGGLE_NUMPAD = 3;
+ int SWIPE_HIDE_KEYBOARD = 4;
+
+ class Adapter implements KeyboardActionListener {
+ @Override
+ public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer, HapticEvent hapticEvent) {}
+ @Override
+ public void onLongPressKey(int primaryCode) {}
+ @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) {}
+ @Override
+ public void onStartBatchInput() {}
+ @Override
+ public void onUpdateBatchInput(InputPointers batchPointers) {}
+ @Override
+ public void onEndBatchInput(InputPointers batchPointers) {}
+ @Override
+ public void onCancelBatchInput() {}
+ @Override
+ public void onCancelInput() {}
+ @Override
+ public void onFinishSlidingInput() {}
+ @Override
+ public boolean onCustomRequest(int requestCode) {
+ return false;
+ }
+ @Override
+ public boolean onHorizontalSpaceSwipe(int steps) {
+ return false;
+ }
+ @Override
+ public boolean onVerticalSpaceSwipe(int steps) {
+ return false;
+ }
+ @Override
+ public boolean toggleNumpad(boolean withSliding, boolean forceReturnToAlpha) {
+ return false;
+ }
+ @Override
+ public void onEndSpaceSwipe() {}
+ @Override
+ public void onMoveDeletePointer(int steps) {}
+ @Override
+ public void onUpWithDeletePointerActive() {}
+ @Override
+ public void resetMetaState() {}
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
new file mode 100644
index 0000000000..d6d03453a8
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.keyboard
+
+import android.text.InputType
+import android.util.SparseArray
+import android.view.KeyEvent
+import android.view.inputmethod.InputMethodSubtype
+import androidx.core.util.forEach
+import helium314.keyboard.event.Event
+import helium314.keyboard.event.HangulEventDecoder
+import helium314.keyboard.event.HapticEvent
+import helium314.keyboard.event.HardwareEventDecoder
+import helium314.keyboard.event.HardwareKeyboardEventDecoder
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.latin.AudioAndHapticFeedbackManager
+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 helium314.keyboard.latin.utils.SubtypeSettings
+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 = SparseArray(1)
+
+ private val keyboardSwitcher = KeyboardSwitcher.getInstance()
+ private val settings = Settings.getInstance()
+ private val audioAndHapticFeedbackManager = AudioAndHapticFeedbackManager.getInstance()
+
+ // language slide state
+ private var initialSubtype: InputMethodSubtype? = null
+ private var subtypeSwitchCount = 0
+
+ override fun onPressKey(primaryCode: Int, repeatCount: Int, isSinglePointer: Boolean, hapticEvent: HapticEvent) {
+ metaOnPressKey(primaryCode)
+ keyboardSwitcher.onPressKey(primaryCode, isSinglePointer, latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
+ // we need to use LatinIME for handling of key-down audio and haptics
+ latinIME.hapticAndAudioFeedback(primaryCode, repeatCount, hapticEvent)
+ }
+
+ override fun onLongPressKey(primaryCode: Int) {
+ metaOnLongPressKey(primaryCode)
+ performHapticFeedback(HapticEvent.KEY_LONG_PRESS)
+ }
+
+ override fun onReleaseKey(primaryCode: Int, withSliding: Boolean) {
+ metaOnReleaseKey(primaryCode)
+ 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.toggleAutoCorrect()
+ KeyCode.TOGGLE_INCOGNITO_MODE -> return settings.toggleAlwaysIncognitoMode()
+ }
+ val mkv = keyboardSwitcher.mainKeyboardView
+
+ // 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)
+ metaAfterCodeInput(primaryCode)
+ }
+
+ override fun onTextInput(text: String?) = latinIME.onTextInput(text)
+
+ override fun onStartBatchInput() = latinIME.onStartBatchInput()
+
+ override fun onUpdateBatchInput(batchPointers: InputPointers?) = latinIME.onUpdateBatchInput(batchPointers)
+
+ override fun onEndBatchInput(batchPointers: InputPointers?) = latinIME.onEndBatchInput(batchPointers)
+
+ override fun onCancelBatchInput() = latinIME.onCancelBatchInput()
+
+ // User released a finger outside any key
+ override fun onCancelInput() { }
+
+ override fun onFinishSlidingInput() =
+ keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
+
+ override fun onCustomRequest(requestCode: Int): Boolean {
+ if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) {
+ return latinIME.showInputPickerDialog()
+ }
+ if (requestCode == Constants.CUSTOM_CODE_HIDE_KEYBOARD) {
+ latinIME.requestHideSelf(0)
+ return true
+ }
+ return false
+ }
+
+ override fun onHorizontalSpaceSwipe(steps: Int): Boolean = when (Settings.getValues().mSpaceSwipeHorizontal) {
+ KeyboardActionListener.SWIPE_MOVE_CURSOR -> onMoveCursorHorizontally(steps)
+ KeyboardActionListener.SWIPE_SWITCH_LANGUAGE -> onLanguageSlide(steps)
+ KeyboardActionListener.SWIPE_TOGGLE_NUMPAD -> toggleNumpad(false, false)
+ else -> false
+ }
+
+ override fun onVerticalSpaceSwipe(steps: Int): Boolean = when (Settings.getValues().mSpaceSwipeVertical) {
+ KeyboardActionListener.SWIPE_MOVE_CURSOR -> onMoveCursorVertically(steps)
+ KeyboardActionListener.SWIPE_SWITCH_LANGUAGE -> onLanguageSlide(steps)
+ KeyboardActionListener.SWIPE_TOGGLE_NUMPAD -> toggleNumpad(false, false)
+ KeyboardActionListener.SWIPE_HIDE_KEYBOARD -> {
+ latinIME.requestHideSelf(0)
+ true
+ }
+ else -> false
+ }
+
+ override fun onEndSpaceSwipe(){
+ initialSubtype = null
+ subtypeSwitchCount = 0
+ }
+
+ override fun toggleNumpad(withSliding: Boolean, forceReturnToAlpha: Boolean): Boolean {
+ keyboardSwitcher.toggleNumpad(withSliding, latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState, forceReturnToAlpha)
+ return true
+ }
+
+ override fun onMoveDeletePointer(steps: Int) {
+ inputLogic.finishInput()
+ val end = connection.expectedSelectionEnd
+ val actualSteps = actualSteps(steps)
+ val start = connection.expectedSelectionStart + actualSteps
+ if (start > end) return
+ gestureMoveBackHaptics()
+ 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 = connection.getSelectedText(0) ?: return steps
+ loopOverCodePoints(text) { cp, charCount ->
+ actualSteps += charCount
+ actualSteps >= steps
+ }
+ } else {
+ val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return steps
+ loopOverCodePointsBackwards(text) { cp, charCount ->
+ actualSteps -= charCount
+ actualSteps <= steps
+ }
+ }
+ return actualSteps
+ }
+
+ override fun onUpWithDeletePointerActive() {
+ if (!connection.hasSelection()) return
+ inputLogic.finishInput()
+ onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ }
+
+ override fun resetMetaState() {
+ metaState = 0
+ }
+
+ private fun onLanguageSlide(steps: Int): Boolean {
+ if (abs(steps) < settings.current.mLanguageSwipeDistance) return false
+ val subtypes = SubtypeSettings.getEnabledSubtypes(true)
+ if (subtypes.size <= 1) { // only allow if we have more than one subtype
+ return false
+ }
+ // decide next or previous dependent on up or down
+ val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype
+ var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1
+ wantedIndex %= subtypes.size
+ 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 == newSubtype) {
+ if ((subtypeSwitchCount > 0 && steps > 0) || (subtypeSwitchCount < 0 && steps < 0)) {
+ return true
+ }
+ }
+ if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount--
+
+ keyboardSwitcher.switchToSubtype(newSubtype)
+ return true
+ }
+
+ private fun onMoveCursorVertically(steps: Int): Boolean {
+ if (steps == 0) return false
+ val code = if (steps < 0) {
+ gestureMoveBackHaptics()
+ KeyCode.ARROW_UP
+ } else {
+ gestureMoveForwardHaptics()
+ KeyCode.ARROW_DOWN
+ }
+ onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ return true
+ }
+
+ private fun onMoveCursorHorizontally(rawSteps: Int): Boolean {
+ if (rawSteps == 0) return false
+ // for RTL languages we want to invert pointer movement
+ val rtl = RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype
+ val steps = if (rtl) -rawSteps else rawSteps
+ val moveSteps: Int
+ if (steps < 0) {
+ 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
+ repeat(-steps) {
+ onCodeInput(if (rtl) KeyCode.ARROW_RIGHT else KeyCode.ARROW_LEFT, Constants.NOT_A_COORDINATE,
+ Constants.NOT_A_COORDINATE, false)
+ }
+ if (text.isNotEmpty()) {
+ gestureMoveBackHaptics()
+ }
+ return true
+ }
+ gestureMoveBackHaptics()
+ } else {
+ 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(if (rtl) KeyCode.ARROW_LEFT else KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE,
+ Constants.NOT_A_COORDINATE, false)
+ }
+ if (text.isNotEmpty()) {
+ gestureMoveForwardHaptics(true)
+ }
+ return true
+ }
+ gestureMoveForwardHaptics(text.isNotEmpty())
+ }
+
+ // the shortcut below causes issues due to horrible handling of text fields by Firefox and forks
+ // issues:
+ // * setSelection "will cause the editor to call onUpdateSelection", see: https://developer.android.com/reference/android/view/inputmethod/InputConnection#setSelection(int,%20int)
+ // but Firefox is simply not doing this within the same word... WTF?
+ // https://github.com/Helium314/HeliBoard/issues/1139#issuecomment-2588169384
+ // * inputType is NOT of variant InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT (variant appears to always be 0)
+ // -> this is "fixed" now using AppWorkarounds.adjustInputType
+ val variation = InputType.TYPE_MASK_VARIATION and Settings.getValues().mInputAttributes.mInputType
+ if (variation != InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+ && 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 = connection.expectedSelectionStart + moveSteps
+ connection.setSelection(newPosition, newPosition)
+ return true
+ }
+
+ inputLogic.finishInput()
+ 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 ->
+ // For emojis we (incorrectly) return 0 so the move is handled by virtual arrow key presses.
+ // This is a simple workaround to avoid determining the correct character count, which can
+ // be tricky because in some cases older Android versions show two emojis where newer ones show one.
+ 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 ->
+ // For emojis we (incorrectly) return 0 so the move is handled by virtual arrow key presses.
+ // This is a simple workaround to avoid determining the correct character count, which can
+ // be tricky because in some cases older Android versions show two emojis where newer ones show one.
+ if (StringUtils.mightBeEmoji(cp)) return 0
+ actualSteps -= charCount
+ actualSteps <= steps
+ }
+ return -min(-actualSteps, text.length)
+ }
+
+ private fun gestureMoveBackHaptics() {
+ if (connection.canDeleteCharacters()) {
+ performHapticFeedback(HapticEvent.GESTURE_MOVE)
+ }
+ }
+
+ // hasTextAfterCursor is used because text before the cursor is cached, going through the InputConnection can be slow
+ private fun gestureMoveForwardHaptics(hasTextAfterCursor: Boolean? = null) {
+ if (hasTextAfterCursor ?: connection.hasTextAfterCursor()) {
+ performHapticFeedback(HapticEvent.GESTURE_MOVE)
+ }
+ }
+
+ private fun performHapticFeedback(hapticEvent: HapticEvent) {
+ audioAndHapticFeedbackManager.performHapticFeedback(keyboardSwitcher.visibleKeyboardView, hapticEvent)
+ }
+
+ 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
+ }
+
+ // -------------------------- meta state handling -----------------------------
+
+ // current state
+ // press enables meta
+ // release keeps meta enabled, unless there was a onCodeInput for a different key in between
+ // onCodeInput ends the meta if it was enabled
+ // long press on meta key also ends meta so popups are handled properly
+ // sliding from a meta key to some other words too, though this was not intended (and there are no sliding key input graphics)
+
+ // todo: move meta state tracking to KeyboardState? seems more suitable, also for handling sliding input
+ // but the issue is that meta state is used in Event to determine whether it's a functional Event (does not add a character)
+ // (and also it's in the hardware keyEvents which are handled by onKeyUp/Down, but that should be manageable)
+
+ /** actual Android metaState like in KeyEvent */
+ private var metaState = 0
+
+ /** keeps track of the state of meta keys by (HeliBoard) KeyCodes */
+ private val metaPressStates = SparseArray(4)
+
+ // todo: lock and non-lock versions interact badly: when any of them is released, the meta state is removed
+ // this is not wanted, especially because the state of the other key is not affected (still looks pressed)
+ private fun metaOnPressKey(primaryCode: Int) {
+ val metaCode = primaryCode.toMetaState() ?: return
+ if (primaryCode.isMetaLock()) {
+ // if unset -> lock, otherwise set to UNSET_ON_RELEASE so it's unset on release
+ if (metaPressStates[primaryCode] != MetaPressState.LOCKED) {
+ metaPressStates[primaryCode] = MetaPressState.LOCKED
+ keyboardSwitcher.mainKeyboardView?.updateLockState(primaryCode, true)
+ metaState = metaState or metaCode
+ } else {
+ metaPressStates[primaryCode] = MetaPressState.UNSET_ON_RELEASE
+ }
+ return
+ }
+ if (metaPressStates[primaryCode] == MetaPressState.RELEASED_BUT_ACTIVE) {
+ // meta key is pressed again without other input -> should be disabled on release
+ metaPressStates[primaryCode] = MetaPressState.UNSET_ON_RELEASE
+ } else {
+ // otherwise just press it normally
+ metaPressStates[primaryCode] = MetaPressState.PRESSED
+ }
+ metaState = metaState or metaCode
+ // pressed graphics are set anyway, no need to lock it
+ }
+
+ // looks like this is not called if there are no popups
+ private fun metaOnLongPressKey(primaryCode: Int) {
+ if (metaPressStates[primaryCode] != MetaPressState.PRESSED) return
+ // we long-pressed a meta key that has popups -> disable so the meta state is not used for the popup
+ metaPressStates[primaryCode] = MetaPressState.UNSET
+ keyboardSwitcher.mainKeyboardView?.updateLockState(primaryCode, false)
+ val metaCode = primaryCode.toMetaState() ?: return
+ metaState = metaState and metaCode.inv()
+ }
+
+ private fun metaOnReleaseKey(primaryCode: Int) {
+ val metaCode = primaryCode.toMetaState() ?: return
+ val metaPressState = metaPressStates[primaryCode]
+ if (metaPressState == MetaPressState.UNSET_ON_RELEASE) {
+ metaPressStates[primaryCode] = MetaPressState.UNSET
+ metaState = metaState and metaCode.inv()
+ keyboardSwitcher.mainKeyboardView?.updateLockState(primaryCode, false)
+ } else if (metaPressState == MetaPressState.PRESSED) {
+ metaPressStates[primaryCode] = MetaPressState.RELEASED_BUT_ACTIVE
+ keyboardSwitcher.mainKeyboardView?.updateLockState(primaryCode, true)
+ }
+ }
+
+ private fun metaAfterCodeInput(primaryCode: Int) {
+ val metaCode = primaryCode.toMetaState()
+ if (metaCode != null) {
+ // meta key might be a popup key, we just toggle between set and unset
+ val metaPressState = metaPressStates[primaryCode] ?: MetaPressState.UNSET
+ if (metaPressState == MetaPressState.UNSET) {
+ metaPressStates[primaryCode] = MetaPressState.SET
+ metaState = metaState or metaCode
+ keyboardSwitcher.mainKeyboardView?.updateLockState(primaryCode, true)
+ } else if (metaPressState == MetaPressState.SET) {
+ metaPressStates[primaryCode] = MetaPressState.UNSET
+ metaState = metaState and metaCode.inv()
+ keyboardSwitcher.mainKeyboardView?.updateLockState(primaryCode, false)
+ }
+ } else if (metaState != 0) {
+ // non-meta key -> unset all set / released_but_active, and mark pressed as UNSET_ON_RELEASE
+ metaPressStates.forEach { key, value ->
+ if (value == MetaPressState.RELEASED_BUT_ACTIVE || value == MetaPressState.SET) {
+ metaPressStates[key] = MetaPressState.UNSET
+ keyboardSwitcher.mainKeyboardView?.updateLockState(key, false)
+ val metaCode = key.toMetaState() ?: return@forEach
+ metaState = metaState and metaCode.inv()
+ } else if (value == MetaPressState.PRESSED) {
+ metaPressStates[key] = MetaPressState.UNSET_ON_RELEASE
+ }
+ }
+ }
+ }
+
+ companion object {
+ private enum class MetaPressState {
+ UNSET, // default state, not active
+ SET, // enabled without onPressKey (e.g. in popup)
+ PRESSED, // key is pressed
+ UNSET_ON_RELEASE, // key is pressed, but state will be unset on release
+ RELEASED_BUT_ACTIVE, // key was released without UNSET_ON_RELEASE state, meta state is still set
+ LOCKED, // key is locked and will be released only by pressing the same key again
+ }
+
+ private fun Int.toMetaState() = when (this) {
+ KeyCode.CTRL, KeyCode.CTRL_LOCK -> KeyEvent.META_CTRL_ON
+ KeyCode.CTRL_LEFT -> KeyEvent.META_CTRL_LEFT_ON
+ KeyCode.CTRL_RIGHT -> KeyEvent.META_CTRL_RIGHT_ON
+ KeyCode.ALT, KeyCode.ALT_LOCK -> KeyEvent.META_ALT_ON
+ KeyCode.ALT_LEFT -> KeyEvent.META_ALT_LEFT_ON
+ KeyCode.ALT_RIGHT -> KeyEvent.META_ALT_RIGHT_ON
+ KeyCode.FN, KeyCode.FN_LOCK -> KeyEvent.META_FUNCTION_ON
+ KeyCode.META, KeyCode.META_LOCK -> KeyEvent.META_META_ON
+ KeyCode.META_LEFT -> KeyEvent.META_META_LEFT_ON
+ KeyCode.META_RIGHT -> KeyEvent.META_META_RIGHT_ON
+ else -> null
+ }
+
+ private fun Int.isMetaLock() = this == KeyCode.CTRL_LOCK || this == KeyCode.ALT_LOCK || this == KeyCode.FN_LOCK || this == KeyCode.META_LOCK
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java
new file mode 100644
index 0000000000..3c5de6925e
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardId.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.keyboard;
+
+import android.text.InputType;
+import android.text.TextUtils;
+import android.view.inputmethod.EditorInfo;
+
+import helium314.keyboard.compat.EditorInfoCompatUtils;
+import helium314.keyboard.latin.RichInputMethodSubtype;
+import helium314.keyboard.latin.WordComposer;
+import helium314.keyboard.latin.utils.InputTypeUtils;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+
+import static helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+
+/**
+ * Unique identifier for each keyboard type.
+ */
+public final class KeyboardId {
+ public static final int MODE_TEXT = 0;
+ public static final int MODE_URL = 1;
+ public static final int MODE_EMAIL = 2;
+ public static final int MODE_IM = 3;
+ public static final int MODE_PHONE = 4;
+ public static final int MODE_NUMBER = 5;
+ public static final int MODE_DATE = 6;
+ public static final int MODE_TIME = 7;
+ public static final int MODE_DATETIME = 8;
+ public static final int MODE_NUMPAD = 9;
+
+ public static final int ELEMENT_ALPHABET = 0;
+ public static final int ELEMENT_ALPHABET_MANUAL_SHIFTED = 1;
+ public static final int ELEMENT_ALPHABET_AUTOMATIC_SHIFTED = 2;
+ public static final int ELEMENT_ALPHABET_SHIFT_LOCKED = 3;
+ public static final int ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED = 4;
+ public static final int ELEMENT_SYMBOLS = 5;
+ public static final int ELEMENT_SYMBOLS_SHIFTED = 6;
+ public static final int ELEMENT_PHONE = 7;
+ public static final int ELEMENT_PHONE_SYMBOLS = 8;
+ public static final int ELEMENT_NUMBER = 9;
+ public static final int ELEMENT_EMOJI_RECENTS = 10;
+ public static final int ELEMENT_EMOJI_CATEGORY1 = 11;
+ public static final int ELEMENT_EMOJI_CATEGORY2 = 12;
+ public static final int ELEMENT_EMOJI_CATEGORY3 = 13;
+ public static final int ELEMENT_EMOJI_CATEGORY4 = 14;
+ public static final int ELEMENT_EMOJI_CATEGORY5 = 15;
+ public static final int ELEMENT_EMOJI_CATEGORY6 = 16;
+ public static final int ELEMENT_EMOJI_CATEGORY7 = 17;
+ public static final int ELEMENT_EMOJI_CATEGORY8 = 18;
+ public static final int ELEMENT_EMOJI_CATEGORY9 = 19;
+ public static final int ELEMENT_EMOJI_CATEGORY10 = 20;
+ public static final int ELEMENT_EMOJI_CATEGORY11 = 21;
+ public static final int ELEMENT_EMOJI_CATEGORY12 = 22;
+ public static final int ELEMENT_EMOJI_CATEGORY13 = 23;
+ public static final int ELEMENT_EMOJI_CATEGORY14 = 24;
+ public static final int ELEMENT_EMOJI_CATEGORY15 = 25;
+ public static final int ELEMENT_EMOJI_CATEGORY16 = 26;
+ public static final int ELEMENT_CLIPBOARD = 27;
+ public static final int ELEMENT_NUMPAD = 28;
+ public static final int ELEMENT_EMOJI_BOTTOM_ROW = 29;
+ public static final int ELEMENT_CLIPBOARD_BOTTOM_ROW = 30;
+
+ public final RichInputMethodSubtype mSubtype;
+ public final int mWidth;
+ public final int mHeight;
+ public final int mMode;
+ public final int mElementId;
+ public final EditorInfo mEditorInfo;
+ public final boolean mDeviceLocked;
+ public final boolean mNumberRowEnabled;
+ public final boolean mNumberRowInSymbols;
+ public final boolean mLanguageSwitchKeyEnabled;
+ public final boolean mEmojiKeyEnabled;
+ public final String mCustomActionLabel;
+ public final boolean mHasShortcutKey;
+ public final boolean mIsSplitLayout;
+ public final boolean mOneHandedModeEnabled;
+ public final KeyboardLayoutSet.InternalAction mInternalAction;
+
+ private final int mHashCode;
+
+ public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
+ mSubtype = params.mSubtype;
+ mWidth = params.mKeyboardWidth;
+ mHeight = params.mKeyboardHeight;
+ mMode = params.mMode;
+ mElementId = elementId;
+ mEditorInfo = params.mEditorInfo;
+ mDeviceLocked = params.mDeviceLocked;
+ mNumberRowEnabled = params.mNumberRowEnabled;
+ mNumberRowInSymbols = params.mNumberRowInSymbols;
+ mLanguageSwitchKeyEnabled = params.mLanguageSwitchKeyEnabled;
+ mEmojiKeyEnabled = params.mEmojiKeyEnabled;
+ mCustomActionLabel = (mEditorInfo.actionLabel != null)
+ ? mEditorInfo.actionLabel.toString() : null;
+ mHasShortcutKey = params.mVoiceInputKeyEnabled;
+ mIsSplitLayout = params.mIsSplitLayoutEnabled;
+ mOneHandedModeEnabled = params.mOneHandedModeEnabled;
+ mInternalAction = params.mInternalAction;
+
+ mHashCode = computeHashCode(this);
+ }
+
+ private static int computeHashCode(final KeyboardId id) {
+ return Arrays.hashCode(new Object[] {
+ id.mElementId,
+ id.mMode,
+ id.mWidth,
+ id.mHeight,
+ id.passwordInput(),
+ id.mDeviceLocked,
+ id.mHasShortcutKey,
+ id.mNumberRowEnabled,
+ id.mLanguageSwitchKeyEnabled,
+ id.mEmojiKeyEnabled,
+ id.isMultiLine(),
+ id.imeAction(),
+ id.mCustomActionLabel,
+ id.navigateNext(),
+ id.navigatePrevious(),
+ id.mSubtype,
+ id.mIsSplitLayout,
+ id.mInternalAction
+ });
+ }
+
+ private boolean equals(final KeyboardId other) {
+ if (other == this)
+ return true;
+ return other.mElementId == mElementId
+ && other.mMode == mMode
+ && other.mWidth == mWidth
+ && other.mHeight == mHeight
+ && other.passwordInput() == passwordInput()
+ && other.mDeviceLocked == mDeviceLocked
+ && other.mHasShortcutKey == mHasShortcutKey
+ && other.mNumberRowEnabled == mNumberRowEnabled
+ && other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled
+ && other.mEmojiKeyEnabled == mEmojiKeyEnabled
+ && other.isMultiLine() == isMultiLine()
+ && other.imeAction() == imeAction()
+ && TextUtils.equals(other.mCustomActionLabel, mCustomActionLabel)
+ && other.navigateNext() == navigateNext()
+ && other.navigatePrevious() == navigatePrevious()
+ && other.mSubtype.equals(mSubtype)
+ && other.mIsSplitLayout == mIsSplitLayout
+ && Objects.equals(other.mInternalAction, mInternalAction);
+ }
+
+ private static boolean isAlphabetKeyboard(final int elementId) {
+ return elementId < ELEMENT_SYMBOLS;
+ }
+
+ public boolean isAlphaOrSymbolKeyboard() {
+ return mElementId <= ELEMENT_SYMBOLS_SHIFTED;
+ }
+
+ public boolean isAlphabetKeyboard() {
+ return isAlphabetKeyboard(mElementId);
+ }
+
+ public boolean navigateNext() {
+ return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0
+ || imeAction() == EditorInfo.IME_ACTION_NEXT;
+ }
+
+ public boolean navigatePrevious() {
+ return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0
+ || imeAction() == EditorInfo.IME_ACTION_PREVIOUS;
+ }
+
+ public boolean passwordInput() {
+ final int inputType = mEditorInfo.inputType;
+ return InputTypeUtils.isAnyPasswordInputType(inputType);
+ }
+
+ public boolean isMultiLine() {
+ return (mEditorInfo.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
+ }
+
+ public boolean isAlphabetShifted() {
+ return mElementId == ELEMENT_ALPHABET_SHIFT_LOCKED || mElementId == ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
+ || 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;
+ }
+
+ public boolean isEmojiKeyboard() {
+ return mElementId >= ELEMENT_EMOJI_RECENTS && mElementId <= ELEMENT_EMOJI_CATEGORY16;
+ }
+
+ public boolean isEmojiClipBottomRow() {
+ return mElementId == ELEMENT_CLIPBOARD_BOTTOM_ROW || mElementId == ELEMENT_EMOJI_BOTTOM_ROW;
+ }
+
+ public int imeAction() {
+ return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo);
+ }
+
+ public Locale getLocale() {
+ return mSubtype.getLocale();
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ return other instanceof KeyboardId && equals((KeyboardId) other);
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s%s%s%s]",
+ elementIdToName(mElementId),
+ mSubtype.getLocale(),
+ mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
+ mWidth, mHeight,
+ modeName(mMode),
+ actionName(imeAction()),
+ (navigateNext() ? " navigateNext" : ""),
+ (navigatePrevious() ? " navigatePrevious" : ""),
+ (mDeviceLocked ? " deviceLocked" : ""),
+ (passwordInput() ? " passwordInput" : ""),
+ (mHasShortcutKey ? " hasShortcutKey" : ""),
+ (mNumberRowEnabled ? " numberRowEnabled" : ""),
+ (mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""),
+ (mEmojiKeyEnabled ? " emojiKeyEnabled" : ""),
+ (isMultiLine() ? " isMultiLine" : ""),
+ (mIsSplitLayout ? " isSplitLayout" : "")
+ );
+ }
+
+ public static boolean equivalentEditorInfoForKeyboard(final EditorInfo a, final EditorInfo b) {
+ if (a == null && b == null) return true;
+ if (a == null || b == null) return false;
+ return a.inputType == b.inputType
+ && a.imeOptions == b.imeOptions
+ && TextUtils.equals(a.privateImeOptions, b.privateImeOptions);
+ }
+
+ public static String elementIdToName(final int elementId) {
+ return switch (elementId) {
+ case ELEMENT_ALPHABET -> "alphabet";
+ case ELEMENT_ALPHABET_MANUAL_SHIFTED -> "alphabetManualShifted";
+ case ELEMENT_ALPHABET_AUTOMATIC_SHIFTED -> "alphabetAutomaticShifted";
+ case ELEMENT_ALPHABET_SHIFT_LOCKED -> "alphabetShiftLocked";
+ case ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> "alphabetShiftLockShifted";
+ case ELEMENT_SYMBOLS -> "symbols";
+ case ELEMENT_SYMBOLS_SHIFTED -> "symbolsShifted";
+ case ELEMENT_PHONE -> "phone";
+ case ELEMENT_PHONE_SYMBOLS -> "phoneSymbols";
+ case ELEMENT_NUMBER -> "number";
+ case ELEMENT_EMOJI_RECENTS -> "emojiRecents";
+ case ELEMENT_EMOJI_CATEGORY1 -> "emojiCategory1";
+ case ELEMENT_EMOJI_CATEGORY2 -> "emojiCategory2";
+ case ELEMENT_EMOJI_CATEGORY3 -> "emojiCategory3";
+ case ELEMENT_EMOJI_CATEGORY4 -> "emojiCategory4";
+ case ELEMENT_EMOJI_CATEGORY5 -> "emojiCategory5";
+ case ELEMENT_EMOJI_CATEGORY6 -> "emojiCategory6";
+ case ELEMENT_EMOJI_CATEGORY7 -> "emojiCategory7";
+ case ELEMENT_EMOJI_CATEGORY8 -> "emojiCategory8";
+ case ELEMENT_EMOJI_CATEGORY9 -> "emojiCategory9";
+ case ELEMENT_EMOJI_CATEGORY10 -> "emojiCategory10";
+ case ELEMENT_EMOJI_CATEGORY11 -> "emojiCategory11";
+ case ELEMENT_EMOJI_CATEGORY12 -> "emojiCategory12";
+ case ELEMENT_EMOJI_CATEGORY13 -> "emojiCategory13";
+ case ELEMENT_EMOJI_CATEGORY14 -> "emojiCategory14";
+ case ELEMENT_EMOJI_CATEGORY15 -> "emojiCategory15";
+ case ELEMENT_EMOJI_CATEGORY16 -> "emojiCategory16";
+ case ELEMENT_CLIPBOARD -> "clipboard";
+ case ELEMENT_NUMPAD -> "numpad";
+ default -> null;
+ };
+ }
+
+ public static String modeName(final int mode) {
+ return switch (mode) {
+ case MODE_TEXT -> "text";
+ case MODE_URL -> "url";
+ case MODE_EMAIL -> "email";
+ case MODE_IM -> "im";
+ case MODE_PHONE -> "phone";
+ case MODE_NUMBER -> "number";
+ case MODE_DATE -> "date";
+ case MODE_TIME -> "time";
+ case MODE_DATETIME -> "datetime";
+ case MODE_NUMPAD -> "numpad";
+ default -> null;
+ };
+ }
+
+ public static String actionName(final int actionId) {
+ return (actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL) ? "actionCustomLabel"
+ : EditorInfoCompatUtils.imeActionName(actionId);
+ }
+
+ public int getKeyboardCapsMode() {
+ return switch (mElementId) {
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED ->
+ WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED;
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> WordComposer.CAPS_MODE_MANUAL_SHIFTED;
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED -> WordComposer.CAPS_MODE_AUTO_SHIFTED;
+ default -> WordComposer.CAPS_MODE_OFF;
+ };
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java
new file mode 100644
index 0000000000..5c293b4e6e
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardLayoutSet.java
@@ -0,0 +1,346 @@
+/*
+ * 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.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;
+import helium314.keyboard.keyboard.internal.UniqueKeysCache;
+import helium314.keyboard.keyboard.internal.keyboard_parser.LayoutParser;
+import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfos;
+import helium314.keyboard.keyboard.internal.keyboard_parser.LocaleKeyboardInfosKt;
+import helium314.keyboard.latin.RichInputMethodManager;
+import helium314.keyboard.latin.RichInputMethodSubtype;
+import helium314.keyboard.latin.settings.Settings;
+import helium314.keyboard.latin.utils.InputTypeUtils;
+import helium314.keyboard.latin.utils.Log;
+import helium314.keyboard.latin.utils.ResourceUtils;
+import helium314.keyboard.latin.utils.ScriptUtils;
+import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
+
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * This class represents a set of keyboard layouts. Each of them represents a different keyboard
+ * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same
+ * {@link KeyboardLayoutSet} are related to each other.
+ * A {@link KeyboardLayoutSet} needs to be created for each
+ * {@link android.view.inputmethod.EditorInfo}.
+ */
+public final class KeyboardLayoutSet {
+ private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
+ private static final boolean DEBUG_CACHE = false;
+
+ private final Context mContext;
+ @NonNull
+ private final Params mParams;
+ public final LocaleKeyboardInfos mLocaleKeyboardInfos;
+
+ // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
+ // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
+ // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
+ private static final int FORCIBLE_CACHE_SIZE = 4;
+ // By construction of soft references, anything that is also referenced somewhere else
+ // will stay in the cache. So we forcibly keep some references in an array to prevent
+ // them from disappearing from sKeyboardCache.
+ private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
+ private static final HashMap> sKeyboardCache = new HashMap<>();
+ @NonNull
+ private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
+
+ public static final class KeyboardLayoutSetException extends RuntimeException {
+ public final KeyboardId mKeyboardId;
+
+ public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
+ super(cause);
+ mKeyboardId = keyboardId;
+ }
+ }
+
+
+ /**
+ * Represents an internal action that overrides the action provided by the input field.
+ * @param code to send on action key press
+ * @param label to display on action key
+ */
+ public record InternalAction(int code, String label) {}
+
+ public static final class Params {
+ int mMode;
+ boolean mDisableTouchPositionCorrectionDataForTest; // remove
+ // TODO: Use {@link InputAttributes} instead of these variables.
+ EditorInfo mEditorInfo;
+ boolean mVoiceInputKeyEnabled;
+ boolean mDeviceLocked;
+ boolean mNumberRowEnabled;
+ boolean mNumberRowInSymbols;
+ boolean mLanguageSwitchKeyEnabled;
+ boolean mEmojiKeyEnabled;
+ boolean mOneHandedModeEnabled;
+ RichInputMethodSubtype mSubtype;
+ boolean mIsSpellChecker;
+ int mKeyboardWidth;
+ int mKeyboardHeight;
+ String mScript = ScriptUtils.SCRIPT_LATIN;
+ // Indicates if the user has enabled the split-layout preference
+ // and the required ProductionFlags are enabled.
+ boolean mIsSplitLayoutEnabled;
+ InternalAction mInternalAction;
+ }
+
+ public static void onSystemLocaleChanged() {
+ clearKeyboardCache();
+ LocaleKeyboardInfosKt.clearCache();
+ SubtypeLocaleUtils.clearSubtypeDisplayNameCache();
+ }
+
+ public static void onKeyboardThemeChanged() {
+ clearKeyboardCache();
+ }
+
+ private static void clearKeyboardCache() {
+ sKeyboardCache.clear();
+ sUniqueKeysCache.clear();
+ LayoutParser.INSTANCE.clearCache();
+ KeyboardIconsSet.Companion.setNeedsReload(true);
+ }
+
+ KeyboardLayoutSet(final Context context, @NonNull final Params params) {
+ mContext = context;
+ mParams = params;
+ mLocaleKeyboardInfos = LocaleKeyboardInfosKt.getOrCreate(context, params.mSubtype.getLocale());
+ }
+
+ @NonNull
+ public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
+ final int keyboardLayoutSetElementId;
+ switch (mParams.mMode) {
+ case KeyboardId.MODE_PHONE -> {
+ if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
+ keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
+ } else {
+ keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
+ }
+ }
+ case KeyboardId.MODE_NUMPAD -> keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMPAD;
+ case KeyboardId.MODE_NUMBER, KeyboardId.MODE_DATE, KeyboardId.MODE_TIME, KeyboardId.MODE_DATETIME ->
+ keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
+ default -> keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
+ }
+
+ // Note: The keyboard for each shift state, and mode are represented as an elementName
+ // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is
+ // specified as an elementKeyboard attribute in the file.
+ // The KeyboardId is an internal key for a Keyboard object.
+
+ final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
+ try {
+ return getKeyboard(id);
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Can't create keyboard: " + id, e);
+ throw new KeyboardLayoutSetException(e, id);
+ }
+ }
+
+ @NonNull
+ private Keyboard getKeyboard(final KeyboardId id) {
+ final SoftReference ref = sKeyboardCache.get(id);
+ final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
+ if (cachedKeyboard != null) {
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
+ }
+ return cachedKeyboard;
+ }
+
+ final KeyboardBuilder builder =
+ new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
+ sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
+ builder.load(id);
+ if (mParams.mDisableTouchPositionCorrectionDataForTest) {
+ builder.disableTouchPositionCorrectionDataForTest();
+ }
+ final Keyboard keyboard = builder.build();
+ sKeyboardCache.put(id, new SoftReference<>(keyboard));
+ if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
+ || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
+ && !mParams.mIsSpellChecker) {
+ // We only forcibly cache the primary, "ALPHABET", layouts.
+ for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
+ sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
+ }
+ sForcibleKeyboardCache[0] = keyboard;
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "forcing caching of keyboard with id=" + id);
+ }
+ }
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
+ + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
+ }
+ return keyboard;
+ }
+
+ public String getScript() {
+ return mParams.mScript;
+ }
+
+ public static final class Builder {
+ private final Context mContext;
+
+ private final Params mParams = new Params();
+
+ private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
+
+ public Builder(final Context context, @Nullable final EditorInfo ei) {
+ mContext = context;
+ final Params params = mParams;
+
+ final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
+ params.mMode = getKeyboardMode(editorInfo);
+ // TODO: Consolidate those with {@link InputAttributes}.
+ params.mEditorInfo = editorInfo;
+
+ // 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) {
+ final Builder builder = new Builder(context, ei);
+ builder.mParams.mMode = KeyboardId.MODE_TEXT;
+ final int width = ResourceUtils.getKeyboardWidth(context, Settings.getValues());
+ // actually the keyboard does not have full height, but at this point we use it to get correct key heights
+ final int height = ResourceUtils.getKeyboardHeight(context.getResources(), Settings.getValues());
+ builder.setKeyboardGeometry(width, height);
+ builder.setSubtype(RichInputMethodManager.getInstance().getCurrentSubtype());
+ return builder.build();
+ }
+
+ public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
+ mParams.mKeyboardWidth = keyboardWidth;
+ mParams.mKeyboardHeight = keyboardHeight;
+ return this;
+ }
+
+ public Builder setSubtype(@NonNull final RichInputMethodSubtype subtype) {
+ final boolean asciiCapable = subtype.getRawSubtype().isAsciiCapable();
+ final boolean forceAscii = (mParams.mEditorInfo.imeOptions & EditorInfo.IME_FLAG_FORCE_ASCII) != 0;
+ mParams.mSubtype = (forceAscii && !asciiCapable)
+ ? RichInputMethodSubtype.Companion.getNoLanguageSubtype()
+ : subtype;
+ return this;
+ }
+
+ public Builder setIsSpellChecker(final boolean isSpellChecker) {
+ mParams.mIsSpellChecker = isSpellChecker;
+ return this;
+ }
+
+ public Builder setVoiceInputKeyEnabled(final boolean enabled) {
+ mParams.mVoiceInputKeyEnabled = enabled;
+ return this;
+ }
+
+ public Builder setNumberRowEnabled(final boolean enabled) {
+ mParams.mNumberRowEnabled = enabled;
+ return this;
+ }
+
+ public Builder setNumberRowInSymbolsEnabled(final boolean enabled) {
+ mParams.mNumberRowInSymbols = enabled;
+ return this;
+ }
+
+ public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
+ mParams.mLanguageSwitchKeyEnabled = enabled;
+ return this;
+ }
+
+ public Builder setEmojiKeyEnabled(final boolean enabled) {
+ mParams.mEmojiKeyEnabled = enabled;
+ return this;
+ }
+
+ public Builder disableTouchPositionCorrectionData() {
+ mParams.mDisableTouchPositionCorrectionDataForTest = true;
+ return this;
+ }
+
+ public Builder setSplitLayoutEnabled(final boolean enabled) {
+ mParams.mIsSplitLayoutEnabled = enabled;
+ return this;
+ }
+
+ public Builder setOneHandedModeEnabled(boolean enabled) {
+ mParams.mOneHandedModeEnabled = enabled;
+ return this;
+ }
+
+ public Builder setInternalAction(InternalAction internalAction) {
+ mParams.mInternalAction = internalAction;
+ return this;
+ }
+
+ public KeyboardLayoutSet build() {
+ if (mParams.mSubtype == null)
+ throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
+ mParams.mScript = ScriptUtils.script(mParams.mSubtype.getLocale());
+ return new KeyboardLayoutSet(mContext, mParams);
+ }
+
+ private static int getKeyboardMode(final EditorInfo editorInfo) {
+ final int inputType = editorInfo.inputType;
+ final int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+ switch (inputType & InputType.TYPE_MASK_CLASS) {
+ case InputType.TYPE_CLASS_NUMBER:
+ return KeyboardId.MODE_NUMBER;
+ case InputType.TYPE_CLASS_DATETIME:
+ return switch (variation) {
+ case InputType.TYPE_DATETIME_VARIATION_DATE -> KeyboardId.MODE_DATE;
+ case InputType.TYPE_DATETIME_VARIATION_TIME -> KeyboardId.MODE_TIME;
+ default -> KeyboardId.MODE_DATETIME; // must be InputType.TYPE_DATETIME_VARIATION_NORMAL
+ };
+ case InputType.TYPE_CLASS_PHONE:
+ return KeyboardId.MODE_PHONE;
+ case InputType.TYPE_CLASS_TEXT:
+ if (InputTypeUtils.isEmailVariation(variation)) {
+ return KeyboardId.MODE_EMAIL;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
+ return KeyboardId.MODE_URL;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
+ //return KeyboardId.MODE_IM;
+ return KeyboardId.MODE_TEXT;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ return KeyboardId.MODE_TEXT;
+ } else {
+ return KeyboardId.MODE_TEXT;
+ }
+ default:
+ return KeyboardId.MODE_TEXT;
+ }
+ }
+ }
+
+ // used for testing keyboard layout files without actually creating a keyboard
+ public static KeyboardId getFakeKeyboardId(final int elementId) {
+ final Params params = new Params();
+ params.mEditorInfo = new EditorInfo();
+ params.mSubtype = RichInputMethodSubtype.Companion.getEmojiSubtype();
+ params.mSubtype.getMainLayoutName();
+ return new KeyboardId(elementId, params);
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
new file mode 100644
index 0000000000..eb29a0c6c6
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.keyboard;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+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;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import helium314.keyboard.event.Event;
+import helium314.keyboard.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException;
+import helium314.keyboard.keyboard.clipboard.ClipboardHistoryView;
+import helium314.keyboard.keyboard.emoji.EmojiPalettesView;
+import helium314.keyboard.keyboard.internal.KeyboardState;
+import helium314.keyboard.latin.InputView;
+import helium314.keyboard.latin.KeyboardWrapperView;
+import helium314.keyboard.latin.LatinIME;
+import helium314.keyboard.latin.R;
+import helium314.keyboard.latin.RichInputMethodManager;
+import helium314.keyboard.latin.RichInputMethodSubtype;
+import helium314.keyboard.latin.WordComposer;
+import helium314.keyboard.latin.settings.Settings;
+import helium314.keyboard.latin.settings.SettingsValues;
+import helium314.keyboard.latin.suggestions.SuggestionStripView;
+import helium314.keyboard.latin.utils.CapsModeUtils;
+import helium314.keyboard.latin.utils.KtxKt;
+import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils;
+import helium314.keyboard.latin.utils.Log;
+import helium314.keyboard.latin.utils.RecapitalizeMode;
+import helium314.keyboard.latin.utils.ResourceUtils;
+import helium314.keyboard.latin.utils.ScriptUtils;
+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();
+
+ private InputView mCurrentInputView;
+ private KeyboardWrapperView mKeyboardViewWrapper;
+ private View mMainKeyboardFrame;
+ private MainKeyboardView mKeyboardView;
+ private EmojiPalettesView mEmojiPalettesView;
+ private View mEmojiTabStripView;
+ private LinearLayout mClipboardStripView;
+ private HorizontalScrollView mClipboardStripScrollView;
+ private SuggestionStripView mSuggestionStripView;
+ private FrameLayout mStripContainer;
+ private ClipboardHistoryView mClipboardHistoryView;
+ private TextView mFakeToastView;
+ private LatinIME mLatinIME;
+ private RichInputMethodManager mRichImm;
+ private boolean mIsHardwareAcceleratedDrawingEnabled;
+
+ private KeyboardState mState;
+
+ private KeyboardLayoutSet mKeyboardLayoutSet;
+
+ private KeyboardTheme mKeyboardTheme;
+ private Context mThemeContext;
+ private int mCurrentUiMode;
+ private int mCurrentOrientation;
+ private int mCurrentDpi;
+ private boolean mThemeNeedsReload;
+
+ @SuppressLint("StaticFieldLeak") // this is a keyboard, we want to keep it alive in background
+ private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
+
+ public static KeyboardSwitcher getInstance() {
+ return sInstance;
+ }
+
+ private KeyboardSwitcher() {
+ // Intentional empty constructor for singleton.
+ }
+
+ public static void init(final LatinIME latinIme) {
+ sInstance.initInternal(latinIme);
+ }
+
+ private void initInternal(final LatinIME latinIme) {
+ mLatinIME = latinIme;
+ mRichImm = RichInputMethodManager.getInstance();
+ mState = new KeyboardState(this);
+ mIsHardwareAcceleratedDrawingEnabled = mLatinIME.enableHardwareAcceleration();
+ }
+
+ public void updateKeyboardTheme(@NonNull Context displayContext) {
+ final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper(
+ displayContext, KeyboardTheme.getKeyboardTheme(displayContext));
+ if (themeUpdated) {
+ Settings settings = Settings.getInstance();
+ settings.loadSettings(displayContext, settings.getCurrent().mLocale, settings.getCurrent().mInputAttributes);
+ if (mKeyboardView != null)
+ mLatinIME.setInputView(onCreateInputView(displayContext, mIsHardwareAcceleratedDrawingEnabled));
+ }
+ }
+
+ private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context, final KeyboardTheme keyboardTheme) {
+ final Resources res = context.getResources();
+ if (mThemeNeedsReload
+ || mThemeContext == null
+ || !keyboardTheme.equals(mKeyboardTheme)
+ || mCurrentDpi != res.getDisplayMetrics().densityDpi
+ || mCurrentOrientation != res.getConfiguration().orientation
+ || (mCurrentUiMode & Configuration.UI_MODE_NIGHT_MASK) != (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ || !mThemeContext.getResources().equals(res)
+ || Settings.getValues().mColors.haveColorsChanged(context)) {
+ mThemeNeedsReload = false;
+ mKeyboardTheme = keyboardTheme;
+ mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId);
+ mCurrentUiMode = res.getConfiguration().uiMode;
+ mCurrentOrientation = res.getConfiguration().orientation;
+ mCurrentDpi = res.getDisplayMetrics().densityDpi;
+ KeyboardLayoutSet.onKeyboardThemeChanged();
+ return true;
+ }
+ return false;
+ }
+
+ public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
+ final int currentAutoCapsState, @Nullable final RecapitalizeMode currentRecapitalizeState,
+ KeyboardLayoutSet.InternalAction internalAction) {
+ final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+ mThemeContext, editorInfo);
+ final int keyboardWidth = ResourceUtils.getKeyboardWidth(mThemeContext, settingsValues);
+ final int keyboardHeight = ResourceUtils.getKeyboardHeight(mThemeContext.getResources(), settingsValues);
+ final boolean oneHandedModeEnabled = settingsValues.mOneHandedModeEnabled;
+ mKeyboardLayoutSet = builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
+ .setSubtype(mRichImm.getCurrentSubtype())
+ .setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey)
+ .setNumberRowEnabled(settingsValues.mShowsNumberRow)
+ .setNumberRowInSymbolsEnabled(settingsValues.mShowsNumberRowInSymbols)
+ .setLanguageSwitchKeyEnabled(settingsValues.isLanguageSwitchKeyEnabled())
+ .setEmojiKeyEnabled(settingsValues.mShowsEmojiKey)
+ .setSplitLayoutEnabled(settingsValues.mIsSplitKeyboardEnabled)
+ .setOneHandedModeEnabled(oneHandedModeEnabled)
+ .setInternalAction(internalAction)
+ .build();
+ try {
+ mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState, oneHandedModeEnabled);
+ } catch (KeyboardLayoutSetException e) {
+ Log.e(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
+ try {
+ final InputMethodSubtype defaults = SubtypeUtilsAdditional.INSTANCE.createDefaultSubtype(mRichImm.getCurrentSubtypeLocale());
+ mKeyboardLayoutSet = builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
+ .setSubtype(RichInputMethodSubtype.Companion.get(defaults))
+ .setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey)
+ .setNumberRowEnabled(settingsValues.mShowsNumberRow)
+ .setNumberRowInSymbolsEnabled(settingsValues.mShowsNumberRowInSymbols)
+ .setLanguageSwitchKeyEnabled(settingsValues.isLanguageSwitchKeyEnabled())
+ .setEmojiKeyEnabled(settingsValues.mShowsEmojiKey)
+ .setSplitLayoutEnabled(settingsValues.mIsSplitKeyboardEnabled)
+ .setOneHandedModeEnabled(oneHandedModeEnabled)
+ .build();
+ mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState, oneHandedModeEnabled);
+ showToast("error loading the keyboard, falling back to defaults", false);
+ } catch (KeyboardLayoutSetException e2) {
+ Log.e(TAG, "even fallback to defaults failed: " + e2.mKeyboardId, e2.getCause());
+ }
+ }
+ }
+
+ public void saveKeyboardState() {
+ if (getKeyboard() != null || isShowingEmojiPalettes() || isShowingClipboardHistory()) {
+ mState.onSaveKeyboardState();
+ }
+ }
+
+ public void onHideWindow() {
+ if (mKeyboardView != null) {
+ mKeyboardView.onHideWindow();
+ }
+ }
+
+ private void setKeyboard(final int keyboardId, @NonNull final KeyboardSwitchState toggleState) {
+ // with a hardware keyboard we might get here without ever calling onCreateInputView, so don't crash
+ if (mKeyboardView == null) return;
+
+ // Make {@link MainKeyboardView} visible and hide {@link EmojiPalettesView}.
+ final SettingsValues currentSettingsValues = Settings.getValues();
+ setMainKeyboardFrame(currentSettingsValues, toggleState);
+ // TODO: pass this object to setKeyboard instead of getting the current values.
+ final MainKeyboardView keyboardView = mKeyboardView;
+ final Keyboard oldKeyboard = keyboardView.getKeyboard();
+ final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(keyboardId);
+ keyboardView.setKeyboard(newKeyboard);
+ mCurrentInputView.setKeyboardTopPadding(newKeyboard.mTopPadding);
+ keyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn);
+ keyboardView.updateShortcutKey(mRichImm.isShortcutImeReady());
+ final boolean subtypeChanged = (oldKeyboard == null) || !newKeyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype);
+ final int languageOnSpacebarFormatType = LanguageOnSpacebarUtils.getLanguageOnSpacebarFormatType(newKeyboard.mId.mSubtype);
+ final boolean hasMultipleEnabledIMEsOrSubtypes = mRichImm.hasMultipleEnabledIMEsOrSubtypes(true);
+ keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType, hasMultipleEnabledIMEsOrSubtypes);
+ }
+
+ public Keyboard getKeyboard() {
+ if (mKeyboardView != null) {
+ return mKeyboardView.getKeyboard();
+ }
+ return null;
+ }
+
+ // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
+ // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
+ public void resetKeyboardStateToAlphabet(final int currentAutoCapsState,
+ @Nullable final RecapitalizeMode currentRecapitalizeState) {
+ mState.onResetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState);
+ }
+
+ public void onPressKey(final int code, final boolean isSinglePointer,
+ final int currentAutoCapsState, @Nullable final RecapitalizeMode currentRecapitalizeState) {
+ mState.onPressKey(code, isSinglePointer, currentAutoCapsState, currentRecapitalizeState);
+ }
+
+ public void onReleaseKey(final int code, final boolean withSliding,
+ final int currentAutoCapsState, @Nullable final RecapitalizeMode currentRecapitalizeState) {
+ mState.onReleaseKey(code, withSliding, currentAutoCapsState, currentRecapitalizeState);
+ }
+
+ public void onFinishSlidingInput(final int currentAutoCapsState,
+ @Nullable final RecapitalizeMode currentRecapitalizeState) {
+ mState.onFinishSlidingInput(currentAutoCapsState, currentRecapitalizeState);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setAlphabetKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_ALPHABET, KeyboardSwitchState.OTHER);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetManualShiftedKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setAlphabetManualShiftedKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardSwitchState.OTHER);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetAutomaticShiftedKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setAlphabetAutomaticShiftedKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardSwitchState.OTHER);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetShiftLockedKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setAlphabetShiftLockedKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardSwitchState.OTHER);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetShiftLockShiftedKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setAlphabetShiftLockShiftedKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardSwitchState.OTHER);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setSymbolsKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setSymbolsKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_SYMBOLS, KeyboardSwitchState.OTHER);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setSymbolsShiftedKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setSymbolsShiftedKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED, KeyboardSwitchState.SYMBOLS_SHIFTED);
+ }
+
+ public boolean isImeSuppressedByHardwareKeyboard(
+ @NonNull final SettingsValues settingsValues,
+ @NonNull final KeyboardSwitchState toggleState) {
+ return settingsValues.mHasHardwareKeyboard && toggleState == KeyboardSwitchState.HIDDEN;
+ }
+
+ private void setMainKeyboardFrame(
+ @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}.
+ // @see #getVisibleKeyboardView() and
+ // @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
+ mMainKeyboardFrame.setVisibility(visibility);
+ mEmojiPalettesView.setVisibility(View.GONE);
+ mEmojiPalettesView.stopEmojiPalettes();
+ mEmojiTabStripView.setVisibility(View.GONE);
+ mClipboardStripScrollView.setVisibility(View.GONE);
+ mSuggestionStripView.setVisibility(stripVisibility);
+ mClipboardHistoryView.setVisibility(View.GONE);
+ mClipboardHistoryView.stopClipboardHistory();
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setEmojiKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setEmojiKeyboard");
+ }
+ mMainKeyboardFrame.setVisibility(View.VISIBLE);
+ // The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
+ // @see #getVisibleKeyboardView() and
+ // @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);
+ mEmojiPalettesView.startEmojiPalettes(mKeyboardView.getKeyVisualAttribute(),
+ mLatinIME.getCurrentInputEditorInfo(), mLatinIME.mKeyboardActionListener);
+ mEmojiPalettesView.setVisibility(View.VISIBLE);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setClipboardKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setClipboardKeyboard");
+ }
+ mMainKeyboardFrame.setVisibility(View.VISIBLE);
+ // The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
+ // @see #getVisibleKeyboardView() and
+ // @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
+ 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);
+ mClipboardHistoryView.startClipboardHistory(mLatinIME.getClipboardHistoryManager(), mKeyboardView.getKeyVisualAttribute(),
+ mLatinIME.getCurrentInputEditorInfo(), mLatinIME.mKeyboardActionListener);
+ mClipboardHistoryView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void setNumpadKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setNumpadKeyboard");
+ }
+ setKeyboard(KeyboardId.ELEMENT_NUMPAD, KeyboardSwitchState.OTHER);
+ }
+
+ @Override
+ public void toggleNumpad(final boolean withSliding, final int autoCapsFlags,
+ @Nullable final RecapitalizeMode recapitalizeMode, final boolean forceReturnToAlpha) {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "toggleNumpad");
+ }
+ mState.toggleNumpad(withSliding, autoCapsFlags, recapitalizeMode, forceReturnToAlpha, true);
+ }
+
+ public enum KeyboardSwitchState {
+ HIDDEN(-1),
+ SYMBOLS_SHIFTED(KeyboardId.ELEMENT_SYMBOLS_SHIFTED),
+ EMOJI(KeyboardId.ELEMENT_EMOJI_RECENTS),
+ CLIPBOARD(KeyboardId.ELEMENT_CLIPBOARD),
+ OTHER(-1);
+
+ final int mKeyboardId;
+
+ KeyboardSwitchState(int keyboardId) {
+ mKeyboardId = keyboardId;
+ }
+ }
+
+ public KeyboardSwitchState getKeyboardSwitchState() {
+ boolean hidden = !isShowingEmojiPalettes() && !isShowingClipboardHistory()
+ && (mKeyboardLayoutSet == null
+ || mKeyboardView == null
+ || !mKeyboardView.isShown());
+ if (hidden) {
+ return KeyboardSwitchState.HIDDEN;
+ } else if (isShowingEmojiPalettes()) {
+ return KeyboardSwitchState.EMOJI;
+ } else if (isShowingClipboardHistory()) {
+ return KeyboardSwitchState.CLIPBOARD;
+ } else if (isShowingKeyboardId(KeyboardId.ELEMENT_SYMBOLS_SHIFTED)) {
+ return KeyboardSwitchState.SYMBOLS_SHIFTED;
+ }
+ return KeyboardSwitchState.OTHER;
+ }
+
+ public void onToggleKeyboard(@NonNull final KeyboardSwitchState toggleState) {
+ KeyboardSwitchState currentState = getKeyboardSwitchState();
+ Log.w(TAG, "onToggleKeyboard() : Current = " + currentState + " : Toggle = " + toggleState);
+ if (currentState == toggleState) {
+ mLatinIME.stopShowingInputView();
+ mLatinIME.hideWindow();
+ setAlphabetKeyboard();
+ } else {
+ mLatinIME.startShowingInputView(true);
+ if (toggleState == KeyboardSwitchState.EMOJI) {
+ setEmojiKeyboard();
+ } else if (toggleState == KeyboardSwitchState.CLIPBOARD) {
+ setClipboardKeyboard();
+ } else {
+ mEmojiPalettesView.stopEmojiPalettes();
+ mEmojiPalettesView.setVisibility(View.GONE);
+
+ mClipboardHistoryView.stopClipboardHistory();
+ mClipboardHistoryView.setVisibility(View.GONE);
+
+ mMainKeyboardFrame.setVisibility(View.VISIBLE);
+ mKeyboardView.setVisibility(View.VISIBLE);
+ setKeyboard(toggleState.mKeyboardId, toggleState);
+ }
+ }
+ }
+
+ // Future method for requesting an updating to the shift state.
+ @Override
+ public void requestUpdatingShiftState(final int autoCapsFlags, @Nullable final RecapitalizeMode recapitalizeMode) {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "requestUpdatingShiftState: "
+ + " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
+ + " recapitalizeMode=" + recapitalizeMode);
+ }
+ mState.onUpdateShiftState(autoCapsFlags, recapitalizeMode);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void startDoubleTapShiftKeyTimer() {
+ if (DEBUG_TIMER_ACTION) {
+ Log.d(TAG, "startDoubleTapShiftKeyTimer");
+ }
+ final MainKeyboardView keyboardView = getMainKeyboardView();
+ if (keyboardView != null) {
+ keyboardView.startDoubleTapShiftKeyTimer();
+ }
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void cancelDoubleTapShiftKeyTimer() {
+ if (DEBUG_TIMER_ACTION) {
+ Log.d(TAG, "setAlphabetKeyboard");
+ }
+ final MainKeyboardView keyboardView = getMainKeyboardView();
+ if (keyboardView != null) {
+ keyboardView.cancelDoubleTapShiftKeyTimer();
+ }
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setOneHandedModeEnabled(boolean enabled) {
+ setOneHandedModeEnabled(enabled, false);
+ }
+
+ public void setOneHandedModeEnabled(boolean enabled, boolean force) {
+ if (!force && mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
+ return;
+ }
+ final Settings settings = Settings.getInstance();
+ mKeyboardViewWrapper.setOneHandedModeEnabled(enabled);
+ mKeyboardViewWrapper.setOneHandedGravity(settings.getCurrent().mOneHandedModeGravity);
+
+ settings.writeOneHandedModeEnabled(enabled);
+ reloadKeyboard();
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void switchOneHandedMode() {
+ mKeyboardViewWrapper.switchOneHandedModeSide();
+ Settings.getInstance().writeOneHandedModeGravity(mKeyboardViewWrapper.getOneHandedGravity());
+ }
+
+ public void toggleSplitKeyboardMode() {
+ final Settings settings = Settings.getInstance();
+ settings.writeSplitKeyboardEnabled(
+ !settings.getCurrent().mIsSplitKeyboardEnabled,
+ mCurrentOrientation == Configuration.ORIENTATION_LANDSCAPE
+ );
+ setOneHandedModeEnabled(settings.getCurrent().mOneHandedModeEnabled, true);
+ reloadKeyboard();
+ }
+
+ public void reloadKeyboard() {
+ if (mCurrentInputView == null)
+ return;
+ mEmojiPalettesView.clearKeyboardCache();
+ reloadMainKeyboard();
+ }
+
+ public void reloadMainKeyboard() {
+ // Reload the entire keyboard, and switch to the previous layout
+ final boolean wasEmoji = isShowingEmojiPalettes();
+ final boolean wasClipboard = isShowingClipboardHistory();
+ loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
+ mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState(), null);
+ if (wasEmoji) {
+ setEmojiKeyboard();
+ } else if (wasClipboard) {
+ setClipboardKeyboard();
+ }
+ }
+
+ /**
+ * Displays a toast message.
+ *
+ * @param text The text to display in the toast message.
+ * @param briefToast If true, the toast duration will be short; otherwise, it will last longer.
+ */
+ public void showToast(final String text, final boolean briefToast){
+ // In API 32 and below, toasts can be shown without a notification permission.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+ final int toastLength = briefToast ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
+ final Toast toast = Toast.makeText(mLatinIME, text, toastLength);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ } else {
+ final int toastLength = briefToast ? 2000 : 3500;
+ showFakeToast(text, toastLength);
+ }
+ }
+
+ 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;
+
+ final Drawable appIcon = mFakeToastView.getCompoundDrawables()[0];
+ if (appIcon != null) {
+ final int bound = mFakeToastView.getLineHeight();
+ appIcon.setBounds(0, 0, bound, bound);
+ mFakeToastView.setCompoundDrawables(appIcon, null, null, null);
+ }
+ mFakeToastView.setText(text);
+ mFakeToastView.setVisibility(View.VISIBLE);
+ mFakeToastView.bringToFront();
+ mFakeToastView.startAnimation(AnimationUtils.loadAnimation(mLatinIME, R.anim.fade_in));
+
+ mFakeToastView.postDelayed(() -> {
+ mFakeToastView.startAnimation(AnimationUtils.loadAnimation(mLatinIME, R.anim.fade_out));
+ mFakeToastView.setVisibility(View.GONE);
+ }, timeMillis);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public boolean isInDoubleTapShiftKeyTimeout() {
+ if (DEBUG_TIMER_ACTION) {
+ Log.d(TAG, "isInDoubleTapShiftKeyTimeout");
+ }
+ final MainKeyboardView keyboardView = getMainKeyboardView();
+ return keyboardView != null && keyboardView.isInDoubleTapShiftKeyTimeout();
+ }
+
+ /**
+ * Updates state machine to figure out when to automatically switch back to the previous mode.
+ */
+ public void onEvent(final Event event, final int currentAutoCapsState,
+ @Nullable final RecapitalizeMode currentRecapitalizeState) {
+ mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState);
+ }
+
+ public boolean isShowingKeyboardId(@NonNull int... keyboardIds) {
+ if (mKeyboardView == null || !mKeyboardView.isShown()) {
+ return false;
+ }
+ 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;
+ }
+ }
+ return false;
+ }
+
+ public boolean isShowingEmojiPalettes() {
+ return mEmojiPalettesView != null && mEmojiPalettesView.isShown();
+ }
+
+ public boolean isShowingClipboardHistory() {
+ return mClipboardHistoryView != null && mClipboardHistoryView.isShown();
+ }
+
+ public boolean isShowingPopupKeysPanel() {
+ if (isShowingEmojiPalettes() || isShowingClipboardHistory()) {
+ return false;
+ }
+ return mKeyboardView.isShowingPopupKeysPanel();
+ }
+
+ public boolean isShowingStripContainer() {
+ return mStripContainer.isShown();
+ }
+
+ public View getVisibleKeyboardView() {
+ if (isShowingEmojiPalettes()) {
+ return mEmojiPalettesView;
+ } else if (isShowingClipboardHistory()) {
+ return mClipboardHistoryView;
+ }
+ return mKeyboardView;
+ }
+
+ public View getWrapperView() {
+ return mKeyboardViewWrapper;
+ }
+
+ public View getEmojiTabStrip() {
+ return mEmojiTabStripView;
+ }
+
+ public LinearLayout getClipboardStrip() {
+ return mClipboardStripView;
+ }
+
+ public MainKeyboardView getMainKeyboardView() {
+ return mKeyboardView;
+ }
+
+ public FrameLayout getStripContainer() { return mStripContainer; }
+
+ public void deallocateMemory() {
+ if (mKeyboardView != null) {
+ mKeyboardView.cancelAllOngoingEvents();
+ mKeyboardView.deallocateMemory();
+ }
+ if (mEmojiPalettesView != null) {
+ mEmojiPalettesView.stopEmojiPalettes();
+ }
+ if (mClipboardHistoryView != null) {
+ mClipboardHistoryView.stopClipboardHistory();
+ }
+ }
+
+ public void trimMemory() {
+ if (mEmojiPalettesView != null) {
+ mEmojiPalettesView.clearKeyboardCache();
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ public View onCreateInputView(@NonNull Context displayContext, final boolean isHardwareAcceleratedDrawingEnabled) {
+ if (mKeyboardView != null) {
+ mKeyboardView.closing();
+ }
+ PointerTracker.clearOldViewData();
+ final SharedPreferences prefs = KtxKt.prefs(displayContext);
+ if (mSuggestionStripView != null)
+ prefs.unregisterOnSharedPreferenceChangeListener(mSuggestionStripView);
+ if (mClipboardHistoryView != null)
+ prefs.unregisterOnSharedPreferenceChangeListener(mClipboardHistoryView);
+ if (mThemeNeedsReload) // necessary in some cases (e.g. theme switch) when mThemeNeedsReload is set before first keyboard load
+ Settings.getInstance().loadSettings(displayContext, Settings.getValues().mLocale, Settings.getValues().mInputAttributes);
+
+ updateKeyboardThemeAndContextThemeWrapper(displayContext, KeyboardTheme.getKeyboardTheme(displayContext));
+ mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(R.layout.input_view, null);
+ mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
+ mEmojiPalettesView = mCurrentInputView.findViewById(R.id.emoji_palettes_view);
+ mClipboardHistoryView = mCurrentInputView.findViewById(R.id.clipboard_history_view);
+ mFakeToastView = mCurrentInputView.findViewById(R.id.fakeToast);
+
+ mKeyboardViewWrapper = mCurrentInputView.findViewById(R.id.keyboard_view_wrapper);
+ mKeyboardViewWrapper.setKeyboardActionListener(mLatinIME.mKeyboardActionListener);
+ mKeyboardView = mCurrentInputView.findViewById(R.id.keyboard_view);
+ mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
+ mKeyboardView.setKeyboardActionListener(mLatinIME.mKeyboardActionListener);
+ mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
+ mEmojiPalettesView.setKeyboardActionListener(mLatinIME.mKeyboardActionListener);
+ mClipboardHistoryView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
+ mClipboardHistoryView.setKeyboardActionListener(mLatinIME.mKeyboardActionListener);
+ mEmojiTabStripView = mCurrentInputView.findViewById(R.id.emoji_tab_strip);
+ 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);
+ PointerTracker.switchTo(mKeyboardView);
+ return mCurrentInputView;
+ }
+
+ public int getKeyboardShiftMode() {
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard == null) {
+ return WordComposer.CAPS_MODE_OFF;
+ }
+ return keyboard.mId.getKeyboardCapsMode();
+ }
+
+ public String getCurrentKeyboardScript() {
+ if (null == mKeyboardLayoutSet) {
+ return ScriptUtils.SCRIPT_UNKNOWN;
+ }
+ return mKeyboardLayoutSet.getScript();
+ }
+
+ public void switchToSubtype(InputMethodSubtype subtype) {
+ mLatinIME.switchToSubtype(subtype);
+ }
+
+ // used for debug
+ public String getLocaleAndConfidenceInfo() {
+ return mLatinIME.getLocaleAndConfidenceInfo();
+ }
+
+ /** Marks the theme as outdated. The theme will be reloaded next time the keyboard is shown.
+ * If the keyboard is currently showing, theme will be reloaded immediately. */
+ public void setThemeNeedsReload() {
+ mThemeNeedsReload = true;
+ if (mLatinIME == null || !mLatinIME.isInputViewShown())
+ return; // will be reloaded right before showing IME
+
+ // Hide and show IME, showing will trigger the reload.
+ // Reloading while IME is shown is glitchy, and hiding / showing is so fast the user shouldn't notice.
+ mLatinIME.hideWindow();
+ try {
+ mLatinIME.showWindow(true);
+ } catch (IllegalStateException e) {
+ // in tests isInputViewShown returns true, but showWindow throws "IllegalStateException: Window token is not set yet."
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt
new file mode 100644
index 0000000000..7bb42bdc8c
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardTheme.kt
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2014 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.content.SharedPreferences
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.TypedValue
+import android.view.ContextThemeWrapper
+import androidx.core.content.ContextCompat
+import androidx.core.content.edit
+import helium314.keyboard.latin.R
+import helium314.keyboard.latin.common.AllColors
+import helium314.keyboard.latin.common.ColorType
+import helium314.keyboard.latin.common.Colors
+import helium314.keyboard.latin.common.DefaultColors
+import helium314.keyboard.latin.common.DynamicColors
+import helium314.keyboard.latin.settings.Defaults
+import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.utils.ResourceUtils
+import helium314.keyboard.latin.utils.brightenOrDarken
+import helium314.keyboard.latin.utils.isBrightColor
+import helium314.keyboard.latin.utils.isGoodContrast
+import helium314.keyboard.latin.utils.prefs
+import helium314.keyboard.settings.SettingsActivity
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.util.EnumMap
+import androidx.core.graphics.toColorInt
+
+class KeyboardTheme // Note: The themeId should be aligned with "themeId" attribute of Keyboard style in values/themes-