From 87aa966a51cb4d87cd0a4e96cfa95dd36a62f137 Mon Sep 17 00:00:00 2001 From: sydxky Date: Sat, 14 Nov 2026 13:55:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=834=C3=974=E5=80=99?= =?UTF-8?q?=E9=80=89=E4=B8=8EAndroid=E6=89=8B=E8=A1=A8=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=86=85=E9=94=AE=E7=9B=98=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 2 + .gitignore | 18 +- LICENSE | 2 +- NOTICE.md | 10 +- PUBLIC_SOURCE_MANIFEST.json | 309 ++- README.md | 141 +- android/.gitignore | 13 + android/LICENSE | 23 + android/NOTICE.md | 17 + android/README.md | 58 + android/VALIDATION_1.2.0.md | 28 + android/app/build.gradle | 60 + android/app/proguard-rules.pro | 2 + android/app/proguard-test.pro | 7 + .../com/sydxky/crabinput/ImeDeviceTest.java | 116 + android/app/src/main/AndroidManifest.xml | 10 + .../com/sydxky/crabinput/MainActivity.java | 55 + .../app/src/main/res/drawable/ic_launcher.xml | 4 + android/app/src/main/res/values/styles.xml | 8 + android/build.gradle | 4 + android/build.ps1 | 11 + android/gradle.properties | 5 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 8 + android/gradlew | 12 + android/gradlew.bat | 91 + android/host/build.gradle | 8 + android/host/src/main/AndroidManifest.xml | 5 + .../sydxky/crabinput/host/HostActivity.java | 20 + android/keyboard/build.gradle | 9 + android/keyboard/consumer-rules.pro | 3 + android/keyboard/src/main/AndroidManifest.xml | 1 + .../src/main/assets/dictionary-manifest.json | 52 + .../main/assets/licenses/CrabInput-LICENSE | 23 + .../main/assets/licenses/CrabInput-NOTICE.txt | 17 + .../main/assets/licenses/Gradle-LICENSE.txt | 422 ++++ .../assets/licenses/crabKeyboard-LICENSE.txt | 22 + .../main/assets/licenses/jieba-LICENSE.txt | 20 + .../java/com/sydxky/crabinput/Candidate.java | 7 + .../com/sydxky/crabinput/CandidatePager.java | 37 + .../com/sydxky/crabinput/CrabKeyboard.java | 61 + .../sydxky/crabinput/KeyboardController.java | 107 + .../com/sydxky/crabinput/KeyboardState.java | 12 + .../com/sydxky/crabinput/KeyboardSurface.java | 175 ++ .../java/com/sydxky/crabinput/Lexicon.java | 61 + .../sydxky/crabinput/CandidatePagerTest.java | 26 + android/scripts/build-dictionary.py | 51 + android/scripts/capture-device.py | 75 + android/scripts/validate-device.py | 39 + android/settings.gradle | 9 + build-rtos-game.ps1 | 162 +- com.sydxky.rtos.imefixture/build.ps1 | 4 +- com.sydxky.rtos.imefixture/glyphix.config.js | 2 +- com.sydxky.rtos.imefixture/src/app.js | 2 +- com.sydxky.rtos.imefixture/src/main/index.ux | 12 +- com.sydxky.rtos.imefixture/src/manifest.json | 38 +- com.sydxky.rtos.inputmethod/DESIGN.md | 4 + com.sydxky.rtos.inputmethod/LICENSE | 2 +- com.sydxky.rtos.inputmethod/NOTICE.md | 4 +- com.sydxky.rtos.inputmethod/README.md | 136 +- .../VALIDATION_1.1.0.md | 112 +- .../VALIDATION_1.2.0.md | 21 + .../src/common/ime-engine.js | 17 + .../src/keyboard/index.ux | 22 +- com.sydxky.rtos.inputmethod/src/manifest.json | 4 +- .../tests/client.test.mjs | 1 + .../tests/engine.test.mjs | 9 +- com.sydxky.rtos.inputmethod/tests/native.py | 152 +- com.sydxky.rtos.inputmethod/tests/visual.py | 2 + .../third_party/remix-icon/icons.json | 190 +- docs/PUBLIC_RELEASE_1.1.0.md | 11 - docs/SCREENSHOTS.md | 20 + docs/UI_PREVIEW.png | Bin 134926 -> 189471 bytes docs/public-validation.json | 2231 ----------------- docs/screenshots/android-candidates-16.png | Bin 0 -> 30477 bytes docs/screenshots/android-fourteen.png | Bin 0 -> 34002 bytes docs/screenshots/android-home.png | Bin 0 -> 31792 bytes docs/screenshots/android-host.png | Bin 0 -> 10575 bytes docs/screenshots/android-phrases.png | Bin 0 -> 29098 bytes docs/screenshots/android-qwerty.png | Bin 0 -> 33582 bytes docs/screenshots/android-result.png | Bin 0 -> 30608 bytes docs/screenshots/rtos-rect-candidates.png | Bin 0 -> 27772 bytes docs/screenshots/rtos-round-14-key.png | Bin 0 -> 155922 bytes docs/screenshots/rtos-round-candidates.png | Bin 0 -> 147768 bytes .../rtos-round-phrase-candidates.png | Bin 0 -> 150810 bytes tests/emulator-validate.py | 238 +- 86 files changed, 2697 insertions(+), 2975 deletions(-) create mode 100644 .gitattributes create mode 100644 android/.gitignore create mode 100644 android/LICENSE create mode 100644 android/NOTICE.md create mode 100644 android/README.md create mode 100644 android/VALIDATION_1.2.0.md create mode 100644 android/app/build.gradle create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/proguard-test.pro create mode 100644 android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/com/sydxky/crabinput/MainActivity.java create mode 100644 android/app/src/main/res/drawable/ic_launcher.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/build.gradle create mode 100644 android/build.ps1 create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/host/build.gradle create mode 100644 android/host/src/main/AndroidManifest.xml create mode 100644 android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java create mode 100644 android/keyboard/build.gradle create mode 100644 android/keyboard/consumer-rules.pro create mode 100644 android/keyboard/src/main/AndroidManifest.xml create mode 100644 android/keyboard/src/main/assets/dictionary-manifest.json create mode 100644 android/keyboard/src/main/assets/licenses/CrabInput-LICENSE create mode 100644 android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt create mode 100644 android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt create mode 100644 android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt create mode 100644 android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java create mode 100644 android/keyboard/src/main/java/com/sydxky/crabinput/Lexicon.java create mode 100644 android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java create mode 100644 android/scripts/build-dictionary.py create mode 100644 android/scripts/capture-device.py create mode 100644 android/scripts/validate-device.py create mode 100644 android/settings.gradle create mode 100644 com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md delete mode 100644 docs/PUBLIC_RELEASE_1.1.0.md create mode 100644 docs/SCREENSHOTS.md delete mode 100644 docs/public-validation.json create mode 100644 docs/screenshots/android-candidates-16.png create mode 100644 docs/screenshots/android-fourteen.png create mode 100644 docs/screenshots/android-home.png create mode 100644 docs/screenshots/android-host.png create mode 100644 docs/screenshots/android-phrases.png create mode 100644 docs/screenshots/android-qwerty.png create mode 100644 docs/screenshots/android-result.png create mode 100644 docs/screenshots/rtos-rect-candidates.png create mode 100644 docs/screenshots/rtos-round-14-key.png create mode 100644 docs/screenshots/rtos-round-candidates.png create mode 100644 docs/screenshots/rtos-round-phrase-candidates.png diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7a31a2f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve the exact bytes recorded by PUBLIC_SOURCE_MANIFEST.json. +* -text -eol diff --git a/.gitignore b/.gitignore index b744327..02ec9af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ -**/.glyphix-work/ -**/__pycache__/ -**/src/assets/ime-dict/ -temp/ -work/ +**/.glyphix-work/ +**/__pycache__/ +**/src/assets/ime-dict/ +temp/ +work/ +**/.gradle/ +**/build/ +**/local.properties +**/dictionary.db +**/release-signing.properties +**/*.jks +**/*.keystore +!android/gradle/wrapper/gradle-wrapper.jar diff --git a/LICENSE b/LICENSE index 5a0b817..1a442a8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 herry +Copyright (c) 2026 sydxky Copyright (c) 2026 kqakqakqa Copyright (c) 2026 crabKun diff --git a/NOTICE.md b/NOTICE.md index eac390a..2dccd98 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,6 +1,6 @@ # 蟹蟹输入 RTOS适配改版 -本项目为基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立RTOS适配改版,由herry进行Glyphix页面、离线词库索引、九宫/14键消歧、标准QWERTY、候选分页及生命周期适配。不是上游原版,也不表示上游作者、华为、OPPO或Glyphix对本改版作出背书。 +本项目为基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立RTOS适配改版,由sydxky进行Glyphix页面、离线词库索引、九宫/14键消歧、标准QWERTY、候选分页及生命周期适配。不是上游原版,也不表示上游作者、华为、OPPO或Glyphix对本改版作出背书。 上游固定提交:`93d36b5002dee80a3d253b1ec7ec9e29800a104c`。保留原作者kqakqakqa、crabKun的版权声明、MIT许可证及原始词库/读取器。RTOS自研适配部分以根目录[MIT许可证](./LICENSE)发布,**允许商用、修改、再分发和闭源集成**,须保留适用版权及许可全文。 @@ -8,10 +8,10 @@ | 内容 | 作者/项目 | 许可与位置 | | --- | --- | --- | -| CrabKeyboard V5词库和原始读取器 | kqakqakqa、crabKun | MIT,`com.sydxky.rtos.inputmethod/third_party/crabKeyboard/LICENSE` | -| 本地候选排序词频 | jieba 0.42.1 / Sun Junyi | MIT,`com.sydxky.rtos.inputmethod/third_party/jieba/LICENSE` | -| 操作图标 | Remix Icon v4.6.0,对应iconfont公开矢量 | Apache-2.0,`com.sydxky.rtos.inputmethod/third_party/remix-icon/LICENSE`;矢量改色并转换PNG,来源/哈希见同目录`icons.json` | +| CrabKeyboard V5词库和原始读取器 | kqakqakqa、crabKun | MIT,`third_party/crabKeyboard/LICENSE` | +| 本地候选排序词频 | jieba 0.42.1 / Sun Junyi | MIT,`third_party/jieba/LICENSE` | +| 操作图标 | Remix Icon v4.6.0,对应iconfont公开矢量 | Apache-2.0,`third_party/remix-icon/LICENSE`;矢量改色并转换PNG,来源/哈希见同目录`icons.json` | 14键参考公开演示中的相邻字母分组,自行实现离线消歧;不包含华为内部模型或固件代码。Glyphix/厂商SDK与运行时不包含在本项目MIT授权中,也不随源码分发,使用者需自行取得适用的厂商授权。 -公开仓库:。匿名问题与建议:。 +公开仓库:。匿名问题与建议:。 diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 62be6df..0b86e9e 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,126 +1,183 @@ -{ - "project": "蟹蟹输入 RTOS适配改版", - "version": "1.1.0", - "license": "MIT", - "independent_history": true, - "files": { - ".gitignore": "5fc1922c8c233ebab1a7a6d3f15f83a727039ad0b5be612bcb274706080c37fc", - "LICENSE": "fbe0034e81020ac52847583ed5a8dab0e212266623b475381c0a468a800f67df", - "NOTICE.md": "c1259ed34498181695d1f04ba60301124525a7e84d383c698fd0202169b652b7", - "README.md": "42d8bfa45434241de50ca6811af21cdeff01ca183885dcceb284db474e582383", - "build-rtos-game.ps1": "207ff9875af96221559f314364c932fe37bfc22c698ad1afabb9aeec40a8679e", - "com.sydxky.rtos.imefixture/build.ps1": "36b41ff37b153c1030756e641d2bb8d8a5404425905897e1f99e728c96493dad", - "com.sydxky.rtos.imefixture/glyphix.config.js": "c402178dc7457c56d9762b464a868b0c9ade304597d81ed46f0af04971fcddcf", - "com.sydxky.rtos.imefixture/src/app.js": "fc00d19bc94e10c24f07bd9be1c2bb24d4978856963c5a68745338ecbf131dfc", - "com.sydxky.rtos.imefixture/src/assets/icon.png": "f712ac5f8c500a33dd72cc1f15f9ad467e6043c284fb6ece1f1683e54d40e683", - "com.sydxky.rtos.imefixture/src/main/index.ux": "d71f1cb5e0342cbb9e9559760631046887c942ecb75436a450acef7c493ec9de", - "com.sydxky.rtos.imefixture/src/manifest.json": "40d7d1464409121b5c06ee77e3ff78d38c3f5271701cd754028cae5b5ef488c0", - "com.sydxky.rtos.inputmethod/.gitignore": "0f139de1ca71ff6aa1e6f02974b5e38d219d7781d4184d524845dce12d947454", - "com.sydxky.rtos.inputmethod/DESIGN.md": "5ba2d52755e9852be46f883507376fade0fbb006b44afc0cccf12447b8ae5a79", - "com.sydxky.rtos.inputmethod/KEYBOARD_14.md": "27ae1fa73dd8fa8fce70743fa1e418b4a4ff71ba610e735f009344896adfa754", - "com.sydxky.rtos.inputmethod/LICENSE": "fbe0034e81020ac52847583ed5a8dab0e212266623b475381c0a468a800f67df", - "com.sydxky.rtos.inputmethod/NOTICE.md": "a16fb81a4ac41e6ebd8730c76194def41ba1abf61221d5c8f658907287c6ac03", - "com.sydxky.rtos.inputmethod/PRIVACY.md": "c3062d8434db2fad3b6824cf2c41561a0bf6ae05e62f05d7c6316efd64d74af9", - "com.sydxky.rtos.inputmethod/README.md": "5b652f6892960eb6d9fe00d1c64207fc9872c7d6f5e5ddaef8ec3590d4a390b6", - "com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md": "891ea8683d0d1cc56d548ba9a7dc66fe18db5d68a94b5f6a7f11f1ad48c0c849", - "com.sydxky.rtos.inputmethod/build.ps1": "32168e753699f48274fe2492e53b7285e4b62c15dc928f3aeb366a11150ebf5a", - "com.sydxky.rtos.inputmethod/glyphix.config.js": "43beae1f2a61eed010149ae6ad77223ba084117ea5aee3cb3fcd48bfc0a4a32b", - "com.sydxky.rtos.inputmethod/scripts/build-icons.py": "51f859a9517e7dd2e5e4bf9f688ce41dc79d4bc491fc66066dfe2db4aae4d704", - "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "83eea8c7adae0254eb69170dec905c285c09c43ffcf10e8e553beb108acf43b3", - "com.sydxky.rtos.inputmethod/scripts/install.mjs": "46c7d95b25c312734c0ca8387f4a782becd4723f21d01492d074da22ca05432d", - "com.sydxky.rtos.inputmethod/src/app.js": "e51c7e0a67563bd64b6bc04aedde22842295824432942bc8f1ab0d4731cc3ed3", - "com.sydxky.rtos.inputmethod/src/assets/crab-keyboard-license.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", - "com.sydxky.rtos.inputmethod/src/assets/icon.png": "f712ac5f8c500a33dd72cc1f15f9ad467e6043c284fb6ece1f1683e54d40e683", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/LICENSE.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/arrow-down-s-line.png": "69bb846769721ed4ad056ed0136da59d4aa6873edccd1e38a7ebd783cb94ed1e", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/arrow-left-s-line.png": "68d8920081fb883f4c59af47549d6b41a9b0a3748383b814e4480bfab0e711ed", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/arrow-right-s-line.png": "c6bffa0a51254c60c92e276e4d21ae8a1595d82c77d9d6a1064afbf534ea3562", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/check-line.png": "dc3c240ba0db808212dae8a29dadbda9b0428d699a42f9e52e789e74ed56967c", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/close-line.png": "d3cfb57fa03b03c512286187681caaf4026a4dc60c18b91cbdc0bcc9e2586621", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/delete-back-2-line.png": "a83002ddc7ba1f1a86b361c0c672ec63205393cae2df275a9ff2d49501fd6b9d", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/edit-2-line.png": "6161624226e2b62b24da7244d86564d6d0856533c2149d3b347e17013c562497", - "com.sydxky.rtos.inputmethod/src/assets/ime-icons/keyboard-box-line.png": "bf4b45e2e54b4fd7e901068df1de865334f7e4616583dc5358d35d14890948af", - "com.sydxky.rtos.inputmethod/src/assets/jieba-license.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", - "com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "fbfc3fa9a09914ce69da1ec0d03bed92505389c9b8bdf254d236f379bb0ecd16", - "com.sydxky.rtos.inputmethod/src/common/ime-lifecycle.js": "93525dffabcca0620acc36defd077ab6677b1032ef67faf695f3fbf3116b8a64", - "com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "8022e5cff0075975377e0c3390ee9d9a541379768dc4732a77ff0ef865306dd3", - "com.sydxky.rtos.inputmethod/src/main/index.ux": "f763f0dc80b9fd46e00830d6ec0de8683063f98c1edee94ee522c2605b695fca", - "com.sydxky.rtos.inputmethod/src/manifest.json": "1adcd2d7610ce92c1c01f9c06f597e64569ae31de2422f1022e49bc04bb804ff", - "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "8633a789b0c5e19ea63881ccf084e4442b0ab0fbd97a9fba4317e34e5570ff81", - "com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "4967791409f7aa418b5d2b60a61aca48bbedfe5f8fad4a51b556a4f8e7c3e5a4", - "com.sydxky.rtos.inputmethod/tests/layout.py": "b632411cd593d369180abaeedbf036002e986e3e5e2c2f15b4e5ff157588aa78", - "com.sydxky.rtos.inputmethod/tests/native.py": "4b92ee16edb92ca919f6bce52974fa114dca7b6b23c83b3300f6e370a3247379", - "com.sydxky.rtos.inputmethod/tests/visual.py": "2874b3446028f7b7e54c4b1ecf594f8801c7249d67a0d17b8e7b0fdf97c8ff0f", - "com.sydxky.rtos.inputmethod/third_party/UPSTREAM.md": "e8311679981a318fef7142f7a2ecb2b1a7860f5fee32282729cab6af2a41d640", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/LICENSE": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/README.md": "88c2f96be16dc5d80677745072940c8a1cbc7aab40e554de50ec11bc22339b75", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/lookupDictV5.original.js": "915605439ebc404d808ccce6cb18abfff3981f1cdf53a78e9a743c78c4a6ddc9", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key0.dat": "795a5316089b67a7bffabf87080bd61f3720f9e0fd2f2866a092901f99b794e8", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key1.dat": "a4bcae2b6eb30084d3cf2ac64e5d9ca71eb94e8ac1f7c8fc8b38a5d478982be2", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key10.dat": "5731d9f3a3cad0304ecc650d74c723185c9d43f74d66d7b5622454d1fc31f99c", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key11.dat": "fa2a4bfa64cb236a051ceaf6b9b227159dde7aaba21ae997baafac6df4332345", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key12.dat": "7685ad03e3b5eeac404e8d6f0fc9e8524262b2c4a42ef0301d9e85c64d3db63b", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key13.dat": "b06f6fc0e2769a8f6e9491a0b97148f8c8d211ba8fe8ed122e1dbffca0eb7399", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key14.dat": "5d21973e2c1d833e93bfe51c42064b26743f6012e35c73ffcd2b5046cca3eeff", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key15.dat": "62c3324ccc143cd4e9ee533b0117ee06fc893d58c5d2ed0353da4dde33f5eab1", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key16.dat": "8c2c0fc7568cf6b674e8630449eb989c7ebdf34f34d0b2041a59748bd20c9638", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key17.dat": "f62c9c886ca4ee387a780a8c1e24116d73dedfda228de3614978c22f994ef7ec", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key18.dat": "fa7fda7b3b452908623383a6285aa15b265a41d48de58524fe0f0879b8aad259", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key19.dat": "00674fb37b56b5973cf0a7ee38b8258652949e4c02782ff6651d2aa63b9c62fc", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key2.dat": "45e860c89d1362de6007e46a6aa84a0d2fcd9682924de6e5f111cd536e18bfd1", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key20.dat": "1715a74e0bd00bf89406806682d865a53782d4cd9c4aee5050e547e8a84ffad6", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key3.dat": "00b464b6ee7ecd7e4406150eecf6c512e22611c6505a5045834cd5c3b7d9068d", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key4.dat": "a5c5719d09f931efc44b629fe92fd3faea0cc8c5a6ec28d0a9c9b9cdb0eb5ad9", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key5.dat": "3d5e43374441150af704b9dae9a6d62ac66406306b37709f12119fe44153e353", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key6.dat": "586c47ee8542f8bf59d7b4bf71fb1715a1bf3e7452d91068edcaf8b566e81e52", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key7.dat": "4525656f4d9fcd85bff07b876af75eb84f4f711a55a63c258c491d22dfeedff5", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key8.dat": "8e88d27fbc09fc3b133cbafd3b8e2f1b2269950cfcae12c23c1e152013f63f47", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key9.dat": "c741aefb551b686a9ade67d3a16a5230d74134af6c3d0faf08557e19698d5825", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value1.dat": "81c1798a0dda463cacae6ac5285ed9ad0cd0730dde167c18ee3500f1e1d8d8f2", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value10.dat": "9c4fded85bb7467304df917d510d987a824785058a9f8bb9c9095571b49fbf0d", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value11.dat": "3be277f87112c7bbc7242882b4ca92ff6e9d38d7be1b51b7899545a5c13d1a19", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value12.dat": "789c47185b5aaa93293239b356a6a4928d3d29daaf9a4d18f3c94f392f861613", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value13.dat": "b7366771afebe2ddc466eb9dc8dd840b1e7a8a8214354e2930d4fa4346f27dc6", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value14.dat": "9c638ef5d554738087505b158482e5b0caba59a36df65b9d0f3e5eee78f91588", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value15.dat": "846ea5776c9e11f44762cc032d690435464a312e82813530b01cb6001bd0a2f6", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value16.dat": "b86c42aaf4a818b4eaccd2663b6fb01f3ef3b1dc8482f20636cbde1a95ca52da", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value17.dat": "2f809c59a318d7d8d8605985092517902b6c627a40936af856785ed596c1133e", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value18.dat": "d64613d9bc90f503b964984939de0dc881c3a0feda2684bade0c572555c2099d", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value19.dat": "ecfb7ba56450c671fc941d56556a42d508569e341c5dd2581b18a3d7d27ab55c", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value2.dat": "d66703e7ac7ab6c2cd3b3c8109e656e1f78ace3e2bb49123ce3a2b3da4ca04d0", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value20.dat": "896757081c224c72586dfa5babb4300c93639a27ba9a70eb34b9dfd5c23735f2", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value3.dat": "19c47c2f8b8483946db81572368b3f3b2aa61c5a03953351e327f3f83cb31275", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value4.dat": "1be64f23183fca36920ae3f002d541febaa1b26b5ca3018ee53ffdd05546d56d", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value5.dat": "7b8a109e705537784a0a8075cc4fe3d1de8288a6b6b0240627685c81ed9695a9", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value6.dat": "5b4028a017b228357446f799cd29b28dd5a24d63340f8b48ce858c75c64029f1", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value7.dat": "b9f6e109ba5f1b5bca758613cb5cccfecf358915ce6415525c333b19515d6428", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value8.dat": "3d1622d4717680b31a6e037c88e7f046fecfe2339e521272e1d1eae0a7094606", - "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value9.dat": "1c0caab370f1f14048ab6dd2da0011bedcddfed0ed0378b5c98171b6b22caf35", - "com.sydxky.rtos.inputmethod/third_party/jieba/LICENSE": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", - "com.sydxky.rtos.inputmethod/third_party/jieba/dict.txt": "7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/README.md": "6a7bee86ad1942d01be10b556dd3036e43a59d708fc94fd2f5f3e3fcfcf84b0b", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-down-s-line.iconfont.svg": "1efa1c713b710c0e9ef5eb47bec762a4b5ce21ffb454d44a3bbb118a8bec3e83", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-down-s-line.upstream.svg": "4b0bf8a45d554ba97d8115ef2713010f196ad49d57eeecb8a42d4839a5ba8f9e", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-left-s-line.iconfont.svg": "ccf02486d37383bbdc3319d9b348c5daef43693526d41aff6ad409d04e0664ef", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-left-s-line.upstream.svg": "7d0407d47b59e7d69446ef81b79e95c5d5a4085e7fa7ba30770556b67197407e", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-right-s-line.iconfont.svg": "b6678427ec45e7b00257b6c2d8eeb533e6b92c6d09864ce7844178e38fade8af", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-right-s-line.upstream.svg": "2c579a4dbaf25e19a128d71e04f4cf43b19ad2487a0076ff7e7e5a94c9b3e013", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/check-line.iconfont.svg": "1b194f3b6ac39e6199fb003ac3140cc2ed2d1213201c0bdbf69abffaba67f6ce", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/check-line.upstream.svg": "b83a64bf881169d5edeaa6134a7ba37192f0ddc80c2e2a890578ae0f2f8f9169", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/close-line.iconfont.svg": "748ea3e8a4fe22671ff3347e998d51679eaf3ff3950f6348f286f6dd36f19a92", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/close-line.upstream.svg": "db3b4dcebb2d962a0231a191538bf6939620798b18f0ba52d4b91c5a4649ce54", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/delete-back-2-line.iconfont.svg": "a6279d35f9fd1578f813bcfa2077fac98ec8e3a4d7bc8f90a5b3f3f2744c3a59", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/delete-back-2-line.upstream.svg": "d6fc7f41c0b84fe48a06685dc081780b6cd103ac9b3db96dc44e624aa0a113e8", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/edit-2-line.iconfont.svg": "74d6590a22053c253c0d00d9ac5e35fb1f8c02e2243d0e3643a959eef233476f", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/edit-2-line.upstream.svg": "d03a3a7b874377a18488b7b24b596b8078afbd9bced93ad791b6fb2f4866613d", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/icons.json": "e80556881b6db1a87e5308679d55e3d2b6bed130a4421407451eae9b0e41ff5a", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/keyboard-box-line.iconfont.svg": "813d79347c357a919317edf84252a505dbdc441c1144076b4e8389a8ce217de0", - "com.sydxky.rtos.inputmethod/third_party/remix-icon/keyboard-box-line.upstream.svg": "e39f28cbf418269405f517126a64cbf4b3332b3295cc8a3af670f77344a669c4", - "docs/PUBLIC_RELEASE_1.1.0.md": "546906aa92b5aa7d3edf1dcf91b264fd80c6b837908ca441b0309397d12644e4", - "docs/UI_PREVIEW.png": "7853297cccbc52122e17605db677a1be723cacdd82dae36efb8fc7ec300ad1ab", - "docs/public-validation.json": "ade9c563773b71fe32a6293eb25a4a7b96051bb91422f9fd157c7d0c45c185f0", - "tests/emulator-validate.py": "dfc550b5873f1d03254e8bc6f819139992a1648b9a10b168575cda8e35224f50" - }, - "sensitive_findings": [] -} +{ + "project": "蟹蟹输入 RTOS / Android手表适配改版", + "version": "1.2.0", + "license": "MIT", + "android": true, + "independent_history": true, + "sensitive_findings": [], + "files": { + ".gitignore": "cf35fe4d5c1c2ca6ae6bd343642fee3d04cd8d3799510a02ca94df73a2d4daad", + "LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", + "NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", + "README.md": "e7d8def19bd47065776b4f9fe772585aa4a98212a1884d4f6c0efb849cfabd5c", + "android/.gitignore": "9907f78ab6ad68ac162f15ed5554c0ea9bafecf6eb25f2388ba0c11d40fab35d", + "android/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", + "android/NOTICE.md": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab", + "android/README.md": "6457b0ef705b4dcaa7fc7d50e98462d48986aaef373f00ef4178abde74f12661", + "android/VALIDATION_1.2.0.md": "7ce04837f631f9e491ecfb96e8e056c4b01c4aa2f24a061d540c5280201848e6", + "android/app/build.gradle": "c6515cb16458dafba126be6c67c65ee2814da542359edf2a5fe69baf8e6fe919", + "android/app/proguard-rules.pro": "41e72ccc70b31f2da13fc597d5196590ec25e0422c2766e1555f3348d64ac6ed", + "android/app/proguard-test.pro": "dc5f87f1ec90eb24823174f80d19e93d3e69919af70891529e4c54a744988aa4", + "android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java": "1d7e8eff090e77212aa465bcc97df8698444ace92d85094cb878a3a2d286c8e6", + "android/app/src/main/AndroidManifest.xml": "cd096a1f1bd560114c865260bbdc50f43971089598968b065581cf07f1dbee69", + "android/app/src/main/java/com/sydxky/crabinput/MainActivity.java": "ee17c3d9f20509f382a9d2d9475d4dbddd7b57cea690bd6577a396b174400b7f", + "android/app/src/main/res/drawable/ic_launcher.xml": "9f22878890b584bb6151223cc280e542f82f354cc69e1d47f2dc6093a606c071", + "android/app/src/main/res/values/styles.xml": "37c2bd1bbf1017e84448c0177f8999fd44f5cbca8c0afde5478594dc7be55279", + "android/build.gradle": "79f56a485c696a3b800776255c37729515464340f568bf5163e5f81b60086198", + "android/build.ps1": "3dc3c31675d3cf946f1cfce6d6e2a17493416be9fce4565cdcc78515345006b5", + "android/gradle.properties": "1a62f4c0a2a08044c706c378451342a11b307842b6b2ad434a86d6f970acc201", + "android/gradle/wrapper/gradle-wrapper.jar": "e996d452d2645e70c01c11143ca2d3742734a28da2bf61f25c82bdc288c9e637", + "android/gradle/wrapper/gradle-wrapper.properties": "f22c1dc156f94b33fa7706633e18a7ea6e87038bfc8ac0f6f4785cb1511700f0", + "android/gradlew": "80a4c62723bb67cbabe92c37606d380089c647184afa1bf2b8c136f0e15d2cd6", + "android/gradlew.bat": "0e82fa239f8f4dca4efc83863c16c3b143cb6faab6f50cbefab4ee0bd03e5b5f", + "android/host/build.gradle": "6a47b08a172355e6e3a162b1b49617363bbc85db1c8b259d4f5a2d9279fcf484", + "android/host/src/main/AndroidManifest.xml": "2013ca2761bc091216dca86d6b14987147e169902c4e784a2a047d473a7c6c95", + "android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java": "0b73b7b5b6286e0a00636a13ec14b81efe37898d68d8f89cd74db266aa34da83", + "android/keyboard/build.gradle": "e0c80feef0b7434ed4fa9889c5d2fbd373aa1634f9ef63229e98e7aed4df8d36", + "android/keyboard/consumer-rules.pro": "74ec127457bb863b30cc41be93b5f6b1eb003783addb2cda36b3fa1cf7e80432", + "android/keyboard/src/main/AndroidManifest.xml": "343d5be1ac17cdd45d1a2f2ddc616c509290e655869125a9422a96d88aa66066", + "android/keyboard/src/main/assets/dictionary-manifest.json": "2b3d4d5564164700b46729394bfbd94349ee4a2cc949f608ec87446a7c613da3", + "android/keyboard/src/main/assets/licenses/CrabInput-LICENSE": "027e84ad11fd2992040da0aad2ece7a74fdf61540dc2d74cc8a281fd76eb942c", + "android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt": "bba74020843ad841f979d346d13625e26a2cb1c0e9a2031f1a45e62957f132ab", + "android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt": "cf234c4188d773406f640bf89be680d9288f40ff96901c70d643fcea96ad46df", + "android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", + "android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", + "android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java": "9b14e96b13150b5ced15188d0eb1b6e09939f8c2c4affc8aa52147aebacf0415", + "android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java": "a5071ddec3cee9899b80181ef1f35c6896cdd1455df37839b600a33c29ea640e", + "android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java": "1f03cda3833ba282d12eaf66ddcbb5f4354bb8b99a1d2c9de1b4f430c9effcac", + "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java": "014dcf83e2d1982299558204893cbd444db424ada4c2b2216585a46e7eaab47d", + "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java": "d11bc8a3ce09980eee77a4258a858ff0c38292ce55900f00d29afb20f2fd7715", + "android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java": "30820264a67e5294afe188349ac78a8e61ead4c1d0e68d353585c61cb359967e", + "android/keyboard/src/main/java/com/sydxky/crabinput/Lexicon.java": "9c21767b579f6f0436175d29bb2c2244958e214d76a1b6e43ccf986dee4c7f02", + "android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java": "7c0447f5eebea944df38179aafd789c4f2eae25ba53aff5ba9e92a2d46044c64", + "android/scripts/build-dictionary.py": "a8c29b57e42e53c5cc1e5ca6471db2ee19892db424701b1ae1cd523d2dfd0e62", + "android/scripts/capture-device.py": "34023ff4fb0ff80a8811b1222aad3b58c70b628333edb56efffa3e6adaf05ace", + "android/scripts/validate-device.py": "cd3fd2d6455eba4c560612a157678d374d43005bc8e8329fd3a206e21d087cd0", + "android/settings.gradle": "73a5f1e8c25c44b0c5f983ed822c4256dea33b14b8d53115ed3be118aeaaee1d", + "build-rtos-game.ps1": "069ebe54e3f0563ef26eca1cfa9e3552c61f50f02704b57bc513c5829ced7d8b", + "com.sydxky.rtos.imefixture/build.ps1": "37cd2f3af83d9c1cb2f1d855d0d95cf2e822ac9e743c8370237c48c4a4d8c0a0", + "com.sydxky.rtos.imefixture/glyphix.config.js": "d53b4b0e8597d9f7fb5fd3d53063d285b245669021527a1bfc3bc054c5122514", + "com.sydxky.rtos.imefixture/src/app.js": "abde5172a3f0254b611346598b6f9ce9e15a01ffcf6d770ce9e5fe095a781334", + "com.sydxky.rtos.imefixture/src/assets/icon.png": "f712ac5f8c500a33dd72cc1f15f9ad467e6043c284fb6ece1f1683e54d40e683", + "com.sydxky.rtos.imefixture/src/main/index.ux": "7a38495cbc36f6ce23dcf45ac7ddbce70c92246749e4963efddbfb2aa9ebe269", + "com.sydxky.rtos.imefixture/src/manifest.json": "3d6ba4288a0653bc8db541ca03999240bc99ece9ea1d056b7ad8cf46a6039bbf", + "com.sydxky.rtos.inputmethod/.gitignore": "0f139de1ca71ff6aa1e6f02974b5e38d219d7781d4184d524845dce12d947454", + "com.sydxky.rtos.inputmethod/DESIGN.md": "ea248cc86069dceaf51c834935a8233501f6855aeaff6e89f432357c17ec5848", + "com.sydxky.rtos.inputmethod/KEYBOARD_14.md": "27ae1fa73dd8fa8fce70743fa1e418b4a4ff71ba610e735f009344896adfa754", + "com.sydxky.rtos.inputmethod/LICENSE": "4f3547e23d7fd227a33aaa6a075185be8299403b03e534627bbb3bc3bdcf97af", + "com.sydxky.rtos.inputmethod/NOTICE.md": "eb75c7be9055410bec57688b363be4761c2ee6f2ae4cfba4ab908d0b9ce68999", + "com.sydxky.rtos.inputmethod/PRIVACY.md": "c3062d8434db2fad3b6824cf2c41561a0bf6ae05e62f05d7c6316efd64d74af9", + "com.sydxky.rtos.inputmethod/README.md": "8ffdcdd0bcf669908da59615f98208cca2807f6c6f8b28499d332c5130aa477b", + "com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md": "3b7346853bf6679c4efeda0568caece9ecb28dfde6347c96a67dc245a1e08ab9", + "com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md": "db7cf247579c3f6be73eaecf75fc9553214df8120ee25dc7d51c402a34ea89d6", + "com.sydxky.rtos.inputmethod/build.ps1": "32168e753699f48274fe2492e53b7285e4b62c15dc928f3aeb366a11150ebf5a", + "com.sydxky.rtos.inputmethod/glyphix.config.js": "43beae1f2a61eed010149ae6ad77223ba084117ea5aee3cb3fcd48bfc0a4a32b", + "com.sydxky.rtos.inputmethod/scripts/build-icons.py": "51f859a9517e7dd2e5e4bf9f688ce41dc79d4bc491fc66066dfe2db4aae4d704", + "com.sydxky.rtos.inputmethod/scripts/convert-dictionary.py": "83eea8c7adae0254eb69170dec905c285c09c43ffcf10e8e553beb108acf43b3", + "com.sydxky.rtos.inputmethod/scripts/install.mjs": "46c7d95b25c312734c0ca8387f4a782becd4723f21d01492d074da22ca05432d", + "com.sydxky.rtos.inputmethod/src/app.js": "e51c7e0a67563bd64b6bc04aedde22842295824432942bc8f1ab0d4731cc3ed3", + "com.sydxky.rtos.inputmethod/src/assets/crab-keyboard-license.txt": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", + "com.sydxky.rtos.inputmethod/src/assets/icon.png": "f712ac5f8c500a33dd72cc1f15f9ad467e6043c284fb6ece1f1683e54d40e683", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/LICENSE.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/arrow-down-s-line.png": "69bb846769721ed4ad056ed0136da59d4aa6873edccd1e38a7ebd783cb94ed1e", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/arrow-left-s-line.png": "68d8920081fb883f4c59af47549d6b41a9b0a3748383b814e4480bfab0e711ed", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/arrow-right-s-line.png": "c6bffa0a51254c60c92e276e4d21ae8a1595d82c77d9d6a1064afbf534ea3562", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/check-line.png": "dc3c240ba0db808212dae8a29dadbda9b0428d699a42f9e52e789e74ed56967c", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/close-line.png": "d3cfb57fa03b03c512286187681caaf4026a4dc60c18b91cbdc0bcc9e2586621", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/delete-back-2-line.png": "a83002ddc7ba1f1a86b361c0c672ec63205393cae2df275a9ff2d49501fd6b9d", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/edit-2-line.png": "6161624226e2b62b24da7244d86564d6d0856533c2149d3b347e17013c562497", + "com.sydxky.rtos.inputmethod/src/assets/ime-icons/keyboard-box-line.png": "bf4b45e2e54b4fd7e901068df1de865334f7e4616583dc5358d35d14890948af", + "com.sydxky.rtos.inputmethod/src/assets/jieba-license.txt": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", + "com.sydxky.rtos.inputmethod/src/common/ime-engine.js": "80e70d69561e424f4e42d5b3f33880bfa87df4d04119eb4f49b46b2dde4dce3d", + "com.sydxky.rtos.inputmethod/src/common/ime-lifecycle.js": "93525dffabcca0620acc36defd077ab6677b1032ef67faf695f3fbf3116b8a64", + "com.sydxky.rtos.inputmethod/src/keyboard/index.ux": "d7b4849824cb31882bce496aaecbbe26b82b772820c54327a263f6758ec5b217", + "com.sydxky.rtos.inputmethod/src/main/index.ux": "f763f0dc80b9fd46e00830d6ec0de8683063f98c1edee94ee522c2605b695fca", + "com.sydxky.rtos.inputmethod/src/manifest.json": "4520f786d4159d1cf8aedd0bad73a66bf3494729ca1de69ea66e2c9fad3fba64", + "com.sydxky.rtos.inputmethod/tests/client.test.mjs": "01bb6191f26d0f50496f1e032a39bf3bcce567bbbc33727491c5246dc6bf9900", + "com.sydxky.rtos.inputmethod/tests/engine.test.mjs": "86408d7629f189a11327ee801e75e472414d3a7979db60c12772debcf447ce23", + "com.sydxky.rtos.inputmethod/tests/layout.py": "b632411cd593d369180abaeedbf036002e986e3e5e2c2f15b4e5ff157588aa78", + "com.sydxky.rtos.inputmethod/tests/native.py": "307d14506af92388c044e30e8095ac4640b710d988ec10d31f3eecd1c9a9a093", + "com.sydxky.rtos.inputmethod/tests/visual.py": "25bb14687278b6961e1e4957dafdb28b95d1056fdbe58e6506bca853cd5f1fec", + "com.sydxky.rtos.inputmethod/third_party/UPSTREAM.md": "e8311679981a318fef7142f7a2ecb2b1a7860f5fee32282729cab6af2a41d640", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/LICENSE": "b140337f94481310f35abfd2210fde308ba30411899095fd961a77b94e022775", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/README.md": "88c2f96be16dc5d80677745072940c8a1cbc7aab40e554de50ec11bc22339b75", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/lookupDictV5.original.js": "915605439ebc404d808ccce6cb18abfff3981f1cdf53a78e9a743c78c4a6ddc9", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key0.dat": "795a5316089b67a7bffabf87080bd61f3720f9e0fd2f2866a092901f99b794e8", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key1.dat": "a4bcae2b6eb30084d3cf2ac64e5d9ca71eb94e8ac1f7c8fc8b38a5d478982be2", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key10.dat": "5731d9f3a3cad0304ecc650d74c723185c9d43f74d66d7b5622454d1fc31f99c", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key11.dat": "fa2a4bfa64cb236a051ceaf6b9b227159dde7aaba21ae997baafac6df4332345", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key12.dat": "7685ad03e3b5eeac404e8d6f0fc9e8524262b2c4a42ef0301d9e85c64d3db63b", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key13.dat": "b06f6fc0e2769a8f6e9491a0b97148f8c8d211ba8fe8ed122e1dbffca0eb7399", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key14.dat": "5d21973e2c1d833e93bfe51c42064b26743f6012e35c73ffcd2b5046cca3eeff", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key15.dat": "62c3324ccc143cd4e9ee533b0117ee06fc893d58c5d2ed0353da4dde33f5eab1", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key16.dat": "8c2c0fc7568cf6b674e8630449eb989c7ebdf34f34d0b2041a59748bd20c9638", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key17.dat": "f62c9c886ca4ee387a780a8c1e24116d73dedfda228de3614978c22f994ef7ec", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key18.dat": "fa7fda7b3b452908623383a6285aa15b265a41d48de58524fe0f0879b8aad259", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key19.dat": "00674fb37b56b5973cf0a7ee38b8258652949e4c02782ff6651d2aa63b9c62fc", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key2.dat": "45e860c89d1362de6007e46a6aa84a0d2fcd9682924de6e5f111cd536e18bfd1", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key20.dat": "1715a74e0bd00bf89406806682d865a53782d4cd9c4aee5050e547e8a84ffad6", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key3.dat": "00b464b6ee7ecd7e4406150eecf6c512e22611c6505a5045834cd5c3b7d9068d", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key4.dat": "a5c5719d09f931efc44b629fe92fd3faea0cc8c5a6ec28d0a9c9b9cdb0eb5ad9", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key5.dat": "3d5e43374441150af704b9dae9a6d62ac66406306b37709f12119fe44153e353", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key6.dat": "586c47ee8542f8bf59d7b4bf71fb1715a1bf3e7452d91068edcaf8b566e81e52", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key7.dat": "4525656f4d9fcd85bff07b876af75eb84f4f711a55a63c258c491d22dfeedff5", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key8.dat": "8e88d27fbc09fc3b133cbafd3b8e2f1b2269950cfcae12c23c1e152013f63f47", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/key9.dat": "c741aefb551b686a9ade67d3a16a5230d74134af6c3d0faf08557e19698d5825", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value1.dat": "81c1798a0dda463cacae6ac5285ed9ad0cd0730dde167c18ee3500f1e1d8d8f2", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value10.dat": "9c4fded85bb7467304df917d510d987a824785058a9f8bb9c9095571b49fbf0d", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value11.dat": "3be277f87112c7bbc7242882b4ca92ff6e9d38d7be1b51b7899545a5c13d1a19", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value12.dat": "789c47185b5aaa93293239b356a6a4928d3d29daaf9a4d18f3c94f392f861613", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value13.dat": "b7366771afebe2ddc466eb9dc8dd840b1e7a8a8214354e2930d4fa4346f27dc6", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value14.dat": "9c638ef5d554738087505b158482e5b0caba59a36df65b9d0f3e5eee78f91588", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value15.dat": "846ea5776c9e11f44762cc032d690435464a312e82813530b01cb6001bd0a2f6", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value16.dat": "b86c42aaf4a818b4eaccd2663b6fb01f3ef3b1dc8482f20636cbde1a95ca52da", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value17.dat": "2f809c59a318d7d8d8605985092517902b6c627a40936af856785ed596c1133e", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value18.dat": "d64613d9bc90f503b964984939de0dc881c3a0feda2684bade0c572555c2099d", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value19.dat": "ecfb7ba56450c671fc941d56556a42d508569e341c5dd2581b18a3d7d27ab55c", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value2.dat": "d66703e7ac7ab6c2cd3b3c8109e656e1f78ace3e2bb49123ce3a2b3da4ca04d0", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value20.dat": "896757081c224c72586dfa5babb4300c93639a27ba9a70eb34b9dfd5c23735f2", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value3.dat": "19c47c2f8b8483946db81572368b3f3b2aa61c5a03953351e327f3f83cb31275", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value4.dat": "1be64f23183fca36920ae3f002d541febaa1b26b5ca3018ee53ffdd05546d56d", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value5.dat": "7b8a109e705537784a0a8075cc4fe3d1de8288a6b6b0240627685c81ed9695a9", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value6.dat": "5b4028a017b228357446f799cd29b28dd5a24d63340f8b48ce858c75c64029f1", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value7.dat": "b9f6e109ba5f1b5bca758613cb5cccfecf358915ce6415525c333b19515d6428", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value8.dat": "3d1622d4717680b31a6e037c88e7f046fecfe2339e521272e1d1eae0a7094606", + "com.sydxky.rtos.inputmethod/third_party/crabKeyboard/pinyinDictV5_20260416/value9.dat": "1c0caab370f1f14048ab6dd2da0011bedcddfed0ed0378b5c98171b6b22caf35", + "com.sydxky.rtos.inputmethod/third_party/jieba/LICENSE": "18ba0984839f85853b29fadaf992f7dba8fd0ca0fbeae34de2b8735222dc7a37", + "com.sydxky.rtos.inputmethod/third_party/jieba/dict.txt": "7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/README.md": "6a7bee86ad1942d01be10b556dd3036e43a59d708fc94fd2f5f3e3fcfcf84b0b", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-down-s-line.iconfont.svg": "1efa1c713b710c0e9ef5eb47bec762a4b5ce21ffb454d44a3bbb118a8bec3e83", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-down-s-line.upstream.svg": "4b0bf8a45d554ba97d8115ef2713010f196ad49d57eeecb8a42d4839a5ba8f9e", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-left-s-line.iconfont.svg": "ccf02486d37383bbdc3319d9b348c5daef43693526d41aff6ad409d04e0664ef", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-left-s-line.upstream.svg": "7d0407d47b59e7d69446ef81b79e95c5d5a4085e7fa7ba30770556b67197407e", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-right-s-line.iconfont.svg": "b6678427ec45e7b00257b6c2d8eeb533e6b92c6d09864ce7844178e38fade8af", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/arrow-right-s-line.upstream.svg": "2c579a4dbaf25e19a128d71e04f4cf43b19ad2487a0076ff7e7e5a94c9b3e013", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/check-line.iconfont.svg": "1b194f3b6ac39e6199fb003ac3140cc2ed2d1213201c0bdbf69abffaba67f6ce", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/check-line.upstream.svg": "b83a64bf881169d5edeaa6134a7ba37192f0ddc80c2e2a890578ae0f2f8f9169", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/close-line.iconfont.svg": "748ea3e8a4fe22671ff3347e998d51679eaf3ff3950f6348f286f6dd36f19a92", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/close-line.upstream.svg": "db3b4dcebb2d962a0231a191538bf6939620798b18f0ba52d4b91c5a4649ce54", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/delete-back-2-line.iconfont.svg": "a6279d35f9fd1578f813bcfa2077fac98ec8e3a4d7bc8f90a5b3f3f2744c3a59", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/delete-back-2-line.upstream.svg": "d6fc7f41c0b84fe48a06685dc081780b6cd103ac9b3db96dc44e624aa0a113e8", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/edit-2-line.iconfont.svg": "74d6590a22053c253c0d00d9ac5e35fb1f8c02e2243d0e3643a959eef233476f", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/edit-2-line.upstream.svg": "d03a3a7b874377a18488b7b24b596b8078afbd9bced93ad791b6fb2f4866613d", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/icons.json": "c556c01eed73d0671f93cbeaeed8d3a628395213ef0b951bd5eb7c3d1c1618db", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/keyboard-box-line.iconfont.svg": "813d79347c357a919317edf84252a505dbdc441c1144076b4e8389a8ce217de0", + "com.sydxky.rtos.inputmethod/third_party/remix-icon/keyboard-box-line.upstream.svg": "e39f28cbf418269405f517126a64cbf4b3332b3295cc8a3af670f77344a669c4", + "docs/SCREENSHOTS.md": "c38fcb14f6140a392b2a044b4e98cc625798a4579cce830075fd7f83aaf882ca", + "docs/UI_PREVIEW.png": "ef7767cd8c38051d332f045ff879d343ebc9966a2a94d2468b8d19604bff7ce1", + "docs/screenshots/android-candidates-16.png": "d244a3fb41300a7e8c478930307cfb3d4e781f016be53a9e93615bb79717ae73", + "docs/screenshots/android-fourteen.png": "88b06bdd1e6a6dc6f45722e01ce7930c94845062b8bf237aad5c5d086f37641c", + "docs/screenshots/android-home.png": "b6823a0bcfb995d6ac771252f31f7ed1d85da12e9b4b98b5ba07e801c5d6e41e", + "docs/screenshots/android-host.png": "06a770510a2802d2d7b74b2410003b3851a98b247f98fcd0dd2e4893610b456b", + "docs/screenshots/android-phrases.png": "144be16bc2d9252a711af0a57b57ba6cb624ac9e7900fadeb31447fa8060fe9e", + "docs/screenshots/android-qwerty.png": "17c9a50782be31225ba8360fb73923a90ac7a98b8ca656fcc2d2cdab2e368e8b", + "docs/screenshots/android-result.png": "65d50c4e6672f26e5f156fa52cadb9b4f4d0282ad71af71fcc1fdb5a738e5a24", + "docs/screenshots/rtos-rect-candidates.png": "fd2342bd5e6b05390097b6b55614db67442ff797afc8e3ae8b2999a478fab232", + "docs/screenshots/rtos-round-14-key.png": "65749bb8197992a331efc2484ce8e425f5958e50fadb6d6c509b0749aa684585", + "docs/screenshots/rtos-round-candidates.png": "ab4f686aaece5abde52b3a7a57a3da10573eb95d56e4664ac03a4fe12267705d", + "docs/screenshots/rtos-round-phrase-candidates.png": "507d09901681bbeae4da39fa0a700663d89b7149f7fab01dd603630c932b4dc4", + "tests/emulator-validate.py": "2a549e3c7bc177b942826425e548f68db39a32d8a55ade080f0d8ab3e9c00060", + ".gitattributes": "8e689518e5810501eb34583fa369475902805d8dd6c3ea78699469de4fa22932" + } +} diff --git a/README.md b/README.md index aa0b417..926d27c 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,85 @@ -# 蟹蟹输入 · RTOS适配改版 - -基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立Glyphix RTOS改版,由herry适配。不是上游原版,不表示上游或设备厂商背书。 - -**MIT开源,允许商用、修改、再分发和闭源集成。** 请保留版权及许可全文;[LICENSE](./LICENSE) / [署名与第三方许可](./NOTICE.md)。Remix Icon图标另按Apache-2.0使用,厂商SDK/运行时不包含在本项目MIT授权中。 - -![实际模拟器界面](./docs/UI_PREVIEW.png) - -## 功能 - -- 浅色满屏键盘,适配圆屏466×466及方屏390×450。 -- 26键全拼/英文均为标准QWERTY,10/9/7三排;九宫拼音与14键双字母模式。 -- 14键采用`QW ER TY UI OP / AS DF GH JK L / ZX CV BN M`,参考公开布局,自研离线消歧,保留完整候选及连续输入。 -- 104,837组拼音/缩写索引、163,238候选项,四项分页,不作100项截断。 -- 英文大小写、数字、符号、Unicode光标与删除、可选本地学习、私密输入和本机草稿。 -- 原生`.ux`页面,可通过`router.push`接入其他Glyphix应用;系统级全局输入法注册需厂商支持。 - -## 下载与反馈 - -- [1.1.0发行版与两屏QuickApp完整包](https://git2.sydxky.cn/herry/crab-input-rtos/releases) -- [已有问题与建议](https://git2.sydxky.cn/herry/crab-input-rtos/issues) -- [免注册匿名提交问题与建议](https://git2.sydxky.cn/feedback/herry/crab-input-rtos) - -Git2免登录浏览、克隆和下载,当前关闭注册,代码与版本由所有者维护。 - -```powershell -git clone https://git2.sydxky.cn/herry/crab-input-rtos.git -cd crab-input-rtos -``` - -## 构建 - -准备Windows PowerShell 7、Node.js和Python 3。建议克隆到较短路径(例如`F:\Project\crab-input-rtos`),厂商图像工具在过长Windows路径可能失败。厂商Glyphix SDK不随仓库分发;自行取得适用授权后,将含`bin/gx.exe`与`share/`的完整SDK放在`temp/glyphix-v0.7.2-windows-x64/`。本版验证使用`gx v0.10.1 / 3ed38835`,需要`generic-watch-466x466-quickapp`和`generic-watch-390x450-quickapp`配置。 - -```powershell -& .\com.sydxky.rtos.inputmethod\build.ps1 -``` - -脚本无损生成词库分片,执行40项引擎及29项UX脚本断言,然后构建普通/QuickApp四配置。QuickApp输出位于应用的`.glyphix-work/dist/`,每屏必须保留完整`bundle.pkg`、`icon.png`、`manifest.json`,不是APK/HAP。 - -## 原生模拟器复现 - -`com.sydxky.rtos.imefixture/`是本仓库附带的最小离线验收页,只用于实际前后台切换,不包含其他内部应用。先构建并启动一次圆屏参考页,获得厂商launcher/global资源,然后退出该模拟器: - -```powershell -& .\com.sydxky.rtos.imefixture\build.ps1 -Push-Location .\com.sydxky.rtos.imefixture -& ..\temp\glyphix-v0.7.2-windows-x64\bin\gx.exe emulate -d generic-watch-466x466 -Pop-Location -python .\com.sydxky.rtos.inputmethod\tests\native.py --work work/native --output work/evidence -python .\com.sydxky.rtos.inputmethod\tests\visual.py --work work/visual --output work/evidence/visual -``` - -视觉脚本需要Pillow。运行时保持8192 KiB内存,不接管其他占用Inspector 14201的进程。1.1.0原产品四配置84项原生操作、84项操作后布局、44个视觉状态均通过,见[验收记录](./com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md)。公开仓库使用最小参考页,已重新完成独立源码四配置构建、84/84原生操作、84/84布局和44/44视觉状态,见[公开树验收](./docs/PUBLIC_RELEASE_1.1.0.md)。 - -真机安装、目标固件性能/续航和商店审核尚未验收。详细接入、词库与14键说明见[应用文档](./com.sydxky.rtos.inputmethod/README.md)和[14键算法](./com.sydxky.rtos.inputmethod/KEYBOARD_14.md)。 +# 蟹蟹输入 · RTOS / Android 手表适配改版 + +为小屏手表准备的离线输入组件:大按键、浅色界面、完整候选、圆屏安全布局。基于 [CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard) 的词库,由 herry 重写 Glyphix RTOS 与原生 Android 适配;本项目为独立改版,保留原作者及第三方署名。 + +**MIT 开源,允许商用、修改、再分发和闭源集成。** 使用时请保留 [LICENSE](LICENSE) 和 [第三方许可](NOTICE.md)。厂商 SDK、运行时与设备固件不包含在本项目授权中。 + +![1.2.0 实际模拟器与手表界面](docs/UI_PREVIEW.png) + +## 1.2.0 带来了什么 + +- 完整候选采用 **4×4 网格,单字 16 项一页**;词组占用更多列,长词增加行高,显示完整文字;展开页不显示拼音。 +- 标准 **QWERTY 26 键**,排列为 `qwertyuiop / asdfghjkl / zxcvbnm`;同时提供九宫和 14 键双字母模式。 +- 14 键为 `QW ER TY UI OP / AS DF GH JK L / ZX CV BN M`,参考公开布局,自行实现离线消歧和连续输入;[算法说明](com.sydxky.rtos.inputmethod/KEYBOARD_14.md)。 +- 保留全部 **104,837 组拼音/缩写索引、163,238 个候选项**。按页浏览全部结果,支持分段选词及剩余编码继续输入。 +- 英文、数字、符号、光标移动及 Unicode 删除。圆屏每行按安全宽度排布,避免四角按钮被裁切。 +- 新增 **Android 应用内键盘 AAR**、演示 APK、独立宿主示例。接入后点击输入框自动弹出,由组件直接回填宿主输入框。 + +## 选择适合的平台 + +| 平台 | 接入与产物 | 已完成的验证 | +| --- | --- | --- | +| Glyphix RTOS | 原生 `.ux` 页面;圆屏 466×466、方屏 390×450 两屏 QuickApp 完整 ZIP | 普通/QuickApp 四配置构建;84 项原生操作、84 项布局、48 个视觉状态 | +| Android 8.1 / API 27+ | 原生 AAR;演示 APK;独立 `host/` 示例 | OWW261 Android 11 真机 8 项交互测试,系统默认输入法保持不变 | + +Android 版本作为**应用内公开组件**,由每个应用自行接入;组件不注册系统输入法。RTOS 页面也需宿主接入,系统级注册依赖厂商支持。 + +完全离线,输入功能无需账号。Android 组件不申请网络/外部存储/悬浮窗权限,不保存输入内容;RTOS 端的可选本地学习、私密模式及草稿说明见 [隐私说明](com.sydxky.rtos.inputmethod/PRIVACY.md)。 + +## 下载、源码与反馈 + +- [v1.2.0 发行包与更新说明](https://git2.sydxky.cn/sydxky/crab-input-rtos/releases/tag/v1.2.0):AAR、演示 APK、两屏 RTOS QuickApp、验收截图及 SHA-256。 +- [Android 接入文档](android/README.md) · [RTOS 接入文档](com.sydxky.rtos.inputmethod/README.md) +- [实际界面截图](docs/SCREENSHOTS.md) · [RTOS 验收](com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md) · [Android 真机验收](android/VALIDATION_1.2.0.md) +- [已有问题](https://git2.sydxky.cn/sydxky/crab-input-rtos/issues) · [免注册提交问题与建议](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos) + +Git2 免登录浏览、克隆和下载;当前关闭注册,代码和版本由所有者维护。 + +```sh +git clone https://git2.sydxky.cn/sydxky/crab-input-rtos.git +``` + +## Android 开发 + +准备 JDK 17、Android SDK compile 37、Python 3;SDK 与签名私钥不随源码分发。 + +```powershell +cd android +python scripts/build-dictionary.py +.\gradlew.bat :keyboard:testDebugUnitTest :keyboard:lintRelease :keyboard:assembleRelease :app:assembleDebug :host:assembleDebug +``` + +生成的 `keyboard/build/outputs/aar/keyboard-release.aar` 可直接放入宿主 `app/libs/`。在 Activity 的 UI 线程使用: + +```java +import com.sydxky.crabinput.CrabKeyboard; + +CrabKeyboard.Binding keyboard = CrabKeyboard.attach(this, editText); +// 有自己的监听器时,在相应事件中调用 keyboard.show()。 +// 绑定自动跟随 Activity 生命周期,也可显式 keyboard.close()。 +``` + +源码不包含生成的 SQLite,构建脚本从本仓库 RTOS 原始词库无损生成。正式演示 APK 签名由开发者自己的受保护配置提供;AAR 无需应用签名。 + +## RTOS 开发 + +准备 Windows PowerShell 7、Node.js、Python 3,以及合法取得的 Glyphix SDK。验证版本为 `gx v0.10.1 / 3ed38835`。将包含 `bin/gx.exe` 和 `share/` 的完整 SDK 放在 `temp/glyphix-v0.7.2-windows-x64/`: + +```powershell +& .\com.sydxky.rtos.inputmethod\build.ps1 +``` + +脚本生成词库分片,执行 44 项引擎、32 项 UX 断言,并构建普通/QuickApp 四配置。每屏 QuickApp 都须保留 `bundle.pkg`、`icon.png`、`manifest.json` 三个文件。 + +复现原生验收时先构建最小参考页并启动一次圆屏模拟器,获得厂商 launcher/global 资源后退出;不要同时占用 Inspector 14201: + +```powershell +& .\com.sydxky.rtos.imefixture\build.ps1 +Push-Location .\com.sydxky.rtos.imefixture +& ..\temp\glyphix-v0.7.2-windows-x64\bin\gx.exe emulate -d generic-watch-466x466 +Pop-Location +python .\com.sydxky.rtos.inputmethod\tests\native.py --work work/native --output work/evidence +python .\com.sydxky.rtos.inputmethod\tests\visual.py --work work/visual --output work/evidence/visual +``` + +视觉脚本依赖 Pillow。RTOS 运行时内存配置保持 8192 KiB。本版本完成模拟器、Android 真机组件及提交包验收;RTOS 真机、长期性能/续航和商店审核尚未验收。 diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..fe95876 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +.gradle/ +local.properties +build/ +app/build/ +keyboard/src/main/assets/dictionary.db +artifacts/ +release-signing.properties +*.jks +*.keystore +**/__pycache__/ + +keyboard/build/ +host/build/ diff --git a/android/LICENSE b/android/LICENSE new file mode 100644 index 0000000..1a442a8 --- /dev/null +++ b/android/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2026 sydxky +Copyright (c) 2026 kqakqakqa +Copyright (c) 2026 crabKun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/android/NOTICE.md b/android/NOTICE.md new file mode 100644 index 0000000..0502476 --- /dev/null +++ b/android/NOTICE.md @@ -0,0 +1,17 @@ +# 蟹蟹输入 Android手表适配改版 + +本项目由sydxky基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的MIT词库重新实现Android应用内键盘组件、圆屏布局、九宫/14键消歧及候选分页。不是上游原版,也不表示上游作者或设备厂商对本改版作出背书。 + +上游固定提交:`93d36b5002dee80a3d253b1ec7ec9e29800a104c`。保留原作者kqakqakqa、crabKun的版权声明和MIT许可证;原始词库位于同仓库RTOS目录。Android自研适配部分以[MIT许可证](./LICENSE)发布,**允许商用、修改、再分发和闭源集成**,须保留适用版权及许可全文。 + +第三方内容继续按各自许可证使用: + +| 内容 | 作者/项目 | 许可与位置 | +| --- | --- | --- | +| CrabKeyboard V5词库 | kqakqakqa、crabKun | MIT,`keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt` | +| 本地候选排序词频 | jieba 0.42.1 / Sun Junyi | MIT,`keyboard/src/main/assets/licenses/jieba-LICENSE.txt` | +| Gradle wrapper | Gradle | Apache-2.0,`keyboard/src/main/assets/licenses/Gradle-LICENSE.txt` | + +14键参考公开演示中的相邻字母分组,自行实现离线消歧;不包含华为内部模型或固件代码。Android SDK、JDK和构建工具不随源码分发,按各自许可使用。AndroidX及JUnit仅用于本地测试,不打入正式APK。 + +公开仓库:。匿名问题与建议:。 diff --git a/android/README.md b/android/README.md new file mode 100644 index 0000000..804459a --- /dev/null +++ b/android/README.md @@ -0,0 +1,58 @@ +# 蟹蟹输入 Android应用内键盘组件 + +基于CrabKeyboard MIT词库,由herry重写的Android手表原生键盘组件,1.2.0 / 10200。提供可集成的 **AAR**、演示APK和独立接入示例;适用Android 8.1 / API 27及以上。 + +应用接入后,点击自己的输入框自动展开内置键盘。**不注册系统输入法、不要求用户切换输入法、不改变系统默认输入法。** 圆屏按每行所在高度计算安全弦宽;4×4完整候选,单字16个一页,词组扩宽或增加行高,候选不显示拼音。九宫、标准QWERTY全拼/英文、14键、数字、三页符号、Unicode删除、按住连删及光标编辑。 + +完全离线,无网络或外部存储权限;组件不持久化输入内容,不启用学习。密码输入默认英文并遮蔽预览。页面隐藏/销毁时清除组件会话与候选,宿主输入框中的内容仍由宿主决定如何处理。异步查询按会话序号隔离,旧结果不能回填新的输入框。 + +## 一分钟接入 + +将发行版`crab-keyboard-1.2.0.aar`放入宿主`app/libs/`: + +```groovy +dependencies { implementation files('libs/crab-keyboard-1.2.0.aar') } +``` + +```java +import com.sydxky.crabinput.CrabKeyboard; + +// Activity 的 UI 线程,已有 EditText;attach 返回一个可关闭绑定。 +CrabKeyboard.Binding keyboard = CrabKeyboard.attach(this, editText); +// 已有自己的焦点/点击处理时,可在相应事件调用 keyboard.show()。 +// Activity 销毁时可显式 keyboard.close(),重复关闭安全。 +``` + +`attach`会接管该输入框的焦点和点击监听器,并关闭此框的系统软键盘自动显示。`show()`、`dismiss()`、`close()`在UI线程调用。自动监听宿主Activity停止和销毁,停止时关闭会话,关闭绑定时恢复输入框原来的软键盘显示属性。组件通过该输入框的Editable/InputConnection回填,支持选区和数字/密码/网址InputType。窗口为宿主应用自己的Dialog,不申请悬浮窗权限。 + +需要保留自定义监听器时,先取得绑定,再在自定义事件中调用`keyboard.show()`;不要重复绑定同一个输入框。`host/`是独立包名的最小接入示例,`app/`是完整演示。未接入的应用继续使用原有系统输入法。 + +## 模块与构建 + +| 模块 | 用途 | +| --- | --- | +| `keyboard/` | 原生公开组件、词库、MIT许可,生成AAR | +| `app/` | 圆屏演示APK,包名`com.sydxky.crabinput` | +| `host/` | 独立应用集成示例,证明组件可由其他应用接入 | + +准备JDK 17、Android SDK compile 37及Python 3。词库位于同仓库RTOS目录,SDK/私钥不随源码分发。 + +```powershell +python scripts/build-dictionary.py +.\gradlew.bat :keyboard:testDebugUnitTest :keyboard:lintRelease :keyboard:assembleRelease :app:lintRelease :app:assembleDebug :host:assembleDebug +# 演示APK正式签名由本机受保护文件提供;组件AAR无须应用签名。 +.\gradlew.bat :app:assembleRelease -PreleaseSigningProperties=<本机签名配置路径> +``` + +输出`keyboard/build/outputs/aar/keyboard-release.aar`。SQLite词库无损转换并全量回读104,837组索引、163,238候选;数据库是可生成的本地资产,不进入Git。正式APK必须使用已批准签名,签名材料不公开。 + +## 真机验证 + +短时验收在已连接的OWW261上使用真实EditText和公开组件,通过UiAutomator点击操作,不直接调用键盘内部状态代替交互;比较测试前后系统默认输入法保持一致。范围及截图见[1.2.0验收](./VALIDATION_1.2.0.md)。演示应用内即可测试,无需启用或切换系统输入法。 + +```powershell +.\gradlew.bat :app:assembleAndroidTest :host:assembleDebug -PimeTestBuildType=release -PreleaseSigningProperties=<本机签名配置路径> +python scripts/validate-device.py --serial <已连接手表序列号> --output <本地证据目录> +``` + +源码、截图及下载:。MIT允许商用,保留[LICENSE](./LICENSE)、[NOTICE](./NOTICE.md)及词库/jieba许可。AndroidX/JUnit仅用于测试;AAR无第三方运行时依赖。当前是组件和演示验收,未进行商店审核。 diff --git a/android/VALIDATION_1.2.0.md b/android/VALIDATION_1.2.0.md new file mode 100644 index 0000000..d52ec56 --- /dev/null +++ b/android/VALIDATION_1.2.0.md @@ -0,0 +1,28 @@ +# Android 应用内键盘 1.2.0 验收 + +日期:2026-11-14。OWW261 真机,Android 11 / API 30,466×466。正式演示 APK 包名 `com.sydxky.crabinput`,版本 1.2.0 / 10200,最低系统 API 27。 + +该版本提供 `keyboard` 原生 AAR,以及演示 APK、独立包名接入示例。组件通过宿主 Dialog 展开、直接回填 EditText,不包含系统输入法 Service。测试前后系统默认输入法一致。 + +| 真实交互项目 | 结果 | +| --- | --- | +| 完整词库在全拼、九宫、14 键中查询 | 通过 | +| 密码及数字输入框选择安全模式 | 通过 | +| Unicode 码点删除、按住连删及松手停止 | 通过 | +| 独立应用接入公开组件并回填 | 通过 | +| 14 键歧义及词组占宽显示 | 通过 | +| 全拼连续输入“你好世界” | 通过 | +| 九宫词组及删除回填 | 通过 | +| 单字 4×4、全部候选翻页、末页选词及圆屏安全边界 | 通过 | + +最终验收 8/8,通过实际 UiAutomator 按钮操作;没有通过调用键盘内部状态代替交互。候选查询完成时控件树会更新,测试仅在注入点击前遇到失效节点时重新查找,行为断言保持严格。截图为最终正式 APK 的实际设备画面。演示首页显式设置输入框内边距及背景,消除厂商默认 EditText 内边距导致的文字裁切。 + +最终演示 APK SHA-256: + +```text +4e2b0ce4a9d82ae6994173cd17472ee4e711a49bc33c5c8e6c202c2fd4857271 +``` + +组件 4 项 Java 单元测试、组件/演示应用 release lint、AAR 和签名 APK 构建通过。SQLite `integrity_check`、全部 104,837 组索引/163,238 候选回读通过。AAR 无第三方运行时依赖,不申请网络、外部存储或悬浮窗权限。 + +本次为短时交互验收,未进行长期压测、功耗或商店审核。系统默认输入法始终由用户原有设置决定,组件只在已接入它的应用中使用。 diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..4947cad --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,60 @@ +import java.util.Properties +plugins { id 'com.android.application' } +def signingPath = providers.gradleProperty('releaseSigningProperties').orElse('release-signing.properties').get() +def signingFile = rootProject.file(signingPath) +def signing = new Properties() +if (signingFile.isFile()) signingFile.withInputStream { signing.load(it) } +def approved = signing.getProperty('releaseApproved','false').toBoolean() +android { + namespace 'com.sydxky.crabinput' + compileSdk 37 + defaultConfig { + applicationId 'com.sydxky.crabinput' + minSdk 27 + targetSdk 35 + versionCode 10200 + versionName '1.2.0' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + signingConfigs { + if (approved) { + release { + storeFile new File(signingFile.parentFile, signing.getProperty('storeFile','')) + storePassword signing.getProperty('storePassword','') + keyAlias signing.getProperty('keyAlias','') + keyPassword signing.getProperty('keyPassword','') + enableV1Signing false + enableV2Signing true + enableV3Signing false + enableV4Signing false + } + } + } + buildTypes { + release { + minifyEnabled true + shrinkResources true + if (approved) signingConfig signingConfigs.release + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testProguardFiles 'proguard-test.pro' + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } + lint { abortOnError true } + testBuildType providers.gradleProperty('imeTestBuildType').orElse('debug').get() +} +tasks.register('checkReleaseSigning') { + doLast { + if (!approved || !signingFile.isFile() || !android.signingConfigs.release.storeFile.isFile()) + throw new GradleException('Supply approved local signing with -PreleaseSigningProperties=') + } +} +tasks.matching { it.name in ['packageRelease','assembleRelease'] }.configureEach { dependsOn 'checkReleaseSigning' } +dependencies { + implementation project(':keyboard') + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' + androidTestImplementation 'com.google.errorprone:error_prone_annotations:2.36.0' + androidTestImplementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22' +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..a0dc5b7 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,2 @@ +-keep class com.sydxky.crabinput.Lexicon { public *; } +-keep class com.sydxky.crabinput.Candidate { public *; } diff --git a/android/app/proguard-test.pro b/android/app/proguard-test.pro new file mode 100644 index 0000000..682da41 --- /dev/null +++ b/android/app/proguard-test.pro @@ -0,0 +1,7 @@ +# error_prone annotation signatures refer to javac's Modifier enum. These are +# compile-time metadata in the test runner, never Android executable paths. +-dontwarn javax.lang.model.element.Modifier +# Keep the instrumented runner intact; production R8 rules remain independent. +-dontshrink +-dontobfuscate +-dontoptimize diff --git a/android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java b/android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java new file mode 100644 index 0000000..e99ca1a --- /dev/null +++ b/android/app/src/androidTest/java/com/sydxky/crabinput/ImeDeviceTest.java @@ -0,0 +1,116 @@ +package com.sydxky.crabinput; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.SystemClock; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.UiScrollable; +import androidx.test.uiautomator.UiSelector; +import androidx.test.uiautomator.Until; +import org.junit.Before; +import org.junit.Test; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import static org.junit.Assert.*; + +/** Uses the public embedded AAR and real editors, never calls page methods. */ +public class ImeDeviceTest { + private UiDevice device; + private String keyboardPackage; + private static final String PKG="com.sydxky.crabinput"; + @Before public void launch() throws Exception { + keyboardPackage=PKG;device=UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());device.wakeUp(); + Context context=InstrumentationRegistry.getInstrumentation().getTargetContext(); + Intent intent=new Intent(context,MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);context.startActivity(intent); + assertTrue(device.wait(Until.hasObject(By.text("蟹蟹输入")),5000)); + } + private void editor(String description) throws Exception { + UiObject2 field=device.findObject(By.desc(description)); + if(field==null){new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().description(description));field=device.findObject(By.desc(description));} + assertNotNull(field);field.click();assertTrue("Embedded keyboard auto-opens",device.wait(Until.hasObject(By.desc("蟹蟹输入键盘")),5000));SystemClock.sleep(500); + } + private void tap(String text){clickFresh(By.pkg(keyboardPackage).text(text),text);SystemClock.sleep(95);} + private void action(String description){clickFresh(By.pkg(keyboardPackage).desc(description),description);SystemClock.sleep(150);} + private void clickFresh(androidx.test.uiautomator.BySelector selector,String label){ + // A dictionary result may rebuild the view tree between lookup and tap. + // Retry only a stale node detected BEFORE click injection, never a + // failed behavioral assertion or an already-injected user operation. + for(int attempt=0;attempt<3;attempt++){ + UiObject2 item=device.wait(Until.findObject(selector),5000);assertNotNull("Missing button "+label,item); + try{item.click();return;}catch(androidx.test.uiautomator.StaleObjectException error){if(attempt==2)throw error;SystemClock.sleep(150);} + } + } + private void type(String text){for(int i=0;i singles(){List result=new ArrayList<>();for(UiObject2 button:device.findObjects(By.pkg(PKG).clazz("android.widget.Button"))){String text=button.getText();if(text!=null&&text.length()==1&&Character.UnicodeScript.of(text.charAt(0))==Character.UnicodeScript.HAN)result.add(button);}return result;} + @Test public void fullGridAndEveryPageAreUsable() throws Exception { + editor("demo_text");tap("26键");type("shi");action("更多候选"); + assertFalse("No reading is rendered on expanded pages",device.hasObject(By.pkg(PKG).text("shi"))); + List cells=singles();assertEquals(16,cells.size());Set rows=new HashSet<>(); + for(UiObject2 cell:cells){Rect r=cell.getVisibleBounds();rows.add(r.centerY());assertTrue(r.width()>35&&r.height()>30);assertRoundSafe(r);} + assertEquals(4,rows.size()); + UiObject2 counter=device.findObject(By.pkg(PKG).text(java.util.regex.Pattern.compile("[0-9]+ 项.*[0-9]+ / [0-9]+"))); + assertNotNull(counter);int pageCount=Integer.parseInt(counter.getText().substring(counter.getText().lastIndexOf('/')+1).trim()); + Set words=new HashSet<>();String last=""; + for(int page=0;page=80);tap(last);action("收起键盘"); + UiObject2 field=device.findObject(By.desc("demo_text"));assertNotNull(field);assertEquals(last,field.getText()); + } + private void assertRoundSafe(Rect r){ + Context context=InstrumentationRegistry.getInstrumentation().getTargetContext();if(!context.getResources().getConfiguration().isScreenRound())return; + float cx=device.getDisplayWidth()/2f,cy=device.getDisplayHeight()/2f,limit=Math.min(cx,cy)-3; + for(int x:new int[]{r.left,r.right})for(int y:new int[]{r.top,r.bottom})assertTrue("Button stays inside round screen: "+r,Math.hypot(x-cx,y-cy)<=limit); + } + @Test public void fullPinyinContinuousInputCommitsIntoEditor() throws Exception { + editor("demo_text");tap("26键");type("nihaoshijie");tap("你好");tap("世界");action("完成"); + assertEquals("你好世界",device.findObject(By.desc("demo_text")).getText()); + } + @Test public void fourteenKeysAndWidePhraseCandidates() throws Exception { + editor("demo_text");tap("14键");for(String key:new String[]{"BN","UI","GH","AS","OP"})tap(key); + assertNotNull(device.wait(Until.findObject(By.pkg(PKG).text("你好")),5000));action("更多候选"); + List phrases=device.findObjects(By.pkg(PKG).clazz("android.widget.Button"));boolean found=false; + for(UiObject2 button:phrases)if("你好".equals(button.getText())){assertTrue(button.getVisibleBounds().width()>device.getDisplayWidth()/3);found=true;} + assertTrue(found);tap("你好");action("完成");assertEquals("你好",device.findObject(By.desc("demo_text")).getText()); + } + @Test public void t9PhraseAndUnicodeDelete() throws Exception { + editor("demo_text");for(String key:new String[]{"MNO","GHI","GHI","ABC","MNO"})tap(key);tap("你好"); + action("删除");action("收起键盘");assertEquals("你",device.findObject(By.desc("demo_text")).getText()); + } + @Test public void passwordAndNumericEditorsChooseSafeModes() throws Exception { + editor("demo_password");assertNotNull(device.findObject(By.pkg(PKG).text("私密输入")));type("secret"); + assertFalse(device.hasObject(By.pkg(PKG).text("secret")));action("收起键盘"); + editor("demo_number");type("1230");action("完成");assertEquals("1230",device.findObject(By.desc("demo_number")).getText()); + } + @Test public void allOriginalDictionaryWordsRemainQueryable() throws Exception { + Context context=InstrumentationRegistry.getInstrumentation().getTargetContext(); + try(Lexicon lexicon=new Lexicon(context)){ + for(String[] word:new String[][]{{"nihao","你好"},{"shijie","世界"},{"zhongguo","中国"},{"shurufa","输入法"},{"beijing","北京"}})for(String mode:new String[]{"py","t9","14"}){ + List result=lexicon.lookup(mode.equals("py")?word[0]:Lexicon.encode(word[0],mode),mode); + assertTrue(word[1]+" in "+mode,result.stream().anyMatch(item->item.word.equals(word[1]))); + } + assertTrue(lexicon.lookup("shi","py").size()>=90); + } + } + @Test public void anotherApplicationEmbedsThePublicComponent() throws Exception { + keyboardPackage=PKG+".host";Intent fixture=new Intent().setComponent(new android.content.ComponentName(PKG+".host",PKG+".host.HostActivity")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + InstrumentationRegistry.getInstrumentation().getTargetContext().startActivity(fixture); + UiObject2 editor=device.wait(Until.findObject(By.desc("fixture_editor")),5000);assertNotNull(editor);editor.click(); + assertTrue(device.wait(Until.hasObject(By.desc("蟹蟹输入键盘")),5000));tap("26键");type("nihao");tap("你好");action("收起键盘"); + assertEquals("你好",device.findObject(By.desc("fixture_editor")).getText()); + } + @Test public void unicodeAndHeldDeleteRespectTheRealEditor() throws Exception { + editor("demo_text");action("收起键盘");UiObject2 field=device.findObject(By.desc("demo_text"));field.setText("你😀");field.click();action("删除");action("收起键盘"); + assertEquals("你",device.findObject(By.desc("demo_text")).getText()); + field=device.findObject(By.desc("demo_text"));field.setText("abcdefghijklmnop");field.click(); + UiObject2 delete=device.wait(Until.findObject(By.desc("删除")),5000);Rect bounds=delete.getVisibleBounds();device.swipe(bounds.centerX(),bounds.centerY(),bounds.centerX(),bounds.centerY(),160);SystemClock.sleep(150);action("收起键盘"); + int count=device.findObject(By.desc("demo_text")).getText().length();assertTrue("Hold repeats deletion",count<14);SystemClock.sleep(300);assertEquals("Release stops repeat",count,device.findObject(By.desc("demo_text")).getText().length()); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f07f299 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java b/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java new file mode 100644 index 0000000..abfac17 --- /dev/null +++ b/android/app/src/main/java/com/sydxky/crabinput/MainActivity.java @@ -0,0 +1,55 @@ +package com.sydxky.crabinput; + +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; +import android.text.InputType; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; + +/** Public component demo: no system IME registration or setting changes. */ +public final class MainActivity extends Activity { + private final List bindings=new ArrayList<>(); + private float scale; + @android.annotation.SuppressLint("WearPasswordInput") // Local masking demo, never an account password prompt. + @Override public void onCreate(Bundle saved){ + super.onCreate(saved);scale=getResources().getDisplayMetrics().widthPixels/410f; + boolean round=getResources().getConfiguration().isScreenRound(); + if(round){getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN|View.SYSTEM_UI_FLAG_HIDE_NAVIGATION|View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);} + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + ScrollView scroll=new ScrollView(this);scroll.setFillViewport(true);scroll.setBackgroundColor(0xffF2F3F6); + LinearLayout body=new LinearLayout(this);body.setOrientation(LinearLayout.VERTICAL);int side=px(round?49:22);body.setPadding(side,px(round?35:26),side,px(round?38:24));scroll.addView(body); + label(body,"蟹蟹输入",27,0xff20232B,Gravity.CENTER); + label(body,"应用内键盘组件",13,0xff8B919D,Gravity.CENTER); + TextView hint=label(body,"轻点输入,自动展开\n无需更换系统输入法",14,0xff2867D9,Gravity.CENTER);hint.setPadding(0,px(12),0,px(10)); + field(body,"点击这里试写",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_MULTI_LINE,"demo_text",1); + field(body,"私密输入",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD,"demo_password",2); + field(body,"数字输入",InputType.TYPE_CLASS_NUMBER|InputType.TYPE_NUMBER_FLAG_DECIMAL,"demo_number",3); + field(body,"网址输入",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_URI,"demo_url",4); + Button license=new Button(this);license.setAllCaps(false);license.setText("开源与接入说明");license.setTextSize(TypedValue.COMPLEX_UNIT_PX,px(14));license.setTextColor(0xff727987);license.setBackgroundColor(Color.TRANSPARENT); + license.setOnClickListener(view->new android.app.AlertDialog.Builder(this).setTitle("蟹蟹输入适配改版").setMessage("MIT允许商用。基于CrabKeyboard词库,由herry重写RTOS及Android应用内适配。原作者kqakqakqa、crabKun;词频jieba。\n\n键盘仅由接入它的应用使用,不更改系统默认输入法。不申请网络权限,不保存输入内容。\n\n开发者接入与源码:git2.sydxky.cn/sydxky/crab-input-rtos").setPositiveButton("知道了",null).show());body.addView(license); + body.setFocusableInTouchMode(true);body.requestFocus();setContentView(scroll); + } + private TextView label(LinearLayout body,String text,int size,int color,int gravity){TextView view=new TextView(this);view.setText(text);view.setTextSize(TypedValue.COMPLEX_UNIT_PX,px(size));view.setTextColor(color);view.setGravity(gravity);body.addView(view);return view;} + private void field(LinearLayout body,String hint,int type,String name,int id){ + EditText editor=new EditText(this);editor.setId(id);editor.setSaveEnabled(false);editor.setHint(hint);editor.setContentDescription(name);editor.setInputType(type);editor.setTextSize(TypedValue.COMPLEX_UNIT_PX,px(19));editor.setTextColor(0xff20232B);editor.setImeOptions(EditorInfo.IME_ACTION_DONE);editor.setMinHeight(px(44)); + // Vendor EditText backgrounds add large vertical insets on watches. + // Explicit padding keeps both empty hints and entered text fully visible. + editor.setPadding(px(12),0,px(12),0);editor.setGravity(Gravity.CENTER_VERTICAL); + editor.setIncludeFontPadding(false);editor.setSingleLine(true); + android.graphics.drawable.GradientDrawable background=new android.graphics.drawable.GradientDrawable();background.setColor(Color.WHITE);background.setCornerRadius(px(12));editor.setBackground(background); + LinearLayout.LayoutParams layout=new LinearLayout.LayoutParams(-1,px(47));layout.topMargin=px(7);body.addView(editor,layout);bindings.add(CrabKeyboard.attach(this,editor)); + } + private int px(int value){return Math.round(value*scale);} + @Override protected void onDestroy(){for(CrabKeyboard.Binding binding:bindings)binding.close();bindings.clear();super.onDestroy();} +} diff --git a/android/app/src/main/res/drawable/ic_launcher.xml b/android/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..5c16b87 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..4314806 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..ede1576 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,4 @@ +plugins { + id 'com.android.application' version '9.2.1' apply false + id 'com.android.library' version '9.2.1' apply false +} diff --git a/android/build.ps1 b/android/build.ps1 new file mode 100644 index 0000000..41748a8 --- /dev/null +++ b/android/build.ps1 @@ -0,0 +1,11 @@ +param([string]$ReleaseSigningProperties) +$ErrorActionPreference='Stop' +Push-Location $PSScriptRoot +try { + & python '.\scripts\build-dictionary.py' + if($LASTEXITCODE -ne 0){throw 'Dictionary build failed'} + $tasks=@(':keyboard:testDebugUnitTest',':keyboard:lintRelease',':keyboard:assembleRelease',':app:lintRelease',':host:assembleDebug') + if($ReleaseSigningProperties){$tasks+=@(':app:assembleRelease',"-PreleaseSigningProperties=$ReleaseSigningProperties")}else{$tasks+=':app:assembleDebug'} + & '.\gradlew.bat' @tasks + if($LASTEXITCODE -ne 0){throw 'Android verification/build failed'} +} finally {Pop-Location} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..e64c7d0 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true +# Production source is Java. Keep Kotlin needed by the test runner in test APKs. +kotlin.stdlib.default.dependency=false diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2ec1bef --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.5.1-bin.zip +networkTimeout=120000 +networkRetries=3 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..2b42e9e --- /dev/null +++ b/android/gradlew @@ -0,0 +1,12 @@ +#!/bin/sh + +APP_HOME=$(cd "${0%/*}" && pwd -P) +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +if [ -n "$JAVA_HOME" ]; then + JAVACMD=$JAVA_HOME/bin/java +else + JAVACMD=java +fi + +exec "$JAVACMD" -Xmx64m -Xms64m -Dorg.gradle.appname=gradlew -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..c0ced34 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%\bin\java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/host/build.gradle b/android/host/build.gradle new file mode 100644 index 0000000..3204740 --- /dev/null +++ b/android/host/build.gradle @@ -0,0 +1,8 @@ +plugins { id 'com.android.application' } +android { + namespace 'com.sydxky.crabinput.host' + compileSdk 37 + defaultConfig { applicationId 'com.sydxky.crabinput.host'; minSdk 27; targetSdk 35; versionCode 10200; versionName '1.2.0' } + compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } +} +dependencies { implementation project(':keyboard') } diff --git a/android/host/src/main/AndroidManifest.xml b/android/host/src/main/AndroidManifest.xml new file mode 100644 index 0000000..55a19cd --- /dev/null +++ b/android/host/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java b/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java new file mode 100644 index 0000000..700f478 --- /dev/null +++ b/android/host/src/main/java/com/sydxky/crabinput/host/HostActivity.java @@ -0,0 +1,20 @@ +package com.sydxky.crabinput.host; +import android.app.Activity; +import android.os.Bundle; +import android.view.Gravity; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.sydxky.crabinput.CrabKeyboard; + +/** Separate application demonstrates the public library API, not a system IME. */ +public final class HostActivity extends Activity { + private CrabKeyboard.Binding binding; + @Override public void onCreate(Bundle saved){ + super.onCreate(saved);LinearLayout layout=new LinearLayout(this);layout.setOrientation(LinearLayout.VERTICAL);layout.setGravity(Gravity.CENTER);layout.setPadding(55,45,55,45); + TextView title=new TextView(this);title.setText("独立应用接入示例");title.setTextSize(13);layout.addView(title); + EditText editor=new EditText(this);editor.setContentDescription("fixture_editor");editor.setHint("点击输入");layout.addView(editor,new LinearLayout.LayoutParams(-1,-2)); + layout.setFocusableInTouchMode(true);layout.requestFocus();setContentView(layout);binding=CrabKeyboard.attach(this,editor); + } + @Override protected void onDestroy(){if(binding!=null)binding.close();super.onDestroy();} +} diff --git a/android/keyboard/build.gradle b/android/keyboard/build.gradle new file mode 100644 index 0000000..db5aa35 --- /dev/null +++ b/android/keyboard/build.gradle @@ -0,0 +1,9 @@ +plugins { id 'com.android.library' } +android { + namespace 'com.sydxky.crabinput.keyboard' + compileSdk 37 + defaultConfig { minSdk 27; consumerProguardFiles 'consumer-rules.pro' } + compileOptions { sourceCompatibility JavaVersion.VERSION_11; targetCompatibility JavaVersion.VERSION_11 } + lint { abortOnError true } +} +dependencies { testImplementation 'junit:junit:4.13.2' } diff --git a/android/keyboard/consumer-rules.pro b/android/keyboard/consumer-rules.pro new file mode 100644 index 0000000..9eecaa6 --- /dev/null +++ b/android/keyboard/consumer-rules.pro @@ -0,0 +1,3 @@ +# Public integration API. No reflection is required by the keyboard engine. +-keep public class com.sydxky.crabinput.CrabKeyboard { public *; } +-keep public class com.sydxky.crabinput.CrabKeyboard$Binding { public *; } diff --git a/android/keyboard/src/main/AndroidManifest.xml b/android/keyboard/src/main/AndroidManifest.xml new file mode 100644 index 0000000..94cbbcf --- /dev/null +++ b/android/keyboard/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/android/keyboard/src/main/assets/dictionary-manifest.json b/android/keyboard/src/main/assets/dictionary-manifest.json new file mode 100644 index 0000000..d708c22 --- /dev/null +++ b/android/keyboard/src/main/assets/dictionary-manifest.json @@ -0,0 +1,52 @@ +{ + "keys": 104837, + "candidates": 163238, + "longest_word": 4, + "sha256": "fa01b1ab0264d8b271a32338b93a64b8670a71d57264c8586c89ae3cf3ddd8a2", + "bytes": 12644352, + "round_trip": true, + "source_files": { + "key0.dat": "795a5316089b67a7bffabf87080bd61f3720f9e0fd2f2866a092901f99b794e8", + "key1.dat": "a4bcae2b6eb30084d3cf2ac64e5d9ca71eb94e8ac1f7c8fc8b38a5d478982be2", + "key10.dat": "5731d9f3a3cad0304ecc650d74c723185c9d43f74d66d7b5622454d1fc31f99c", + "key11.dat": "fa2a4bfa64cb236a051ceaf6b9b227159dde7aaba21ae997baafac6df4332345", + "key12.dat": "7685ad03e3b5eeac404e8d6f0fc9e8524262b2c4a42ef0301d9e85c64d3db63b", + "key13.dat": "b06f6fc0e2769a8f6e9491a0b97148f8c8d211ba8fe8ed122e1dbffca0eb7399", + "key14.dat": "5d21973e2c1d833e93bfe51c42064b26743f6012e35c73ffcd2b5046cca3eeff", + "key15.dat": "62c3324ccc143cd4e9ee533b0117ee06fc893d58c5d2ed0353da4dde33f5eab1", + "key16.dat": "8c2c0fc7568cf6b674e8630449eb989c7ebdf34f34d0b2041a59748bd20c9638", + "key17.dat": "f62c9c886ca4ee387a780a8c1e24116d73dedfda228de3614978c22f994ef7ec", + "key18.dat": "fa7fda7b3b452908623383a6285aa15b265a41d48de58524fe0f0879b8aad259", + "key19.dat": "00674fb37b56b5973cf0a7ee38b8258652949e4c02782ff6651d2aa63b9c62fc", + "key2.dat": "45e860c89d1362de6007e46a6aa84a0d2fcd9682924de6e5f111cd536e18bfd1", + "key20.dat": "1715a74e0bd00bf89406806682d865a53782d4cd9c4aee5050e547e8a84ffad6", + "key3.dat": "00b464b6ee7ecd7e4406150eecf6c512e22611c6505a5045834cd5c3b7d9068d", + "key4.dat": "a5c5719d09f931efc44b629fe92fd3faea0cc8c5a6ec28d0a9c9b9cdb0eb5ad9", + "key5.dat": "3d5e43374441150af704b9dae9a6d62ac66406306b37709f12119fe44153e353", + "key6.dat": "586c47ee8542f8bf59d7b4bf71fb1715a1bf3e7452d91068edcaf8b566e81e52", + "key7.dat": "4525656f4d9fcd85bff07b876af75eb84f4f711a55a63c258c491d22dfeedff5", + "key8.dat": "8e88d27fbc09fc3b133cbafd3b8e2f1b2269950cfcae12c23c1e152013f63f47", + "key9.dat": "c741aefb551b686a9ade67d3a16a5230d74134af6c3d0faf08557e19698d5825", + "value1.dat": "81c1798a0dda463cacae6ac5285ed9ad0cd0730dde167c18ee3500f1e1d8d8f2", + "value10.dat": "9c4fded85bb7467304df917d510d987a824785058a9f8bb9c9095571b49fbf0d", + "value11.dat": "3be277f87112c7bbc7242882b4ca92ff6e9d38d7be1b51b7899545a5c13d1a19", + "value12.dat": "789c47185b5aaa93293239b356a6a4928d3d29daaf9a4d18f3c94f392f861613", + "value13.dat": "b7366771afebe2ddc466eb9dc8dd840b1e7a8a8214354e2930d4fa4346f27dc6", + "value14.dat": "9c638ef5d554738087505b158482e5b0caba59a36df65b9d0f3e5eee78f91588", + "value15.dat": "846ea5776c9e11f44762cc032d690435464a312e82813530b01cb6001bd0a2f6", + "value16.dat": "b86c42aaf4a818b4eaccd2663b6fb01f3ef3b1dc8482f20636cbde1a95ca52da", + "value17.dat": "2f809c59a318d7d8d8605985092517902b6c627a40936af856785ed596c1133e", + "value18.dat": "d64613d9bc90f503b964984939de0dc881c3a0feda2684bade0c572555c2099d", + "value19.dat": "ecfb7ba56450c671fc941d56556a42d508569e341c5dd2581b18a3d7d27ab55c", + "value2.dat": "d66703e7ac7ab6c2cd3b3c8109e656e1f78ace3e2bb49123ce3a2b3da4ca04d0", + "value20.dat": "896757081c224c72586dfa5babb4300c93639a27ba9a70eb34b9dfd5c23735f2", + "value3.dat": "19c47c2f8b8483946db81572368b3f3b2aa61c5a03953351e327f3f83cb31275", + "value4.dat": "1be64f23183fca36920ae3f002d541febaa1b26b5ca3018ee53ffdd05546d56d", + "value5.dat": "7b8a109e705537784a0a8075cc4fe3d1de8288a6b6b0240627685c81ed9695a9", + "value6.dat": "5b4028a017b228357446f799cd29b28dd5a24d63340f8b48ce858c75c64029f1", + "value7.dat": "b9f6e109ba5f1b5bca758613cb5cccfecf358915ce6415525c333b19515d6428", + "value8.dat": "3d1622d4717680b31a6e037c88e7f046fecfe2339e521272e1d1eae0a7094606", + "value9.dat": "1c0caab370f1f14048ab6dd2da0011bedcddfed0ed0378b5c98171b6b22caf35" + }, + "jieba_sha256": "7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8" +} diff --git a/android/keyboard/src/main/assets/licenses/CrabInput-LICENSE b/android/keyboard/src/main/assets/licenses/CrabInput-LICENSE new file mode 100644 index 0000000..6536c36 --- /dev/null +++ b/android/keyboard/src/main/assets/licenses/CrabInput-LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2026 sydxky +Copyright (c) 2026 kqakqakqa +Copyright (c) 2026 crabKun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt b/android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt new file mode 100644 index 0000000..0502476 --- /dev/null +++ b/android/keyboard/src/main/assets/licenses/CrabInput-NOTICE.txt @@ -0,0 +1,17 @@ +# 蟹蟹输入 Android手表适配改版 + +本项目由sydxky基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的MIT词库重新实现Android应用内键盘组件、圆屏布局、九宫/14键消歧及候选分页。不是上游原版,也不表示上游作者或设备厂商对本改版作出背书。 + +上游固定提交:`93d36b5002dee80a3d253b1ec7ec9e29800a104c`。保留原作者kqakqakqa、crabKun的版权声明和MIT许可证;原始词库位于同仓库RTOS目录。Android自研适配部分以[MIT许可证](./LICENSE)发布,**允许商用、修改、再分发和闭源集成**,须保留适用版权及许可全文。 + +第三方内容继续按各自许可证使用: + +| 内容 | 作者/项目 | 许可与位置 | +| --- | --- | --- | +| CrabKeyboard V5词库 | kqakqakqa、crabKun | MIT,`keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt` | +| 本地候选排序词频 | jieba 0.42.1 / Sun Junyi | MIT,`keyboard/src/main/assets/licenses/jieba-LICENSE.txt` | +| Gradle wrapper | Gradle | Apache-2.0,`keyboard/src/main/assets/licenses/Gradle-LICENSE.txt` | + +14键参考公开演示中的相邻字母分组,自行实现离线消歧;不包含华为内部模型或固件代码。Android SDK、JDK和构建工具不随源码分发,按各自许可使用。AndroidX及JUnit仅用于本地测试,不打入正式APK。 + +公开仓库:。匿名问题与建议:。 diff --git a/android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt b/android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt new file mode 100644 index 0000000..5ab898e --- /dev/null +++ b/android/keyboard/src/main/assets/licenses/Gradle-LICENSE.txt @@ -0,0 +1,422 @@ + + 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 + + +============================================================================== +Licenses for included components: + +------------------------------------------------------------------------------ +Eclipse Public License 1.0 +https://opensource.org/licenses/EPL-1.0 + +junit:junit +org.sonatype.aether:aether-api +org.sonatype.aether:aether-connector-wagon +org.sonatype.aether:aether-impl +org.sonatype.aether:aether-spi +org.sonatype.aether:aether-util + +------------------------------------------------------------------------------ +3-Clause BSD +https://opensource.org/licenses/BSD-3-Clause + +com.google.code.findbugs:jsr305 + +org.hamcrest:hamcrest +BSD 3-Clause License + +Copyright (c) 2000-2024, www.hamcrest.org + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +com.esotericsoftware.kryo:kryo +com.esotericsoftware.minlog:minlog +Copyright (c) 2008-2018, Nathan Sweet All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +org.ow2.asm:asm +org.ow2.asm:asm-analysis +org.ow2.asm:asm-commons +org.ow2.asm:asm-tree +org.ow2.asm:asm-util +ASM: a very small and fast Java bytecode manipulation framework + Copyright (c) 2000-2011 INRIA, France Telecom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +MIT + +com.googlecode.plist:dd-plist +dd-plist - An open source library to parse and generate property lists +Copyright (C) 2016 Daniel Dreibrodt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +org.bouncycastle:bcpg-jdk15on +org.bouncycastle:bcprov-jdk15on +Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +org.slf4j:jcl-over-slf4j +org.slf4j:jul-to-slf4j +org.slf4j:log4j-over-slf4j +org.slf4j:slf4j-api +Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland) +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------------------ +CDDL +https://opensource.org/licenses/CDDL-1.0 + +com.sun.xml.bind:jaxb-impl + +------------------------------------------------------------------------------ +LGPL 2.1 +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html + +org.samba.jcifs:jcifs + +org.jetbrains.intellij.deps:trove4j + +------------------------------------------------------------------------------ +License for the GNU Trove library included by the Kotlin embeddable compiler +------------------------------------------------------------------------------ +The source code for GNU Trove is licensed under the Lesser GNU Public License (LGPL). + + Copyright (c) 2001, Eric D. Friedman All Rights Reserved. This library is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + +Two classes (HashFunctions and PrimeFinder) included in Trove are licensed under the following terms: + + Copyright (c) 1999 CERN - European Organization for Nuclear Research. Permission to use, copy, modify, distribute and sell this software + and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and + that both that copyright notice and this permission notice appear in supporting documentation. CERN makes no representations about the + suitability of this software for any purpose. It is provided "as is" without expressed or implied warranty. + +The source code of modified GNU Trove library is available at + https://github.com/JetBrains/intellij-deps-trove4j (with trove4j_changes.txt describing the changes) + +------------------------------------------------------------------------------ +Eclipse Distribution License 1.0 +https://www.eclipse.org/org/documents/edl-v10.php + +org.eclipse.jgit:org.eclipse.jgit + +------------------------------------------------------------------------------ +BSD-style + +com.jcraft:jzlib +com.github.mwiede:jsch + +Copyright (c) 2002-2015 Atsuhiko Yamanaka, JCraft,Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +Eclipse Public License 2.0 +https://www.eclipse.org/legal/epl-2.0/ + +org.junit.platform:junit-platform-launcher + +------------------------------------------------------------------------------ +Mozilla Public License 2.0 +https://www.mozilla.org/en-US/MPL/2.0/ + +org.mozilla:rhino diff --git a/android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt b/android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt new file mode 100644 index 0000000..097f88a --- /dev/null +++ b/android/keyboard/src/main/assets/licenses/crabKeyboard-LICENSE.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 kqakqakqa +Copyright (c) 2026 crabKun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt b/android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt new file mode 100644 index 0000000..9d7e66b --- /dev/null +++ b/android/keyboard/src/main/assets/licenses/jieba-LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Sun Junyi + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java b/android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java new file mode 100644 index 0000000..73b5e56 --- /dev/null +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/Candidate.java @@ -0,0 +1,7 @@ +package com.sydxky.crabinput; + +public final class Candidate { + public final String word; + public final int consume; + public Candidate(String word, int consume) { this.word=word; this.consume=consume; } +} diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java b/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java new file mode 100644 index 0000000..da99544 --- /dev/null +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/CandidatePager.java @@ -0,0 +1,37 @@ +package com.sydxky.crabinput; + +import java.util.ArrayList; +import java.util.List; + +/** 4 columns x 4 row units; long phrases get full width and extra row units. */ +public final class CandidatePager { + public static final class Cell { + public final Candidate candidate; + public final int span; + Cell(Candidate candidate,int span) { this.candidate=candidate; this.span=span; } + } + public static final class Row { + public final List cells=new ArrayList<>(); + public final int units; + public int slots; + Row(int units) { this.units=units; } + } + public static List> pages(List candidates) { + List> pages=new ArrayList<>();List page=new ArrayList<>(); + Row row=null;int used=0; + for(Candidate candidate:candidates) { + int length=candidate.word.codePointCount(0,candidate.word.length()); + int span=length==1?1:length<=4?2:4; + int units=span==4?Math.max(1,(length+11)/12):1; + if(row!=null&&(row.slots+span>4||row.units!=units)) { page.add(row);used+=row.units;row=null; } + if(used+units>4) { if(!page.isEmpty()) pages.add(page);page=new ArrayList<>();used=0; } + if(row==null) row=new Row(units); + row.cells.add(new Cell(candidate,span));row.slots+=span; + if(row.slots==4) { page.add(row);used+=row.units;row=null; } + } + if(row!=null)page.add(row); + if(!page.isEmpty())pages.add(page); + return pages; + } + private CandidatePager() {} +} diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java b/android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java new file mode 100644 index 0000000..e7d8690 --- /dev/null +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/CrabKeyboard.java @@ -0,0 +1,61 @@ +package com.sydxky.crabinput; + +import android.app.Activity; +import android.app.Application; +import android.app.Dialog; +import android.os.Bundle; +import android.text.Editable; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +/** Public AAR entry point. Works inside the host app; no system IME settings. */ +public final class CrabKeyboard { + public static Binding attach(Activity activity,EditText editor){return new Binding(activity,editor);} + public static final class Binding implements AutoCloseable,Application.ActivityLifecycleCallbacks { + private final Activity activity; + private final EditText editor; + private final boolean originalShowSoftInput; + private Dialog dialog; + private boolean closed; + private Binding(Activity activity,EditText editor){ + if(android.os.Looper.myLooper()!=android.os.Looper.getMainLooper())throw new IllegalStateException("Attach on the UI thread"); + this.activity=activity;this.editor=editor;originalShowSoftInput=editor.getShowSoftInputOnFocus();editor.setShowSoftInputOnFocus(false); + editor.setOnFocusChangeListener((view,focus)->{if(focus)show();});editor.setOnClickListener(view->show());activity.getApplication().registerActivityLifecycleCallbacks(this); + } + public void show(){ + if(closed||dialog!=null||activity.isFinishing()||activity.isDestroyed())return; + if(editor.getSelectionStart()<0)editor.setSelection(editor.length()); + EditorInfo info=new EditorInfo();info.inputType=editor.getInputType();info.imeOptions=editor.getImeOptions(); + BaseInputConnection connection=new BaseInputConnection(editor,true){ + @Override public Editable getEditable(){return editor.getText();} + @Override public boolean sendKeyEvent(KeyEvent event){return editor.dispatchKeyEvent(event);} + @Override public boolean performEditorAction(int actionCode){editor.onEditorAction(actionCode);return true;} + }; + Dialog session=new Dialog(activity,android.R.style.Theme_Material_Light_NoActionBar);dialog=session; + KeyboardController controller=new KeyboardController(activity,connection,info,session::dismiss); + session.setOwnerActivity(activity);session.setContentView(controller.surface,new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,controller.surface.desiredHeight())); + Window window=session.getWindow(); + if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);window.setGravity(Gravity.BOTTOM); + if(activity.getResources().getConfiguration().isScreenRound()){window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN|View.SYSTEM_UI_FLAG_HIDE_NAVIGATION|View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}} + session.setOnDismissListener(ignored->{controller.close();connection.closeConnection();if(dialog==session)dialog=null;});session.show(); + if(window!=null)window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,activity.getResources().getConfiguration().isScreenRound()?ViewGroup.LayoutParams.MATCH_PARENT:ViewGroup.LayoutParams.WRAP_CONTENT); + } + public void dismiss(){if(dialog!=null)dialog.dismiss();} + @Override public void close(){if(closed)return;closed=true;dismiss();editor.setOnFocusChangeListener(null);editor.setOnClickListener(null);editor.setShowSoftInputOnFocus(originalShowSoftInput);activity.getApplication().unregisterActivityLifecycleCallbacks(this);} + @Override public void onActivityStopped(Activity value){if(value==activity)dismiss();} + @Override public void onActivityDestroyed(Activity value){if(value==activity)close();} + @Override public void onActivityCreated(Activity value,Bundle state){} + @Override public void onActivityStarted(Activity value){} + @Override public void onActivityResumed(Activity value){} + @Override public void onActivityPaused(Activity value){} + @Override public void onActivitySaveInstanceState(Activity value,Bundle state){} + } + private CrabKeyboard(){} +} diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java new file mode 100644 index 0000000..e07181d --- /dev/null +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardController.java @@ -0,0 +1,107 @@ +package com.sydxky.crabinput; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.InputType; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** One transient in-app editing session. It never selects a system IME. */ +final class KeyboardController implements KeyboardSurface.Actions,AutoCloseable { + private final Handler main=new Handler(Looper.getMainLooper()); + private final ExecutorService worker=Executors.newSingleThreadExecutor(); + final KeyboardState state=new KeyboardState(); + final KeyboardSurface surface; + private final Lexicon lexicon; + private final InputConnection input; + private final EditorInfo info; + private final Runnable dismiss; + private final Context context; + private Future pending; + private int revision; + private boolean active=true,repeat,closed; + private final Runnable lookup=()->query(revision,state.composition,state.mode); + private final Runnable deleteAgain=new Runnable(){@Override public void run(){if(repeat&&active){delete();main.postDelayed(this,115);}}}; + @android.annotation.SuppressLint("WearPasswordInput") // Detect host password editors, no account login. + KeyboardController(Context context,InputConnection input,EditorInfo info,Runnable dismiss){ + this.context=context;this.input=input;this.info=info;this.dismiss=dismiss;lexicon=new Lexicon(context); + int kind=info.inputType&InputType.TYPE_MASK_CLASS,variation=info.inputType&InputType.TYPE_MASK_VARIATION; + state.secret=kind==InputType.TYPE_CLASS_TEXT&&(variation==InputType.TYPE_TEXT_VARIATION_PASSWORD||variation==InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD||variation==InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)||kind==InputType.TYPE_CLASS_NUMBER&&variation==InputType.TYPE_NUMBER_VARIATION_PASSWORD; + state.mode=kind==InputType.TYPE_CLASS_NUMBER||kind==InputType.TYPE_CLASS_PHONE?"num":state.secret||variation==InputType.TYPE_TEXT_VARIATION_URI||variation==InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS?"en":"t9"; + surface=new KeyboardSurface(context,state,this);refresh(); + } + private void invalidateLookup(){revision++;main.removeCallbacks(lookup);if(pending!=null){pending.cancel(true);pending=null;}state.busy=false;} + private void refresh(){ + if(active&&!state.secret){CharSequence text=input.getTextBeforeCursor(24,0);state.preview=text==null?"":text.toString();}else state.preview=""; + if(!repeat)surface.refresh(); + } + private void message(String text){state.message=text;android.widget.Toast.makeText(context,text,android.widget.Toast.LENGTH_SHORT).show();refresh();} + private boolean chinese(){return state.mode.equals("py")||state.mode.equals("t9")||state.mode.equals("14");} + private void schedule(){ + invalidateLookup();state.page=0;state.candidates=new ArrayList<>();state.pages=new ArrayList<>(); + if(!state.composition.isEmpty()&&active){state.busy=true;main.postDelayed(lookup,40);}refresh(); + } + private void query(int owner,String composition,String mode){ + pending=worker.submit(()->{ + try {List result=lexicon.lookup(composition,mode);main.post(()->{if(!active||revision!=owner)return;state.candidates=result;state.pages=CandidatePager.pages(result);state.busy=false;state.message=result.isEmpty()?"暂无匹配,请退格修改":"离线输入";refresh();});} + catch(Exception ignored){main.post(()->{if(active&&revision==owner){state.busy=false;state.message="词库暂不可用,可切换英文";refresh();}});} + }); + } + private void commit(String text){if(active)input.commitText(text,1);refresh();} + @Override public void choose(Candidate item){ + if(!active||state.busy||state.composition.isEmpty()||!state.candidates.contains(item))return; + String remaining=state.composition.substring(Math.min(item.consume,state.composition.length())); + invalidateLookup();commit(item.word);state.composition=remaining;state.expanded=false;schedule(); + } + private void delete(){ + if(!active)return; + if(!state.composition.isEmpty()){state.composition=state.composition.substring(0,state.composition.length()-1);schedule();return;} + CharSequence selection=input.getSelectedText(0);if(selection!=null&&selection.length()>0)input.commitText("",1);else input.deleteSurroundingTextInCodePoints(1,0);refresh(); + } + @Override public void repeatDelete(boolean start){boolean previous=repeat;main.removeCallbacks(deleteAgain);repeat=start&&active;if(repeat){delete();main.postDelayed(deleteAgain,450);}else if(previous)refresh();} + @Override public void action(String value){ + if(!active)return; + if(value.startsWith("key:")){ + String key=value.substring(4); + if(chinese()){if(state.composition.length()<32){state.composition+=key;schedule();}else message("每段最多32位");}else commit(key);return; + } + if(value.startsWith("mode:")){ + String mode=value.substring(5);if(!state.composition.isEmpty()&&!mode.equals(state.mode)){message("请先选择候选词");return;} + if(mode.equals("en")&&state.mode.equals("en"))state.upper=!state.upper; + if(mode.equals("sym"))state.symbolPage=state.mode.equals("sym")?(state.symbolPage+1)%3:0; + state.mode=mode;state.expanded=false;state.tools=false;schedule();return; + } + switch(value){ + case "delete":delete();return; + case "space":if(!state.composition.isEmpty()){if(!state.busy&&!state.candidates.isEmpty())choose(state.candidates.get(0));}else commit(" ");return; + case "expand":if(!state.candidates.isEmpty()){state.expanded=true;state.tools=false;state.page=0;}break; + case "previous":state.page=Math.max(0,state.page-1);break; + case "next":state.page=Math.min(Math.max(0,state.pages.size()-1),state.page+1);break; + case "back":state.expanded=false;state.tools=false;break; + case "tools":state.expanded=false;state.tools=true;break; + case "clear-composition":state.composition="";schedule();return; + case "hide":dismiss.run();return; + case "left":case "right":if(state.composition.isEmpty()){int key=value.equals("left")?KeyEvent.KEYCODE_DPAD_LEFT:KeyEvent.KEYCODE_DPAD_RIGHT;input.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,key));input.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,key));}break; + case "raw":if(state.mode.equals("py")&&!state.composition.isEmpty()){String text=state.composition;state.composition="";commit(text);schedule();}break; + case "newline":if(!state.secret&&(info.inputType&InputType.TYPE_TEXT_FLAG_MULTI_LINE)!=0)commit("\n");break; + case "done": + if(!state.composition.isEmpty()){message("请先选择候选词");state.expanded=false;break;} + int editorAction=info.imeOptions&EditorInfo.IME_MASK_ACTION; + if(editorAction!=EditorInfo.IME_ACTION_NONE&&editorAction!=EditorInfo.IME_ACTION_UNSPECIFIED)input.performEditorAction(editorAction); + dismiss.run();return; + default:break; + } + refresh(); + } + @Override public void close(){ + if(closed)return;closed=true;active=false;invalidateLookup();repeatDelete(false);main.removeCallbacksAndMessages(null); + state.composition="";state.preview="";state.candidates=new ArrayList<>();state.pages=new ArrayList<>();worker.execute(lexicon::close);worker.shutdown(); + } +} diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java new file mode 100644 index 0000000..bd17ee4 --- /dev/null +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardState.java @@ -0,0 +1,12 @@ +package com.sydxky.crabinput; + +import java.util.ArrayList; +import java.util.List; + +final class KeyboardState { + String mode="t9",composition="",preview="",message="离线输入"; + boolean expanded,tools,upper,secret,busy; + int page,symbolPage; + List candidates=new ArrayList<>(); + List> pages=new ArrayList<>(); +} diff --git a/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java new file mode 100644 index 0000000..b694749 --- /dev/null +++ b/android/keyboard/src/main/java/com/sydxky/crabinput/KeyboardSurface.java @@ -0,0 +1,175 @@ +package com.sydxky.crabinput; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** Native accessible buttons laid out within the actual round screen chord. */ +@android.annotation.SuppressLint("ViewConstructor") // Created by the IME, never inflated from XML. +final class KeyboardSurface extends ViewGroup { + interface Actions { void action(String value);void choose(Candidate item);void repeatDelete(boolean start); } + private static final int BLUE=Color.rgb(40,103,217),INK=Color.rgb(32,35,43),MUTED=Color.rgb(132,139,151),BG=Color.rgb(236,238,242); + private final KeyboardState state; + private final Actions actions; + private final boolean round; + private final List placements=new ArrayList<>(); + private int builtWidth,builtHeight; + private float sx=1,sy=1; + private static final class Placement { + final View view;final Rect bounds; + Placement(View view,Rect bounds){this.view=view;this.bounds=bounds;} + } + KeyboardSurface(Context context,KeyboardState state,Actions actions) { + super(context);this.state=state;this.actions=actions;round=getResources().getConfiguration().isScreenRound(); + setBackgroundColor(BG);setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);setContentDescription("蟹蟹输入键盘"); + } + int desiredHeight() { + android.util.DisplayMetrics dm=getResources().getDisplayMetrics(); + boolean watch=round||(dm.heightPixels/(float)dm.widthPixels<1.35f); + return watch?dm.heightPixels:Math.min(dm.widthPixels,Math.round(dm.heightPixels*.46f)); + } + void refresh() { if(builtWidth>0)build(builtWidth,builtHeight);requestLayout();invalidate(); } + private Rect bounds(float x,float y,float width,float height) { + return new Rect(Math.round(x*sx),Math.round(y*sy),Math.round((x+width)*sx),Math.round((y+height)*sy)); + } + private float inset(float y,float height) { + if(!round)return 8; + float cy=builtHeight/2f,r=builtWidth/2f-7*sx; + float distance=Math.max(Math.abs(y*sy-cy),Math.abs((y+height)*sy-cy)); + float half=(float)Math.sqrt(Math.max(0,r*r-distance*distance)); + return Math.max(8,(builtWidth/2f-half)/sx+2); + } + private void place(View view,float x,float y,float width,float height) { + addView(view);placements.add(new Placement(view,bounds(x,y,width,height))); + } + private TextView label(String text,float font,int color) { + TextView view=new TextView(getContext());view.setText(text);view.setTextColor(color);view.setGravity(Gravity.CENTER); + view.setTextSize(TypedValue.COMPLEX_UNIT_PX,font*Math.min(sx,sy));view.setIncludeFontPadding(false);return view; + } + private Button button(String text,String description,float font,boolean primary,Runnable action) { + Button button=new Button(getContext());button.setText(text);button.setContentDescription(description);button.setAllCaps(false); + button.setGravity(Gravity.CENTER);button.setIncludeFontPadding(false);button.setMinHeight(0);button.setMinimumHeight(0);button.setMinWidth(0);button.setMinimumWidth(0); + button.setPadding(Math.round(3*sx),0,Math.round(3*sx),0);button.setTextSize(TypedValue.COMPLEX_UNIT_PX,font*Math.min(sx,sy)); + button.setTextColor(primary?Color.WHITE:INK);button.setStateListAnimator(null); + GradientDrawable shape=new GradientDrawable();shape.setColor(primary?BLUE:Color.WHITE);shape.setCornerRadius(10*Math.min(sx,sy)); + button.setBackground(new RippleDrawable(ColorStateList.valueOf(0x332867D9),shape,null));button.setOnClickListener(v->action.run());return button; + } + // Native Button click remains available to accessibility; touch deletion starts on down. + @android.annotation.SuppressLint("ClickableViewAccessibility") + private void action(String text,String value,float x,float y,float width,float height,float font,boolean primary) { + String name=value.equals("delete")?"删除":value.equals("done")?"完成":value.equals("hide")?"收起键盘":value.equals("expand")?"更多候选":value.equals("back")?"返回键盘":value.equals("previous")?"上一页":value.equals("next")?"下一页":text; + Button button=button(text,name,font,primary,()->actions.action(value)); + if(value.equals("delete"))button.setOnTouchListener((v,event)->{ + if(event.getActionMasked()==MotionEvent.ACTION_DOWN)actions.repeatDelete(true); + if(event.getActionMasked()==MotionEvent.ACTION_UP||event.getActionMasked()==MotionEvent.ACTION_CANCEL)actions.repeatDelete(false); + return true; + }); + place(button,x,y,width,height); + } + private void row(String[] captions,String[] values,float y,float height,float font) { + float inset=inset(y,height),width=410-2*inset,cell=width/captions.length; + for(int i=0;iactions.choose(item));b.setMaxLines(1);b.setAutoSizeTextTypeUniformWithConfiguration(10,22,1,TypedValue.COMPLEX_UNIT_PX);place(b,left+i*cell+2,candidateY,cell-4,32);} + if(count>0)action("⌄","expand",410-left-35,candidateY,33,32,24,false); + else place(label(state.busy?"正在查词…":state.message,12,MUTED),left,candidateY,available,32); + } + float keyY=candidateY+38; + String[][] captions,values; + switch(state.mode) { + case "t9":captions=new String[][]{{"标点","ABC","DEF"},{"GHI","JKL","MNO"},{"PQRS","TUV","WXYZ"}};values=new String[][]{{"mode:sym","key:2","key:3"},{"key:4","key:5","key:6"},{"key:7","key:8","key:9"}};break; + case "14":captions=new String[][]{{"QW","ER","TY","UI","OP"},{"AS","DF","GH","JK","L"},{"ZX","CV","BN","M"}};values=new String[][]{{"key:0","key:1","key:2","key:3","key:4"},{"key:5","key:6","key:7","key:8","key:9"},{"key:a","key:b","key:c","key:d"}};break; + case "num":captions=new String[][]{{"1","2","3"},{"4","5","6"},{"7","8","9"},{".","0","-"}};values=values(captions);break; + case "sym":String[] symbols={",。?!:;","()【】《》","、…·“”「", "!\"#$%&","'()*+,","-./:;<", "=>?@[\\","]^_`{|","}~€£¥°"};captions=new String[][]{split(symbols[state.symbolPage*3]),split(symbols[state.symbolPage*3+1]),split(symbols[state.symbolPage*3+2])};values=values(captions);break; + default:captions=new String[][]{split("qwertyuiop"),split("asdfghjkl"),split("zxcvbnm")};if(state.upper&&state.mode.equals("en"))for(String[] line:captions)for(int i=0;i=7?21:24); + float modeY=keyY+captions.length*(keyHeight+4)+3; + row(new String[]{"九宫","26键","14键",state.upper?"ABC":"abc","123","符号"},new String[]{"mode:t9","mode:py","mode:14","mode:en","mode:num","mode:sym"},modeY,27,13); + bottom(modeY+34,false); + } + private void buildCandidates(float y) { + int count=state.pages.size();float safe=inset(y,18); + place(label(state.candidates.size()+" 项 · "+(state.page+1)+" / "+Math.max(1,count),12,MUTED),safe,y,410-2*safe,18); + float gridY=y+25;int unit=43; + if(count>0)for(CandidatePager.Row row:state.pages.get(state.page)) { + float rowHeight=row.units*unit-4,left=inset(gridY,rowHeight),cell=(410-2*left)/4;int slot=0; + for(CandidatePager.Cell entry:row.cells) { + Candidate item=entry.candidate;Button button=button(item.word,item.word,entry.span==1?25:entry.span==2?23:20,false,()->actions.choose(item)); + button.setMaxLines(row.units*2);button.setEllipsize(null); + place(button,left+slot*cell+2,gridY,entry.span*cell-4,rowHeight);slot+=entry.span; + } + gridY+=row.units*unit; + } + float pagerY=y+25+4*unit+3; + row(new String[]{"‹","›"},new String[]{"previous","next"},pagerY,29,27); + bottom(pagerY+36,true); + } + private void buildTools(float y) { + row(new String[]{"← 光标","光标 →"},new String[]{"left","right"},y+7,37,18); + row(new String[]{"换行","拼音上屏"},new String[]{"newline","raw"},y+53,37,18); + row(new String[]{"清除拼音","空格"},new String[]{"clear-composition","space"},y+99,37,18); + place(label(state.secret?"私密输入 · 不学习、不保存":"完全离线 · 不采集输入内容",12,MUTED),45,y+150,320,26); + bottom(y+190,true); + } + private void bottom(float y,boolean back) { + float left=inset(y,34),cell=(410-2*left)/3; + action(back?"键盘":"空格",back?"back":"space",left+2,y,cell-4,34,17,false); + action("⌫","delete",left+cell+2,y,cell-4,34,25,false); + action("✓","done",left+2*cell+2,y,cell-4,34,23,true); + // Preview taps open cursor tools without taking an extra keyboard key. + for(Placement placement:placements)if(placement.view instanceof TextView&&!(placement.view instanceof Button)&&placement.view.getContentDescription()==null){if(((TextView)placement.view).getText().toString().equals(state.secret?"私密输入":state.preview.isEmpty()?"开始输入…":state.preview)){placement.view.setOnClickListener(v->actions.action("tools"));placement.view.setContentDescription("编辑光标");}} + } + private boolean isChinese(){return state.mode.equals("py")||state.mode.equals("t9")||state.mode.equals("14");} + private String modeName(){return state.mode.equals("py")?"26键全拼 · v 代表 ü":state.mode.equals("t9")?"九宫拼音":state.mode.equals("14")?"14键拼音":state.mode.equals("en")?"英文":state.mode.equals("sym")?"符号 · "+(state.symbolPage+1)+"/3 · 再点符号翻页":"数字";} + private static String[] split(String text){String[] result=new String[text.length()];for(int i=0;i=0){index=i;break;} + if(index<0)return "";result.append(tokens.charAt(index)); + } + return result.toString(); + } + private void open() throws Exception { + if(db!=null)return; + File directory=context.getNoBackupFilesDir();File target=new File(directory,"crab-v5-1.db"); + if(!target.isFile()) { + File temporary=new File(directory,"crab-v5-1.tmp"); + try(InputStream input=context.getAssets().open("dictionary.db");FileOutputStream output=new FileOutputStream(temporary)) { + byte[] buffer=new byte[32768];int length;while((length=input.read(buffer))!=-1)output.write(buffer,0,length);output.getFD().sync(); + } + if(!temporary.renameTo(target))throw new java.io.IOException("Dictionary installation failed"); + } + db=SQLiteDatabase.openDatabase(target.getPath(),null,SQLiteDatabase.OPEN_READONLY|SQLiteDatabase.NO_LOCALIZED_COLLATORS); + } + public List lookup(String input,String mode) throws Exception { + List result=new ArrayList<>(); + String pattern=mode.equals("14")?"[0-9a-d]{1,32}":mode.equals("t9")?"[2-9]{1,32}":"[a-z]{1,32}"; + if(!input.matches(pattern))return result;open(); + String column=mode.equals("14")?"code14":mode.equals("t9")?"code9":"pinyin"; + StringBuilder marks=new StringBuilder();String[] prefixes=new String[input.length()]; + for(int i=0;i0)marks.append(',');marks.append('?');prefixes[i]=input.substring(0,i+1);} + String order=mode.equals("py")?"length(e.pinyin) DESC,e.pinyin,w.ordinal":"length(e.pinyin) DESC,w.frequency DESC,e.pinyin,w.ordinal"; + Set seen=new HashSet<>(); + try(Cursor cursor=db.rawQuery("SELECT w.word,length(e.pinyin) FROM entries e JOIN words w ON w.entry=e.id WHERE e."+column+" IN ("+marks+") ORDER BY "+order,prefixes)) { + while(cursor.moveToNext()) { + if(Thread.currentThread().isInterrupted())return new ArrayList<>(); + String word=cursor.getString(0);int consume=cursor.getInt(1); + if(seen.add(word+"@"+consume))result.add(new Candidate(word,consume)); + } + } + return result; + } + @Override public void close() { if(db!=null){db.close();db=null;} } +} diff --git a/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java b/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java new file mode 100644 index 0000000..27344b3 --- /dev/null +++ b/android/keyboard/src/test/java/com/sydxky/crabinput/CandidatePagerTest.java @@ -0,0 +1,26 @@ +package com.sydxky.crabinput; + +import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import static org.junit.Assert.*; + +public class CandidatePagerTest { + @Test public void singleCharactersFillSixteenCellsAndKeepAllResults(){ + List input=new ArrayList<>();for(int i=0;i<90;i++)input.add(new Candidate(String.valueOf((char)(0x4E00+i)),3)); + List> pages=CandidatePager.pages(input);assertEquals(6,pages.size());assertEquals(4,pages.get(0).size()); + List restored=new ArrayList<>();for(List page:pages)for(CandidatePager.Row row:page)for(CandidatePager.Cell cell:row.cells)restored.add(cell.candidate); + assertEquals(input,restored);assertEquals(16,pages.get(0).stream().mapToInt(row->row.cells.size()).sum()); + } + @Test public void phrasesUseWiderCellsAndLongPhrasesGetExtraRows(){ + List input=Arrays.asList(new Candidate("你好",5),new Candidate("完整词组",4),new Candidate("长词组必须完整展示而不能省略任何文字",19),new Candidate("😀",1)); + List> pages=CandidatePager.pages(input);assertEquals(2,pages.get(0).get(0).cells.size());assertEquals(2,pages.get(0).get(0).cells.get(0).span); + assertEquals(2,pages.get(0).get(1).units);assertEquals(1,pages.get(0).get(2).cells.get(0).span); + for(List page:pages)assertTrue(page.stream().mapToInt(row->row.units).sum()<=4); + } + @Test public void emptyListHasNoPhantomButtons(){assertTrue(CandidatePager.pages(new ArrayList<>()).isEmpty());} + @Test public void pairedAndT9MappingsCoverExpectedWords(){ + assertEquals("c3754",Lexicon.encode("nihao","14"));assertEquals("64426",Lexicon.encode("nihao","t9"));assertEquals("",Lexicon.encode("a!","14")); + } +} diff --git a/android/scripts/build-dictionary.py b/android/scripts/build-dictionary.py new file mode 100644 index 0000000..e8f3029 --- /dev/null +++ b/android/scripts/build-dictionary.py @@ -0,0 +1,51 @@ +"""Losslessly build the Android SQLite asset from the licensed original V5 trie.""" +import argparse,hashlib,json,pathlib,sqlite3 +APP=pathlib.Path(__file__).resolve().parents[1] + +def build(rtos): + source=rtos/'third_party/crabKeyboard/pinyinDictV5_20260416' + files={p.name:p.read_bytes()for p in source.glob('*.dat')};entries={} + assert len(files)==41,'Expected the complete upstream V5 dictionary' + def number(text): + result=0 + for ch in text:assert 33<=ord(ch)<=126;result=result*94+ord(ch)-33 + return result + def walk(key,depth,position,length): + for item in files[f'key{depth}.dat'][position:position+length].decode('ascii').split('\x1e'): + if not item:continue + offset,size=map(number,item[1:].split('\x1f')) + if item[0]=='_':entries[key]=[word for word in files[f'value{depth}.dat'][offset:offset+size].decode('utf-8').split(' ')if word] + else:walk(key+item[0],depth+1,offset,size) + walk('',0,0,len(files['key0.dat'])) + frequency_path=rtos/'third_party/jieba/dict.txt' + frequencies={} + for line in frequency_path.read_text(encoding='utf-8').splitlines(): + parts=line.split(' ') + if len(parts)>=2 and parts[1].isdigit():frequencies[parts[0]]=int(parts[1]) + groups9=['abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'];groups14=['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m'] + map9={ch:str(i+2)for i,group in enumerate(groups9)for ch in group} + map14={ch:'0123456789abcd'[i]for i,group in enumerate(groups14)for ch in group} + target=APP/'keyboard/src/main/assets/dictionary.db';target.parent.mkdir(parents=True,exist_ok=True) + temporary=target.with_suffix('.tmp') + if temporary.exists():temporary.unlink() + db=sqlite3.connect(temporary) + db.executescript('PRAGMA page_size=4096; PRAGMA journal_mode=OFF; CREATE TABLE entries(id INTEGER PRIMARY KEY,pinyin TEXT NOT NULL UNIQUE,code9 TEXT NOT NULL,code14 TEXT NOT NULL); CREATE TABLE words(entry INTEGER NOT NULL,ordinal INTEGER NOT NULL,word TEXT NOT NULL,frequency INTEGER NOT NULL,PRIMARY KEY(entry,ordinal)) WITHOUT ROWID;') + for index,(key,words)in enumerate(sorted(entries.items()),1): + db.execute('INSERT INTO entries VALUES(?,?,?,?)',(index,key,''.join(map9[ch]for ch in key),''.join(map14[ch]for ch in key))) + db.executemany('INSERT INTO words VALUES(?,?,?,?)',[(index,ordinal,word,frequencies.get(word,0))for ordinal,word in enumerate(words)]) + db.executescript('CREATE INDEX entries_code9 ON entries(code9);CREATE INDEX entries_code14 ON entries(code14);ANALYZE;PRAGMA user_version=1;') + db.commit() + restored={} + for key,word in db.execute('SELECT e.pinyin,w.word FROM entries e JOIN words w ON w.entry=e.id ORDER BY e.pinyin,w.ordinal'):restored.setdefault(key,[]).append(word) + assert restored==entries,'Full dictionary round-trip mismatch' + assert db.execute('PRAGMA integrity_check').fetchone()[0]=='ok';db.close();temporary.replace(target) + report={'keys':len(entries),'candidates':sum(map(len,entries.values())),'longest_word':max(len(word)for words in entries.values()for word in words),'sha256':hashlib.sha256(target.read_bytes()).hexdigest(),'bytes':target.stat().st_size,'round_trip':True,'source_files':{name:hashlib.sha256(data).hexdigest()for name,data in sorted(files.items())},'jieba_sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()} + (APP/'keyboard/src/main/assets/dictionary-manifest.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + print(json.dumps({k:v for k,v in report.items()if k!='source_files'},ensure_ascii=False)) + +if __name__=='__main__': + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--rtos-source',type=pathlib.Path) + args=parser.parse_args();rtos=args.rtos_source + if rtos is None: + rtos=next((p for p in [APP.parent/'com.sydxky.rtos.inputmethod',APP.parent/'RTOS/com.sydxky.rtos.inputmethod']if p.exists()),None) + assert rtos is not None,'Provide the licensed RTOS source with --rtos-source';build(rtos) diff --git a/android/scripts/capture-device.py b/android/scripts/capture-device.py new file mode 100644 index 0000000..1eb123c --- /dev/null +++ b/android/scripts/capture-device.py @@ -0,0 +1,75 @@ +"""Capture public screenshots by operating the actual embedded keyboard UI.""" +import argparse +from pathlib import Path +import re +import subprocess +import time +import xml.etree.ElementTree as ET + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--serial', required=True) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + + def adb(*command): + return subprocess.check_output(['adb', '-s', args.serial, *command], timeout=30) + + def tap(value, description=False): + adb('shell', 'uiautomator', 'dump', '/sdcard/crab-capture.xml') + tree = ET.fromstring(adb('exec-out', 'cat', '/sdcard/crab-capture.xml')) + for node in tree.iter('node'): + if node.get('content-desc' if description else 'text') == value: + x1, y1, x2, y2 = map(int, re.findall(r'\d+', node.get('bounds'))) + adb('shell', 'input', 'tap', str((x1 + x2)//2), str((y1 + y2)//2)) + time.sleep(.22) + return + raise RuntimeError('Missing UI control: ' + value) + + def shot(name): + time.sleep(2) + (args.output / (name + '.png')).write_bytes(adb('exec-out', 'screencap', '-p')) + + adb('shell', 'input', 'keyevent', '224') + adb('shell', 'wm', 'dismiss-keyguard') + adb('shell', 'am', 'force-stop', 'com.sydxky.crabinput') + adb('shell', 'am', 'start', '-n', 'com.sydxky.crabinput/.MainActivity') + time.sleep(3) + shot('android-home') + tap('demo_text', True) + tap('26键') + shot('android-qwerty') + for letter in 'shi': + tap(letter) + tap('更多候选', True) + shot('android-candidates-16') + tap('返回键盘', True) + # Begin a clean composition by closing and reopening the real editor. + tap('收起键盘', True) + tap('demo_text', True) + tap('14键') + shot('android-fourteen') + for key in ['BN', 'UI', 'GH', 'AS', 'OP']: + tap(key) + tap('更多候选', True) + shot('android-phrases') + tap('你好') + tap('收起键盘', True) + shot('android-result') + adb('shell', 'am', 'start', '-n', 'com.sydxky.crabinput.host/.HostActivity') + time.sleep(2) + tap('fixture_editor', True) + tap('26键') + for letter in 'nihao': + tap(letter) + tap('你好') + tap('收起键盘', True) + shot('android-host') + adb('shell', 'rm', '-f', '/sdcard/crab-capture.xml') + print('Captured 7 real-device UI states; no system IME changes.') + + +if __name__ == '__main__': + main() diff --git a/android/scripts/validate-device.py b/android/scripts/validate-device.py new file mode 100644 index 0000000..8b4d662 --- /dev/null +++ b/android/scripts/validate-device.py @@ -0,0 +1,39 @@ +"""Install the release and run embedded-component instrumentation on one explicit device.""" +import argparse,hashlib,json,pathlib,subprocess,time +APP=pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--serial',required=True);parser.add_argument('--output',type=pathlib.Path,required=True) + args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True) + def adb(*command,binary=False,timeout=30,retries=3): + last=None + for attempt in range(retries): + result=subprocess.run(['adb','-s',args.serial,*command],stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=timeout) + if result.returncode==0:return result.stdout if binary else result.stdout.decode('utf-8',errors='replace').strip() + last=(result.stdout+result.stderr).decode('utf-8',errors='replace').strip() + if command[0]!='install'and'device' not in last.lower()and'closed' not in last.lower():break + print('Device connection interrupted; retrying this command.',flush=True);time.sleep(1) + raise RuntimeError(last) + apk=APP/'app/build/outputs/apk/release/app-release.apk';test=APP/'app/build/outputs/apk/androidTest/release/app-release-androidTest.apk' + assert apk.is_file()and test.is_file() + model=adb('shell','getprop','ro.product.model');sdk=adb('shell','getprop','ro.build.version.sdk') + original=args.output/'original-ime.txt' + if not original.exists():original.write_text(adb('shell','settings','get','secure','default_input_method')+'\n',encoding='utf-8') + host=APP/'host/build/outputs/apk/debug/host-debug.apk' + for path in [apk,test,host]:print(adb('install','-r',str(path),timeout=90),flush=True) + before=adb('shell','settings','get','secure','default_input_method') + print('Default system IME retained; testing the embedded keyboard only.',flush=True) + adb('shell','input','keyevent','224');adb('shell','wm','dismiss-keyguard') + # The instrumentation is bounded short acceptance, not a stress/soak run. + command=['adb','-s',args.serial,'shell','am','instrument','-w','-r','com.sydxky.crabinput.test/androidx.test.runner.AndroidJUnitRunner'] + result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=240) + output=result.stdout.decode('utf-8',errors='replace');(args.output/'instrumentation.txt').write_text(output,encoding='utf-8');print(output,flush=True) + passed=result.returncode==0 and 'OK (8 tests)'in output and 'FAILURES!!!'not in output + report={'model':model,'sdk':sdk,'version':'1.2.0','apk_sha256':hashlib.sha256(apk.read_bytes()).hexdigest(),'instrumentation_tests':8,'embedded_component':True,'system_ime_unchanged':adb('shell','settings','get','secure','default_input_method')==before,'passed':passed} + (args.output/'device-validation.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') + assert passed and report['system_ime_unchanged'],'See instrumentation.txt; failed/unfinished cases are not accepted' + adb('shell','am','start','-n','com.sydxky.crabinput/.MainActivity');time.sleep(.5) + (args.output/'home.png').write_bytes(adb('exec-out','screencap','-p',binary=True)) + print(json.dumps(report),flush=True) + +if __name__=='__main__':main() diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..89b7701 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } } +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { google(); mavenCentral() } +} +rootProject.name = 'CrabInputWatch' +include ':app' +include ':keyboard' +include ':host' diff --git a/build-rtos-game.ps1 b/build-rtos-game.ps1 index 5c614ac..5fe3094 100644 --- a/build-rtos-game.ps1 +++ b/build-rtos-game.ps1 @@ -1,81 +1,81 @@ -param( - [Parameter(Mandatory = $true)][string]$ProjectRoot, - [switch]$QuickAppOnly, - [switch]$Clean -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' -$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path -$toolBin = Join-Path $PSScriptRoot 'temp\glyphix-v0.7.2-windows-x64\bin' -$gx = Join-Path $toolBin 'gx.exe' -if (-not (Test-Path -LiteralPath $gx)) { throw "Glyphix toolchain not found: $gx" } -$manifestPath = Join-Path $ProjectRoot 'src\manifest.json' -$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json -if ($manifest.package -cnotmatch '^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$') { - throw 'Invalid Glyphix package name' -} -$quickAppTargets = @('generic-watch-466x466-quickapp', 'generic-watch-390x450-quickapp') - -$targets = if ($QuickAppOnly) { - $quickAppTargets -} else { - @('generic-watch-466x466', 'generic-watch-390x450', 'generic-watch-466x466-quickapp', 'generic-watch-390x450-quickapp') -} - -$originalPath = $env:Path -Push-Location $ProjectRoot -try { - $env:Path = "$toolBin;$originalPath" - $availableDevices = @(& $gx list device) - if ($LASTEXITCODE -ne 0) { throw 'Unable to read Glyphix device profiles' } - foreach ($device in $targets) { - if ($availableDevices -cnotcontains $device) { throw "Missing Glyphix device profile: $device" } - } - if ($Clean) { - & $gx clean - if ($LASTEXITCODE -ne 0) { throw 'Glyphix clean failed' } - } - foreach ($device in $targets) { - $arguments = @('build', '-d', $device) - if (-not $device.Contains('quickapp')) { $arguments += '-e' } - $output = @(& $gx @arguments 2>&1 | ForEach-Object { $_.ToString() }) - $output | ForEach-Object { Write-Host $_ } - if ($LASTEXITCODE -ne 0 -or ($output -join "`n") -match '(?im)^\s*(?:error|fatal):\s') { - throw "Glyphix build failed for $device" - } - } - - foreach ($device in $quickAppTargets) { - $deliveryRoot = Join-Path $ProjectRoot ".glyphix-work\dist\$device\$($manifest.package)" - foreach ($fileName in @('bundle.pkg', 'manifest.json', 'icon.png')) { - $artifact = Join-Path $deliveryRoot $fileName - if (-not (Test-Path -LiteralPath $artifact -PathType Leaf) -or (Get-Item -LiteralPath $artifact).Length -eq 0) { - throw "Missing or empty QuickApp artifact: $artifact" - } - } - $packedManifest = Get-Content -LiteralPath (Join-Path $deliveryRoot 'manifest.json') -Raw | ConvertFrom-Json - if ($packedManifest.package -cne $manifest.package -or - $packedManifest.versionName -cne $manifest.versionName -or - $packedManifest.versionCode -ne $manifest.versionCode) { - throw "QuickApp package/version mismatch: $device" - } - $icon = [IO.File]::ReadAllBytes((Join-Path $deliveryRoot 'icon.png')) - if ($icon.Length -lt 8 -or [Convert]::ToHexString($icon[0..7]) -cne '89504E470D0A1A0A') { - throw "Invalid QuickApp PNG icon: $device" - } - foreach ($fileName in @('bundle.pkg', 'manifest.json', 'icon.png')) { - $artifact = Join-Path $deliveryRoot $fileName - [pscustomobject]@{ - Package = $manifest.package - Device = $device - File = $fileName - Bytes = (Get-Item -LiteralPath $artifact).Length - SHA256 = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash - } - } - } -} finally { - Pop-Location - $env:Path = $originalPath -} +param( + [Parameter(Mandatory = $true)][string]$ProjectRoot, + [switch]$QuickAppOnly, + [switch]$Clean +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path +$toolBin = Join-Path $PSScriptRoot 'temp\glyphix-v0.7.2-windows-x64\bin' +$gx = Join-Path $toolBin 'gx.exe' +if (-not (Test-Path -LiteralPath $gx)) { throw "Glyphix toolchain not found: $gx" } +$manifestPath = Join-Path $ProjectRoot 'src\manifest.json' +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.package -cnotmatch '^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$') { + throw 'Invalid Glyphix package name' +} +$quickAppTargets = @('generic-watch-466x466-quickapp', 'generic-watch-390x450-quickapp') + +$targets = if ($QuickAppOnly) { + $quickAppTargets +} else { + @('generic-watch-466x466', 'generic-watch-390x450', 'generic-watch-466x466-quickapp', 'generic-watch-390x450-quickapp') +} + +$originalPath = $env:Path +Push-Location $ProjectRoot +try { + $env:Path = "$toolBin;$originalPath" + $availableDevices = @(& $gx list device) + if ($LASTEXITCODE -ne 0) { throw 'Unable to read Glyphix device profiles' } + foreach ($device in $targets) { + if ($availableDevices -cnotcontains $device) { throw "Missing Glyphix device profile: $device" } + } + if ($Clean) { + & $gx clean + if ($LASTEXITCODE -ne 0) { throw 'Glyphix clean failed' } + } + foreach ($device in $targets) { + $arguments = @('build', '-d', $device) + if (-not $device.Contains('quickapp')) { $arguments += '-e' } + $output = @(& $gx @arguments 2>&1 | ForEach-Object { $_.ToString() }) + $output | ForEach-Object { Write-Host $_ } + if ($LASTEXITCODE -ne 0 -or ($output -join "`n") -match '(?im)^\s*(?:error|fatal):\s') { + throw "Glyphix build failed for $device" + } + } + + foreach ($device in $quickAppTargets) { + $deliveryRoot = Join-Path $ProjectRoot ".glyphix-work\dist\$device\$($manifest.package)" + foreach ($fileName in @('bundle.pkg', 'manifest.json', 'icon.png')) { + $artifact = Join-Path $deliveryRoot $fileName + if (-not (Test-Path -LiteralPath $artifact -PathType Leaf) -or (Get-Item -LiteralPath $artifact).Length -eq 0) { + throw "Missing or empty QuickApp artifact: $artifact" + } + } + $packedManifest = Get-Content -LiteralPath (Join-Path $deliveryRoot 'manifest.json') -Raw | ConvertFrom-Json + if ($packedManifest.package -cne $manifest.package -or + $packedManifest.versionName -cne $manifest.versionName -or + $packedManifest.versionCode -ne $manifest.versionCode) { + throw "QuickApp package/version mismatch: $device" + } + $icon = [IO.File]::ReadAllBytes((Join-Path $deliveryRoot 'icon.png')) + if ($icon.Length -lt 8 -or [Convert]::ToHexString($icon[0..7]) -cne '89504E470D0A1A0A') { + throw "Invalid QuickApp PNG icon: $device" + } + foreach ($fileName in @('bundle.pkg', 'manifest.json', 'icon.png')) { + $artifact = Join-Path $deliveryRoot $fileName + [pscustomobject]@{ + Package = $manifest.package + Device = $device + File = $fileName + Bytes = (Get-Item -LiteralPath $artifact).Length + SHA256 = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash + } + } + } +} finally { + Pop-Location + $env:Path = $originalPath +} diff --git a/com.sydxky.rtos.imefixture/build.ps1 b/com.sydxky.rtos.imefixture/build.ps1 index e0d49c9..2e2f3cb 100644 --- a/com.sydxky.rtos.imefixture/build.ps1 +++ b/com.sydxky.rtos.imefixture/build.ps1 @@ -1,2 +1,2 @@ -param([switch]$QuickAppOnly) -& (Join-Path $PSScriptRoot '..\build-rtos-game.ps1') -ProjectRoot $PSScriptRoot -QuickAppOnly:$QuickAppOnly +param([switch]$QuickAppOnly) +& (Join-Path $PSScriptRoot '..\build-rtos-game.ps1') -ProjectRoot $PSScriptRoot -QuickAppOnly:$QuickAppOnly diff --git a/com.sydxky.rtos.imefixture/glyphix.config.js b/com.sydxky.rtos.imefixture/glyphix.config.js index cb088cc..1ece447 100644 --- a/com.sydxky.rtos.imefixture/glyphix.config.js +++ b/com.sydxky.rtos.imefixture/glyphix.config.js @@ -1 +1 @@ -module.exports = {minify:false} +module.exports = {minify:false} diff --git a/com.sydxky.rtos.imefixture/src/app.js b/com.sydxky.rtos.imefixture/src/app.js index b1c6ea4..04efae1 100644 --- a/com.sydxky.rtos.imefixture/src/app.js +++ b/com.sydxky.rtos.imefixture/src/app.js @@ -1 +1 @@ -export default {} +export default {} diff --git a/com.sydxky.rtos.imefixture/src/main/index.ux b/com.sydxky.rtos.imefixture/src/main/index.ux index 40543e1..4c85331 100644 --- a/com.sydxky.rtos.imefixture/src/main/index.ux +++ b/com.sydxky.rtos.imefixture/src/main/index.ux @@ -1,6 +1,6 @@ - - - + + + diff --git a/com.sydxky.rtos.imefixture/src/manifest.json b/com.sydxky.rtos.imefixture/src/manifest.json index 33b83e0..2e8f484 100644 --- a/com.sydxky.rtos.imefixture/src/manifest.json +++ b/com.sydxky.rtos.imefixture/src/manifest.json @@ -1,19 +1,19 @@ -{ - "package": "com.sydxky.rtos.imefixture", - "name": "输入验收参考页", - "icon": "/assets/icon.png", - "versionName": "1.0.0", - "versionCode": 10000, - "features": [], - "router": { - "entry": "main", - "pages": { - "main": { - "component": "index" - } - } - }, - "config": { - "designWidth": 410 - } -} +{ + "package": "com.sydxky.rtos.imefixture", + "name": "输入验收参考页", + "icon": "/assets/icon.png", + "versionName": "1.0.0", + "versionCode": 10000, + "features": [], + "router": { + "entry": "main", + "pages": { + "main": { + "component": "index" + } + } + }, + "config": { + "designWidth": 410 + } +} diff --git a/com.sydxky.rtos.inputmethod/DESIGN.md b/com.sydxky.rtos.inputmethod/DESIGN.md index 65247be..5f8e154 100644 --- a/com.sydxky.rtos.inputmethod/DESIGN.md +++ b/com.sydxky.rtos.inputmethod/DESIGN.md @@ -20,3 +20,7 @@ - 使用实际 Inspector 树检查按钮和文字是否超出任一父容器;另检查屏幕边界、边缘黑色缺口、四候选与翻页/底栏的整体显示。几何与截图同时通过才可用于最终预览。 浏览器1.1.0同步使用浅色主界面和同一个键盘维护源。14键使用独立编码索引,候选仍来自同一完整词库;账号、私密输入和草稿持久化规则保持有效。 + +## 1.2.0候选网格 + +完整候选默认4列4行、单字16个一页,隐藏注音及该页拼音栏。2~4字词组占两格,长词占整行并按长度增加行高,单页最多四行单位。不得省略或截断文字,不丢候选;每页只将当前可见项放入响应式数据。圆屏网格保持中段安全宽度,底部返回、退格和完成使用原有矢量来源图标。 diff --git a/com.sydxky.rtos.inputmethod/LICENSE b/com.sydxky.rtos.inputmethod/LICENSE index 5a0b817..1a442a8 100644 --- a/com.sydxky.rtos.inputmethod/LICENSE +++ b/com.sydxky.rtos.inputmethod/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 herry +Copyright (c) 2026 sydxky Copyright (c) 2026 kqakqakqa Copyright (c) 2026 crabKun diff --git a/com.sydxky.rtos.inputmethod/NOTICE.md b/com.sydxky.rtos.inputmethod/NOTICE.md index d269ef4..2dccd98 100644 --- a/com.sydxky.rtos.inputmethod/NOTICE.md +++ b/com.sydxky.rtos.inputmethod/NOTICE.md @@ -1,6 +1,6 @@ # 蟹蟹输入 RTOS适配改版 -本项目为基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立RTOS适配改版,由herry进行Glyphix页面、离线词库索引、九宫/14键消歧、标准QWERTY、候选分页及生命周期适配。不是上游原版,也不表示上游作者、华为、OPPO或Glyphix对本改版作出背书。 +本项目为基于[CrabKeyboard / 蟹蟹输入](https://github.com/crabPangXie/crabKeyboard)的独立RTOS适配改版,由sydxky进行Glyphix页面、离线词库索引、九宫/14键消歧、标准QWERTY、候选分页及生命周期适配。不是上游原版,也不表示上游作者、华为、OPPO或Glyphix对本改版作出背书。 上游固定提交:`93d36b5002dee80a3d253b1ec7ec9e29800a104c`。保留原作者kqakqakqa、crabKun的版权声明、MIT许可证及原始词库/读取器。RTOS自研适配部分以根目录[MIT许可证](./LICENSE)发布,**允许商用、修改、再分发和闭源集成**,须保留适用版权及许可全文。 @@ -14,4 +14,4 @@ 14键参考公开演示中的相邻字母分组,自行实现离线消歧;不包含华为内部模型或固件代码。Glyphix/厂商SDK与运行时不包含在本项目MIT授权中,也不随源码分发,使用者需自行取得适用的厂商授权。 -公开仓库:。匿名问题与建议:。 +公开仓库:。匿名问题与建议:。 diff --git a/com.sydxky.rtos.inputmethod/README.md b/com.sydxky.rtos.inputmethod/README.md index 4b8d701..9247ed0 100644 --- a/com.sydxky.rtos.inputmethod/README.md +++ b/com.sydxky.rtos.inputmethod/README.md @@ -1,67 +1,69 @@ -# 蟹蟹输入 / Glyphix RTOS - -**蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:;免注册[匿名反馈](https://git2.sydxky.cn/feedback/herry/crab-input-rtos)。 - -独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.1.0 / 10100`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 - -1.0.1 按用户选择重做为浅色手机输入法风格:浅灰底、白色大按键、深色候选和蓝色主操作。候选栏与模式栏减少色块,英语和数字模式隐藏无用候选区;首页只突出一个输入主按钮。设计规范见 [DESIGN.md](./DESIGN.md)。 - -1.1.0 将全拼和英文改为标准 **QWERTY 10/9/7 三排**,新增公开华为风格的 **14键双字母拼音**,布局及独立离线消歧算法见 [14键说明](./KEYBOARD_14.md)。修复键盘容器被压缩导致末排裁切、四候选列表间距超出父容器、背景留黑的问题,圆屏下方逐行收窄。取消、退格、完成、编辑、键盘返回和翻页使用同一套 iconfont / Remix Icon 线性图标,保留“空格”和输入模式文字。SVG 原稿、来源和 Apache-2.0 许可见 [图标说明](./third_party/remix-icon/README.md);运行包采用本地透明 PNG,不增加网络请求。 - -上游固定提交 `93d36b5002dee80a3d253b1ec7ec9e29800a104c`(v2.0.0-patch01)。41 个原始词库文件均按 Git blob SHA-1 验证后保存于 `third_party/crabKeyboard/`,许可证和原始读取器一并保留。全量数据包含 **104,837 组拼音/缩写索引、163,238 个候选项**;这是索引项计数,不表示不同词语数。按原词库内容逐字/逐词组合输入长句。 - -## 使用 - -- 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。 -- 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。 -- 第一行显示三个候选。点击右侧下箭头按每页四项查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。拼音候选保留每组原始顺序;九宫和14键歧义通过本地 jieba 0.42.1 词频排序,学习偏好可优先排序。 -- 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。 -- 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。 -- 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。 - -## 自动弹出的实际范围 - -Glyphix 官方 [text-field 文档](https://docs.xfaith-tech.com/glyphix/docs/components/text-field.html)明确说明普通输入框不会弹出输入法。当前厂商工具没有提供系统级输入法注册/全局输入焦点注入接口。本项目交付独立文本应用和可接入页面,**不能接管未接入的第三方应用或固件界面**。 - -腕上浏览 `1.1.0` 同步使用此键盘:点击地址、配对码及兼容网页输入项后直接打开此页。输入法本身全离线;浏览器原有统一账号验证和兼容模式数据告知继续有效。其他应用可按下述方式接入。 - -```powershell -# 先构建本项目生成资源,再复制页面/引擎/资源并注册路由。 -& '.\build.ps1' -node .\scripts\install.mjs '<目标 Glyphix 工程根目录>' -``` - -在业务输入区的 `on:click` 中调用;页面上保留单一 `opening` 标志防止重复弹出: - -```javascript -const result = await router.push({uri: 'keyboard', params: { - initial: this.value, title: '输入内容', inputType: 'text', - maxLength: 2048, allowNewline: true, secret: false -}}) -if (result && result.accepted) this.value = result.text -``` - -`inputType` 支持 `text`、`url`、`number`、`pin`、`password`。`pin` 仅接受数字;`number` 允许小数点和负号;`maxLength` 上限 4096 个 Unicode 码点。业务仍需校验数值、网址或配对码的实际格式。不要在异步生命周期中等待页面结果。在宿主 `app.js` 的 `onHide` 中调用 `clearPrivateInputs(router)`(`common/ime-lifecycle.js`),覆盖厂商页面生命周期差异。宿主销毁后不得继续处理异步结果。 - -## 构建和复现 - -使用非 MSIX PowerShell 7,从本目录执行 `build.ps1`。脚本先无损转换词库、运行真实引擎和 UX 脚本测试,再用 RTOS 父目录的 OPPO 附件 `gx.exe` 生成圆/方普通镜像及圆/方 QuickApp 完整目录。SDK 不随源码分发。新机器另需从厂商附件准备工具,先构建/启动 `com.sydxky.rtos.snakeov` 一次取得 launcher/global 资源;通用 Inspector 支持位于 `../tests/emulator-validate.py`。 - -```powershell -python .\tests\native.py --work '<独立临时目录>' --output '<原生证据目录>' -python .\tests\visual.py --work '<另一个临时目录>' --output '<原生证据目录>/visual' -``` - -原生验收覆盖四配置,每配置 21 项,共 **84/84**;真实引擎及 UX 脚本分别通过 **40 + 29** 项断言。实际结果与运行包 SHA-256 见生成的 `results.json`。测试脚本使用既有离线参考应用执行真实前后台切换;不会接管被其他进程占用的 Inspector 14201。 - -1.1.0 的原生检查同时逐层核对可见 Button、图标、Label 与父容器边界;圆屏按钮四角限制在半径 230px 内。独立视觉回归覆盖四配置各11个页面状态,共 **44/44**,并统计靠近屏幕边缘的黑色缺口,避免只检查屏幕边界而漏掉内部裁切。截图保留实际原生画面,未绘制替代界面。详见 [本版验收记录](./VALIDATION_1.1.0.md)。 - -1.0.0 历史版本的 **72/72** 原生用例及 **26 + 22** 项逻辑断言见 [此前验收记录](./VALIDATION_2026-11-13.md)。1.0.1 浅色版重新完成四配置 **72/72** 原生验证,详见 [本版验收记录](./VALIDATION_1.0.1.md)。 - -## 内存与词库 - -`convert-dictionary.py` 将原始 V5 trie 无损转换为 **6779** 个编码分片(九宫/全拼4194个、14键2585个),最大分片 **116,686** 字节。九宫/全拼使用四位前缀,14键使用三位前缀;短编码单独索引。解码缓存限制为四项且 JSON 文本合计不超过 90,000 字符,大分片不缓存。仅当前页的四个候选进入展开列表,所有候选对象不放进响应式状态。转换器分别回读两种索引,验证与原始索引完全相同;不改写原始字典。 - -初期两位编码分片曾触发原生模拟器内存回收,已改为四位编码分片。验收保持既有 `--memory-profile 8192`,没有通过增大模拟器内存规避问题。模拟器耗时不能替代真机帧率、延迟或功耗结论。 - -MIT 许可副本也随运行包保留。jieba 词频只用于歧义候选排序,不联网、不分词上传。官方公开 API 不足以实现全局输入法安装;本轮仍按用户确认的模拟器与提交包范围交付,真机安装与平台审核单独处理。 +# 蟹蟹输入 / Glyphix RTOS + +**蟹蟹输入 RTOS适配改版**:基于CrabKeyboard的独立改版,RTOS适配代码采用[MIT协议](./LICENSE),允许商用、修改及再分发,须保留版权与许可。上游与第三方署名见[NOTICE.md](./NOTICE.md)。公开源码:;免注册[匿名反馈](https://git2.sydxky.cn/feedback/sydxky/crab-input-rtos)。 + +独立应用 `com.sydxky.rtos.inputmethod`,版本 `1.2.0 / 10200`。基于 [CrabKeyboard](https://github.com/crabPangXie/crabKeyboard) 的 MIT 全量词库和拼音前缀消费方式,重写为 Glyphix `.ux` 页面;不依赖 HarmonyOS HML、Android 输入法服务、DOM 或网络。 + +1.2.0 完整候选重做为 **4×4 网格**:单字每页16个,2~4字词组占两格,长词占整行并可增加行高;保持原有顺序,最后一页可达,不显示候选拼音。圆/方普通及QuickApp四配置已通过84项原生操作、84项布局及48个视觉状态,见[验收记录](./VALIDATION_1.2.0.md)。同源新增[Android应用内键盘组件](../android/README.md),独立包名、无需联网,接入应用的输入框自动弹出,保持系统默认输入法。 + +1.0.1 按用户选择重做为浅色手机输入法风格:浅灰底、白色大按键、深色候选和蓝色主操作。候选栏与模式栏减少色块,英语和数字模式隐藏无用候选区;首页只突出一个输入主按钮。设计规范见 [DESIGN.md](./DESIGN.md)。 + +1.1.0 将全拼和英文改为标准 **QWERTY 10/9/7 三排**,新增公开华为风格的 **14键双字母拼音**,布局及独立离线消歧算法见 [14键说明](./KEYBOARD_14.md)。修复键盘容器被压缩导致末排裁切、四候选列表间距超出父容器、背景留黑的问题,圆屏下方逐行收窄。取消、退格、完成、编辑、键盘返回和翻页使用同一套 iconfont / Remix Icon 线性图标,保留“空格”和输入模式文字。SVG 原稿、来源和 Apache-2.0 许可见 [图标说明](./third_party/remix-icon/README.md);运行包采用本地透明 PNG,不增加网络请求。 + +上游固定提交 `93d36b5002dee80a3d253b1ec7ec9e29800a104c`(v2.0.0-patch01)。41 个原始词库文件均按 Git blob SHA-1 验证后保存于 `third_party/crabKeyboard/`,许可证和原始读取器一并保留。全量数据包含 **104,837 组拼音/缩写索引、163,238 个候选项**;这是索引项计数,不表示不同词语数。按原词库内容逐字/逐词组合输入长句。 + +## 使用 + +- 点击首页文字区或“开始输入”即打开满屏键盘。完成回填;取消保留原文。草稿需主动保存,仅保存在本应用。 +- 默认九宫大按键。“26键”切到标准 QWERTY 全拼,`v` 表示 `ü`;“九宫”切回九宫。“14键”直接进入双字母模式。连续点击对应字母组,选择候选后只消费匹配部分,剩余编码继续查词。存在未选词的九宫或14键编码时禁止切换模式/完成,避免丢字或把编码当文字提交。 +- 第一行显示三个候选。点击右侧下箭头按每页四项查看全部匹配候选,左右箭头翻页,不使用原项目扩展列表的 100 项截断。拼音候选保留每组原始顺序;九宫和14键歧义通过本地 jieba 0.42.1 词频排序,学习偏好可优先排序。 +- 支持英文大小写、数字、完整可打印 ASCII 标点与常用中文标点。符号分两组大按键,通过“下一组符号”切换。网址额外提供 `https://`、`.com`。 +- 点击文本预览或铅笔图标移动光标、换行、上屏全拼、清空全文。Unicode 字符按码点计数和删除,避免拆坏 emoji。清空全文需第二次点击确认;长按退格图标连续删除,松开/隐藏/销毁停止。勾号完成,叉号取消,键盘图标返回输入。 +- 学习默认关闭,可开启或清除,仅保存最多 100 组、每组三个已选择的真实候选,不记录整段正文。私密输入始终遮盖、不学习、不保存;切到其他应用时清空私密编辑状态。 + +## 自动弹出的实际范围 + +Glyphix 官方 [text-field 文档](https://docs.xfaith-tech.com/glyphix/docs/components/text-field.html)明确说明普通输入框不会弹出输入法。当前厂商工具没有提供系统级输入法注册/全局输入焦点注入接口。本项目交付独立文本应用和可接入页面,**不能接管未接入的第三方应用或固件界面**。 + +腕上浏览 `1.1.0` 同步使用此键盘:点击地址、配对码及兼容网页输入项后直接打开此页。输入法本身全离线;浏览器原有统一账号验证和兼容模式数据告知继续有效。其他应用可按下述方式接入。 + +```powershell +# 先构建本项目生成资源,再复制页面/引擎/资源并注册路由。 +& '.\build.ps1' +node .\scripts\install.mjs '<目标 Glyphix 工程根目录>' +``` + +在业务输入区的 `on:click` 中调用;页面上保留单一 `opening` 标志防止重复弹出: + +```javascript +const result = await router.push({uri: 'keyboard', params: { + initial: this.value, title: '输入内容', inputType: 'text', + maxLength: 2048, allowNewline: true, secret: false +}}) +if (result && result.accepted) this.value = result.text +``` + +`inputType` 支持 `text`、`url`、`number`、`pin`、`password`。`pin` 仅接受数字;`number` 允许小数点和负号;`maxLength` 上限 4096 个 Unicode 码点。业务仍需校验数值、网址或配对码的实际格式。不要在异步生命周期中等待页面结果。在宿主 `app.js` 的 `onHide` 中调用 `clearPrivateInputs(router)`(`common/ime-lifecycle.js`),覆盖厂商页面生命周期差异。宿主销毁后不得继续处理异步结果。 + +## 构建和复现 + +使用非 MSIX PowerShell 7,从本目录执行 `build.ps1`。脚本先无损转换词库、运行真实引擎和 UX 脚本测试,再用 RTOS 父目录的 OPPO 附件 `gx.exe` 生成圆/方普通镜像及圆/方 QuickApp 完整目录。SDK 不随源码分发。新机器另需从厂商附件准备工具,先构建/启动 `com.sydxky.rtos.snakeov` 一次取得 launcher/global 资源;通用 Inspector 支持位于 `../tests/emulator-validate.py`。 + +```powershell +python .\tests\native.py --work '<独立临时目录>' --output '<原生证据目录>' +python .\tests\visual.py --work '<另一个临时目录>' --output '<原生证据目录>/visual' +``` + +原生验收覆盖四配置,每配置 21 项,共 **84/84**;真实引擎及 UX 脚本分别通过 **40 + 29** 项断言。实际结果与运行包 SHA-256 见生成的 `results.json`。测试脚本使用既有离线参考应用执行真实前后台切换;不会接管被其他进程占用的 Inspector 14201。 + +1.1.0 的原生检查同时逐层核对可见 Button、图标、Label 与父容器边界;圆屏按钮四角限制在半径 230px 内。独立视觉回归覆盖四配置各11个页面状态,共 **44/44**,并统计靠近屏幕边缘的黑色缺口,避免只检查屏幕边界而漏掉内部裁切。截图保留实际原生画面,未绘制替代界面。详见 [本版验收记录](./VALIDATION_1.1.0.md)。 + +1.0.0 历史版本的 **72/72** 原生用例及 **26 + 22** 项逻辑断言见 [此前验收记录](./VALIDATION_2026-11-13.md)。1.0.1 浅色版重新完成四配置 **72/72** 原生验证,详见 [本版验收记录](./VALIDATION_1.0.1.md)。 + +## 内存与词库 + +`convert-dictionary.py` 将原始 V5 trie 无损转换为 **6779** 个编码分片(九宫/全拼4194个、14键2585个),最大分片 **116,686** 字节。九宫/全拼使用四位前缀,14键使用三位前缀;短编码单独索引。解码缓存限制为四项且 JSON 文本合计不超过 90,000 字符,大分片不缓存。仅当前页最多16个候选进入展开网格,所有候选对象不放进响应式状态。转换器分别回读两种索引,验证与原始索引完全相同;不改写原始字典。 + +初期两位编码分片曾触发原生模拟器内存回收,已改为四位编码分片。验收保持既有 `--memory-profile 8192`,没有通过增大模拟器内存规避问题。模拟器耗时不能替代真机帧率、延迟或功耗结论。 + +MIT 许可副本也随运行包保留。jieba 词频只用于歧义候选排序,不联网、不分词上传。官方公开 API 不足以实现全局输入法安装;本轮仍按用户确认的模拟器与提交包范围交付,真机安装与平台审核单独处理。 diff --git a/com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md b/com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md index 69b93d8..56275ae 100644 --- a/com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md +++ b/com.sydxky.rtos.inputmethod/VALIDATION_1.1.0.md @@ -1,56 +1,56 @@ -# 蟹蟹输入1.1.0验收 - -版本`1.1.0 / 10100`,包名`com.sydxky.rtos.inputmethod`。本轮沿用厂商`gx v0.10.1 / 3ed38835`,普通/QuickApp圆屏466×466、方屏390×450四配置,内存配置保持8192 KiB。视觉记录时间:`2026-11-14T09:48:39.828292+08:00`。 - -## 本版修正 - -- 全拼和英文均为标准QWERTY:`qwertyuiop`、`asdfghjkl`、`zxcvbnm`,10/9/7三排。 -- 新增14键:`QW ER TY UI OP / AS DF GH JK L / ZX CV BN M`。参考公开布局,独立离线消歧;完整词库、连续前缀消费、候选分页和模式保护见[算法说明](./KEYBOARD_14.md)。 -- 键盘容器禁止纵向压缩,背景外层铺满屏幕,圆屏按行收窄;完整候选上下间距减少,页码禁止收缩,修复末排及四候选在父容器内被截断。 -- iconfont / Remix Icon统一操作图标,SVG原稿及Apache-2.0许可保留,运行用本地透明PNG。 - -## 验证结果 - -| 验证 | 结果 | -| --- | --- | -| 真实引擎逻辑 | 40项断言通过 | -| 真实UX脚本 | 29项断言通过 | -| 全量词库转换 | 两套索引分别回读与全部104,837组原始索引、163,238候选项一致 | -| 四配置构建 | 4/4,每个QuickApp目录含bundle.pkg、icon.png、manifest.json | -| 原生操作回归 | 84/84,四配置各21项 | -| 原生操作后布局 | 84/84 | -| 独立视觉/布局回归 | 44/44,四配置各11个状态 | - -原生回归保留原有18项,新增14键词组歧义与模式保护、连续输入、完整候选分页三项。实际点击原生按钮,检查“你好/不好”共存、未确认组合禁止完成/切模式、只消费选词匹配编码和候选前后翻页。全拼与英文额外检查实际三排顺序。 - -视觉状态覆盖首页、九宫、QWERTY全拼、四候选、编辑、14键、14键完整候选、英文、数字及两组符号。检查可见Button/ImageBox/Label是否超出任一父容器,容忍1px原生舍入;圆屏按钮/图标四角限制半径230px,实测按钮最大半径227.86px,小于屏幕233px。全部44个状态的指定边缘采样带黑色缺口像素为0。此结论针对验收状态,不代替未来新增页面的检查。 - -Inspector空闲时可能将新树关联到旧back buffer。视觉脚本通过给提示文本暂加空格后恢复,触发真实原生重绘,再重复snapshot保留新帧。没有绘制替代界面。`UI_PREVIEW.png`排除模拟器外壳周边像素,所有按钮完整保留;未裁剪原图及控件树在证据ZIP中。 - -## 查询与内存 - -八个全拼样本依次为nihao、zhongguo、shijie、shurufa、beijing、tianqi、shanghai、gongzuo,原生记录如下,单位ms: - -| 配置 | 八个全拼样本耗时 | -| --- | --- | -| round | 80, 45, 42, 41, 44, 45, 46, 43 | -| rect | 78, 45, 44, 22, 44, 45, 45, 45 | -| round-quickapp | 79, 45, 44, 22, 44, 46, 64, 44 | -| rect-quickapp | 77, 44, 43, 23, 44, 45, 47, 43 | - -上述为模拟器全拼查词样本;不是专门的14键基准,也不表示真机端到端延迟、帧率或功耗。14键原生用例等待词库稳定上限5秒并验证实际结果,未另行声明毫秒级指标。 - -共6779个分片(4194九宫/全拼、2585十四键),最大116,686字节;缓存最多四项、解码JSON文本合计90,000字符,大分片不驻留。未扩大模拟器内存。 - -## 运行包与交付 - -| 配置 | 实际运行 bundle.pkg SHA-256 | -| --- | --- | -| round | `D0D0BBCBACA78A3300F9CE7538BD39CC0810BCF9872B3B42F0E4ED040EDB0A51` | -| rect | `767D7A034D943D4B2D75CBE1EA623BEAAEF8CCBF0033B7944A3763513E74B291` | -| round-quickapp | `C82E78F47B4CDCBD33B6E39D2C08005CCFFD617608336A17CF5C4F808370E31F` | -| rect-quickapp | `A1CA39663B85D36FF464BE5D62949C7ADEFD2EFA7939C8E65ABD90DF41F192E8` | - -提交目录包含两屏QuickApp完整6文件,`crab-keyboard-1.1.0-quickapp.zip`为打包副本,源码和`keyboard-emulator-evidence.zip`另行提供。`results.json`与`visual-results.json`分别记录操作和视觉结果,交付脚本核对两份证据使用同一构建包;`archive-hashes.json`及`DELIVERY_INTEGRITY.json`记录归档哈希、CRC及敏感扫描。 - -自动弹出仅覆盖本应用与已接入宿主,厂商公开API没有已验证的系统级IME注册能力。输入法全离线,无账号或联网请求;浏览器联网维持统一账号验证。按用户确认仅做模拟器和提交包验收;RTOS真机安装、目标固件行为、性能、续航及平台审核未验收。 +# 蟹蟹输入1.1.0验收 + +版本`1.1.0 / 10100`,包名`com.sydxky.rtos.inputmethod`。本轮沿用厂商`gx v0.10.1 / 3ed38835`,普通/QuickApp圆屏466×466、方屏390×450四配置,内存配置保持8192 KiB。视觉记录时间:`2026-11-14T09:48:39.828292+08:00`。 + +## 本版修正 + +- 全拼和英文均为标准QWERTY:`qwertyuiop`、`asdfghjkl`、`zxcvbnm`,10/9/7三排。 +- 新增14键:`QW ER TY UI OP / AS DF GH JK L / ZX CV BN M`。参考公开布局,独立离线消歧;完整词库、连续前缀消费、候选分页和模式保护见[算法说明](./KEYBOARD_14.md)。 +- 键盘容器禁止纵向压缩,背景外层铺满屏幕,圆屏按行收窄;完整候选上下间距减少,页码禁止收缩,修复末排及四候选在父容器内被截断。 +- iconfont / Remix Icon统一操作图标,SVG原稿及Apache-2.0许可保留,运行用本地透明PNG。 + +## 验证结果 + +| 验证 | 结果 | +| --- | --- | +| 真实引擎逻辑 | 40项断言通过 | +| 真实UX脚本 | 29项断言通过 | +| 全量词库转换 | 两套索引分别回读与全部104,837组原始索引、163,238候选项一致 | +| 四配置构建 | 4/4,每个QuickApp目录含bundle.pkg、icon.png、manifest.json | +| 原生操作回归 | 84/84,四配置各21项 | +| 原生操作后布局 | 84/84 | +| 独立视觉/布局回归 | 44/44,四配置各11个状态 | + +原生回归保留原有18项,新增14键词组歧义与模式保护、连续输入、完整候选分页三项。实际点击原生按钮,检查“你好/不好”共存、未确认组合禁止完成/切模式、只消费选词匹配编码和候选前后翻页。全拼与英文额外检查实际三排顺序。 + +视觉状态覆盖首页、九宫、QWERTY全拼、四候选、编辑、14键、14键完整候选、英文、数字及两组符号。检查可见Button/ImageBox/Label是否超出任一父容器,容忍1px原生舍入;圆屏按钮/图标四角限制半径230px,实测按钮最大半径227.86px,小于屏幕233px。全部44个状态的指定边缘采样带黑色缺口像素为0。此结论针对验收状态,不代替未来新增页面的检查。 + +Inspector空闲时可能将新树关联到旧back buffer。视觉脚本通过给提示文本暂加空格后恢复,触发真实原生重绘,再重复snapshot保留新帧。没有绘制替代界面。`UI_PREVIEW.png`排除模拟器外壳周边像素,所有按钮完整保留;未裁剪原图及控件树在证据ZIP中。 + +## 查询与内存 + +八个全拼样本依次为nihao、zhongguo、shijie、shurufa、beijing、tianqi、shanghai、gongzuo,原生记录如下,单位ms: + +| 配置 | 八个全拼样本耗时 | +| --- | --- | +| round | 80, 45, 42, 41, 44, 45, 46, 43 | +| rect | 78, 45, 44, 22, 44, 45, 45, 45 | +| round-quickapp | 79, 45, 44, 22, 44, 46, 64, 44 | +| rect-quickapp | 77, 44, 43, 23, 44, 45, 47, 43 | + +上述为模拟器全拼查词样本;不是专门的14键基准,也不表示真机端到端延迟、帧率或功耗。14键原生用例等待词库稳定上限5秒并验证实际结果,未另行声明毫秒级指标。 + +共6779个分片(4194九宫/全拼、2585十四键),最大116,686字节;缓存最多四项、解码JSON文本合计90,000字符,大分片不驻留。未扩大模拟器内存。 + +## 运行包与交付 + +| 配置 | 实际运行 bundle.pkg SHA-256 | +| --- | --- | +| round | `D0D0BBCBACA78A3300F9CE7538BD39CC0810BCF9872B3B42F0E4ED040EDB0A51` | +| rect | `767D7A034D943D4B2D75CBE1EA623BEAAEF8CCBF0033B7944A3763513E74B291` | +| round-quickapp | `C82E78F47B4CDCBD33B6E39D2C08005CCFFD617608336A17CF5C4F808370E31F` | +| rect-quickapp | `A1CA39663B85D36FF464BE5D62949C7ADEFD2EFA7939C8E65ABD90DF41F192E8` | + +提交目录包含两屏QuickApp完整6文件,`crab-keyboard-1.1.0-quickapp.zip`为打包副本,源码和`keyboard-emulator-evidence.zip`另行提供。`results.json`与`visual-results.json`分别记录操作和视觉结果,交付脚本核对两份证据使用同一构建包;`archive-hashes.json`及`DELIVERY_INTEGRITY.json`记录归档哈希、CRC及敏感扫描。 + +自动弹出仅覆盖本应用与已接入宿主,厂商公开API没有已验证的系统级IME注册能力。输入法全离线,无账号或联网请求;浏览器联网维持统一账号验证。按用户确认仅做模拟器和提交包验收;RTOS真机安装、目标固件行为、性能、续航及平台审核未验收。 diff --git a/com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md b/com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md new file mode 100644 index 0000000..bebef8c --- /dev/null +++ b/com.sydxky.rtos.inputmethod/VALIDATION_1.2.0.md @@ -0,0 +1,21 @@ +# 蟹蟹输入 RTOS 1.2.0 验收 + +日期:2026-11-14,版本 1.2.0 / 10200,包名 `com.sydxky.rtos.inputmethod`。 + +完整候选改为 4×4 网格。单字占一列,每页最多 16 项;词组按长度扩宽或增加行高,保留原顺序与全部候选,展开页不显示拼音。查询“shi”时实际 90 项、自适应分为 7 页。QWERTY 和 14 键布局维持原有正确顺序。 + +| 验证 | 结果 | +| --- | --- | +| 引擎逻辑 | 44 项断言通过,包含单字网格、混合词组、长词、分页完整性 | +| UX 脚本 | 32 项断言通过 | +| 原始词库 | 104,837 组索引、163,238 候选,无损转换 | +| 构建 | 普通/QuickApp 圆屏 466×466、方屏 390×450 四配置通过 | +| 真实原生操作 | 84/84 通过 | +| 操作后布局 | 84/84 通过 | +| 视觉状态 | 48/48 通过,包括候选网格与词组候选 | + +厂商工具 `gx v0.10.1 / 3ed38835`,内存配置保持 8192 KiB。使用真实 Inspector、原生控件树及截图,不用浏览器绘制页面代替运行时验收。当前页至多 16 项进入响应式状态,分页元数据不进入深度响应式遍历,页面销毁时清理候选及定时任务。 + +QuickApp 两屏交付各包含 `bundle.pkg`、`icon.png`、`manifest.json`。实际截图、汇总报告和产物 SHA-256 另附发行版。Android 应用内组件验证见同仓库 Android 文档。 + +按用户确认,本平台仅完成模拟器及提交包验收;RTOS 真机安装、目标固件长期运行、性能/功耗与商店审核尚未验收。 diff --git a/com.sydxky.rtos.inputmethod/src/common/ime-engine.js b/com.sydxky.rtos.inputmethod/src/common/ime-engine.js index 4086d8e..afa7ed2 100644 --- a/com.sydxky.rtos.inputmethod/src/common/ime-engine.js +++ b/com.sydxky.rtos.inputmethod/src/common/ime-engine.js @@ -26,6 +26,23 @@ export function digitsOf(text) { return result } export function chars(text){return Array.from(String(text||''))} +// Four columns and four row units. Phrases take more width/height, never ellipsis. +export function candidatePages(candidates) { + const pages=[];let rows=[],usedRows=0,row=null + function flushRow(){if(row){rows.push(row);usedRows+=row.units;row=null}} + function flushPage(){flushRow();if(rows.length)pages.push(rows);rows=[];usedRows=0} + for(const item of candidates){ + const length=chars(item.word).length + const span=length===1?1:length<=4?2:4 + const units=span===4?Math.max(1,Math.ceil(length/12)):1 + if(row&&(row.slots+span>4||row.units!==units))flushRow() + if(usedRows+units>4)flushPage() + if(!row)row={items:[],slots:0,units} + row.items.push({item,span});row.slots+=span + if(row.slots===4)flushRow() + } + flushPage();return pages +} export function edit(text,cursor,insertion,maxLength) { const value=chars(text), at=Math.max(0,Math.min(value.length,cursor)) const room=Math.max(0,maxLength-value.length), added=chars(insertion).slice(0,room) diff --git a/com.sydxky.rtos.inputmethod/src/keyboard/index.ux b/com.sydxky.rtos.inputmethod/src/keyboard/index.ux index f8ba31f..894f3fb 100644 --- a/com.sydxky.rtos.inputmethod/src/keyboard/index.ux +++ b/com.sydxky.rtos.inputmethod/src/keyboard/index.ux @@ -3,7 +3,7 @@

