diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
deleted file mode 100644
index c31cdb8f6..000000000
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-name: 问题报告 / Bug report
-about: 创建问题报告以帮助我们改进 / Create a report to help us improve
-title: ''
-labels: bug
-assignees: ''
-
----
-
-**描述问题 / Describe the bug**
-
-
-**重现步骤 / To Reproduce**
-
-
-**预期的行为 / Expected behavior**
-
-
-**日志 / Log**
-
-
-**截图 / Screenshots**
-
-
-**设备信息 / Device Infomation**
-
-- 操作系统 / OS: [e.g. Android 13 (MIUI 14)]
-- 应用版本 / App Version: [e.g. 0.0.6-g024241cf-release]
-
-**附加信息 / Additional context**
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 000000000..9a79cd21b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,83 @@
+name: 问题报告 / Bug Report
+description: 创建问题报告以帮助我们改进 / Create a bug report to help us improve
+labels:
+ - bug
+
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: 摘要 / Summary
+ description: 简要描述遇到的问题。 / Briefly describe the bug.
+ validations:
+ required: true
+
+ - type: textarea
+ id: step_to_reproduce
+ attributes:
+ label: 重现步骤 / Steps to Reproduce
+ description: 如何重现该问题。 / How to reproduce the bug.
+ placeholder: |
+ 1. 打开某界面 / Open page ...
+ 2. 点击某菜单 / Click menu ...
+ 3. 某处出问题 / Something went wrong ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected_behavior
+ attributes:
+ label: 预期行为 / Expected Behavior
+ description: 完成上述步骤后应该发生什么。 / What is expected to happen after the steps above.
+ validations:
+ required: true
+
+ - type: textarea
+ id: log
+ attributes:
+ label: 日志 / Log
+ description: 附上日志以帮助定位问题。 / Attach log to help locate the bug.
+ validations:
+ required: false
+
+ - type: textarea
+ id: screenshot
+ attributes:
+ label: 截图 / Screenshot
+ description: 附上截图以帮助解释问题。 / Attach screenshots to help explain the bug.
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional_context
+ attributes:
+ label: 附加信息 / Additional Context
+ description: 与此问题相关的上下文信息,比如在问题出现前做了什么。 / Additional context about the bug, eg. what did you do before the bug appears.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ ### 设备信息 / Device Infomation
+
+ - type: input
+ id: os_version
+ attributes:
+ label: 系统版本 / OS Version
+ validations:
+ required: true
+
+ - type: input
+ id: app_version
+ attributes:
+ label: 应用版本 / App Version
+ validations:
+ required: true
+
+ - type: input
+ id: plugins_version
+ attributes:
+ label: 插件版本 / Plugins Version
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 99d680b0a..35299216a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,2 +1,14 @@
blank_issues_enabled: false
+contact_links:
+ - name: GitHub 讨论区 / GitHub Discussions
+ url: https://github.com/fcitx5-android/fcitx5-android/discussions
+ about: 请在这里提出有关如何使用本输入法的疑问。 / Please ask questions about how to use the input method here.
+
+ - name: Telegram 群组 / Telegram Group
+ url: https://t.me/fcitx5_android_group
+ about: 也可以群组中提问或讨论新功能。 / You may also ask questions or discuss new features in the group.
+
+ - name: Matrix 房间 / Matrix Room
+ url: https://matrix.to/#/#fcitx5-android:mozilla.org
+ about: Matrix 房间与 Telegram 群组通过桥接机器人互通。 / Matrix Room and Telegram Group are connected through bridge bot.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
deleted file mode 100644
index ffc3e2496..000000000
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-name: 功能请求 / Feature request
-about: 为本项目提供建议和意见 / Suggest an idea for this project
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-**描述你请求的功能是否和问题有关 / Is your feature request related to a problem? Please describe.**
-
-
-**描述解决方案 / Describe the solution you'd like**
-
-
-**描述替代方案 / Describe alternatives you've considered**
-
-
-**附加信息 / Additional context**
-
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 000000000..30f66b8bf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,55 @@
+name: 功能请求 / Feature Request
+description: 为本项目提供新功能建议 / Suggest a new feature for this project
+labels:
+ - enhancement
+
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: 摘要 / Summary
+ description: 新功能应该做什么。 / What the new feature should do.
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternative
+ attributes:
+ label: 替代方案 / Alternative Solution
+ description: 其它可能的解决方案(如果有)。 / Other possible solutions, if any.
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional_context
+ attributes:
+ label: 附加信息 / Additional Context
+ description: 与此功能请求有关的上下文信息或截图。 / Additional context or screenshots about the feature request.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ ### 设备信息 / Device Infomation
+
+ - type: input
+ id: os_version
+ attributes:
+ label: 系统版本 / OS Version
+ validations:
+ required: true
+
+ - type: input
+ id: app_version
+ attributes:
+ label: 应用版本 / App Version
+ validations:
+ required: true
+
+ - type: input
+ id: plugins_version
+ attributes:
+ label: 插件版本 / Plugins Version
+ validations:
+ required: false
diff --git a/.github/workflows/fdroid.yml b/.github/workflows/fdroid.yml
index eb5d194ea..8d612b32e 100644
--- a/.github/workflows/fdroid.yml
+++ b/.github/workflows/fdroid.yml
@@ -20,8 +20,8 @@ defaults:
jobs:
fdroid-build:
- runs-on: ubuntu-latest
- container: registry.gitlab.com/fdroid/fdroidserver:buildserver-bullseye
+ runs-on: ubuntu-24.04
+ container: registry.gitlab.com/fdroid/fdroidserver:buildserver-bookworm
strategy:
matrix:
abi:
@@ -66,6 +66,8 @@ jobs:
BUILD_NUMBER: ${{ inputs.build_number || 'lastSuccessfulBuild' }}
run: |
set -x
+ # prevent prebuilder from writing to build summary file
+ unset GITHUB_ACTIONS GITHUB_STEP_SUMMARY
curl -Lo /usr/bin/yq "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64"
chmod +x /usr/bin/yq
build_metadata=$(curl "https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/$BUILD_NUMBER/artifact/out/build-metadata.json")
@@ -115,7 +117,7 @@ jobs:
$fdroid build --verbose --test --scan-binary --on-server --no-tarball $build
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: ${{ success() || failure() }}
with:
name: fdroid-${{ matrix.abi }}
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 22ae1a897..01968ac76 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -7,24 +7,21 @@ on:
jobs:
develop:
- strategy:
- matrix:
- os: [ubuntu-latest, macOS-latest]
- runs-on: ${{ matrix.os }}
+ runs-on: ubuntu-24.04
steps:
- name: Fetch source code
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- - uses: cachix/install-nix-action@v23
+ - uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: cachix/cachix-action@v12
+ - uses: cachix/cachix-action@v16
with:
name: fcitx5-android
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- - name: Build Debug APK
+ - name: Build Release APK
run: |
- nix develop .#noAS --command ./gradlew :app:assembleDebug
- nix develop .#noAS --command ./gradlew :assembleDebugPlugins
+ nix develop .#noAS --command ./gradlew :app:assembleRelease
+ nix develop .#noAS --command ./gradlew :assembleReleasePlugins
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index c49702b34..43438a178 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,10 +10,7 @@ on:
jobs:
publish:
- runs-on: ubuntu-22.04
- env:
- GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
- GITHUB_ACTOR: android-fcitx5
+ runs-on: ubuntu-24.04
steps:
- name: Fetch source code
uses: actions/checkout@v4
@@ -21,30 +18,30 @@ jobs:
fetch-depth: 0
submodules: recursive
+ - name: Install system dependencies
+ run: |
+ sudo apt update
+ sudo apt install extra-cmake-modules gettext
+
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Setup Android environment
uses: android-actions/setup-android@v3
-
- - name: Install Android NDK
- run: |
- sdkmanager --install "cmake;3.22.1"
-
- - name: Install system dependencies
- run: |
- sudo apt update
- sudo apt install extra-cmake-modules gettext
+ with:
+ packages: cmake;3.31.6
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/actions/setup-gradle@v4
- name: Publish build convention and libs
+ env:
+ GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
+ GITHUB_ACTOR: fcitx5-android-bot
run: |
./gradlew :build-logic:convention:publish
./gradlew :lib:common:publish
./gradlew :lib:plugin-base:publish
-
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index d527bbc3d..dcc86c2e1 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -12,18 +12,13 @@ jobs:
build_pull_request:
runs-on: ${{ matrix.os }}
strategy:
+ fail-fast: false
matrix:
os:
- - ubuntu-22.04
+ - ubuntu-24.04
- macos-13
+ - macos-14
- windows-2022
- abi:
- - armeabi-v7a
- - arm64-v8a
- - x86
- - x86_64
- env:
- BUILD_ABI: ${{ matrix.abi }}
steps:
- name: Fetch source code
uses: actions/checkout@v4
@@ -31,14 +26,8 @@ jobs:
fetch-depth: 0
submodules: recursive
- - name: Regenerate symlinks pointing to submodule (Windows)
- if: ${{ matrix.os == 'windows-2022' }}
- run: |
- Remove-Item -Recurse app/src/main/assets/usr/share, plugin/hangul/src/main/assets/usr/share/libhangul, plugin/chewing/src/main/assets/usr/share/libchewing, plugin/jyutping/src/main/assets/usr/share/libime
- git checkout -- .
-
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
@@ -48,38 +37,39 @@ jobs:
- name: Install Android NDK
run: |
- sdkmanager --install "cmake;3.22.1"
+ sdkmanager --install "cmake;3.31.6"
- name: Install system dependencies (Ubuntu)
- if: ${{ matrix.os == 'ubuntu-22.04' }}
+ if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
sudo apt update
sudo apt install extra-cmake-modules gettext
- name: Install system dependencies (macOS)
- if: ${{ matrix.os == 'macos-13' }}
+ if: ${{ startsWith(matrix.os, 'macos') }}
run: |
brew install extra-cmake-modules
- name: Install system dependencies (Windows)
- if: ${{ matrix.os == 'windows-2022' }}
+ if: ${{ startsWith(matrix.os, 'windows') }}
run: |
+ C:/msys64/usr/bin/pacman -Syu --noconfirm
C:/msys64/usr/bin/pacman -S --noconfirm mingw-w64-ucrt-x86_64-gettext mingw-w64-ucrt-x86_64-extra-cmake-modules
Add-Content $env:GITHUB_PATH "C:/msys64/ucrt64/bin"
- name: Setup Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/actions/setup-gradle@v4
- - name: Build Debug APK
+ - name: Build Release APK
run: |
- ./gradlew :app:assembleDebug
- ./gradlew :assembleDebugPlugins
+ ./gradlew :app:assembleRelease
+ ./gradlew :assembleReleasePlugins
- name: Upload app
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: app-${{ matrix.os }}-${{ matrix.abi }}
- path: app/build/outputs/apk/debug/
+ name: app-${{ matrix.os }}
+ path: app/build/outputs/apk/release/
- name: Pack plugins
shell: bash
@@ -89,12 +79,12 @@ jobs:
do
if [ -d "plugin/${i}" ]
then
- mv "plugin/${i}/build/outputs/apk/debug" "plugins-to-upload/${i}"
+ mv "plugin/${i}/build/outputs/apk/release" "plugins-to-upload/${i}"
fi
done
- name: Upload plugins
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: plugins-${{ matrix.os }}-${{ matrix.abi }}
+ name: plugins-${{ matrix.os }}
path: plugins-to-upload
diff --git a/.gitignore b/.gitignore
index 7538326e1..0bd6d8615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,21 +1,12 @@
# Module :app
# Installed data
-/app/src/main/assets/usr/share/fcitx5/addon
-/app/src/main/assets/usr/share/fcitx5/chttrans
-/app/src/main/assets/usr/share/fcitx5/data
-/app/src/main/assets/usr/share/fcitx5/default
-/app/src/main/assets/usr/share/fcitx5/inputmethod
-/app/src/main/assets/usr/share/fcitx5/lua
-/app/src/main/assets/usr/share/fcitx5/punctuation
-/app/src/main/assets/usr/share/fcitx5/unicode
-/app/src/main/assets/usr/share/locale
+/app/src/main/assets/usr/
# Generated asset descriptor
/app/src/main/assets/descriptor.json
# Plugins
# Installed data
-/plugin/*/src/main/assets/usr/share/fcitx5
-/plugin/*/src/main/assets/usr/share/locale
+/plugin/*/src/main/assets/usr/
# Generated asset descriptor
/plugin/*/src/main/assets/descriptor.json
@@ -108,3 +99,6 @@ lint/tmp/
# Android Profiling
*.hprof
+
+### Kotlin ###
+.kotlin/
diff --git a/.gitmodules b/.gitmodules
index 3bd559222..2f3d38b6d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,51 +1,58 @@
[submodule "lib/fcitx5/src/main/cpp/fcitx5"]
path = lib/fcitx5/src/main/cpp/fcitx5
- url = git@github.com:fcitx/fcitx5.git
+ url = https://github.com/fcitx/fcitx5.git
[submodule "lib/fcitx5/src/main/cpp/prebuilt"]
path = lib/fcitx5/src/main/cpp/prebuilt
- url = git@github.com:fcitx5-android/prebuilt.git
+ url = https://github.com/fcitx5-android/prebuilt.git
+ shallow = true
[submodule "lib/fcitx5-lua/src/main/cpp/fcitx5-lua"]
path = lib/fcitx5-lua/src/main/cpp/fcitx5-lua
- url = git@github.com:fcitx/fcitx5-lua.git
+ url = https://github.com/fcitx/fcitx5-lua.git
[submodule "lib/libime/src/main/cpp/libime"]
path = lib/libime/src/main/cpp/libime
- url = git@github.com:fcitx/libime.git
+ url = https://github.com/fcitx/libime.git
[submodule "lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons"]
path = lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons
- url = git@github.com:fcitx/fcitx5-chinese-addons.git
+ url = https://github.com/fcitx/fcitx5-chinese-addons.git
[submodule "plugin/anthy/src/main/cpp/anthy-cmake"]
path = plugin/anthy/src/main/cpp/anthy-cmake
- url = git@github.com:fcitx5-android/anthy-cmake.git
+ url = https://github.com/fcitx5-android/anthy-cmake.git
[submodule "plugin/anthy/src/main/cpp/fcitx5-anthy"]
path = plugin/anthy/src/main/cpp/fcitx5-anthy
- url = git@github.com:fcitx/fcitx5-anthy.git
+ url = https://github.com/fcitx/fcitx5-anthy.git
[submodule "plugin/unikey/src/main/cpp/fcitx5-unikey"]
path = plugin/unikey/src/main/cpp/fcitx5-unikey
- url = git@github.com:fcitx/fcitx5-unikey.git
+ url = https://github.com/fcitx/fcitx5-unikey.git
[submodule "plugin/rime/src/main/cpp/fcitx5-rime"]
path = plugin/rime/src/main/cpp/fcitx5-rime
- url = git@github.com:fcitx/fcitx5-rime.git
+ url = https://github.com/fcitx/fcitx5-rime.git
[submodule "plugin/rime/src/main/cpp/rime-prelude"]
path = plugin/rime/src/main/cpp/rime-prelude
- url = git@github.com:rime/rime-prelude.git
+ url = https://github.com/rime/rime-prelude.git
[submodule "plugin/rime/src/main/cpp/rime-essay"]
path = plugin/rime/src/main/cpp/rime-essay
- url = git@github.com:rime/rime-essay.git
+ url = https://github.com/rime/rime-essay.git
[submodule "plugin/rime/src/main/cpp/rime-luna-pinyin"]
path = plugin/rime/src/main/cpp/rime-luna-pinyin
- url = git@github.com:rime/rime-luna-pinyin.git
+ url = https://github.com/rime/rime-luna-pinyin.git
[submodule "plugin/rime/src/main/cpp/rime-stroke"]
path = plugin/rime/src/main/cpp/rime-stroke
- url = git@github.com:rime/rime-stroke.git
+ url = https://github.com/rime/rime-stroke.git
[submodule "plugin/hangul/src/main/cpp/fcitx5-hangul"]
path = plugin/hangul/src/main/cpp/fcitx5-hangul
- url = git@github.com:fcitx/fcitx5-hangul.git
+ url = https://github.com/fcitx/fcitx5-hangul.git
[submodule "plugin/chewing/src/main/cpp/fcitx5-chewing"]
path = plugin/chewing/src/main/cpp/fcitx5-chewing
- url = git@github.com:fcitx/fcitx5-chewing.git
+ url = https://github.com/fcitx/fcitx5-chewing.git
[submodule "plugin/sayura/src/main/cpp/fcitx5-sayura"]
path = plugin/sayura/src/main/cpp/fcitx5-sayura
- url = git@github.com:fcitx/fcitx5-sayura.git
+ url = https://github.com/fcitx/fcitx5-sayura.git
[submodule "plugin/jyutping/src/main/cpp/libime-jyutping"]
path = plugin/jyutping/src/main/cpp/libime-jyutping
- url = git@github.com:fcitx/libime-jyutping.git
+ url = https://github.com/fcitx/libime-jyutping.git
+[submodule "plugin/clipboard-filter/ClearURLsRules"]
+ path = plugin/clipboard-filter/ClearURLsRules
+ url = https://github.com/ClearURLs/Rules.git
+[submodule "plugin/thai/src/main/cpp/fcitx5-libthai"]
+ path = plugin/thai/src/main/cpp/fcitx5-libthai
+ url = https://github.com/fcitx/fcitx5-libthai
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index b38416157..cf0517db7 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,5 +1,40 @@
+
+
+
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 5c40d32cd..fa009839e 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -13,6 +13,7 @@
fcitx
fmtlib
icuuid
+ inputmethodservice
iostreams
iter
jbytes
@@ -21,6 +22,7 @@
jstring
jyutping
kawaii
+ keypress
lgpl
libevent
libime
@@ -37,11 +39,13 @@
shijienihao
shuangpin
sinhala
+ snackbar
spdx
stringutils
unfocus
unikey
zhuyin
+ zlib
\ No newline at end of file
diff --git a/README.md b/README.md
index eaca2a140..85f9bba0f 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ GitHub: [)
- Korean (via [Hangul Plugin](./plugin/hangul))
- Sinhala (via [Sayura Plugin](./plugin/sayura))
+- Thai (via [Thai Plugin](./plugin/thai))
- Generic (via [RIME Plugin](./plugin/rime), supports importing custom schemas)
### Implemented Features
@@ -35,7 +36,7 @@ GitHub: [
- Expandable candidate view
- Clipboard management (plain text only)
-- Theming (custom color scheme and background image)
+- Theming (custom color scheme, background image and dynamic color aka monet color after Android 12)
- Popup preview on key press
- Long press popup keyboard for convenient symbol input
- Symbol and Emoji picker
@@ -62,14 +63,14 @@ Trello kanban: https://trello.com/b/gftk6ZdV/kanban
Matrix Room: https://matrix.to/#/#fcitx5-android:mozilla.org
-Discuss on Telegram: https://t.me/+hci-DrFVWUM3NTUx ([@fcitx5_android](https://t.me/fcitx5_android) originally)
+Discuss on Telegram: [@fcitx5_android_group](https://t.me/fcitx5_android_group) ([@fcitx5_android](https://t.me/fcitx5_android) originally)
## Build
### Dependencies
-- Android SDK Platform & Build-Tools 33.
-- Android NDK (Side by side) 25 & CMake 3.22.1, they can be installed using SDK Manager in Android Studio or `sdkmanager` command line. **Note:** NDK 21 & 22 are confirmed not working with this project.
+- Android SDK Platform & Build-Tools 35.
+- Android NDK (Side by side) 25 & CMake 3.22.1, they can be installed using SDK Manager in Android Studio or `sdkmanager` command line.
- [KDE/extra-cmake-modules](https://github.com/KDE/extra-cmake-modules)
- GNU Gettext >= 0.20 (for `msgfmt` binary; or install `appstream` if you really have to use gettext <= 0.19.)
@@ -82,7 +83,7 @@ Discuss on Telegram: https://t.me/+hci-DrFVWUM3NTUx ([@fcitx5_android](https://t
- Enable symlink support for `git`:
- ```powershell
+ ```shell
git config --global core.symlinks true
```
@@ -90,24 +91,14 @@ Discuss on Telegram: https://t.me/+hci-DrFVWUM3NTUx ([@fcitx5_android](https://t
First, clone this repository and fetch all submodules:
-```sh
+```shell
git clone git@github.com:fcitx5-android/fcitx5-android.git
git submodule update --init --recursive
```
-
-On Windows, you may need to regenerate symlinks to submodules.
-
-```powershell
-Remove-Item -Recurse app/src/main/assets/usr/share, plugin/hangul/src/main/assets/usr/share/libhangul, plugin/chewing/src/main/assets/usr/share/libchewing, plugin/jyutping/src/main/assets/usr/share/libime
-git checkout -- .
-```
-
-
-
Install `extra-cmake-modules` and `gettext` with your system package manager:
-```sh
+```shell
# For Arch Linux (Arch has gettext in it's base meta package)
sudo pacman -S extra-cmake-modules
@@ -119,7 +110,7 @@ brew install extra-cmake-modules gettext
# For Windows, install MSYS2 and execute in its shell (UCRT64)
pacman -S mingw-w64-ucrt-x86_64-extra-cmake-modules mingw-w64-ucrt-x86_64-gettext
-# then add C:/msys64/ucrt64/bin to PATH
+# then add C:\msys64\ucrt64\bin to PATH
```
Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via SDK Manager in Android Studio:
@@ -148,7 +139,7 @@ The current recommended versions are recorded in [Versions.kt](build-logic/conve
Switch to "Project" view in the "Project" tool window (namely the file tree side bar), right click `lib/fcitx5/src/main/cpp/prebuilt` directory, then select "Mark Directory as > Excluded". You may also need to restart the IDE to interrupt ongoing indexing process.
-- Gradle error: "No variants found for ':app'. Check build files to ensure at least one variant exists."
+- Gradle error: "No variants found for ':app'. Check build files to ensure at least one variant exists." or "[CXX1210] /CMakeLists.txt debug|arm64-v8a : No compatible library found"
Examine if there are environment variables set such as `_JAVA_OPTIONS` or `JAVA_TOOL_OPTIONS`. You might want to clear them (maybe in the startup script `studio.sh` of Android Studio), as some gradle plugin treats anything in stderr as errors and aborts.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fc6c1364d..1f435a29b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,3 @@
-@file:Suppress("UnstableApiUsage")
-
plugins {
id("org.fcitx.fcitx5.android.app-convention")
id("org.fcitx.fcitx5.android.native-app-convention")
@@ -18,6 +16,7 @@ android {
applicationId = "org.fcitx.fcitx5.android"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ @Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
targets(
@@ -64,21 +63,22 @@ android {
kotlin {
sourceSets.configureEach {
- kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/")
+ kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/$name/kotlin"))
}
}
-aboutLibraries {
- configPath = "app/licenses"
-}
-
fcitxComponent {
- installLibraries = listOf(
+ includeLibs = listOf(
"fcitx5",
"fcitx5-lua",
"libime",
"fcitx5-chinese-addons"
)
+ // exclude (delete immediately after install) tables that nobody would use
+ excludeFiles = listOf("cangjie", "erbi", "qxm", "wanfeng").map {
+ "usr/share/fcitx5/inputmethod/$it.conf"
+ }
+ installPrebuiltAssets = true
}
ksp {
@@ -114,9 +114,11 @@ dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.paging)
+ implementation(libs.androidx.startup)
implementation(libs.androidx.viewpager2)
implementation(libs.material)
- implementation(libs.arrow)
+ implementation(libs.arrow.core)
+ implementation(libs.arrow.functions)
implementation(libs.imagecropper)
implementation(libs.flexbox)
implementation(libs.dependency)
diff --git a/app/licenses/libraries/boost.json b/app/licenses/libraries/boost.json
index 7a2285fb6..cebc09b62 100644
--- a/app/licenses/libraries/boost.json
+++ b/app/licenses/libraries/boost.json
@@ -1,6 +1,6 @@
{
"uniqueId": "boostorg/boost",
- "artifactVersion": "1.83.0",
+ "artifactVersion": "1.86.0",
"description": "Free peer-reviewed portable C++ source libraries",
"name": "boostorg/boost",
"website": "https://www.boost.org/",
diff --git a/app/licenses/libraries/fcitx5-chinese-addons.json b/app/licenses/libraries/fcitx5-chinese-addons.json
index f1edc7feb..8200bcbed 100644
--- a/app/licenses/libraries/fcitx5-chinese-addons.json
+++ b/app/licenses/libraries/fcitx5-chinese-addons.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-chinese-addons",
- "artifactVersion": "5.1.2",
+ "artifactVersion": "5.1.7",
"description": "Chinese related addon for fcitx5",
"name": "fcitx/fcitx5-chinese-addons",
"website": "https://github.com/fcitx/fcitx5-chinese-addons",
diff --git a/app/licenses/libraries/fcitx5-lua.json b/app/licenses/libraries/fcitx5-lua.json
index 1d3e279ad..e3ff2ca38 100644
--- a/app/licenses/libraries/fcitx5-lua.json
+++ b/app/licenses/libraries/fcitx5-lua.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5-lua",
- "artifactVersion": "5.0.11",
+ "artifactVersion": "5.0.14",
"description": "Lua support for fcitx5",
"name": "fcitx/fcitx5-lua",
"website": "https://github.com/fcitx/fcitx5-lua",
diff --git a/app/licenses/libraries/fcitx5.json b/app/licenses/libraries/fcitx5.json
index 0683c6262..f20f85043 100644
--- a/app/licenses/libraries/fcitx5.json
+++ b/app/licenses/libraries/fcitx5.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5",
- "artifactVersion": "5.1.5",
+ "artifactVersion": "5.1.12",
"description": "Next generation of fcitx",
"name": "fcitx/fcitx5",
"website": "https://github.com/fcitx/fcitx5",
diff --git a/app/licenses/libraries/fmt.json b/app/licenses/libraries/fmt.json
index b69becddf..07f2dbdc1 100644
--- a/app/licenses/libraries/fmt.json
+++ b/app/licenses/libraries/fmt.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fmtlib/fmt",
- "artifactVersion": "9.1.0",
+ "artifactVersion": "11.0.2",
"description": "Open-source formatting library for C++",
"name": "fmtlib/fmt",
"website": "https://fmt.dev",
diff --git a/app/licenses/libraries/libevent.json b/app/licenses/libraries/libevent.json
deleted file mode 100644
index b7ed73770..000000000
--- a/app/licenses/libraries/libevent.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "uniqueId": "libevent/libevent",
- "artifactVersion": "release-2.1.12-stable",
- "description": "Event notification library",
- "name": "libevent/libevent",
- "website": "https://libevent.org/",
- "tag": "native",
- "licenses": [
- "BSD-3-Clause"
- ]
-}
diff --git a/app/licenses/libraries/libime.json b/app/licenses/libraries/libime.json
index 899f13945..18507f632 100644
--- a/app/licenses/libraries/libime.json
+++ b/app/licenses/libraries/libime.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/libime",
- "artifactVersion": "1.1.3",
+ "artifactVersion": "1.1.10",
"description": "library to support generic input method implementation",
"name": "fcitx/libime",
"website": "https://github.com/fcitx/libime",
diff --git a/app/licenses/libraries/libintl-lite.json b/app/licenses/libraries/libintl-lite.json
index 86daf4cce..e08b58baf 100644
--- a/app/licenses/libraries/libintl-lite.json
+++ b/app/licenses/libraries/libintl-lite.json
@@ -1,6 +1,6 @@
{
"uniqueId": "j-jorge/libintl-lite",
- "artifactVersion": "5750d92",
+ "artifactVersion": "ba15146",
"description": "simple (but less powerful) GNU gettext libintl replacement",
"name": "j-jorge/libintl-lite",
"website": "https://github.com/j-jorge/libintl-lite",
diff --git a/app/licenses/libraries/libuv.json b/app/licenses/libraries/libuv.json
new file mode 100644
index 000000000..65d992b93
--- /dev/null
+++ b/app/licenses/libraries/libuv.json
@@ -0,0 +1,11 @@
+{
+ "uniqueId": "libuv/libuv",
+ "artifactVersion": "1.49.2",
+ "description": "Cross-platform asynchronous I/O",
+ "name": "libuv/libuv",
+ "website": "https://libuv.org/",
+ "tag": "native",
+ "licenses": [
+ "MIT"
+ ]
+}
diff --git a/app/licenses/libraries/lua.json b/app/licenses/libraries/lua.json
index 386077cda..11c9ef4c7 100644
--- a/app/licenses/libraries/lua.json
+++ b/app/licenses/libraries/lua.json
@@ -1,6 +1,6 @@
{
"uniqueId": "lua/lua",
- "artifactVersion": "5.4.6",
+ "artifactVersion": "5.4.7",
"description": "Powerful lightweight programming language designed for extending applications",
"name": "lua/lua",
"website": "https://www.lua.org/",
diff --git a/app/licenses/libraries/opencc.json b/app/licenses/libraries/opencc.json
index 5aa3ea77b..ed4109ee0 100644
--- a/app/licenses/libraries/opencc.json
+++ b/app/licenses/libraries/opencc.json
@@ -1,6 +1,6 @@
{
"uniqueId": "BYVoid/OpenCC",
- "artifactVersion": "1.1.7",
+ "artifactVersion": "1.1.9",
"description": "opensource project for conversions between Traditional Chinese, Simplified Chinese and Japanese Kanji (Shinjitai).",
"name": "BYVoid/OpenCC",
"website": "https://opencc.byvoid.com/",
diff --git a/app/org.fcitx.fcitx5.android.yml b/app/org.fcitx.fcitx5.android.yml
index 62fdba70e..7c2ffc2eb 100644
--- a/app/org.fcitx.fcitx5.android.yml
+++ b/app/org.fcitx.fcitx5.android.yml
@@ -20,11 +20,10 @@ Builds:
submodules: true
sudo:
- apt-get update
- - apt-get install -y g++ libtool make automake gettext bzip2 xz-utils pkg-config
- cmake extra-cmake-modules ninja-build libfmt-dev libboost-all-dev libfcitx5utils-dev opencc openjdk-17-jdk-headless
- ghc cabal-install libghc-shake-dev libghc-aeson-pretty-dev libghc-js-flot-data haskell-js-dgtable-utils
- - update-java-alternatives -a
- - apt-get install -y -t bullseye-backports fcitx5-modules
+ - apt-get install -y g++ libtool make automake gettext bzip2 xz-utils zstd pkg-config
+ cmake extra-cmake-modules ninja-build libfmt-dev libsystemd-dev libboost-all-dev
+ ghc cabal-install libghc-shake-dev libghc-aeson-pretty-dev libghc-js-flot-data haskell-js-dgtable-utils
+ python-is-python3 opencc
gradle:
- yes
binary: https://jenkins.fcitx-im.org/job/android/job/fcitx5-android/lastSuccessfulBuild/artifact/out/org.fcitx.fcitx5.android-%v-%abi-release.apk
@@ -33,7 +32,7 @@ Builds:
rm:
- lib/fcitx5/src/main/cpp/prebuilt
prebuild:
- - sdkmanager 'cmake;3.22.1'
+ - sdkmanager 'cmake;3.31.6'
- sed -i -e '/ImportQualifiedPost/d' $$fcitx5-android-prebuilder$$/src/Main.hs
- sed -i -e 's/import \(.*\) qualified as/import qualified \1 as/g' $$fcitx5-android-prebuilder$$/src/*.hs
- sed -i -e 's|https://maven.pkg.github.com|https://jitpack.io|g' ../build-logic/convention/build.gradle.kts
@@ -44,13 +43,11 @@ Builds:
- build-logic/convention/build
build:
- pushd $$fcitx5-android-prebuilder$$
- - cabal configure --disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic
- - ABI=%abi ANDROID_NDK_ROOT=$$NDK$$ CMAKE_VERSION=3.22.1 ANDROID_PLATFORM=23
- COMP_SPELL_DICT=/usr/lib/x86_64-linux-gnu/fcitx5/libexec/comp-spell-dict
- ./build-cabal -j spell-dict fmt libevent libintl-lite boost marisa opencc libime lua chinese-addons-data zstd
+ - ABI=%abi ANDROID_NDK_ROOT=$$NDK$$ CMAKE_VERSION=3.31.6 ANDROID_PLATFORM=23
+ ./build-cabal -j app
- popd
- mv $$fcitx5-android-prebuilder$$/build ../lib/fcitx5/src/main/cpp/prebuilt
- ndk: 25.2.9519653
+ ndk: 28.0.13004108
gradleprops:
- buildABI=%abi
- buildTimestamp=%ts
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index a853cde43..d154724d5 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -22,8 +22,13 @@
# remove kotlin null checks
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
- static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
- static void checkNotNullParameter(java.lang.Object, java.lang.String);
+ static void checkNotNull(...);
+ static void checkExpressionValueIsNotNull(...);
+ static void checkNotNullExpressionValue(...);
+ static void checkReturnedValueIsNotNull(...);
+ static void checkFieldIsNotNull(...);
+ static void checkParameterIsNotNull(...);
+ static void checkNotNullParameter(...);
}
# Uncomment this to preserve the line number information for
diff --git a/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/4.json b/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/4.json
new file mode 100644
index 000000000..14363cb60
--- /dev/null
+++ b/app/schemas/org.fcitx.fcitx5.android.data.clipboard.db.ClipboardDatabase/4.json
@@ -0,0 +1,74 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "b0fe6cdac09e0d7deaff17d8b45fe565",
+ "entities": [
+ {
+ "tableName": "clipboard",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL DEFAULT -1, `type` TEXT NOT NULL DEFAULT 'text/plain', `deleted` INTEGER NOT NULL DEFAULT 0, `sensitive` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pinned",
+ "columnName": "pinned",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'text/plain'"
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "sensitive",
+ "columnName": "sensitive",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b0fe6cdac09e0d7deaff17d8b45fe565')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7c16296c4..fcc089b60 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,6 +21,9 @@
+
+
+
-
+ android:name=".ui.main.CropImageActivity"
+ android:exported="false" />
+
@@ -99,6 +103,10 @@
+
+
+
+
@@ -116,7 +124,7 @@
@@ -131,7 +139,8 @@
+ android:permission="${applicationId}.permission.IPC"
+ tools:ignore="SystemPermissionTypo">
@@ -148,18 +157,23 @@
+
+
+
+
+
+
-
-
${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- # fcitx5-lua
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- # fcitx5-chinese-addons
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ $
+ ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
COMMENT "Copying fcitx5 module libraries to :app"
)
+
+# install prebuilt assets
+install(FILES "${PREBUILT_DIR}/spell-dict/en_dict.fscd" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/spell" COMPONENT prebuilt-assets)
+install(FILES "${PREBUILT_DIR}/chinese-addons-data/pinyin/chaizi.dict" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/pinyin" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/chinese-addons-data/pinyinhelper" DESTINATION "${FCITX_INSTALL_PKGDATADIR}" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/libime/table" DESTINATION "${FCITX_INSTALL_PKGDATADIR}" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/libime/data/" DESTINATION "${FCITX_INSTALL_DATADIR}/libime" COMPONENT prebuilt-assets)
+install(DIRECTORY "${PREBUILT_DIR}/opencc/data/" DESTINATION "${FCITX_INSTALL_DATADIR}/opencc" COMPONENT prebuilt-assets)
diff --git a/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp b/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp
deleted file mode 100644
index 26ae10387..000000000
--- a/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer
- * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
- * SPDX-FileComment: Modified from https://github.com/fcitx/fcitx5/blob/5.1.1/src/lib/fcitx/addonloader.cpp
- */
-#include "androidaddonloader.h"
-
-#define FCITX_LIBRARY_SUFFIX ".so"
-
-namespace fcitx {
-
-AndroidSharedLibraryLoader::AndroidSharedLibraryLoader(AndroidLibraryDependency dependency)
- : dependency_(std::move(dependency)) {}
-
-AddonInstance *AndroidSharedLibraryLoader::load(const AddonInfo &info,
- AddonManager *manager) {
- auto iter = registry_.find(info.uniqueName());
- if (iter == registry_.end()) {
- std::string libname = info.library();
- Flags flag = LibraryLoadHint::DefaultHint;
- if (stringutils::startsWith(libname, "export:")) {
- libname = libname.substr(7);
- flag |= LibraryLoadHint::ExportExternalSymbolsHint;
- }
- auto file = libname + FCITX_LIBRARY_SUFFIX;
- auto libs = standardPath_.locateAll(StandardPath::Type::Addon, file);
- if (libs.empty()) {
- FCITX_ERROR() << "Could not locate library " << file
- << " for addon " << info.uniqueName() << ".";
- }
- // ========== Android specific start ========== //
- auto deps = dependency_.find(libname);
- if (deps != dependency_.end()) {
- for (const auto &dep: deps->second) {
- auto depFile = dep + FCITX_LIBRARY_SUFFIX;
- auto depPaths = standardPath_.locateAll(StandardPath::Type::Addon, depFile);
- if (depPaths.empty()) {
- FCITX_ERROR() << "Could not locate dependency " << depFile
- << " for library " << file << ".";
- } else {
- for (const auto &depPath: depPaths) {
- Library depLib(depPath);
- if (!depLib.load()) {
- FCITX_ERROR() << "Failed to load dependency " << depPath
- << " for library " << file << ".";
- } else {
- FCITX_INFO() << "Loaded dependency " << depFile
- << " for library " << file << ".";
- break;
- }
- }
- }
- }
- }
- // ========== Android specific end ========== //
- for (const auto &libraryPath: libs) {
- Library lib(libraryPath);
- if (!lib.load(flag)) {
- FCITX_ERROR()
- << "Failed to load library for addon " << info.uniqueName()
- << " on " << libraryPath << ". Error: " << lib.error();
- continue;
- }
- try {
- registry_.emplace(
- info.uniqueName(),
- std::make_unique(std::move(lib)));
- } catch (const std::exception &e) {
- }
- break;
- }
- iter = registry_.find(info.uniqueName());
- }
-
- if (iter == registry_.end()) {
- return nullptr;
- }
-
- try {
- return iter->second->factory()->create(manager);
- } catch (const std::exception &e) {
- FCITX_ERROR() << "Failed to create addon: " << info.uniqueName() << " "
- << e.what();
- } catch (...) {
- FCITX_ERROR() << "Failed to create addon: " << info.uniqueName();
- }
- return nullptr;
-}
-
-}
diff --git a/app/src/main/cpp/androidaddonloader/androidaddonloader.h b/app/src/main/cpp/androidaddonloader/androidaddonloader.h
deleted file mode 100644
index ff3a444b9..000000000
--- a/app/src/main/cpp/androidaddonloader/androidaddonloader.h
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer
- * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
- * SPDX-FileComment: Modified from https://github.com/fcitx/fcitx5/blob/5.1.1/src/lib/fcitx/addonloader_p.h
- */
-#ifndef FCITX5_ANDROID_ANDROIDADDONLOADER_H
-#define FCITX5_ANDROID_ANDROIDADDONLOADER_H
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace fcitx {
-
-class AndroidSharedLibraryFactory {
-public:
- AndroidSharedLibraryFactory(Library lib) : library_(std::move(lib)) {
- auto *funcPtr = library_.resolve("fcitx_addon_factory_instance");
- if (!funcPtr) {
- throw std::runtime_error(library_.error());
- }
- auto func = Library::toFunction(funcPtr);
- factory_ = func();
- if (!factory_) {
- throw std::runtime_error("Failed to get a factory");
- }
- }
-
- AddonFactory *factory() { return factory_; }
-
-private:
- Library library_;
- AddonFactory *factory_;
-};
-
-typedef std::unordered_map> AndroidLibraryDependency;
-
-class AndroidSharedLibraryLoader : public AddonLoader {
-public:
- AndroidSharedLibraryLoader(AndroidLibraryDependency dependency);
- ~AndroidSharedLibraryLoader() = default;
- std::string type() const override { return "SharedLibrary"; }
-
- AddonInstance *load(const AddonInfo &info, AddonManager *manager) override;
-
-private:
- StandardPath standardPath_;
- std::unordered_map> registry_;
- AndroidLibraryDependency dependency_;
-};
-
-}
-
-#endif //FCITX5_ANDROID_ANDROIDADDONLOADER_H
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.cpp b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
index 60ce1ae36..620452604 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.cpp
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
@@ -4,6 +4,7 @@
*/
#include
#include
+#include
#include
#include
#include
@@ -49,7 +50,8 @@ class AndroidInputContext : public InputContextV2 {
const int before = -offset;
const int after = offset + static_cast(size);
if (before < 0 || after < 0) {
- FCITX_WARN() << "Invalid deleteSurrounding request: offset=" << offset << ", size=" << size;
+ FCITX_WARN() << "Invalid deleteSurrounding request: offset=" << offset << ", size="
+ << size;
return;
}
frontend_->deleteSurrounding(before, after);
@@ -60,24 +62,18 @@ class AndroidInputContext : public InputContextV2 {
}
void updateInputPanel() {
- // Normally input method engine should check CapabilityFlag::Preedit before update clientPreedit,
- // and fcitx5 won't trigger UpdatePreeditEvent when that flag is not present, in which case
- // InputContext::updatePreeditImpl() won't be called.
- // However on Android, androidkeyboard uses clientPreedit unconditionally in order to provide
- // a more integrated experience, so we need to check clientPreedit update manually even if
- // clientPreedit is not enabled.
const InputPanel &ip = inputPanel();
- if (!isPreeditEnabled() && frontend_->instance()->inputMethod(this) == "keyboard-us") {
- frontend_->updateClientPreedit(filterText(ip.clientPreedit()));
- }
frontend_->updateInputPanel(
filterText(ip.preedit()),
filterText(ip.auxUp()),
filterText(ip.auxDown())
);
+ }
+
+ void updateCandidatesBulk() {
std::vector candidates;
int size = 0;
- const auto &list = ip.candidateList();
+ const auto &list = inputPanel().candidateList();
if (list) {
const auto &bulk = list->toBulk();
if (bulk) {
@@ -89,7 +85,7 @@ class AndroidInputContext : public InputContextV2 {
auto &candidate = bulk->candidateFromAll(i);
// maybe unnecessary; I don't see anywhere using `CandidateWord::setPlaceHolder`
// if (candidate.isPlaceHolder()) continue;
- candidates.emplace_back(filterString(candidate.text()));
+ candidates.emplace_back(filterString(candidate.textWithComment()));
} catch (const std::invalid_argument &e) {
size = static_cast(candidates.size());
break;
@@ -98,14 +94,44 @@ class AndroidInputContext : public InputContextV2 {
} else {
size = list->size();
for (int i = 0; i < size; i++) {
- candidates.emplace_back(filterString(list->candidate(i).text()));
+ candidates.emplace_back(filterString(list->candidate(i).textWithComment()));
}
}
}
frontend_->updateCandidateList(candidates, size);
}
- bool selectCandidate(int idx) {
+ void updateCandidatesPaged() {
+ const auto &list = inputPanel().candidateList();
+ if (!list) {
+ frontend_->updatePagedCandidate(PagedCandidateEntity::Empty);
+ return;
+ }
+ int cursorIndex = list->cursorIndex();
+ CandidateLayoutHint layoutHint = list->layoutHint();
+ bool hasPrev = false;
+ bool hasNext = false;
+ const auto &pageable = list->toPageable();
+ if (pageable) {
+ hasPrev = pageable->hasPrev();
+ hasNext = pageable->hasNext();
+ }
+ int size = list->size();
+ std::vector candidates;
+ candidates.reserve(size);
+ for (int i = 0; i < size; i++) {
+ const auto &c = list->candidate(i);
+ candidates.emplace_back(
+ filterString(list->label(i)),
+ filterString(c.text()),
+ filterString(c.comment())
+ );
+ }
+ PagedCandidateEntity paged(candidates, cursorIndex, layoutHint, hasPrev, hasNext);
+ frontend_->updatePagedCandidate(paged);
+ }
+
+ bool selectCandidateBulk(int idx) {
const auto &list = inputPanel().candidateList();
if (!list) {
return false;
@@ -124,6 +150,20 @@ class AndroidInputContext : public InputContextV2 {
return true;
}
+ bool selectCandidatePaged(int idx) {
+ const auto &list = inputPanel().candidateList();
+ if (!list) {
+ return false;
+ }
+ try {
+ list->candidate(idx).select(this);
+ } catch (const std::invalid_argument &e) {
+ FCITX_WARN() << "selectCandidate index out of range";
+ return false;
+ }
+ return true;
+ }
+
std::vector getCandidates(const int offset, const int limit) {
std::vector candidates;
const auto &list = inputPanel().candidateList();
@@ -136,7 +176,7 @@ class AndroidInputContext : public InputContextV2 {
for (int i = offset; i < end; i++) {
try {
auto &candidate = bulk->candidateFromAll(i);
- candidates.emplace_back(filterString(candidate.text()));
+ candidates.emplace_back(filterString(candidate.textWithComment()));
} catch (const std::invalid_argument &e) {
break;
}
@@ -144,13 +184,84 @@ class AndroidInputContext : public InputContextV2 {
} else {
const int end = std::min(list->size(), last);
for (int i = offset; i < end; i++) {
- candidates.emplace_back(filterString(list->candidate(i).text()));
+ candidates.emplace_back(filterString(list->candidate(i).textWithComment()));
}
}
}
return candidates;
}
+ std::vector getCandidateAction(const int idx) {
+ std::vector actions;
+ const auto &list = inputPanel().candidateList();
+ if (list) {
+ const auto &actionable = list->toActionable();
+ if (actionable) {
+ if (idx >= list->size()) {
+ const auto &bulk = list->toBulk();
+ if (bulk) {
+ try {
+ const auto &c = bulk->candidateFromAll(idx);
+ for (const auto &a: actionable->candidateActions(c)) {
+ actions.emplace_back(a);
+ }
+ } catch (const std::exception &e) {
+ FCITX_WARN() << "getCandidateAction(" << idx << ") failed:" << e.what();
+ }
+ }
+ } else {
+ const auto &c = list->candidate(idx);
+ for (const auto &a: actionable->candidateActions(c)) {
+ actions.emplace_back(a);
+ }
+ }
+ }
+ }
+ return actions;
+ }
+
+ void triggerCandidateAction(const int idx, const int actionIdx) {
+ const auto &list = inputPanel().candidateList();
+ if (!list) return;
+ const auto &actionable = list->toActionable();
+ if (!actionable) return;
+ if (idx >= list->size()) {
+ const auto &bulk = list->toBulk();
+ if (bulk) {
+ try {
+ const auto &c = bulk->candidateFromAll(idx);
+ actionable->triggerAction(c, actionIdx);
+ } catch (const std::exception &e) {
+ FCITX_WARN() << "triggerCandidateAction(" << idx << ") failed:" << e.what();
+ }
+ }
+ } else {
+ const auto &c = list->candidate(idx);
+ actionable->triggerAction(c, actionIdx);
+ }
+ }
+
+ void offsetCandidatePage(int delta) {
+ if (delta == 0) {
+ return;
+ }
+ const auto &list = inputPanel().candidateList();
+ if (!list) {
+ return;
+ }
+ const auto &pageable = list->toPageable();
+ if (!pageable) {
+ return;
+ }
+ if (delta > 0 && pageable->hasNext()) {
+ pageable->next();
+ updateUserInterface(UserInterfaceComponent::InputPanel);
+ } else if (delta < 0 && pageable->hasPrev()) {
+ pageable->prev();
+ updateUserInterface(UserInterfaceComponent::InputPanel);
+ }
+ }
+
private:
AndroidFrontend *frontend_;
int uid_;
@@ -169,7 +280,8 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
focusGroup_("android", instance->inputContextManager()),
activeIC_(nullptr),
icCache_(),
- eventHandlers_() {
+ eventHandlers_(),
+ pagingMode_(0) {
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextInputMethodActivated,
EventWatcherPhase::Default,
@@ -185,7 +297,14 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
auto &e = static_cast(event);
switch (e.component()) {
case UserInterfaceComponent::InputPanel: {
- if (activeIC_) activeIC_->updateInputPanel();
+ if (activeIC_) {
+ activeIC_->updateInputPanel();
+ if (pagingMode_ == 0) {
+ activeIC_->updateCandidatesBulk();
+ } else {
+ activeIC_->updateCandidatesPaged();
+ }
+ }
break;
}
case UserInterfaceComponent::StatusArea: {
@@ -234,7 +353,21 @@ void AndroidFrontend::releaseInputContext(const int uid) {
bool AndroidFrontend::selectCandidate(int idx) {
if (!activeIC_) return false;
- return activeIC_->selectCandidate(idx);
+ if (pagingMode_) {
+ return activeIC_->selectCandidatePaged(idx);
+ } else {
+ return activeIC_->selectCandidateBulk(idx);
+ }
+}
+
+std::vector AndroidFrontend::getCandidateActions(const int idx) {
+ if (!activeIC_) return {};
+ return activeIC_->getCandidateAction(idx);
+}
+
+void AndroidFrontend::triggerCandidateAction(const int idx, const int actionIdx) {
+ if (!activeIC_) return;
+ activeIC_->triggerCandidateAction(idx, actionIdx);
}
bool AndroidFrontend::isInputPanelEmpty() {
@@ -307,6 +440,19 @@ void AndroidFrontend::showToast(const std::string &s) {
toastCallback(s);
}
+void AndroidFrontend::setCandidatePagingMode(const int mode) {
+ pagingMode_ = mode;
+}
+
+void AndroidFrontend::updatePagedCandidate(const PagedCandidateEntity &paged) {
+ pagedCandidateCallback(paged);
+}
+
+void AndroidFrontend::offsetCandidatePage(int delta) {
+ if (!activeIC_) return;
+ activeIC_->offsetCandidatePage(delta);
+}
+
void AndroidFrontend::setCommitStringCallback(const CommitStringCallback &callback) {
commitStringCallback = callback;
}
@@ -339,6 +485,10 @@ void AndroidFrontend::setToastCallback(const ToastCallback &callback) {
toastCallback = callback;
}
+void AndroidFrontend::setPagedCandidateCallback(const PagedCandidateCallback &callback) {
+ pagedCandidateCallback = callback;
+}
+
class AndroidFrontendFactory : public AddonFactory {
public:
AddonInstance *create(AddonManager *manager) override {
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.h b/app/src/main/cpp/androidfrontend/androidfrontend.h
index 9dc662cfe..9b9db800b 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.h
@@ -2,8 +2,9 @@
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
*/
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_H_
+
+#ifndef FCITX5_ANDROID_ANDROIDFRONTEND_H
+#define FCITX5_ANDROID_ANDROIDFRONTEND_H
#include
#include
@@ -18,7 +19,7 @@ class AndroidInputContext;
class AndroidFrontend : public AddonInstance {
public:
- AndroidFrontend(Instance *instance);
+ explicit AndroidFrontend(Instance *instance);
Instance *instance() { return instance_; }
@@ -27,6 +28,7 @@ class AndroidFrontend : public AddonInstance {
void updateClientPreedit(const Text &clientPreedit);
void updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown);
void releaseInputContext(const int uid);
+ void updatePagedCandidate(const PagedCandidateEntity &paged);
void keyEvent(const Key &key, bool isRelease, const int timestamp);
void forwardKey(const Key &key, bool isRelease);
@@ -37,11 +39,15 @@ class AndroidFrontend : public AddonInstance {
void focusInputContext(bool focus);
void activateInputContext(const int uid, const std::string &pkgName);
void deactivateInputContext(const int uid);
- InputContext *activeInputContext() const;
+ [[nodiscard]] InputContext *activeInputContext() const;
void setCapabilityFlags(uint64_t flag);
std::vector getCandidates(const int offset, const int limit);
+ std::vector getCandidateActions(const int idx);
+ void triggerCandidateAction(const int idx, const int actionIdx);
void deleteSurrounding(const int before, const int after);
void showToast(const std::string &s);
+ void setCandidatePagingMode(const int mode);
+ void offsetCandidatePage(int delta);
void setCandidateListCallback(const CandidateListCallback &callback);
void setCommitStringCallback(const CommitStringCallback &callback);
void setPreeditCallback(const ClientPreeditCallback &callback);
@@ -51,6 +57,7 @@ class AndroidFrontend : public AddonInstance {
void setStatusAreaUpdateCallback(const StatusAreaUpdateCallback &callback);
void setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback);
void setToastCallback(const ToastCallback &callback);
+ void setPagedCandidateCallback(const PagedCandidateCallback &callback);
private:
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, keyEvent);
@@ -64,7 +71,11 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, deactivateInputContext);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCapabilityFlags);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, getCandidates);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, getCandidateActions);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, triggerCandidateAction);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, showToast);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCandidatePagingMode);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, offsetCandidatePage);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCandidateListCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setCommitStringCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPreeditCallback);
@@ -74,12 +85,14 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setStatusAreaUpdateCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setToastCallback);
+ FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setPagedCandidateCallback);
Instance *instance_;
FocusGroup focusGroup_;
AndroidInputContext *activeIC_;
InputContextCache icCache_;
std::vector>> eventHandlers_;
+ int pagingMode_;
CandidateListCallback candidateListCallback = [](const std::vector &, const int) {};
CommitStringCallback commitStringCallback = [](const std::string &, const int) {};
@@ -90,7 +103,8 @@ class AndroidFrontend : public AddonInstance {
StatusAreaUpdateCallback statusAreaUpdateCallback = [] {};
DeleteSurroundingCallback deleteSurroundingCallback = [](const int, const int) {};
ToastCallback toastCallback = [](const std::string &) {};
+ PagedCandidateCallback pagedCandidateCallback = [](const PagedCandidateEntity &) {};
};
} // namespace fcitx
-#endif //_FCITX5_ANDROID_ANDROIDFRONTEND_H_
+#endif //FCITX5_ANDROID_ANDROIDFRONTEND_H
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend_public.h b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
index 878309093..69bcbbdd5 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend_public.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
@@ -2,12 +2,16 @@
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
*/
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+#ifndef FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
+#define FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
+#include
#include
+#include
#include
+#include "../helper-types.h"
+
typedef std::function &, const int)> CandidateListCallback;
typedef std::function CommitStringCallback;
typedef std::function ClientPreeditCallback;
@@ -17,6 +21,7 @@ typedef std::function InputMethodChangeCallback;
typedef std::function StatusAreaUpdateCallback;
typedef std::function DeleteSurroundingCallback;
typedef std::function ToastCallback;
+typedef std::function PagedCandidateCallback;
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, keyEvent,
void(const fcitx::Key &, bool isRelease, const int timestamp))
@@ -51,9 +56,21 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCapabilityFlags,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, getCandidates,
std::vector(const int, const int))
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, getCandidateActions,
+ std::vector(const int))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, triggerCandidateAction,
+ void(const int, const int))
+
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, showToast,
void(const std::string &))
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCandidatePagingMode,
+ void(const int))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, offsetCandidatePage,
+ void(int))
+
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCandidateListCallback,
void(const CandidateListCallback &))
@@ -81,4 +98,7 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setToastCallback,
void(const ToastCallback &))
-#endif // _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setPagedCandidateCallback,
+ void(const PagedCandidateCallback &))
+
+#endif // FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
index 414f85a4a..a84d55ab7 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
@@ -108,8 +108,7 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
}
if (key.isLAZ() || key.isUAZ() || validSym ||
(!buffer.empty() && key.checkKeyList(FCITX_HYPHEN_APOS))) {
- auto text = Key::keySymToUTF8(key.sym());
- if (updateBuffer(inputContext, text)) {
+ if (updateBuffer(inputContext, event)) {
return event.filterAndAccept();
}
}
@@ -142,16 +141,16 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
auto cursor = buffer.cursor();
if (cursor > 0) {
buffer.setCursor(cursor - 1);
+ event.filterAndAccept();
+ return updateCandidate(entry, inputContext);
}
- event.filterAndAccept();
- return updateCandidate(entry, inputContext);
} else if (key.check(FcitxKey_Right) || key.check(FcitxKey_KP_Right)) {
auto cursor = buffer.cursor();
if (cursor < buffer.size()) {
buffer.setCursor(buffer.cursor() + 1);
+ event.filterAndAccept();
+ return updateCandidate(entry, inputContext);
}
- event.filterAndAccept();
- return updateCandidate(entry, inputContext);
}
}
@@ -249,14 +248,22 @@ void AndroidKeyboardEngine::resetState(InputContext *inputContext, bool fromCand
void AndroidKeyboardEngine::updateCandidate(const InputMethodEntry &entry, InputContext *inputContext) {
inputContext->inputPanel().reset();
auto *state = inputContext->propertyFor(&factory_);
+ const auto userInput = state->buffer_.userInput();
std::vector> results;
if (spell()) {
results = spell()->call(entry.languageCode(),
SpellProvider::Default,
- state->buffer_.userInput(),
+ userInput,
SpellCandidateSize);
}
auto candidateList = std::make_unique();
+ if (results.empty() || results.front().second != userInput) {
+ // TODO: comply with fcitx5 spell module's delim " _-,./?!%"
+ // it's fine in androidkeyboard because only "-" won't commit buffer
+ const auto segments = stringutils::split(userInput, "-");
+ const auto label = segments.size() > 1 ? segments.back() : userInput;
+ candidateList->append(this, Text(label), userInput);
+ }
for (const auto &result: results) {
candidateList->append(this, Text(result.first), result.second);
}
@@ -269,30 +276,32 @@ void AndroidKeyboardEngine::updateCandidate(const InputMethodEntry &entry, Input
}
void AndroidKeyboardEngine::updateUI(InputContext *inputContext) {
- auto [preedit, cursor] = preeditWithCursor(inputContext);
- Text clientPreedit(preedit, TextFormatFlag::Underline);
- clientPreedit.setCursor(static_cast(cursor));
- inputContext->inputPanel().setClientPreedit(clientPreedit);
- // we don't want preedit here ...
-// if (!inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) {
-// inputContext->inputPanel().setPreedit(preedit);
-// }
- inputContext->updatePreedit();
+ auto [text, cursor] = preeditWithCursor(inputContext);
+ if (inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) {
+ Text clientPreedit(text, TextFormatFlag::Underline);
+ clientPreedit.setCursor(static_cast(cursor));
+ inputContext->inputPanel().setClientPreedit(clientPreedit);
+ inputContext->updatePreedit();
+ } else {
+ Text preedit(text);
+ preedit.setCursor(static_cast(cursor));
+ inputContext->inputPanel().setPreedit(preedit);
+ }
inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}
-bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::string &chr) {
+bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const KeyEvent& event) {
auto *entry = instance_->inputMethodEntry(inputContext);
if (!entry) {
return false;
}
auto *state = inputContext->propertyFor(&factory_);
- const CapabilityFlags noPredictFlag{CapabilityFlag::Password,
- CapabilityFlag::NoSpellCheck};
- // no spell hint enabled or no supported dictionary
+ // word hint is disabled, input is password, or language not supported
if (!*config_.enableWordHint ||
- inputContext->capabilityFlags().testAny(noPredictFlag) ||
+ (!*config_.hintOnPhysicalKeyboard && !event.isVirtual()) ||
+ (*config_.editorControlledWordHint && inputContext->capabilityFlags().test(CapabilityFlag::NoSpellCheck)) ||
+ inputContext->capabilityFlags().test(CapabilityFlag::Password) ||
!supportHint(entry->languageCode())) {
return false;
}
@@ -304,7 +313,7 @@ bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::
buffer.type(preedit);
}
- buffer.type(chr);
+ buffer.type(Key::keySymToUTF8(event.key().sym()));
if (buffer.size() >= MaxBufferSize) {
commitBuffer(inputContext);
@@ -320,8 +329,9 @@ void AndroidKeyboardEngine::commitBuffer(InputContext *inputContext) {
if (preedit.empty()) {
return;
}
+ auto characterCount = utf8::length(preedit, 0, cursor);
if (inputContext->capabilityFlags().test(CapabilityFlag::CommitStringWithCursor)) {
- inputContext->commitStringWithCursor(preedit, cursor);
+ inputContext->commitStringWithCursor(preedit, characterCount);
} else {
inputContext->commitString(preedit);
}
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.h b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
index 377ed2de8..b365ac527 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.h
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
@@ -2,8 +2,8 @@
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
*/
-#ifndef _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
-#define _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
+#ifndef FCITX5_ANDROID_ANDROIDKEYBOARD_H
+#define FCITX5_ANDROID_ANDROIDKEYBOARD_H
#include
#include
@@ -30,6 +30,10 @@ FCITX_CONFIGURATION(
AndroidKeyboardEngineConfig,
Option
enableWordHint{this, "EnableWordHint", _("Enable word hint"), true};
+ Option
+ hintOnPhysicalKeyboard{this, "WordHintOnPhysicalKeyboard", _("Enable word hint when using physical keyboard"), false};
+ Option
+ editorControlledWordHint{this, "EditorControlledWordHint", _("Disable word hint based on editor attributes"), true};
Option
pageSize{this, "PageSize", _("Word hint page size"), 5, IntConstrain(3, 10)};
OptionWithAnnotation
@@ -57,8 +61,7 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
static int constexpr MaxBufferSize = 20;
static int constexpr SpellCandidateSize = 20;
- AndroidKeyboardEngine(Instance *instance);
- ~AndroidKeyboardEngine() = default;
+ explicit AndroidKeyboardEngine(Instance *instance);
Instance *instance() { return instance_; }
@@ -94,10 +97,10 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
auto factory() { return &factory_; }
- // Return true if chr is pushed to buffer.
- // Return false if chr will be skipped by buffer, usually this means caller
- // need to call commit buffer and forward chr manually.
- bool updateBuffer(InputContext *inputContext, const std::string &chr);
+ // Return true if event is pushed to buffer.
+ // Return false if event will be skipped by buffer, usually this means caller
+ // need to call commit buffer and forward event manually.
+ bool updateBuffer(InputContext *inputContext, const KeyEvent& event);
// Commit current buffer, also reset the state.
// See also preeditString().
@@ -107,6 +110,9 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
private:
bool supportHint(const std::string &language);
+ /**
+ * preedit string and byte cursor
+ */
std::pair preeditWithCursor(InputContext *inputContext);
Instance *instance_;
@@ -128,4 +134,4 @@ class AndroidKeyboardEngineFactory : public AddonFactory {
}
-#endif //_FCITX5_ANDROID_ANDROIDKEYBOARD_H_
+#endif //FCITX5_ANDROID_ANDROIDKEYBOARD_H
diff --git a/app/src/main/cpp/androidnotification/androidnotification.cpp b/app/src/main/cpp/androidnotification/androidnotification.cpp
index dae404865..b2e06b5e4 100644
--- a/app/src/main/cpp/androidnotification/androidnotification.cpp
+++ b/app/src/main/cpp/androidnotification/androidnotification.cpp
@@ -13,20 +13,18 @@
namespace fcitx {
-void Notifications::updateConfig() {
- hiddenNotifications_.clear();
- for (const auto &id: config_.hiddenNotifications.value()) {
- hiddenNotifications_.insert(id);
- }
+Notifications::Notifications(Instance *instance) : instance_(instance) {
+ reloadConfig();
}
void Notifications::reloadConfig() {
readAsIni(config_, ConfPath);
- updateConfig();
+ updateHiddenNotifications();
}
void Notifications::save() {
std::vector values_;
+ values_.reserve(hiddenNotifications_.size());
for (const auto &id: hiddenNotifications_) {
values_.push_back(id);
}
@@ -34,6 +32,19 @@ void Notifications::save() {
safeSaveAsIni(config_, ConfPath);
}
+void Notifications::setConfig(const fcitx::RawConfig &config) {
+ config_.load(config, true);
+ safeSaveAsIni(config_, ConfPath);
+ updateHiddenNotifications();
+}
+
+void Notifications::updateHiddenNotifications() {
+ hiddenNotifications_.clear();
+ for (const auto &id: config_.hiddenNotifications.value()) {
+ hiddenNotifications_.insert(id);
+ }
+}
+
uint32_t Notifications::sendNotification(
const std::string &appName,
uint32_t replaceId,
diff --git a/app/src/main/cpp/androidnotification/androidnotification.h b/app/src/main/cpp/androidnotification/androidnotification.h
index 3733b6c68..242e2857b 100644
--- a/app/src/main/cpp/androidnotification/androidnotification.h
+++ b/app/src/main/cpp/androidnotification/androidnotification.h
@@ -22,26 +22,21 @@ namespace fcitx {
FCITX_CONFIGURATION(NotificationsConfig,
fcitx::Option> hiddenNotifications{
this, "HiddenNotifications",
- _("Hidden Notifications")};);
+ _("Hidden Notifications")};)
class Notifications final : public AddonInstance {
public:
- Notifications(Instance *instance) : instance_(instance) {};
- ~Notifications() = default;
+ explicit Notifications(Instance *instance);
Instance *instance() { return instance_; }
- void updateConfig();
void reloadConfig() override;
+
void save() override;
const Configuration *getConfig() const override { return &config_; }
- void setConfig(const RawConfig &config) override {
- config_.load(config, true);
- safeSaveAsIni(config_, ConfPath);
- updateConfig();
- }
+ void setConfig(const RawConfig &config) override;
FCITX_ADDON_DEPENDENCY_LOADER(androidfrontend, instance_->addonManager());
@@ -65,13 +60,15 @@ class Notifications final : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(Notifications, showTip);
FCITX_ADDON_EXPORT_FUNCTION(Notifications, closeNotification);
- static const inline std::string ConfPath = "conf/androidnotification.conf";
+ static const inline char* ConfPath = "conf/androidnotification.conf";
NotificationsConfig config_;
Instance *instance_;
std::unordered_set hiddenNotifications_;
+ void updateHiddenNotifications();
+
}; // class Notifications
} // namespace fcitx
diff --git a/app/src/main/cpp/helper-types.h b/app/src/main/cpp/helper-types.h
index a0be956c9..1f13c9494 100644
--- a/app/src/main/cpp/helper-types.h
+++ b/app/src/main/cpp/helper-types.h
@@ -8,6 +8,12 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+
+#include
class InputMethodStatus {
public:
@@ -83,4 +89,63 @@ class ActionEntity {
}
};
+class CandidateActionEntity {
+public:
+ int id;
+ std::string text;
+ bool isSeparator;
+ std::string icon;
+ bool isCheckable;
+ bool isChecked;
+
+ explicit CandidateActionEntity(const fcitx::CandidateAction &act) :
+ id(act.id()),
+ text(act.text()),
+ isSeparator(act.isSeparator()),
+ icon(act.icon()),
+ isCheckable(act.isCheckable()),
+ isChecked(act.isChecked()) {}
+};
+
+class CandidateEntity {
+public:
+ std::string label;
+ std::string text;
+ std::string comment;
+
+ explicit CandidateEntity(std::string label, std::string text, std::string comment) :
+ label(std::move(label)),
+ text(std::move(text)),
+ comment(std::move(comment)) {}
+};
+
+class PagedCandidateEntity {
+public:
+ std::vector candidates;
+ int cursorIndex;
+ fcitx::CandidateLayoutHint layoutHint;
+ bool hasPrev;
+ bool hasNext;
+
+ explicit PagedCandidateEntity(std::vector candidates,
+ int cursorIndex,
+ fcitx::CandidateLayoutHint layoutHint,
+ bool hasPrev,
+ bool hasNext) :
+ candidates(std::move(candidates)),
+ cursorIndex(cursorIndex),
+ layoutHint(layoutHint),
+ hasPrev(hasPrev),
+ hasNext(hasNext) {}
+
+ static PagedCandidateEntity Empty;
+
+private:
+ PagedCandidateEntity() :
+ candidates({}), cursorIndex(-1), layoutHint(fcitx::CandidateLayoutHint::NotSet),
+ hasPrev(false), hasNext(false) {}
+};
+
+PagedCandidateEntity PagedCandidateEntity::Empty = PagedCandidateEntity();
+
#endif //FCITX5_ANDROID_HELPER_TYPES_H
diff --git a/app/src/main/cpp/jni-utils.h b/app/src/main/cpp/jni-utils.h
index 6a7d30b0a..6807b531d 100644
--- a/app/src/main/cpp/jni-utils.h
+++ b/app/src/main/cpp/jni-utils.h
@@ -77,10 +77,10 @@ class JString {
class JEnv {
private:
- JNIEnv *env;
+ JNIEnv *env = nullptr;
public:
- JEnv(JavaVM *jvm) {
+ explicit JEnv(JavaVM *jvm) {
if (jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
jvm->AttachCurrentThread(&env, nullptr);
}
@@ -138,7 +138,13 @@ class GlobalRefSingleton {
jfieldID PinyinCustomPhraseOrder;
jfieldID PinyinCustomPhraseValue;
- GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
+ jclass CandidateAction;
+ jmethodID CandidateActionInit;
+
+ jclass Candidate;
+ jmethodID CandidateInit;
+
+ explicit GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
JNIEnv *env;
jvm->AttachCurrentThread(&env, nullptr);
@@ -157,8 +163,8 @@ class GlobalRefSingleton {
HandleFcitxEvent = env->GetStaticMethodID(Fcitx, "handleFcitxEvent", "(I[Ljava/lang/Object;)V");
InputMethodEntry = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/InputMethodEntry")));
- InputMethodEntryInit = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V");
- InputMethodEntryInitWithSubMode = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
+ InputMethodEntryInit = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V");
+ InputMethodEntryInitWithSubMode = env->GetMethodID(InputMethodEntry, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
RawConfig = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/RawConfig")));
RawConfigName = env->GetFieldID(RawConfig, "name", "Ljava/lang/String;");
@@ -184,9 +190,15 @@ class GlobalRefSingleton {
PinyinCustomPhraseKey = env->GetFieldID(PinyinCustomPhrase, "key", "Ljava/lang/String;");
PinyinCustomPhraseOrder = env->GetFieldID(PinyinCustomPhrase, "order", "I");
PinyinCustomPhraseValue = env->GetFieldID(PinyinCustomPhrase, "value", "Ljava/lang/String;");
+
+ CandidateAction = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/CandidateAction")));
+ CandidateActionInit = env->GetMethodID(CandidateAction, "", "(ILjava/lang/String;ZLjava/lang/String;ZZ)V");
+
+ Candidate = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/FcitxEvent$Candidate")));
+ CandidateInit = env->GetMethodID(Candidate, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
}
- const JEnv AttachEnv() const { return JEnv(jvm); }
+ [[nodiscard]] JEnv AttachEnv() const { return JEnv(jvm); }
};
extern GlobalRefSingleton *GlobalRef;
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
index 1ed0cd934..bdeb17191 100644
--- a/app/src/main/cpp/native-lib.cpp
+++ b/app/src/main/cpp/native-lib.cpp
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
#include
@@ -12,7 +12,7 @@
#include
-#include
+#include
#include
#include
@@ -41,7 +41,6 @@
#include "customphrase.h"
#include "androidfrontend/androidfrontend_public.h"
-#include "androidaddonloader/androidaddonloader.h"
#include "jni-utils.h"
#include "nativestreambuf.h"
#include "helper-types.h"
@@ -72,19 +71,18 @@ class Fcitx {
return p_instance != nullptr && p_dispatcher != nullptr && p_frontend != nullptr;
}
- event_base *get_event_base() {
+ uv_loop_t *get_event_base() {
fcitx::EventLoop &event_loop = p_instance->eventLoop();
- return static_cast(event_loop.nativeHandle());
+ return static_cast(event_loop.nativeHandle());
}
int loopOnce() {
- return event_base_loop(get_event_base(), EVLOOP_ONCE);
+ return uv_run(get_event_base(), UV_RUN_ONCE);
}
- void startup(fcitx::AndroidLibraryDependency dependency,
- const std::function &setupCallback) {
+ void startup(const std::function &setupCallback) {
p_instance = std::make_unique(0, nullptr);
- p_instance->addonManager().registerLoader(std::make_unique(dependency));
+ p_instance->addonManager().registerDefaultLoader(nullptr);
p_dispatcher = std::make_unique();
p_dispatcher->attach(&p_instance->eventLoop());
p_instance->initialize();
@@ -364,9 +362,9 @@ class Fcitx {
p_unicode->call(ic);
}
- void setClipboard(const std::string &string) {
+ void setClipboard(const std::string &string, bool password) {
if (!p_clipboard) return;
- p_clipboard->call("", string);
+ p_clipboard->call("", string, password);
}
void focusInputContext(bool focus) {
@@ -415,13 +413,33 @@ class Fcitx {
return p_frontend->call(offset, limit);
}
+ std::vector getCandidateActions(int idx) {
+ auto actions = std::vector();
+ for (const auto &a: p_frontend->call(idx)) {
+ actions.emplace_back(a);
+ }
+ return actions;
+ }
+
+ void triggerCandidateAction(int idx, int actionIdx) {
+ return p_frontend->call(idx, actionIdx);
+ }
+
+ void setCandidatePagingMode(int mode) {
+ return p_frontend->call(mode);
+ }
+
+ void offsetCandidatePage(int delta) {
+ return p_frontend->call(delta);
+ }
+
void save() {
p_instance->save();
}
void exit() {
// Make sure that the exec doesn't get blocked
- event_base_loopexit(get_event_base(), nullptr);
+ uv_stop(get_event_base());
// Normally, we would use exec to drive the event loop.
// Since we are calling loopOnce in JVM repeatedly, we shouldn't have used this function.
// However, exit events would lose chance to be called in this case.
@@ -491,9 +509,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
jstring appLib,
jstring extData,
jstring extCache,
- jobjectArray extDomains,
- jobjectArray libraryNames,
- jobjectArray libraryDependencies) {
+ jobjectArray extDomains) {
if (Fcitx::Instance().isRunning()) {
FCITX_ERROR() << "Fcitx is already running!";
return;
@@ -567,21 +583,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
fcitx::registerDomain(CString(env, domain), locale_dir_char);
}
- std::unordered_map> depsMap;
- const int librarySize = env->GetArrayLength(libraryNames);
- for (int i = 0; i < librarySize; i++) {
- auto jstringName = JRef(env, env->GetObjectArrayElement(libraryNames, i));
- auto lib = CString(env, jstringName);
- auto jobjectArrayDeps = JRef(env, env->GetObjectArrayElement(libraryDependencies, i));
- const int depSize = env->GetArrayLength(jobjectArrayDeps);
- std::unordered_set depSet(depSize);
- for (int j = 0; j < depSize; j++) {
- auto jstringDepName = JRef(env, env->GetObjectArrayElement(jobjectArrayDeps, j));
- depSet.emplace(CString(env, jstringDepName));
- }
- depsMap.emplace(lib, depSet);
- }
-
auto candidateListCallback = [](const std::vector &candidates, const int size) {
auto env = GlobalRef->AttachEnv();
auto candidatesArray = JRef(env, env->NewObjectArray(static_cast(candidates.size()), GlobalRef->String, nullptr));
@@ -633,15 +634,17 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 5, *vararg);
};
auto imChangeCallback = []() {
- auto env = GlobalRef->AttachEnv();
- auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
if (!status) return;
+ auto env = GlobalRef->AttachEnv();
+ auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
env->SetObjectArrayElement(vararg, 0, obj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 6, *vararg);
};
auto statusAreaUpdateCallback = []() {
+ std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
+ if (!status) return;
auto env = GlobalRef->AttachEnv();
auto vararg = JRef(env, env->NewObjectArray(static_cast(2), GlobalRef->Object, nullptr));
const auto actions = Fcitx::Instance().statusAreaActions();
@@ -652,7 +655,6 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(actionArray, i++, obj);
}
env->SetObjectArrayElement(vararg, 0, actionArray);
- std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
auto statusObj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
env->SetObjectArrayElement(vararg, 1, statusObj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 7, *vararg);
@@ -666,15 +668,39 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
env->SetObjectArrayElement(vararg, 0, intArray);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 8, *vararg);
};
+ auto pagedCandidateCallback = [](const PagedCandidateEntity &paged) {
+ auto env = GlobalRef->AttachEnv();
+ const int size = static_cast(paged.candidates.size());
+ if (size == 0) {
+ auto vararg = JRef(env, env->NewObjectArray(0, GlobalRef->Object, nullptr));
+ env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 9, *vararg);
+ return;
+ }
+ auto candidatesArray = JRef(env, env->NewObjectArray(size, GlobalRef->Candidate, nullptr));
+ for (int i = 0; i < size; ++i) {
+ env->SetObjectArrayElement(candidatesArray, i, candidateEntityToObject(env, paged.candidates[i]));
+ }
+ auto cursorIndex = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, paged.cursorIndex));
+ auto layoutHint = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, static_cast(paged.layoutHint)));
+ auto hasPrev = JRef(env, env->NewObject(GlobalRef->Boolean, GlobalRef->BooleanInit, paged.hasPrev));
+ auto hasNext = JRef(env, env->NewObject(GlobalRef->Boolean, GlobalRef->BooleanInit, paged.hasNext));
+ auto vararg = JRef(env, env->NewObjectArray(5, GlobalRef->Object, nullptr));
+ env->SetObjectArrayElement(vararg, 0, candidatesArray);
+ env->SetObjectArrayElement(vararg, 1, cursorIndex);
+ env->SetObjectArrayElement(vararg, 2, layoutHint);
+ env->SetObjectArrayElement(vararg, 3, hasPrev);
+ env->SetObjectArrayElement(vararg, 4, hasNext);
+ env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 9, *vararg);
+ };
auto toastCallback = [](const std::string &s) {
auto env = GlobalRef->AttachEnv();
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->ShowToast, *JString(env, s));
};
umask(007);
- fcitx::StandardPath::global().syncUmask();
+ fcitx::StandardPaths::global().syncUmask();
- Fcitx::Instance().startup(depsMap, [&](auto *androidfrontend) {
+ Fcitx::Instance().startup([&](auto *androidfrontend) {
FCITX_INFO() << "Setting up callback";
readyCallback();
androidfrontend->template call(candidateListCallback);
@@ -685,6 +711,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
androidfrontend->template call(imChangeCallback);
androidfrontend->template call(statusAreaUpdateCallback);
androidfrontend->template call(deleteSurroundingCallback);
+ androidfrontend->template call(pagedCandidateCallback);
androidfrontend->template call(toastCallback);
});
FCITX_INFO() << "Finishing startup";
@@ -720,28 +747,31 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_reloadFcitxConfig(JNIEnv *env, jclass c
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxString(JNIEnv *env, jclass clazz, jstring key, jint state, jboolean up, jint timestamp) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxString(JNIEnv *env, jclass clazz, jstring key, jint state, jint code, jboolean up, jint timestamp) {
RETURN_IF_NOT_RUNNING
fcitx::Key parsedKey{fcitx::Key::keySymFromString(CString(env, key)),
- fcitx::KeyStates(static_cast(state))};
+ fcitx::KeyStates(static_cast(state)),
+ code + /* evdev offset */ 8};
Fcitx::Instance().sendKey(parsedKey, up, timestamp);
}
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxChar(JNIEnv *env, jclass clazz, jchar c, jint state, jboolean up, jint timestamp) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeyToFcitxChar(JNIEnv *env, jclass clazz, jchar c, jint state, jint code, jboolean up, jint timestamp) {
RETURN_IF_NOT_RUNNING
const fcitx::Key parsedKey{fcitx::Key::keySymFromString(reinterpret_cast(&c)),
- fcitx::KeyStates(static_cast(state))};
+ fcitx::KeyStates(static_cast(state)),
+ code + /* evdev offset */ 8};
Fcitx::Instance().sendKey(parsedKey, up, timestamp);
}
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeySymToFcitx(JNIEnv *env, jclass clazz, jint sym, jint state, jboolean up, jint timestamp) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_sendKeySymToFcitx(JNIEnv *env, jclass clazz, jint sym, jint state, jint code, jboolean up, jint timestamp) {
RETURN_IF_NOT_RUNNING
fcitx::Key key{fcitx::KeySym(static_cast(sym)),
- fcitx::KeyStates(static_cast(state))};
+ fcitx::KeyStates(static_cast(state)),
+ code + /* evdev offset */ 8};
Fcitx::Instance().sendKey(key, up, timestamp);
}
@@ -749,7 +779,6 @@ extern "C"
JNIEXPORT jboolean JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_selectCandidate(JNIEnv *env, jclass clazz, jint idx) {
RETURN_VALUE_IF_NOT_RUNNING(false)
- FCITX_DEBUG() << "selectCandidate: #" << idx;
return Fcitx::Instance().select(idx);
}
@@ -947,9 +976,9 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_triggerUnicodeInput(JNIEnv *env, jclass
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxClipboard(JNIEnv *env, jclass clazz, jstring string) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxClipboard(JNIEnv *env, jclass clazz, jstring string, jboolean password) {
RETURN_IF_NOT_RUNNING
- Fcitx::Instance().setClipboard(CString(env, string));
+ Fcitx::Instance().setClipboard(CString(env, string), password == JNI_TRUE);
}
extern "C"
@@ -1015,6 +1044,41 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxCandidates(JNIEnv *env, jclass
return array;
}
+extern "C"
+JNIEXPORT jobjectArray JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxCandidateActions(JNIEnv *env, jclass clazz, jint idx) {
+ RETURN_VALUE_IF_NOT_RUNNING(nullptr)
+ auto actions = Fcitx::Instance().getCandidateActions(idx);
+ int size = static_cast(actions.size());
+ jobjectArray array = env->NewObjectArray(size, GlobalRef->CandidateAction, nullptr);
+ for (int i = 0; i < size; i++) {
+ auto obj = JRef(env, fcitxCandidateActionToObject(env, actions[i]));
+ env->SetObjectArrayElement(array, i, obj);
+ }
+ return array;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_triggerFcitxCandidateAction(JNIEnv *env, jclass clazz, jint idx, jint action_idx) {
+ RETURN_IF_NOT_RUNNING
+ Fcitx::Instance().triggerCandidateAction(idx, action_idx);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_setFcitxCandidatePagingMode(JNIEnv *env, jclass clazz, jint mode) {
+ RETURN_IF_NOT_RUNNING
+ Fcitx::Instance().setCandidatePagingMode(mode);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_offsetFcitxCandidatePage(JNIEnv *env, jclass clazz, jint delta) {
+ RETURN_IF_NOT_RUNNING
+ Fcitx::Instance().offsetCandidatePage(delta);
+}
+
extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_loopOnce(JNIEnv *env, jclass clazz) {
@@ -1108,7 +1172,7 @@ Java_org_fcitx_fcitx5_android_data_table_TableManager_checkTableDictFormat(JNIEn
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_org_fcitx_fcitx5_android_data_pinyin_CustomPhraseManager_load(JNIEnv *env, jclass clazz) {
- auto fp = fcitx::StandardPath::global().open(fcitx::StandardPath::Type::PkgData, "pinyin/customphrase", O_RDONLY);
+ auto fp = fcitx::StandardPaths::global().open(fcitx::StandardPathsType::PkgData, "pinyin/customphrase");
if (fp.fd() < 0) {
FCITX_INFO() << "cannot open pinyin/customphrase";
return nullptr;
@@ -1154,8 +1218,8 @@ Java_org_fcitx_fcitx5_android_data_pinyin_CustomPhraseManager_save(JNIEnv *env,
*CString(env, phraseValue),
static_cast(phraseOrder));
}
- fcitx::StandardPath::global().safeSave(
- fcitx::StandardPath::Type::PkgData, "pinyin/customphrase",
+ fcitx::StandardPaths::global().safeSave(
+ fcitx::StandardPathsType::PkgData, "pinyin/customphrase",
[&](int fd) {
boost::iostreams::stream_buffer
buffer(fd, boost::iostreams::file_descriptor_flags::never_close_handle);
diff --git a/app/src/main/cpp/object-conversion.h b/app/src/main/cpp/object-conversion.h
index 1472e3c52..aa4155095 100644
--- a/app/src/main/cpp/object-conversion.h
+++ b/app/src/main/cpp/object-conversion.h
@@ -20,6 +20,7 @@ jobject fcitxInputMethodEntryToJObject(JNIEnv *env, const fcitx::InputMethodEntr
*JString(env, entry->nativeName()),
*JString(env, entry->label()),
*JString(env, entry->languageCode()),
+ *JString(env, entry->addon()),
entry->isConfigurable()
);
}
@@ -42,6 +43,7 @@ jobject fcitxInputMethodStatusToJObject(JNIEnv *env, const InputMethodStatus &st
*JString(env, status.nativeName),
*JString(env, status.label),
*JString(env, status.languageCode),
+ *JString(env, status.addon),
status.configurable,
*JString(env, status.subMode),
*JString(env, status.subModeLabel),
@@ -159,4 +161,25 @@ jobject fcitxTextToJObject(JNIEnv *env, const fcitx::Text &text) {
return obj;
}
+jobject fcitxCandidateActionToObject(JNIEnv *env, const CandidateActionEntity &act) {
+ auto obj = env->NewObject(GlobalRef->CandidateAction, GlobalRef->CandidateActionInit,
+ act.id,
+ *JString(env, act.text),
+ act.isSeparator,
+ *JString(env, act.icon),
+ act.isCheckable,
+ act.isChecked
+ );
+ return obj;
+}
+
+jobject candidateEntityToObject(JNIEnv *env, const CandidateEntity &c) {
+ auto obj = env->NewObject(GlobalRef->Candidate, GlobalRef->CandidateInit,
+ *JString(env, c.label),
+ *JString(env, c.text),
+ *JString(env, c.comment)
+ );
+ return obj;
+}
+
#endif //FCITX5_ANDROID_OBJECT_CONVERSION_H
diff --git a/app/src/main/cpp/po/de.po b/app/src/main/cpp/po/de.po
index b04353cf8..e9737c8d4 100644
--- a/app/src/main/cpp/po/de.po
+++ b/app/src/main/cpp/po/de.po
@@ -9,7 +9,7 @@ msgstr ""
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
"Last-Translator: Ettore Atalan , 2022\n"
-"Language-Team: German (https://www.transifex.com/fcitx/teams/12005/de/)\n"
+"Language-Team: German (https://app.transifex.com/fcitx/teams/12005/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -28,6 +28,12 @@ msgstr "Worthinweis"
msgid "Enable word hint"
msgstr "Worthinweis aktivieren"
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
+msgid "Disable word hint based on editor attributes"
+msgstr ""
+
msgid "Word hint page size"
msgstr "Seitengröße des Worthinweises"
@@ -36,3 +42,9 @@ msgstr ""
msgid "Insert space between words"
msgstr ""
+
+msgid "Android Toast & Notification"
+msgstr ""
+
+msgid "Hidden Notifications"
+msgstr ""
diff --git a/app/src/main/cpp/po/es.po b/app/src/main/cpp/po/es.po
index 71d03f71f..d78fede55 100644
--- a/app/src/main/cpp/po/es.po
+++ b/app/src/main/cpp/po/es.po
@@ -28,6 +28,12 @@ msgstr ""
msgid "Enable word hint"
msgstr ""
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
+msgid "Disable word hint based on editor attributes"
+msgstr ""
+
msgid "Word hint page size"
msgstr ""
@@ -36,3 +42,9 @@ msgstr ""
msgid "Insert space between words"
msgstr "Insertar espacio entre palabras"
+
+msgid "Android Toast & Notification"
+msgstr ""
+
+msgid "Hidden Notifications"
+msgstr ""
diff --git a/app/src/main/cpp/po/fcitx5-android.pot b/app/src/main/cpp/po/fcitx5-android.pot
index a1582b1d7..0efb63065 100644
--- a/app/src/main/cpp/po/fcitx5-android.pot
+++ b/app/src/main/cpp/po/fcitx5-android.pot
@@ -23,6 +23,12 @@ msgstr ""
msgid "Enable word hint"
msgstr ""
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
+msgid "Disable word hint based on editor attributes"
+msgstr ""
+
msgid "Word hint page size"
msgstr ""
diff --git a/app/src/main/cpp/po/ja.po b/app/src/main/cpp/po/ja.po
index defa4355e..d612fdf36 100644
--- a/app/src/main/cpp/po/ja.po
+++ b/app/src/main/cpp/po/ja.po
@@ -1,6 +1,7 @@
#
# Translators:
# Takuro Onoue , 2022
+# NPL, 2024
#
msgid ""
msgstr ""
@@ -8,8 +9,8 @@ msgstr ""
"Report-Msgid-Bugs-To: https://github.com/fcitx5-android/fcitx5-android/issues\n"
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
-"Last-Translator: Takuro Onoue , 2022\n"
-"Language-Team: Japanese (https://www.transifex.com/fcitx/teams/12005/ja/)\n"
+"Last-Translator: NPL, 2024\n"
+"Language-Team: Japanese (https://app.transifex.com/fcitx/teams/12005/ja/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -21,3 +22,30 @@ msgstr "Android フロントエンド"
msgid "Android Keyboard"
msgstr "Android キーボード"
+
+msgid "Word hint"
+msgstr "単語ヒント"
+
+msgid "Enable word hint"
+msgstr "単語ヒントを有効にする"
+
+msgid "Enable word hint when using physical keyboard"
+msgstr ""
+
+msgid "Disable word hint based on editor attributes"
+msgstr ""
+
+msgid "Word hint page size"
+msgstr "単語ヒントのページサイズ"
+
+msgid "Choose key modifier"
+msgstr "修飾キーを選択"
+
+msgid "Insert space between words"
+msgstr "単語間に空白を入力する"
+
+msgid "Android Toast & Notification"
+msgstr "Android トーストと通知"
+
+msgid "Hidden Notifications"
+msgstr "通知を非表示にする"
diff --git a/app/src/main/cpp/po/ru.po b/app/src/main/cpp/po/ru.po
index 20040eec1..9af294da3 100644
--- a/app/src/main/cpp/po/ru.po
+++ b/app/src/main/cpp/po/ru.po
@@ -1,7 +1,7 @@
#
# Translators:
# Potato Hatsue, 2022
-# Dmitry , 2022
+# Dmitry , 2024
#
msgid ""
msgstr ""
@@ -9,8 +9,8 @@ msgstr ""
"Report-Msgid-Bugs-To: https://github.com/fcitx5-android/fcitx5-android/issues\n"
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
-"Last-Translator: Dmitry , 2022\n"
-"Language-Team: Russian (https://www.transifex.com/fcitx/teams/12005/ru/)\n"
+"Last-Translator: Dmitry , 2024\n"
+"Language-Team: Russian (https://app.transifex.com/fcitx/teams/12005/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -29,6 +29,12 @@ msgstr "Подсказка слова"
msgid "Enable word hint"
msgstr "Включить подсказку слова"
+msgid "Enable word hint when using physical keyboard"
+msgstr "Включить подсказки слов при использовании физической клавиатуры"
+
+msgid "Disable word hint based on editor attributes"
+msgstr "Отключить подсказки слов в зависимости от свойств редактора"
+
msgid "Word hint page size"
msgstr "Размер страницы подсказки слов"
@@ -37,3 +43,9 @@ msgstr "Выберите клавишу-модификатор"
msgid "Insert space between words"
msgstr "Вставить пробел между словами"
+
+msgid "Android Toast & Notification"
+msgstr "Всплывающие подсказки и уведомления Android"
+
+msgid "Hidden Notifications"
+msgstr "Скрытые уведомления"
diff --git a/app/src/main/cpp/po/zh_CN.po b/app/src/main/cpp/po/zh_CN.po
index 1bab9bbd2..5be82e30c 100644
--- a/app/src/main/cpp/po/zh_CN.po
+++ b/app/src/main/cpp/po/zh_CN.po
@@ -1,7 +1,8 @@
#
# Translators:
# Potato Hatsue, 2022
-# rocka, 2022
+# rocka, 2024
+# Yiyu Liu, 2024
#
msgid ""
msgstr ""
@@ -9,8 +10,8 @@ msgstr ""
"Report-Msgid-Bugs-To: https://github.com/fcitx5-android/fcitx5-android/issues\n"
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
-"Last-Translator: rocka, 2022\n"
-"Language-Team: Chinese (China) (https://www.transifex.com/fcitx/teams/12005/zh_CN/)\n"
+"Last-Translator: Yiyu Liu, 2024\n"
+"Language-Team: Chinese (China) (https://app.transifex.com/fcitx/teams/12005/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -29,6 +30,12 @@ msgstr "单词提示"
msgid "Enable word hint"
msgstr "启用单词提示"
+msgid "Enable word hint when using physical keyboard"
+msgstr "在使用物理键盘时启用单词提示"
+
+msgid "Disable word hint based on editor attributes"
+msgstr "根据编辑器属性禁用单词提示"
+
msgid "Word hint page size"
msgstr "单词提示页大小"
@@ -38,8 +45,8 @@ msgstr "选词修饰键"
msgid "Insert space between words"
msgstr "在单词间插入空格"
-msgid "Android 消息框与通知"
-msgstr ""
+msgid "Android Toast & Notification"
+msgstr "Android 弹出提示与通知"
-msgid "隐藏的通知"
-msgstr ""
+msgid "Hidden Notifications"
+msgstr "隐藏的通知"
diff --git a/app/src/main/cpp/po/zh_TW.po b/app/src/main/cpp/po/zh_TW.po
index c5ace5e70..36cd22166 100644
--- a/app/src/main/cpp/po/zh_TW.po
+++ b/app/src/main/cpp/po/zh_TW.po
@@ -1,8 +1,8 @@
#
# Translators:
# 黃柏諺 , 2022
-# Zhang Jia-Bin , 2022
-# rocka, 2022
+# Jia-Bin, 2022
+# Yiyu Liu, 2024
#
msgid ""
msgstr ""
@@ -10,8 +10,8 @@ msgstr ""
"Report-Msgid-Bugs-To: https://github.com/fcitx5-android/fcitx5-android/issues\n"
"POT-Creation-Date: 2022-02-14 18:51+0800\n"
"PO-Revision-Date: 2022-03-18 22:18+0000\n"
-"Last-Translator: rocka, 2022\n"
-"Language-Team: Chinese (Taiwan) (https://www.transifex.com/fcitx/teams/12005/zh_TW/)\n"
+"Last-Translator: Yiyu Liu, 2024\n"
+"Language-Team: Chinese (Taiwan) (https://app.transifex.com/fcitx/teams/12005/zh_TW/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -25,13 +25,19 @@ msgid "Android Keyboard"
msgstr "Android 鍵盤"
msgid "Word hint"
-msgstr "單字提示"
+msgstr "字詞提示"
msgid "Enable word hint"
-msgstr "啟用單字提示"
+msgstr "啟用字詞提示"
+
+msgid "Enable word hint when using physical keyboard"
+msgstr "在使用物理鍵盤時啟用字詞提示"
+
+msgid "Disable word hint based on editor attributes"
+msgstr "依據編輯器屬性禁用字詞提示"
msgid "Word hint page size"
-msgstr "單字提示頁大小"
+msgstr "字詞提示頁大小"
msgid "Choose key modifier"
msgstr "選詞修飾鍵"
@@ -39,8 +45,8 @@ msgstr "選詞修飾鍵"
msgid "Insert space between words"
msgstr "在單字間插入空格"
-msgid "Android 浮動式訊息与通知"
-msgstr ""
+msgid "Android Toast & Notification"
+msgstr "Android 浮動式訊息與通知"
-msgid "隐藏的通知"
-msgstr ""
+msgid "Hidden Notifications"
+msgstr "隱藏的通知"
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
index 3e68edc13..e52553b36 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/FcitxApplication.kt
@@ -14,6 +14,8 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Process
import android.util.Log
+import androidx.core.content.ContextCompat
+import androidx.core.content.edit
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.MainScope
@@ -26,6 +28,7 @@ import org.fcitx.fcitx5.android.ui.main.LogActivity
import org.fcitx.fcitx5.android.utils.AppUtil
import org.fcitx.fcitx5.android.utils.Locales
import org.fcitx.fcitx5.android.utils.isDarkMode
+import org.fcitx.fcitx5.android.utils.startActivity
import org.fcitx.fcitx5.android.utils.userManager
import timber.log.Timber
import kotlin.system.exitProcess
@@ -57,6 +60,18 @@ class FcitxApplication : Application() {
}
}
+ private val restartFcitxInstanceReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_RESTART_FCITX_INSTANCE) return
+ if (FcitxDaemon.getFirstConnectionOrNull() != null) {
+ Timber.i("Received broadcast '${intent.action}', try to restart fcitx instance ...")
+ FcitxDaemon.restartFcitx()
+ } else {
+ Timber.i("Received broadcast '${intent.action}', but there's no fcitx instance")
+ }
+ }
+ }
+
var isDirectBootMode = false
private set
@@ -74,7 +89,19 @@ class FcitxApplication : Application() {
if (!BuildConfig.DEBUG) {
Thread.setDefaultUncaughtExceptionHandler { _, e ->
- startActivity(Intent(ctx, LogActivity::class.java).apply {
+ val crashTime = System.currentTimeMillis()
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx)
+ val lastCrashTimePrefKey = "last_crash_time"
+ val lastCrashTime = sharedPreferences.getLong(lastCrashTimePrefKey, -1L)
+ // make sure it was written to persistent storage
+ sharedPreferences.edit(commit = true) {
+ putLong(lastCrashTimePrefKey, crashTime)
+ }
+ if (crashTime - lastCrashTime <= 10_000L) {
+ // continuous crashes within 10 seconds, maybe in a crash loop. just bail
+ exitProcess(10)
+ }
+ startActivity {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(LogActivity.FROM_CRASH, true)
// avoid transaction overflow
@@ -85,7 +112,7 @@ class FcitxApplication : Application() {
it
}
putExtra(LogActivity.CRASH_STACK_TRACE, truncated)
- })
+ }
exitProcess(10)
}
}
@@ -126,12 +153,20 @@ class FcitxApplication : Application() {
AppPrefs.getInstance().syncToDeviceEncryptedStorage()
ThemeManager.syncToDeviceEncryptedStorage()
}
+ ContextCompat.registerReceiver(
+ this,
+ restartFcitxInstanceReceiver,
+ IntentFilter(ACTION_RESTART_FCITX_INSTANCE),
+ PERMISSION_TEST_INPUT_METHOD,
+ null,
+ ContextCompat.RECEIVER_EXPORTED
+ )
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- ThemeManager.onSystemDarkModeChange(newConfig.isDarkMode())
- Locales.onLocaleChange(resources.configuration)
+ ThemeManager.onSystemPlatteChange(newConfig)
+ Locales.onLocaleChange(newConfig)
}
companion object {
@@ -142,5 +177,21 @@ class FcitxApplication : Application() {
fun getLastPid() = lastPid
private const val MAX_STACKTRACE_SIZE = 128000
+
+ const val ACTION_RESTART_FCITX_INSTANCE =
+ "${BuildConfig.APPLICATION_ID}.action.RESTART_FCITX_INSTANCE"
+
+ /**
+ * This permission is requested by com.android.shell, makes it possible to restart
+ * fcitx instance from `adb shell am` command:
+ * ```sh
+ * adb shell am broadcast -a org.fcitx.fcitx5.android.action.RESTART_FCITX_INSTANCE
+ * ```
+ * https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.0.0_r1/packages/Shell/AndroidManifest.xml#67
+ *
+ * other candidate: android.permission.TEST_INPUT_METHOD requires Android 14
+ * https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-14.0.0_r1/packages/Shell/AndroidManifest.xml#628
+ */
+ const val PERMISSION_TEST_INPUT_METHOD = "android.permission.READ_INPUT_STATE"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/FcitxRemoteService.kt b/app/src/main/java/org/fcitx/fcitx5/android/FcitxRemoteService.kt
index 4762ed92c..282a31f62 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/FcitxRemoteService.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/FcitxRemoteService.kt
@@ -39,7 +39,6 @@ class FcitxRemoteService : Service() {
PriorityQueue(3, compareByDescending { it.priority })
private fun transformClipboard(source: String): String {
- MainScope()
var result = source
clipboardTransformers.forEach {
try {
@@ -74,11 +73,8 @@ class FcitxRemoteService : Service() {
override fun registerClipboardEntryTransformer(transformer: IClipboardEntryTransformer) {
Timber.d("registerClipboardEntryTransformer: ${transformer.desc}")
- try {
- transformer.description!!.isNotEmpty() || throw Exception()
- } catch (e: Exception) {
+ if (transformer.description.isNullOrBlank()) {
Timber.w("Cannot register ClipboardEntryTransformer of null or empty description")
- return
}
if (clipboardTransformers.any { it.descEquals(transformer) }) {
Timber.w("ClipboardEntryTransformer ${transformer.desc} has already been registered")
@@ -97,7 +93,7 @@ class FcitxRemoteService : Service() {
Timber.d("unregisterClipboardEntryTransformer: ${transformer.desc}")
scope.launch {
clipboardTransformers.remove(transformer)
- || clipboardTransformers.removeIf { it.descEquals(transformer) }
+ || clipboardTransformers.removeAll { it.descEquals(transformer) }
|| return@launch
updateClipboardManager()
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt
index 800ede315..098f34c06 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/CapabilityFlag.kt
@@ -148,6 +148,7 @@ value class CapabilityFlags constructor(val flags: ULong) {
}
if (equals(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) {
flags += CapabilityFlag.Sensitive
+ flags += CapabilityFlag.NoSpellCheck
}
if (equals(InputType.TYPE_TEXT_VARIATION_URI)) {
flags += CapabilityFlag.Url
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
index d47361431..54aa7a084 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Fcitx.kt
@@ -1,10 +1,11 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
import android.content.Context
+import android.os.Build
import androidx.annotation.Keep
import androidx.core.content.ContextCompat
import kotlinx.coroutines.channels.BufferOverflow
@@ -72,17 +73,29 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun save() = withFcitxContext { saveFcitxState() }
override suspend fun reloadConfig() = withFcitxContext { reloadFcitxConfig() }
- override suspend fun sendKey(key: String, states: UInt, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeyToFcitxString(key, states.toInt(), up, timestamp) }
-
- override suspend fun sendKey(c: Char, states: UInt, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeyToFcitxChar(c, states.toInt(), up, timestamp) }
-
- override suspend fun sendKey(sym: Int, states: UInt, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeySymToFcitx(sym, states.toInt(), up, timestamp) }
-
- override suspend fun sendKey(sym: KeySym, states: KeyStates, up: Boolean, timestamp: Int) =
- withFcitxContext { sendKeySymToFcitx(sym.sym, states.toInt(), up, timestamp) }
+ override suspend fun sendKey(
+ key: String,
+ states: UInt,
+ code: Int,
+ up: Boolean,
+ timestamp: Int
+ ) =
+ withFcitxContext { sendKeyToFcitxString(key, states.toInt(), code, up, timestamp) }
+
+ override suspend fun sendKey(c: Char, states: UInt, code: Int, up: Boolean, timestamp: Int) =
+ withFcitxContext { sendKeyToFcitxChar(c, states.toInt(), code, up, timestamp) }
+
+ override suspend fun sendKey(sym: Int, states: UInt, code: Int, up: Boolean, timestamp: Int) =
+ withFcitxContext { sendKeySymToFcitx(sym, states.toInt(), code, up, timestamp) }
+
+ override suspend fun sendKey(
+ sym: KeySym,
+ states: KeyStates,
+ code: Int,
+ up: Boolean,
+ timestamp: Int
+ ) =
+ withFcitxContext { sendKeySymToFcitx(sym.sym, states.toInt(), code, up, timestamp) }
override suspend fun select(idx: Int): Boolean = withFcitxContext { selectCandidate(idx) }
override suspend fun isEmpty(): Boolean = withFcitxContext { isInputPanelEmpty() }
@@ -142,8 +155,8 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun triggerQuickPhrase() = withFcitxContext { triggerQuickPhraseInput() }
override suspend fun triggerUnicode() = withFcitxContext { triggerUnicodeInput() }
- private suspend fun setClipboard(string: String) =
- withFcitxContext { setFcitxClipboard(string) }
+ private suspend fun setClipboard(string: String, password: Boolean = false) =
+ withFcitxContext { setFcitxClipboard(string, password) }
override suspend fun focus(focus: Boolean) = withFcitxContext { focusInputContext(focus) }
override suspend fun activate(uid: Int, pkgName: String) =
@@ -162,6 +175,18 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
override suspend fun getCandidates(offset: Int, limit: Int): Array =
withFcitxContext { getFcitxCandidates(offset, limit) ?: emptyArray() }
+ override suspend fun getCandidateActions(idx: Int): Array =
+ withFcitxContext { getFcitxCandidateActions(idx) ?: emptyArray() }
+
+ override suspend fun triggerCandidateAction(idx: Int, actionIdx: Int) =
+ withFcitxContext { triggerFcitxCandidateAction(idx, actionIdx) }
+
+ override suspend fun setCandidatePagingMode(mode: Int) =
+ withFcitxContext { setFcitxCandidatePagingMode(mode) }
+
+ override suspend fun offsetCandidatePage(delta: Int) =
+ withFcitxContext { offsetFcitxCandidatePage(delta) }
+
init {
if (lifecycle.currentState != FcitxLifecycle.State.STOPPED)
throw IllegalAccessException("Fcitx5 has already been created!")
@@ -207,9 +232,7 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
appLib: String,
extData: String,
extCache: String,
- extDomains: Array,
- libraryNames: Array,
- libraryDependencies: Array>
+ extDomains: Array
)
@JvmStatic
@@ -225,13 +248,19 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
external fun reloadFcitxConfig()
@JvmStatic
- external fun sendKeyToFcitxString(key: String, state: Int, up: Boolean, timestamp: Int)
+ external fun sendKeyToFcitxString(
+ key: String,
+ state: Int,
+ code: Int,
+ up: Boolean,
+ timestamp: Int
+ )
@JvmStatic
- external fun sendKeyToFcitxChar(c: Char, state: Int, up: Boolean, timestamp: Int)
+ external fun sendKeyToFcitxChar(c: Char, state: Int, code: Int, up: Boolean, timestamp: Int)
@JvmStatic
- external fun sendKeySymToFcitx(sym: Int, state: Int, up: Boolean, timestamp: Int)
+ external fun sendKeySymToFcitx(sym: Int, state: Int, code: Int, up: Boolean, timestamp: Int)
@JvmStatic
external fun selectCandidate(idx: Int): Boolean
@@ -303,7 +332,7 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
external fun triggerUnicodeInput()
@JvmStatic
- external fun setFcitxClipboard(string: String)
+ external fun setFcitxClipboard(string: String, password: Boolean)
@JvmStatic
external fun focusInputContext(focus: Boolean)
@@ -326,6 +355,18 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
@JvmStatic
external fun getFcitxCandidates(offset: Int, limit: Int): Array?
+ @JvmStatic
+ external fun getFcitxCandidateActions(idx: Int): Array?
+
+ @JvmStatic
+ external fun triggerFcitxCandidateAction(idx: Int, actionIdx: Int)
+
+ @JvmStatic
+ external fun setFcitxCandidatePagingMode(mode: Int)
+
+ @JvmStatic
+ external fun offsetFcitxCandidatePage(delta: Int)
+
@JvmStatic
external fun loopOnce()
@@ -356,19 +397,6 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
// will be called in fcitx main thread
private fun onFirstRun() {
Timber.i("onFirstRun")
- getFcitxGlobalConfig()?.get("cfg")?.apply {
- get("Behavior").apply {
- get("ShareInputState").value = "All"
- get("PreeditEnabledByDefault").value = "False"
- }
- setFcitxGlobalConfig(this)
- }
- getFcitxAddonConfig("pinyin")?.get("cfg")?.apply {
- get("PreeditInApplication").value = "False"
- get("PreeditCursorPositionAtBeginning").value = "False"
- get("QuickPhraseKey").value = ""
- setFcitxAddonConfig("pinyin", this)
- }
firstRun = false
}
@@ -394,18 +422,14 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
val plugins = DataManager.getLoadedPlugins()
val nativeLibDir = StringBuilder(context.applicationInfo.nativeLibraryDir)
val extDomains = arrayListOf()
- val libraryNames = arrayListOf()
- val libraryDependency = arrayListOf>()
plugins.forEach {
- nativeLibDir.append(':')
- nativeLibDir.append(it.nativeLibraryDir)
+ if (it.nativeLibraryDir.isNotBlank()) {
+ nativeLibDir.append(':')
+ nativeLibDir.append(it.nativeLibraryDir)
+ }
it.domain?.let { d ->
extDomains.add(d)
}
- it.libraryDependency.forEach { (lib, dep) ->
- libraryNames.add(lib)
- libraryDependency.add(dep.toTypedArray())
- }
}
Timber.d(
"""
@@ -423,11 +447,14 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
nativeLibDir.toString(),
(getExternalFilesDir(null) ?: filesDir).absolutePath,
(externalCacheDir ?: cacheDir).absolutePath,
- extDomains.toTypedArray(),
- libraryNames.toTypedArray(),
- libraryDependency.toTypedArray()
+ extDomains.toTypedArray()
)
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ lifecycle.launchWhenReady {
+ SubtypeManager.syncWith(enabledIme())
+ }
+ }
}
override fun nativeLoopOnce() {
@@ -451,7 +478,7 @@ class Fcitx(private val context: Context) : FcitxAPI, FcitxLifecycleOwner {
@Keep
private val onClipboardUpdate = ClipboardManager.OnClipboardUpdateListener {
- lifecycle.lifecycleScope.launch { setClipboard(it.text) }
+ lifecycle.lifecycleScope.launch { setClipboard(it.text, it.sensitive) }
}
private fun computeAddonGraph() = runBlocking {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
index c8d218ce4..b6fd8ce8e 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxAPI.kt
@@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.SharedFlow
*/
interface FcitxAPI {
-
enum class AddonDep {
Required,
Optional
@@ -43,13 +42,13 @@ interface FcitxAPI {
suspend fun reloadConfig()
- suspend fun sendKey(key: String, states: UInt = 0u, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(key: String, states: UInt = 0u, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
- suspend fun sendKey(c: Char, states: UInt = 0u, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(c: Char, states: UInt = 0u, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
- suspend fun sendKey(sym: Int, states: UInt = 0u, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(sym: Int, states: UInt = 0u, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
- suspend fun sendKey(sym: KeySym, states: KeyStates, up: Boolean = false, timestamp: Int = -1)
+ suspend fun sendKey(sym: KeySym, states: KeyStates, code: Int = 0, up: Boolean = false, timestamp: Int = -1)
suspend fun select(idx: Int): Boolean
suspend fun isEmpty(): Boolean
@@ -100,4 +99,10 @@ interface FcitxAPI {
suspend fun getCandidates(offset: Int, limit: Int): Array
+ suspend fun getCandidateActions(idx: Int): Array
+ suspend fun triggerCandidateAction(idx: Int, actionIdx: Int)
+
+ suspend fun setCandidatePagingMode(mode: Int)
+ suspend fun offsetCandidatePage(delta: Int)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt
index 33e0025e4..c596c8117 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxDispatcher.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
@@ -21,23 +21,18 @@ import kotlin.coroutines.CoroutineContext
class FcitxDispatcher(private val controller: FcitxController) : CoroutineDispatcher() {
- class WrappedRunnable(private val runnable: Runnable, private val name: String? = null) :
- Runnable by runnable {
+ class WrappedRunnable(private val runnable: Runnable) : Runnable by runnable {
private val time = System.currentTimeMillis()
- var started = false
- private set
-
- private val delta
- get() = System.currentTimeMillis() - time
override fun run() {
- if (delta > JOB_WAITING_LIMIT)
- Timber.w("${toString()} has waited $delta ms to get run since created!")
- started = true
+ val delta = System.currentTimeMillis() - time
+ if (delta > JOB_WAITING_LIMIT) {
+ Timber.w("$this has waited $delta ms to get run since created!")
+ }
runnable.run()
}
- override fun toString(): String = "WrappedRunnable[${name ?: hashCode()}]"
+ override fun toString(): String = "WrappedRunnable[${hashCode()}]"
}
// this is fcitx main thread
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
index 3172ba6f1..1c119fab2 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxEvent.kt
@@ -6,6 +6,8 @@ package org.fcitx.fcitx5.android.core
sealed class FcitxEvent(open val data: T) {
+ data class Candidate(val label: String, val text: String, val comment: String)
+
abstract val eventType: EventType
data class CandidateListEvent(override val data: Data) :
@@ -13,7 +15,7 @@ sealed class FcitxEvent(open val data: T) {
override val eventType = EventType.Candidate
- data class Data(val total: Int, val candidates: Array) {
+ data class Data(val total: Int = -1, val candidates: Array = emptyArray()) {
override fun toString(): String =
"total=$total, candidates=[${candidates.joinToString(limit = 5)}]"
@@ -121,11 +123,63 @@ sealed class FcitxEvent(open val data: T) {
data class DeleteSurroundingEvent(override val data: Data) :
FcitxEvent(data) {
- override val eventType = EventType.DeleteSurrounding
+ override val eventType = EventType.DeleteSurrounding
data class Data(val before: Int, val after: Int)
}
+ data class PagedCandidateEvent(override val data: Data) :
+ FcitxEvent(data) {
+
+ override val eventType = EventType.PagedCandidate
+
+ enum class LayoutHint(value: Int) {
+ NotSet(0), Vertical(1), Horizontal(2);
+
+ companion object {
+ private val Types = entries.toTypedArray()
+ fun of(value: Int) = Types[value]
+ }
+ }
+
+ data class Data(
+ val candidates: Array,
+ val cursorIndex: Int,
+ val layoutHint: LayoutHint,
+ val hasPrev: Boolean,
+ val hasNext: Boolean
+ ) {
+ companion object {
+ @Suppress("BooleanLiteralArgument")
+ val Empty = Data(emptyArray(), -1, LayoutHint.NotSet, false, false)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Data
+
+ if (!candidates.contentEquals(other.candidates)) return false
+ if (cursorIndex != other.cursorIndex) return false
+ if (layoutHint != other.layoutHint) return false
+ if (hasPrev != other.hasPrev) return false
+ if (hasNext != other.hasNext) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = candidates.contentHashCode()
+ result = 31 * result + cursorIndex
+ result = 31 * result + layoutHint.hashCode()
+ result = 31 * result + hasPrev.hashCode()
+ result = 31 * result + hasNext.hashCode()
+ return result
+ }
+ }
+ }
+
data class UnknownEvent(override val data: Array) : FcitxEvent>(data) {
override val eventType = EventType.Unknown
@@ -156,12 +210,13 @@ sealed class FcitxEvent(open val data: T) {
Change,
StatusArea,
DeleteSurrounding,
+ PagedCandidate,
Unknown
}
companion object {
- private val Types = EventType.values()
+ private val Types = EventType.entries.toTypedArray()
@Suppress("UNCHECKED_CAST")
fun create(type: Int, params: Array) =
@@ -206,6 +261,19 @@ sealed class FcitxEvent(open val data: T) {
EventType.DeleteSurrounding -> (params[0] as IntArray).let {
DeleteSurroundingEvent(DeleteSurroundingEvent.Data(it[0], it[1]))
}
+ EventType.PagedCandidate -> if (params.isEmpty()) {
+ PagedCandidateEvent(PagedCandidateEvent.Data.Empty)
+ } else {
+ PagedCandidateEvent(
+ PagedCandidateEvent.Data(
+ params[0] as Array,
+ params[1] as Int,
+ PagedCandidateEvent.LayoutHint.of(params[2] as Int),
+ params[3] as Boolean,
+ params[4] as Boolean
+ )
+ )
+ }
else -> UnknownEvent(params)
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxUtils.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxUtils.kt
new file mode 100644
index 000000000..374788e3e
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/FcitxUtils.kt
@@ -0,0 +1,64 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.core
+
+object FcitxUtils {
+
+ // https://github.com/fcitx/fcitx5/blob/5.1.8/src/lib/fcitx-utils/stringutils.cpp#L323
+ // https://github.com/fcitx/fcitx5/blob/5.1.8/src/lib/fcitx-utils/stringutils.cpp#L362
+ fun unescapeForValue(str: String): String {
+ val quoted = str.length >= 2 && str.first() == '"' && str.last() == '"'
+ val s = if (quoted) str.substring(1, str.length - 1) else str
+ if (s.isEmpty()) return s
+ var escape = false
+ return buildString {
+ s.forEach { c ->
+ when (escape) {
+ false -> {
+ if (c == '\\') {
+ escape = true
+ } else {
+ append(c)
+ }
+ }
+ true -> {
+ if (c == '\\') {
+ append('\\')
+ } else if (c == 'n') {
+ append('\n')
+ } else if (c == '"' && quoted) {
+ append('"')
+ } else {
+ throw IllegalStateException("Unexpected escape sequence '\\${c}' when unescaping string '${str}'.")
+ }
+ escape = false
+ }
+ }
+ }
+ }
+ }
+
+ private val QuotedChars = charArrayOf(' ', '"', '\t', '\r', '\u000b', '\u000c')
+
+ // https://github.com/fcitx/fcitx5/blob/5.1.8/src/lib/fcitx-utils/stringutils.cpp#L380
+ fun escapeForValue(str: String): String {
+ val needsQuote = str.lastIndexOfAny(QuotedChars) >= 0
+ return buildString {
+ if (needsQuote) append('"')
+ str.forEach { c ->
+ append(
+ when (c) {
+ '\\' -> "\\\\"
+ '\n' -> "\\n"
+ '"' -> "\\\""
+ else -> c
+ }
+ )
+ }
+ if (needsQuote) append('"')
+ }
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
index 4d4f2da3b..19ab51286 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/KeyState.kt
@@ -92,6 +92,8 @@ value class KeyStates(val states: UInt) {
companion object {
val Empty = KeyStates(0u)
+ val Virtual = KeyStates(KeyState.Virtual)
+
fun of(v: Int) = KeyStates(v.toUInt())
fun fromKeyEvent(event: KeyEvent): KeyStates {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt
new file mode 100644
index 000000000..a07d60cbe
--- /dev/null
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/SubtypeManager.kt
@@ -0,0 +1,59 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
+ */
+
+package org.fcitx.fcitx5.android.core
+
+import android.os.Build
+import android.view.inputmethod.InputMethodSubtype
+import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder
+import androidx.annotation.RequiresApi
+import org.fcitx.fcitx5.android.utils.InputMethodUtil
+import org.fcitx.fcitx5.android.utils.appContext
+import org.fcitx.fcitx5.android.utils.inputMethodManager
+
+object SubtypeManager {
+
+ private const val MODE_KEYBOARD = "keyboard"
+
+ private const val IM_KEYBOARD = "keyboard-us"
+
+ private val knownSubtypes: HashMap = hashMapOf()
+
+ fun subtypeOf(inputMethod: String): InputMethodSubtype? {
+ return knownSubtypes[inputMethod]
+ }
+
+ fun inputMethodOf(subtype: InputMethodSubtype): String {
+ return subtype.extraValue.ifEmpty { IM_KEYBOARD }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun syncWith(inputMethods: Array) {
+ knownSubtypes.clear()
+ val size = inputMethods.size
+ val subtypes = arrayOfNulls(size)
+ val hashCodes = IntArray(size)
+ inputMethods.forEachIndexed { i, im ->
+ val subtype = InputMethodSubtypeBuilder()
+ .setSubtypeId(im.uniqueName.hashCode())
+ .setSubtypeExtraValue(im.uniqueName)
+ .setSubtypeNameOverride(im.displayName)
+ .setSubtypeMode(MODE_KEYBOARD)
+ .setIsAsciiCapable(im.uniqueName == IM_KEYBOARD)
+ .build()
+ val hashCode = subtype.hashCode()
+ subtypes[i] = subtype
+ hashCodes[i] = hashCode
+ knownSubtypes[im.uniqueName] = subtype
+ }
+ val imm = appContext.inputMethodManager
+ val imiId = InputMethodUtil.componentName
+ // although this method has been marked as deprecated,
+ // dynamic subtypes have to be "registered" before they can be "enabled"
+ @Suppress("DEPRECATION")
+ imm.setAdditionalInputMethodSubtypes(imiId, subtypes)
+ imm.setExplicitlyEnabledInputMethodSubtypes(imiId, hashCodes)
+ }
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt
index e2e868fd7..12911945b 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/Types.kt
@@ -1,11 +1,12 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
data class InputMethodSubMode(val name: String, val label: String, val icon: String) {
constructor() : this("", "", "")
@@ -18,6 +19,7 @@ data class InputMethodEntry(
val nativeName: String,
val label: String,
val languageCode: String,
+ val addon: String,
val isConfigurable: Boolean,
val subMode: InputMethodSubMode
) {
@@ -28,6 +30,7 @@ data class InputMethodEntry(
nativeName: String,
label: String,
languageCode: String,
+ addon: String,
isConfigurable: Boolean
) : this(
uniqueName,
@@ -36,6 +39,7 @@ data class InputMethodEntry(
nativeName,
label,
languageCode,
+ addon,
isConfigurable,
InputMethodSubMode()
)
@@ -47,6 +51,7 @@ data class InputMethodEntry(
nativeName: String,
label: String,
languageCode: String,
+ addon: String,
isConfigurable: Boolean,
subMode: String,
subModeLabel: String,
@@ -58,17 +63,19 @@ data class InputMethodEntry(
nativeName,
label,
languageCode,
+ addon,
isConfigurable,
InputMethodSubMode(subMode, subModeLabel, subModeIcon)
)
- constructor(name: String) : this("", name, "", "", "×", "", false)
+ constructor(name: String) : this("", name, "", "", "×", "", "", false)
val displayName: String
get() = name.ifEmpty { uniqueName }
}
@Parcelize
+@Serializable
data class RawConfig(
val name: String,
val comment: String,
@@ -137,8 +144,7 @@ enum class AddonCategory {
InputMethod, Frontend, Loader, Module, UI;
companion object {
- private val Values = values()
- fun fromInt(i: Int) = Values[i]
+ fun fromInt(i: Int) = entries[i]
}
}
@@ -154,6 +160,7 @@ data class AddonInfo(
val dependencies: Array = arrayOf(),
val optionalDependencies: Array = arrayOf(),
) {
+ @Suppress("UNUSED") // used in JNI
constructor(
uniqueName: String,
name: String,
@@ -256,3 +263,12 @@ data class Action(
return result
}
}
+
+data class CandidateAction(
+ val id: Int,
+ val text: String,
+ val isSeparator: Boolean,
+ val icon: String,
+ val isCheckable: Boolean,
+ val isChecked: Boolean
+)
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt
index a458e2212..f134cc767 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/data/DataManager.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core.data
@@ -13,10 +13,9 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.fcitx.fcitx5.android.BuildConfig
import org.fcitx.fcitx5.android.core.data.DataManager.dataDir
-import org.fcitx.fcitx5.android.utils.Const
import org.fcitx.fcitx5.android.utils.FileUtil
import org.fcitx.fcitx5.android.utils.appContext
-import org.fcitx.fcitx5.android.utils.javaIdRegex
+import org.fcitx.fcitx5.android.utils.isJavaIdentifier
import org.xmlpull.v1.XmlPullParser
import timber.log.Timber
import java.io.File
@@ -30,6 +29,11 @@ import kotlin.concurrent.withLock
*/
object DataManager {
+ data class PluginSet(
+ val loaded: Set,
+ val failed: Map
+ )
+
const val PLUGIN_INTENT = "${BuildConfig.APPLICATION_ID}.plugin.MANIFEST"
private val lock = ReentrantLock()
@@ -39,13 +43,13 @@ object DataManager {
var synced = false
private set
- // should be consistent with the deserialization in build.gradle.kts (:app)
- private fun deserializeDataDescriptor(raw: String) = runCatching {
- json.decodeFromString(raw)
+ // should be consistent with the deserialization in DataDescriptorPlugin (:build-logic)
+ private fun deserializeDataDescriptor(raw: String): DataDescriptor {
+ return json.decodeFromString(raw)
}
- private fun serializeDataDescriptor(descriptor: DataDescriptor) = runCatching {
- json.encodeToString(descriptor)
+ private fun serializeDataDescriptor(descriptor: DataDescriptor): String {
+ return json.encodeToString(descriptor)
}
// If Android version supports direct boot, we put the hierarchy in device encrypted storage
@@ -57,14 +61,12 @@ object DataManager {
File(appContext.applicationInfo.dataDir)
}
- private val destDescriptorFile = File(dataDir, Const.dataDescriptorName)
-
- private fun AssetManager.getDataDescriptor() =
- open(Const.dataDescriptorName)
+ private fun AssetManager.getDataDescriptor(): DataDescriptor {
+ return open(BuildConfig.DATA_DESCRIPTOR_NAME)
.bufferedReader()
.use { it.readText() }
.let { deserializeDataDescriptor(it) }
- .getOrThrow()
+ }
private val loadedPlugins = mutableSetOf()
private val failedPlugins = mutableMapOf()
@@ -72,6 +74,8 @@ object DataManager {
fun getLoadedPlugins(): Set = loadedPlugins
fun getFailedPlugins(): Map = failedPlugins
+ fun getSyncedPluginSet() = PluginSet(loadedPlugins, failedPlugins)
+
/**
* Will be cleared after each sync
*/
@@ -80,8 +84,7 @@ object DataManager {
fun addOnNextSyncedCallback(block: () -> Unit) =
callbacks.add(block)
- @SuppressLint("DiscouragedApi")
- fun detectPlugins(): Pair, Map> {
+ fun detectPlugins(): PluginSet {
val toLoad = mutableSetOf()
val preloadFailed = mutableMapOf()
@@ -93,7 +96,6 @@ object DataManager {
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL.toLong())
)
} else {
- @Suppress("DEPRECATION")
pm.queryIntentActivities(Intent(PLUGIN_INTENT), PackageManager.MATCH_ALL)
}.map {
it.activityInfo.packageName
@@ -104,6 +106,8 @@ object DataManager {
// Parse plugin.xml
for (packageName in pluginPackages) {
val res = pm.getResourcesForApplication(packageName)
+
+ @SuppressLint("DiscouragedApi")
val resId = res.getIdentifier("plugin", "xml", packageName)
if (resId == 0) {
Timber.w("Failed to get the plugin descriptor of $packageName")
@@ -116,50 +120,28 @@ object DataManager {
var apiVersion: String? = null
var description: String? = null
var hasService = false
- val libraryDependency = mutableMapOf>()
- var library: String? = null
- var dependency: ArrayList? = null
var text: String? = null
while ((eventType != XmlPullParser.END_DOCUMENT)) {
when (eventType) {
XmlPullParser.TEXT -> text = parser.text
- XmlPullParser.START_TAG -> when (parser.name) {
- "library" -> {
- dependency = arrayListOf()
- for (i in 0.. when (parser.name) {
"apiVersion" -> apiVersion = text
"domain" -> domain = text
"description" -> description = text
"hasService" -> hasService = text?.lowercase() == "true"
- "dependency" -> dependency?.add(text!!)
- "library" -> {
- if (library != null && dependency != null) {
- libraryDependency[library] = dependency
- library = null
- dependency = null
- }
- }
}
}
eventType = parser.next()
}
parser.close()
- // Replace @string/ with string resource
- description = description?.let { d ->
- d.removePrefix("@string/").let { s ->
- if (s.matches(javaIdRegex)) {
- res.getIdentifier(s, "string", packageName).let { id ->
- if (id != 0) res.getString(id) else d
- }
- } else d
+ if (description?.startsWith("@string/") == true) {
+ // Replace "@string/" with string resource
+ val s = description.substring(8)
+ if (s.isJavaIdentifier()) {
+ @SuppressLint("DiscouragedApi")
+ val id = res.getIdentifier(s, "string", packageName)
+ if (id != 0) description = res.getString(id)
}
}
@@ -171,7 +153,6 @@ object DataManager {
PackageManager.PackageInfoFlags.of(PackageManager.GET_META_DATA.toLong())
)
} else {
- @Suppress("DEPRECATION")
pm.getPackageInfo(packageName, PackageManager.GET_META_DATA)
}
toLoad.add(
@@ -181,9 +162,8 @@ object DataManager {
domain,
description,
hasService,
- info.versionName,
- info.applicationInfo.nativeLibraryDir,
- libraryDependency
+ info.versionName ?: "",
+ info.applicationInfo?.nativeLibraryDir ?: ""
)
)
} else {
@@ -195,7 +175,7 @@ object DataManager {
preloadFailed[packageName] = PluginLoadFailed.PluginDescriptorParseError
}
}
- return toLoad to preloadFailed
+ return PluginSet(toLoad, preloadFailed)
}
fun sync() = lock.withLock {
@@ -203,24 +183,15 @@ object DataManager {
loadedPlugins.clear()
failedPlugins.clear()
+ val destDescriptorFile = File(dataDir, BuildConfig.DATA_DESCRIPTOR_NAME)
+
// load last run's data descriptor
- val oldDescriptor =
- destDescriptorFile
- .takeIf { it.exists() && it.isFile }
- ?.runCatching { readText() }
- ?.getOrNull()
- ?.let { deserializeDataDescriptor(it) }
- ?.getOrNull()
- ?: DataDescriptor("", mapOf(), mapOf())
+ val oldDescriptor = destDescriptorFile
+ .runCatching { deserializeDataDescriptor(bufferedReader().use { it.readText() }) }
+ .getOrElse { DataDescriptor("", emptyMap(), emptyMap()) }
// load app's data descriptor
- val mainDescriptor =
- appContext.assets
- .open(Const.dataDescriptorName)
- .bufferedReader()
- .use { it.readText() }
- .let { deserializeDataDescriptor(it) }
- .getOrThrow()
+ val mainDescriptor = appContext.assets.getDataDescriptor()
val (parsedDescriptors, failed) = detectPlugins()
failedPlugins.putAll(failed)
@@ -292,13 +263,15 @@ object DataManager {
}
}
// save the new hierarchy as the data descriptor to be used in the next run
- destDescriptorFile.writeText(serializeDataDescriptor(newHierarchy.downToDataDescriptor()).getOrThrow())
+ destDescriptorFile.bufferedWriter().use {
+ it.write(serializeDataDescriptor(newHierarchy.downToDataDescriptor()))
+ }
callbacks.forEach { it() }
callbacks.clear()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// remove old assets from credential encrypted storage
val oldDataDir = appContext.dataDir
- val oldDataDescriptor = oldDataDir.resolve(Const.dataDescriptorName)
+ val oldDataDescriptor = oldDataDir.resolve(BuildConfig.DATA_DESCRIPTOR_NAME)
if (oldDataDescriptor.exists()) {
oldDataDescriptor.delete()
oldDataDir.resolve("README.md").delete()
@@ -326,11 +299,11 @@ object DataManager {
fun deleteAndSync() {
lock.withLock {
- dataDir.resolve(Const.dataDescriptorName).delete()
+ dataDir.resolve(BuildConfig.DATA_DESCRIPTOR_NAME).delete()
dataDir.resolve("README.md").delete()
dataDir.resolve("usr").deleteRecursively()
}
sync()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt b/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt
index 52671a9bc..cec9078ec 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/core/data/PluginDescriptor.kt
@@ -1,11 +1,11 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core.data
+import org.fcitx.fcitx5.android.BuildConfig
import org.fcitx.fcitx5.android.core.data.PluginDescriptor.Companion.pluginPackagePrefix
-import org.fcitx.fcitx5.android.utils.Const
/**
* Metadata of a plugin, at `res/xml/plugin.xml`
@@ -32,15 +32,13 @@ data class PluginDescriptor(
*/
val hasService: Boolean,
val versionName: String,
- val nativeLibraryDir: String,
- val libraryDependency: Map>
+ val nativeLibraryDir: String
) {
- val name by lazy {
- packageName.removePrefix("$pluginPackagePrefix.").removeSuffix(".${Const.buildType}")
- }
+ val name = packageName.removePrefix(pluginPackagePrefix).removeSuffix(pluginPackageSuffix)
companion object {
- const val pluginPackagePrefix = "org.fcitx.fcitx5.android.plugin"
+ const val pluginPackagePrefix = "org.fcitx.fcitx5.android.plugin."
+ const val pluginPackageSuffix = ".${BuildConfig.BUILD_TYPE}"
const val pluginAPI = "0.1"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt
index f9d71acc6..fef6d17b1 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/InputFeedbacks.kt
@@ -10,56 +10,56 @@ import android.os.VibrationEffect
import android.provider.Settings
import android.view.HapticFeedbackConstants
import android.view.View
+import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
-import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
+import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.audioManager
-import org.fcitx.fcitx5.android.utils.isSystemSettingEnabled
+import org.fcitx.fcitx5.android.utils.getSystemSettings
import org.fcitx.fcitx5.android.utils.vibrator
object InputFeedbacks {
- enum class InputFeedbackMode {
- Enabled, Disabled, FollowingSystem;
-
- companion object : ManagedPreference.StringLikeCodec {
- override fun decode(raw: String) = InputFeedbackMode.valueOf(raw)
- }
+ enum class InputFeedbackMode(override val stringRes: Int) : ManagedPreferenceEnum {
+ FollowingSystem(R.string.following_system_settings),
+ Enabled(R.string.enabled),
+ Disabled(R.string.disabled);
}
private var systemSoundEffects = false
private var systemHapticFeedback = false
fun syncSystemPrefs() {
- systemSoundEffects = isSystemSettingEnabled(Settings.System.SOUND_EFFECTS_ENABLED)
+ systemSoundEffects = getSystemSettings(Settings.System.SOUND_EFFECTS_ENABLED) == 1
// it says "Replaced by using android.os.VibrationAttributes.USAGE_TOUCH"
// but gives no clue about how to use it, and this one still works
@Suppress("DEPRECATION")
- systemHapticFeedback = isSystemSettingEnabled(Settings.System.HAPTIC_FEEDBACK_ENABLED)
+ systemHapticFeedback = getSystemSettings(Settings.System.HAPTIC_FEEDBACK_ENABLED) == 1
}
- private val soundOnKeyPress by AppPrefs.getInstance().keyboard.soundOnKeyPress
- private val soundOnKeyPressVolume by AppPrefs.getInstance().keyboard.soundOnKeyPressVolume
- private val hapticOnKeyPress by AppPrefs.getInstance().keyboard.hapticOnKeyPress
- private val buttonPressVibrationMilliseconds by AppPrefs.getInstance().keyboard.buttonPressVibrationMilliseconds
- private val buttonLongPressVibrationMilliseconds by AppPrefs.getInstance().keyboard.buttonLongPressVibrationMilliseconds
- private val buttonPressVibrationAmplitude by AppPrefs.getInstance().keyboard.buttonPressVibrationAmplitude
- private val buttonLongPressVibrationAmplitude by AppPrefs.getInstance().keyboard.buttonLongPressVibrationAmplitude
+ private val keyboardPrefs = AppPrefs.getInstance().keyboard
+
+ private val soundOnKeyPress by keyboardPrefs.soundOnKeyPress
+ private val soundOnKeyPressVolume by keyboardPrefs.soundOnKeyPressVolume
+ private val hapticOnKeyPress by keyboardPrefs.hapticOnKeyPress
+ private val hapticOnKeyUp by keyboardPrefs.hapticOnKeyUp
+ private val buttonPressVibrationMilliseconds by keyboardPrefs.buttonPressVibrationMilliseconds
+ private val buttonLongPressVibrationMilliseconds by keyboardPrefs.buttonLongPressVibrationMilliseconds
+ private val buttonPressVibrationAmplitude by keyboardPrefs.buttonPressVibrationAmplitude
+ private val buttonLongPressVibrationAmplitude by keyboardPrefs.buttonLongPressVibrationAmplitude
private val vibrator = appContext.vibrator
private val hasAmplitudeControl =
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && vibrator.hasAmplitudeControl()
- private val audioManager = appContext.audioManager
-
- fun hapticFeedback(view: View, longPress: Boolean = false) {
+ fun hapticFeedback(view: View, longPress: Boolean = false, keyUp: Boolean = false) {
when (hapticOnKeyPress) {
InputFeedbackMode.Enabled -> {}
InputFeedbackMode.Disabled -> return
InputFeedbackMode.FollowingSystem -> if (!systemHapticFeedback) return
}
-
+ if (keyUp && !hapticOnKeyUp) return
val duration: Long
val amplitude: Int
val hfc: Int
@@ -70,25 +70,36 @@ object InputFeedbacks {
} else {
duration = buttonPressVibrationMilliseconds.toLong()
amplitude = buttonPressVibrationAmplitude
- hfc = HapticFeedbackConstants.KEYBOARD_TAP
+ hfc = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && keyUp) {
+ HapticFeedbackConstants.KEYBOARD_RELEASE
+ } else {
+ HapticFeedbackConstants.KEYBOARD_TAP
+ }
}
- val useVibrator = duration != 0L
- if (useVibrator) {
+ // there is `VibrationEffect.DEFAULT_AMPLITUDE` but no default duration;
+ // also `VibrationEffect.createOneShot()` only accepts positive duration.
+ // so changing amplitude without changing duration makes no sense
+ if (duration != 0L) {
// on Android 13, if system haptic feedback was disabled, `vibrator.vibrate()` won't work
// but `view.performHapticFeedback()` with `FLAG_IGNORE_GLOBAL_SETTING` still works
if (hasAmplitudeControl && amplitude != 0) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val ve = VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE)
+ vibrator.vibrate(ve)
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
}
} else {
- // it says "Starting TIRAMISU only privileged apps can ignore user settings for touch feedback"
- // but we still seem to be able to use `FLAG_IGNORE_GLOBAL_SETTING`
- @Suppress("DEPRECATION")
- val flags =
- HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING or HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ var flags = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+ if (hapticOnKeyPress == InputFeedbackMode.Enabled) {
+ // it says "Starting TIRAMISU only privileged apps can ignore user settings for touch feedback"
+ // but we still seem to be able to use `FLAG_IGNORE_GLOBAL_SETTING`
+ @Suppress("DEPRECATION")
+ flags = flags or HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
+ }
view.performHapticFeedback(hfc, flags)
}
}
@@ -97,6 +108,8 @@ object InputFeedbacks {
Standard, SpaceBar, Delete, Return
}
+ private val audioManager = appContext.audioManager
+
fun soundEffect(effect: SoundEffect) {
when (soundOnKeyPress) {
InputFeedbackMode.Enabled -> {}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
index 9afc698aa..deeadf0db 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/RecentlyUsed.kt
@@ -1,43 +1,76 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
+
package org.fcitx.fcitx5.android.data
+import android.content.Context
+import androidx.core.content.edit
+import kotlinx.serialization.json.Json
import org.fcitx.fcitx5.android.FcitxApplication
+import timber.log.Timber
-// Not thread-safe
-class RecentlyUsed(
- val fileName: String,
- val capacity: Int
-) : LinkedHashMap(0, .75f, true) {
+class RecentlyUsed(val type: String, val limit: Int) {
companion object {
+ // for backwords compatibility only
const val DIR_NAME = "recently_used"
+ const val PREFERENCE_NAME = "picker_recently_used"
}
- private val file =
- FcitxApplication.getInstance().directBootAwareContext.filesDir.resolve(DIR_NAME).run {
- mkdirs()
- resolve(fileName).apply { createNewFile() }
- }
+ private val sharedPreferences = FcitxApplication.getInstance().directBootAwareContext
+ .getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
- fun load() {
- val xs = file.readLines()
- xs.forEach {
- if (it.isNotBlank())
- put(it, it)
- }
+ private val map = LinkedHashMap(limit).apply {
+ (migrate() ?: load()).forEach { put(it, true) }
}
- fun save() {
- file.writeText(values.joinToString("\n"))
+ val items: List get() = map.keys.reversed()
+
+ private fun load(): List {
+ val rawValue = sharedPreferences.getString(type, "") ?: ""
+ if (rawValue.isEmpty()) {
+ return emptyList()
+ }
+ return try {
+ Json.decodeFromString>(rawValue)
+ } catch (_: Exception) {
+ sharedPreferences.edit {
+ remove(type)
+ }
+ emptyList()
+ }
}
- override fun removeEldestEntry(eldest: MutableMap.MutableEntry?) =
- size > capacity
+ private fun save() {
+ sharedPreferences.edit {
+ putString(type, Json.encodeToString>(map.keys.toList()))
+ }
+ }
- fun insert(s: String) = put(s, s)
+ fun insert(item: String) {
+ map.put(item, true)
+ save()
+ }
- fun toOrderedList() = values.toList().reversed()
+ fun migrate(): List? {
+ val dir = FcitxApplication.getInstance().directBootAwareContext.filesDir.resolve(DIR_NAME)
+ val file = dir.resolve(type)
+ if (file.exists()) {
+ try {
+ val lines = file.readLines()
+ file.delete()
+ if (dir.list()?.isEmpty() == true) {
+ dir.delete()
+ }
+ return lines
+ } catch (e: Exception) {
+ Timber.w("Failed to migrate RecentlyUsed(type=$type)")
+ Timber.w(e)
+ return null
+ }
+ }
+ return null
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt
index c0c65ec39..39a31ab64 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/UserDataManager.kt
@@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.data
@@ -14,6 +14,7 @@ import org.fcitx.fcitx5.android.utils.Const
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.errorRuntime
import org.fcitx.fcitx5.android.utils.extract
+import org.fcitx.fcitx5.android.utils.versionCodeCompat
import org.fcitx.fcitx5.android.utils.withTempDir
import timber.log.Timber
import java.io.File
@@ -30,7 +31,7 @@ object UserDataManager {
@Serializable
data class Metadata(
val packageName: String,
- val versionCode: Int,
+ val versionCode: Long,
val versionName: String,
val exportTime: Long
)
@@ -64,13 +65,13 @@ object UserDataManager {
writeFileTree(dataBasesDir, "databases", zipStream)
// external
writeFileTree(externalDir, "external", zipStream)
- // recently_used
- writeFileTree(recentlyUsedDir, "recently_used", zipStream)
+ // recently_used moved to SharedPreference and shoud not be exported
// metadata
zipStream.putNextEntry(ZipEntry("metadata.json"))
+ val pkgInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
val metadata = Metadata(
- BuildConfig.APPLICATION_ID,
- BuildConfig.VERSION_CODE,
+ pkgInfo.packageName,
+ pkgInfo.versionCodeCompat,
Const.versionName,
timestamp
)
@@ -85,7 +86,6 @@ object UserDataManager {
if (exists && isDir) {
source.copyRecursively(target, overwrite = true)
} else {
- source.toString()
Timber.w("Cannot import user data: path='${source.path}', exists=$exists, isDir=$isDir")
}
}
@@ -102,6 +102,7 @@ object UserDataManager {
copyDir(File(tempDir, "shared_prefs"), sharedPrefsDir)
copyDir(File(tempDir, "databases"), dataBasesDir)
copyDir(File(tempDir, "external"), externalDir)
+ // keep importing recently_used for backwords compatibility
copyDir(File(tempDir, "recently_used"), recentlyUsedDir)
metadata
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt
index 6e93b933d..92bc85285 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/ClipboardManager.kt
@@ -6,8 +6,10 @@ package org.fcitx.fcitx5.android.data.clipboard
import android.content.ClipboardManager
import android.content.Context
+import android.os.Build
import androidx.annotation.Keep
import androidx.room.Room
+import androidx.room.withTransaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -23,6 +25,7 @@ import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
import org.fcitx.fcitx5.android.utils.WeakHashSet
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.clipboardManager
+import timber.log.Timber
object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener,
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
@@ -84,6 +87,8 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener,
fun init(context: Context) {
clbDb = Room
.databaseBuilder(context, ClipboardDatabase::class.java, "clbdb")
+ // allow wipe the database instead of crashing when downgrade
+ .fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true)
.build()
clbDao = clbDb.clipboardDao()
enabledListener.onChange(enabledPref.key, enabledPref.getValue())
@@ -142,26 +147,50 @@ object ClipboardManager : ClipboardManager.OnPrimaryClipChangedListener,
}
}
+ private var lastClipTimestamp = -1L
+ private var lastClipHash = 0
+
override fun onPrimaryClipChanged() {
- clipboardManager.primaryClip
- ?.let { ClipboardEntry.fromClipData(it, transformer) }
- ?.takeIf { it.text.isNotBlank() }
- ?.let { e ->
- launch {
- mutex.withLock {
- clbDao.find(e.text)?.let {
- updateLastEntry(it.copy(timestamp = e.timestamp))
- clbDao.updateTime(it.id, e.timestamp)
- return@launch
- }
- val rowId = clbDao.insert(e)
+ val clip = clipboardManager.primaryClip ?: return
+ /**
+ * skip duplicate ClipData
+ * https://developer.android.com/reference/android/content/ClipboardManager.OnPrimaryClipChangedListener#onPrimaryClipChanged()
+ */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val timestamp = clip.description.timestamp
+ if (timestamp == lastClipTimestamp) return
+ lastClipTimestamp = timestamp
+ } else {
+ val timestamp = System.currentTimeMillis()
+ val hash = clip.hashCode()
+ if (timestamp - lastClipTimestamp < 100L && hash == lastClipHash) return
+ lastClipTimestamp = timestamp
+ lastClipHash = hash
+ }
+ launch {
+ mutex.withLock {
+ val entry = ClipboardEntry.fromClipData(clip, transformer) ?: return@withLock
+ if (entry.text.isBlank()) return@withLock
+ try {
+ clbDao.find(entry.text, entry.sensitive)?.let {
+ updateLastEntry(it.copy(timestamp = entry.timestamp))
+ clbDao.updateTime(it.id, entry.timestamp)
+ return@withLock
+ }
+ val insertedEntry = clbDb.withTransaction {
+ val rowId = clbDao.insert(entry)
removeOutdated()
- updateItemCount()
// new entry can be deleted immediately if clipboard limit == 0
- updateLastEntry(clbDao.get(rowId) ?: e)
+ clbDao.get(rowId) ?: entry
}
+ updateLastEntry(insertedEntry)
+ updateItemCount()
+ } catch (exception: Exception) {
+ Timber.w("Failed to update clipboard database: $exception")
+ updateLastEntry(entry)
}
}
+ }
}
private suspend fun removeOutdated() {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt
index fe8dd6700..cf55dad10 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDao.kt
@@ -41,8 +41,8 @@ interface ClipboardDao {
@Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE deleted=0 ORDER BY pinned DESC, timestamp DESC")
fun allEntries(): PagingSource
- @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE text=:text AND deleted=0 LIMIT 1")
- suspend fun find(text: String): ClipboardEntry?
+ @Query("SELECT * FROM ${ClipboardEntry.TABLE_NAME} WHERE text=:text AND sensitive=:sensitive AND deleted=0 LIMIT 1")
+ suspend fun find(text: String, sensitive: Boolean = false): ClipboardEntry?
@Query("SELECT id FROM ${ClipboardEntry.TABLE_NAME} WHERE deleted=0")
suspend fun findAllIds(): IntArray
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt
index 8d5b69f01..1d7113877 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardDatabase.kt
@@ -10,10 +10,11 @@ import androidx.room.RoomDatabase
@Database(
entities = [ClipboardEntry::class],
- version = 3,
+ version = 4,
autoMigrations = [
AutoMigration(from = 1, to = 2),
- AutoMigration(from = 2, to = 3)
+ AutoMigration(from = 2, to = 3),
+ AutoMigration(from = 3, to = 4)
]
)
abstract class ClipboardDatabase : RoomDatabase() {
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt
index e7ed17075..c98252b75 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/clipboard/db/ClipboardEntry.kt
@@ -6,9 +6,11 @@ package org.fcitx.fcitx5.android.data.clipboard.db
import android.content.ClipData
import android.content.ClipDescription
+import android.os.Build
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
+import org.fcitx.fcitx5.android.utils.timestamp
@Entity(tableName = ClipboardEntry.TABLE_NAME)
data class ClipboardEntry(
@@ -22,18 +24,38 @@ data class ClipboardEntry(
val type: String = ClipDescription.MIMETYPE_TEXT_PLAIN,
@ColumnInfo(defaultValue = "0")
val deleted: Boolean = false,
+ @ColumnInfo(defaultValue = "0")
+ val sensitive: Boolean = false
) {
companion object {
+ const val BULLET = "•"
+
const val TABLE_NAME = "clipboard"
+ private val IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ClipDescription.EXTRA_IS_SENSITIVE
+ } else {
+ "android.content.extra.IS_SENSITIVE"
+ }
+
fun fromClipData(
clipData: ClipData,
transformer: ((String) -> String)? = null
): ClipboardEntry? {
- val str = clipData.getItemAt(0).text?.toString() ?: return null
+ val desc = clipData.description
+ // TODO: handle multiple items (when does this happen?)
+ val item = clipData.getItemAt(0) ?: return null
+ val str = item.text?.toString() ?: return null
+ val sensitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ desc.extras?.getBoolean(IS_SENSITIVE) ?: false
+ } else {
+ false
+ }
return ClipboardEntry(
- text = transformer?.let { it(str) } ?: str,
- type = clipData.description.getMimeType(0)
+ text = if (transformer != null) transformer(str) else str,
+ timestamp = clipData.timestamp(),
+ type = desc.getMimeType(0),
+ sensitive = sensitive
)
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/customphrase/PinyinCustomPhrase.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/customphrase/PinyinCustomPhrase.kt
index a6edd8ec7..96dd68982 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/customphrase/PinyinCustomPhrase.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/pinyin/customphrase/PinyinCustomPhrase.kt
@@ -4,6 +4,7 @@
*/
package org.fcitx.fcitx5.android.data.pinyin.customphrase
+import org.fcitx.fcitx5.android.core.FcitxUtils
import kotlin.math.absoluteValue
data class PinyinCustomPhrase(
@@ -17,5 +18,5 @@ data class PinyinCustomPhrase(
return copy(order = (if (e) 1 else -1) * order.absoluteValue)
}
- fun serialize() = "$key,${order.absoluteValue}=$value"
+ fun serialize() = "$key,${order.absoluteValue}=${FcitxUtils.escapeForValue(value)}"
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt
index 0f03a18f5..d84a0215b 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt
@@ -1,22 +1,26 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
- * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.data.prefs
import android.content.SharedPreferences
import android.os.Build
+import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.InputFeedbacks.InputFeedbackMode
-import org.fcitx.fcitx5.android.input.candidates.HorizontalCandidateMode
import org.fcitx.fcitx5.android.input.candidates.expanded.ExpandedCandidateStyle
+import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesMode
+import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesOrientation
+import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateMode
import org.fcitx.fcitx5.android.input.keyboard.LangSwitchBehavior
import org.fcitx.fcitx5.android.input.keyboard.SpaceLongPressBehavior
import org.fcitx.fcitx5.android.input.keyboard.SwipeSymbolDirection
import org.fcitx.fcitx5.android.input.picker.PickerWindow
+import org.fcitx.fcitx5.android.input.popup.EmojiModifier
import org.fcitx.fcitx5.android.utils.DeviceUtil
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.vibrator
@@ -34,7 +38,7 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
}
inner class Advanced : ManagedPreferenceCategory(R.string.advanced, sharedPreferences) {
- val ignoreSystemCursor = switch(R.string.ignore_sys_cursor, "ignore_system_cursor", true)
+ val ignoreSystemCursor = switch(R.string.ignore_sys_cursor, "ignore_system_cursor", false)
val hideKeyConfig = switch(R.string.hide_key_config, "hide_key_config", true)
val disableAnimation = switch(R.string.disable_animation, "disable_animation", false)
val vivoKeypressWorkaround = switch(
@@ -42,26 +46,25 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
"vivo_keypress_workaround",
DeviceUtil.isVivoOriginOS
)
+ val ignoreSystemWindowInsets = switch(
+ R.string.ignore_system_window_insets, "ignore_system_window_insets", false
+ )
}
- inner class Keyboard : ManagedPreferenceCategory(R.string.keyboard, sharedPreferences) {
+ inner class Keyboard : ManagedPreferenceCategory(R.string.virtual_keyboard, sharedPreferences) {
val hapticOnKeyPress =
- list(
+ enumList(
R.string.button_haptic_feedback,
"haptic_on_keypress",
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode,
- listOf(
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode.Enabled,
- InputFeedbackMode.Disabled
- ),
- listOf(
- R.string.following_system_settings,
- R.string.enabled,
- R.string.disabled
- )
+ InputFeedbackMode.FollowingSystem
)
+ val hapticOnKeyUp = switch(
+ R.string.button_up_haptic_feedback,
+ "haptic_on_keyup",
+ false
+ ) { hapticOnKeyPress.getValue() != InputFeedbackMode.Disabled }
+ val hapticOnRepeat = switch(R.string.haptic_on_repeat, "haptic_on_repeat", false)
+
val buttonPressVibrationMilliseconds: ManagedPreference.PInt
val buttonLongPressVibrationMilliseconds: ManagedPreference.PInt
@@ -108,21 +111,10 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
buttonLongPressVibrationAmplitude = secondary
}
- val soundOnKeyPress = list(
+ val soundOnKeyPress = enumList(
R.string.button_sound,
"sound_on_keypress",
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode,
- listOf(
- InputFeedbackMode.FollowingSystem,
- InputFeedbackMode.Enabled,
- InputFeedbackMode.Disabled
- ),
- listOf(
- R.string.following_system_settings,
- R.string.enabled,
- R.string.disabled
- )
+ InputFeedbackMode.FollowingSystem
)
val soundOnKeyPressVolume = int(
R.string.button_sound_volume,
@@ -152,21 +144,10 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
switch(R.string.show_voice_input_button, "show_voice_input_button", false)
val expandKeypressArea =
switch(R.string.expand_keypress_area, "expand_keypress_area", false)
- val swipeSymbolDirection = list(
+ val swipeSymbolDirection = enumList(
R.string.swipe_symbol_behavior,
"swipe_symbol_behavior",
- SwipeSymbolDirection.Down,
- SwipeSymbolDirection,
- listOf(
- SwipeSymbolDirection.Up,
- SwipeSymbolDirection.Down,
- SwipeSymbolDirection.Disabled
- ),
- listOf(
- R.string.swipe_up,
- R.string.swipe_down,
- R.string.disabled
- )
+ SwipeSymbolDirection.Down
)
val longPressDelay = int(
R.string.keyboard_long_press_delay,
@@ -177,41 +158,19 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
"ms",
10
)
- val spaceKeyLongPressBehavior = list(
+ val spaceKeyLongPressBehavior = enumList(
R.string.space_long_press_behavior,
"space_long_press_behavior",
- SpaceLongPressBehavior.None,
- SpaceLongPressBehavior,
- listOf(
- SpaceLongPressBehavior.None,
- SpaceLongPressBehavior.Enumerate,
- SpaceLongPressBehavior.ToggleActivate,
- SpaceLongPressBehavior.ShowPicker
- ),
- listOf(
- R.string.space_behavior_none,
- R.string.space_behavior_enumerate,
- R.string.space_behavior_activate,
- R.string.space_behavior_picker
- )
+ SpaceLongPressBehavior.None
)
+ val spaceSwipeMoveCursor =
+ switch(R.string.space_swipe_move_cursor, "space_swipe_move_cursor", true)
val showLangSwitchKey =
switch(R.string.show_lang_switch_key, "show_lang_switch_key", true)
- val langSwitchKeyBehavior = list(
+ val langSwitchKeyBehavior = enumList(
R.string.lang_switch_key_behavior,
"lang_switch_key_behavior",
- LangSwitchBehavior.Enumerate,
- LangSwitchBehavior,
- listOf(
- LangSwitchBehavior.Enumerate,
- LangSwitchBehavior.ToggleActivate,
- LangSwitchBehavior.NextInputMethodApp
- ),
- listOf(
- R.string.space_behavior_enumerate,
- R.string.space_behavior_activate,
- R.string.lang_switch_behavior_next_ime_app
- )
+ LangSwitchBehavior.Enumerate
) { showLangSwitchKey.getValue() }
val keyboardHeightPercent: ManagedPreference.PInt
@@ -247,7 +206,7 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
"keyboard_side_padding_landscape",
0,
0,
- 200,
+ 300,
"dp"
)
keyboardSidePadding = primary
@@ -274,35 +233,15 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
keyboardBottomPaddingLandscape = secondary
}
- val horizontalCandidateStyle = list(
+ val horizontalCandidateStyle = enumList(
R.string.horizontal_candidate_style,
"horizontal_candidate_style",
- HorizontalCandidateMode.AutoFillWidth,
- HorizontalCandidateMode,
- listOf(
- HorizontalCandidateMode.NeverFillWidth,
- HorizontalCandidateMode.AutoFillWidth,
- HorizontalCandidateMode.AlwaysFillWidth,
- ),
- listOf(
- R.string.horizontal_candidate_never_fill,
- R.string.horizontal_candidate_auto_fill,
- R.string.horizontal_candidate_always_fill
- )
+ HorizontalCandidateMode.AutoFillWidth
)
- val expandedCandidateStyle = list(
+ val expandedCandidateStyle = enumList(
R.string.expanded_candidate_style,
"expanded_candidate_style",
- ExpandedCandidateStyle.Grid,
- ExpandedCandidateStyle,
- listOf(
- ExpandedCandidateStyle.Grid,
- ExpandedCandidateStyle.Flexbox
- ),
- listOf(
- R.string.expanded_candidate_style_grid,
- R.string.expanded_candidate_style_flexbox
- )
+ ExpandedCandidateStyle.Grid
)
val expandedCandidateGridSpanCount: ManagedPreference.PInt
@@ -326,6 +265,60 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
}
+ inner class Candidates :
+ ManagedPreferenceCategory(R.string.candidates_window, sharedPreferences) {
+ val mode = enumList(
+ R.string.show_candidates_window,
+ "show_candidates_window",
+ FloatingCandidatesMode.InputDevice
+ )
+
+ val orientation = enumList(
+ R.string.candidates_orientation,
+ "candidates_window_orientation",
+ FloatingCandidatesOrientation.Automatic
+ )
+
+ val windowMinWidth = int(
+ R.string.candidates_window_min_width,
+ "candidates_window_min_width",
+ 0,
+ 0,
+ 640,
+ "dp",
+ 10
+ )
+
+ val windowPadding =
+ int(R.string.candidates_window_padding, "candidates_window_padding", 4, 0, 32, "dp")
+
+ val fontSize =
+ int(R.string.candidates_font_size, "candidates_window_font_size", 20, 4, 64, "sp")
+
+ val windowRadius =
+ int(R.string.candidates_window_radius, "candidates_window_radius", 0, 0, 48, "dp")
+
+ val itemPaddingVertical: ManagedPreference.PInt
+ val itemPaddingHorizontal: ManagedPreference.PInt
+
+ init {
+ val (primary, secondary) = twinInt(
+ R.string.candidates_padding,
+ R.string.vertical,
+ "candidates_item_padding_vertical",
+ 2,
+ R.string.horizontal,
+ "candidates_item_padding_horizontal",
+ 4,
+ 0,
+ 64,
+ "dp"
+ )
+ itemPaddingVertical = primary
+ itemPaddingHorizontal = secondary
+ }
+ }
+
inner class Clipboard : ManagedPreferenceCategory(R.string.clipboard, sharedPreferences) {
val clipboardListening = switch(R.string.clipboard_listening, "clipboard_enable", true)
val clipboardHistoryLimit = int(
@@ -347,6 +340,17 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
val clipboardReturnAfterPaste = switch(
R.string.clipboard_return_after_paste, "clipboard_return_after_paste", false
) { clipboardListening.getValue() }
+ val clipboardMaskSensitive = switch(
+ R.string.clipboard_mask_sensitive, "clipboard_mask_sensitive", true
+ ) { clipboardListening.getValue() }
+ }
+
+ inner class Symbols : ManagedPreferenceCategory(R.string.emoji_and_symbols, sharedPreferences) {
+ val defaultEmojiSkinTone = enumList(
+ R.string.default_emoji_skin_tone,
+ "default_emoji_skin_tone",
+ EmojiModifier.SkinTone.Default,
+ )
}
private val providers = mutableListOf()
@@ -365,13 +369,17 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
val internal = Internal().register()
val keyboard = Keyboard().register()
+ val candidates = Candidates().register()
val clipboard = Clipboard().register()
+ val symbols = Symbols().register()
val advanced = Advanced().register()
+ @Keep
private val onSharedPreferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key == null) return@OnSharedPreferenceChangeListener
providers.forEach {
- it.managedPreferences[key]?.fireChange()
+ it.fireChange(key)
}
}
@@ -389,11 +397,14 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
).forEach {
it.putValueTo(this@edit)
}
- keyboard.managedPreferences.forEach {
- it.value.putValueTo(this@edit)
- }
- clipboard.managedPreferences.forEach {
- it.value.putValueTo(this@edit)
+ listOf(
+ keyboard,
+ candidates,
+ clipboard
+ ).forEach { category ->
+ category.managedPreferences.forEach {
+ it.value.putValueTo(this@edit)
+ }
}
}
}
diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt
index 6bc725a7e..ca8367b0c 100644
--- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt
+++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreference.kt
@@ -35,9 +35,7 @@ abstract class ManagedPreference(
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = setValue(value)
- private val listeners by lazy {
- WeakHashSet>()
- }
+ private lateinit var listeners: MutableSet