{{title}}

{{count}} 字

-

{{compositionDisplay || modeLabel}}

{{hint}}

+

{{compositionDisplay || modeLabel}}

{{hint}}

{{secret ? '私密输入' : '选择候选词'}}

@@ -16,7 +16,7 @@

完整候选 · {{total}} 项 · {{candidatePage + 1}}/{{pageCount}}

-

{{busy ? '正在查词…' : '继续输入拼音后查看候选'}}

+

{{busy ? '正在查词…' : '继续输入后查看候选'}}

@@ -47,22 +47,22 @@ .keyrow{align-items:stretch;align-self:center;width:100%}.key{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;height:52px;min-width:0;background-color:#FFFFFF;border-radius:12px}.key-label{font-size:.88rem;color:#22252E}.key-sub{font-size:.35rem;color:#9DA2AD;margin-top:1px} .modes{height:36px;margin-top:3px}.modes button{flex:1;height:31px;background-color:transparent;font-size:.53rem;color:#636A77} .bottom{height:41px;margin-top:2px}.bottom button{flex:1;height:37px;font-size:.6rem;background-color:#FFFFFF}.bottom .done{background-color:#2867D9;color:#FFFFFF}.bottom .delete{background-color:#D9DDE4;color:#515866} - .page-info{text-align:center;color:#8B919D;font-size:.45rem;margin:5px 0;flex-shrink:0}.full-list{flex:1;justify-content:center}.full-candidate{display:flex;flex-direction:row;align-items:center;justify-content:space-between;height:41px;margin:4px 5px;padding:0 16px;background-color:#FFFFFF}.word{font-size:.8rem;color:#22252E}.reading{font-size:.4rem;color:#A1A7B1;margin-left:7px}.empty{margin:15px 8px;color:#9299A5;text-align:center} + .page-info{text-align:center;color:#8B919D;font-size:.45rem;margin:5px 0;flex-shrink:0}.full-list{flex:1;justify-content:center}.candidate-grid-row{display:flex;flex-direction:row;align-self:flex-start;align-items:stretch;flex-shrink:0}.full-candidate{display:flex;align-items:center;justify-content:center;flex:1;min-width:0;margin:2px;padding:2px 4px;background-color:#FFFFFF;border-radius:10px}.word{width:100%;text-align:center;font-size:25px;color:#22252E}.empty{margin:15px 8px;color:#9299A5;text-align:center} .pager{height:38px}.pager button{flex:1;height:34px;background-color:transparent;color:#2867D9;font-size:.55rem}.toolrow button{flex:1;height:40px;font-size:.57rem}.tools{justify-content:center}.tools .danger{color:#C85656}.tools .bottom{margin-top:8px} - @media (shape:circle){.ime{padding:30px 8px}.top{align-self:center;width:186px;height:24px}.cancel{height:24px}.preview{align-self:center;width:280px;height:32px;margin-top:5px}.composition{align-self:center;width:300px;height:19px}.candidates{align-self:center;width:352px;height:38px}.candidate{height:34px}.more{height:34px}.keypad{align-self:center;width:376px}.key{height:49px}.modes{align-self:center;width:286px;height:33px}.modes button{height:29px}.bottom{align-self:center;width:200px;height:38px}.bottom button{height:34px}.full-list{align-self:center;width:320px}.full-candidate{height:37px;margin:2px 5px}.pager{align-self:center;width:276px}.toolrow{align-self:center;width:300px}.toolrow button{height:35px}.tools .page-info{align-self:center;width:280px}.tools .bottom{margin-top:4px}.full-candidate .word{font-size:.7rem}} + @media (shape:circle){.ime{padding:30px 8px}.top{align-self:center;width:186px;height:24px}.cancel{height:24px}.preview{align-self:center;width:280px;height:32px;margin-top:5px}.composition{align-self:center;width:300px;height:19px}.candidates{align-self:center;width:352px;height:38px}.candidate{height:34px}.more{height:34px}.keypad{align-self:center;width:376px}.key{height:49px}.modes{align-self:center;width:286px;height:33px}.modes button{height:29px}.bottom{align-self:center;width:200px;height:38px}.bottom button{height:34px}.full-list{align-self:center;width:340px}.pager{align-self:center;width:276px}.toolrow{align-self:center;width:300px}.toolrow button{height:35px}.tools .page-info{align-self:center;width:280px}.tools .bottom{margin-top:4px}}