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
new file mode 100644
index 000000000..35299216a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +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.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/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 8bc77bcbd..000000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Build
-
-on:
- pull_request:
-
-jobs:
- Release:
- runs-on: ubuntu-22.04
- strategy:
- matrix:
- abi:
- - armeabi-v7a
- - arm64-v8a
- - x86
- - x86_64
- env:
- ABI: ${{ matrix.abi }}
- steps:
- - name: Fetch source code
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup Java
- uses: actions/setup-java@v3
- with:
- distribution: "temurin"
- java-version: "17"
-
- - name: Setup Android Environment
- uses: android-actions/setup-android@v2
-
- - 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
-
- - name: Setup Gradle
- uses: gradle/gradle-build-action@v2
-
- - name: Build Release APK
- run: ./gradlew assembleRelease --no-daemon
diff --git a/.github/workflows/fdroid.yml b/.github/workflows/fdroid.yml
new file mode 100644
index 000000000..8d612b32e
--- /dev/null
+++ b/.github/workflows/fdroid.yml
@@ -0,0 +1,124 @@
+name: F-Droid
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/fdroid.yml'
+ - 'app/org.fcitx.fcitx5.android.yml'
+ workflow_dispatch:
+ inputs:
+ build_number:
+ description: build number on Jenkins Job fcitx5-android
+ type: string
+ required: true
+ default: lastSuccessfulBuild
+ repository_dispatch:
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ fdroid-build:
+ runs-on: ubuntu-24.04
+ container: registry.gitlab.com/fdroid/fdroidserver:buildserver-bookworm
+ strategy:
+ matrix:
+ abi:
+ - armeabi-v7a
+ - arm64-v8a
+ - x86
+ - x86_64
+ fail-fast: false
+ steps:
+ - name: Fetch fdroiddata
+ uses: actions/checkout@v4
+ with:
+ repository: f-droid/fdroiddata
+
+ - name: Fetch fdroidserver
+ uses: actions/checkout@v4
+ with:
+ repository: f-droid/fdroidserver
+ path: fdroidserver
+
+ - name: Setup fdroidserver
+ run: |
+ source /etc/profile.d/bsenv.sh
+ rm -rf $fdroidserver
+ mv $GITHUB_WORKSPACE/fdroidserver $fdroidserver
+
+ rm -rf $ANDROID_HOME/tools
+ sdkmanager "tools" "platform-tools"
+ for d in logs tmp unsigned $home_vagrant/metadata $home_vagrant/.android $home_vagrant/.gradle $ANDROID_HOME; do
+ test -d $d || mkdir $d;
+ chown -R vagrant $d;
+ done
+ ln -s $home_vagrant/.gradle $GITHUB_WORKSPACE/.gradle
+ ln -s $GITHUB_WORKSPACE/tmp $home_vagrant/tmp
+ ln -s $GITHUB_WORKSPACE/srclibs $home_vagrant/srclibs
+ mv $GITHUB_WORKSPACE/build $home_vagrant/build
+ ln -s $home_vagrant/build $GITHUB_WORKSPACE/build
+ chown -R vagrant $GITHUB_WORKSPACE
+
+ - name: Build
+ env:
+ 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")
+ versionName=$(echo $build_metadata | yq ".versionName")
+ commitHash=$(echo $build_metadata | yq ".commitHash")
+ timestamp=$(echo $build_metadata | yq ".timestamp")
+ baseVersionCode=$(curl -L "https://github.com/fcitx5-android/fcitx5-android/raw/$commitHash/build-logic/convention/src/main/kotlin/Versions.kt" | grep "baseVersionCode =" | sed 's/.*= //')
+ declare -A abi_list
+ abi_list=([armeabi-v7a]=1 [arm64-v8a]=2 [x86]=3 [x86_64]=4)
+ i=${abi_list[${{ matrix.abi }}]}
+ versionCode=$(($baseVersionCode * 10 + $i))
+
+ source /etc/profile.d/bsenv.sh
+ metadata="$home_vagrant/metadata/org.fcitx.fcitx5.android.yml"
+ curl -Lo $metadata "https://github.com/${{ github.repository }}/raw/${{ github.sha }}/app/org.fcitx.fcitx5.android.yml"
+ sed -i s/%ts/$timestamp/g $metadata
+ sed -i s/%abi/${{ matrix.abi }}/g $metadata
+ yq -i ".Builds[0] |=
+ (.versionName = \"$versionName\") |=
+ (.versionCode = $versionCode) |=
+ (.commit = \"$commitHash\")
+ " $metadata
+ prebuiltTreeURL=$(curl -L \
+ -H "Accept: application/vnd.github+json" \
+ -H "Authorization: Bearer ${{ github.token }}" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "https://api.github.com/repos/${{ github.repository }}/contents/lib/fcitx5/src/main/cpp/prebuilt?ref=${{ github.sha }}" \
+ | yq .html_url)
+ prebuilderSHA=$(curl -L "${prebuiltTreeURL/\/tree\//\/raw\/}/toolchain-versions.json" | yq ".prebuilder")
+ yq -i ".Builds[0].srclibs[0] |=
+ \"fcitx5-android-prebuilder@${prebuilderSHA}\"
+ " $metadata
+ yq ".Builds[0]" $metadata
+ cp $metadata $GITHUB_WORKSPACE/metadata
+
+ fdroid="sudo --preserve-env --user vagrant
+ env HOME=$home_vagrant
+ PYTHONPATH=$fdroidserver:$fdroidserver/examples
+ PYTHONUNBUFFERED=true
+ GRADLE_USER_HOME=$home_vagrant/.gradle
+ $fdroidserver/fdroid"
+
+ build="org.fcitx.fcitx5.android:$versionCode"
+ chown -R vagrant $home_vagrant
+ $fdroid fetchsrclibs $build --verbose
+ cd $home_vagrant
+ $fdroid build --verbose --test --scan-binary --on-server --no-tarball $build
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ if: ${{ success() || failure() }}
+ with:
+ name: fdroid-${{ matrix.abi }}
+ path: tmp/
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
new file mode 100644
index 000000000..01968ac76
--- /dev/null
+++ b/.github/workflows/nix.yml
@@ -0,0 +1,27 @@
+name: Nix
+
+on:
+ pull_request:
+ push:
+ branches: [master]
+
+jobs:
+ develop:
+ 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@v31
+ with:
+ github_access_token: ${{ secrets.GITHUB_TOKEN }}
+ - uses: cachix/cachix-action@v16
+ with:
+ name: fcitx5-android
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ - name: Build Release APK
+ run: |
+ 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
new file mode 100644
index 000000000..43438a178
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,47 @@
+name: Publish
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - 'build-logic/**'
+ - 'lib/**'
+ - '.github/workflows/publish.yml'
+
+jobs:
+ publish:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Fetch source code
+ uses: actions/checkout@v4
+ with:
+ 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@v4
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ - name: Setup Android environment
+ uses: android-actions/setup-android@v3
+ with:
+ packages: cmake;3.31.6
+
+ - name: Setup Gradle
+ 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
new file mode 100644
index 000000000..dcc86c2e1
--- /dev/null
+++ b/.github/workflows/pull_request.yml
@@ -0,0 +1,90 @@
+name: Pull Request
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ # pull request's head branch was updated
+ - synchronize
+
+jobs:
+ build_pull_request:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-24.04
+ - macos-13
+ - macos-14
+ - windows-2022
+ steps:
+ - name: Fetch source code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Setup Java
+ 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.31.6"
+
+ - name: Install system dependencies (Ubuntu)
+ if: ${{ startsWith(matrix.os, 'ubuntu') }}
+ run: |
+ sudo apt update
+ sudo apt install extra-cmake-modules gettext
+
+ - name: Install system dependencies (macOS)
+ if: ${{ startsWith(matrix.os, 'macos') }}
+ run: |
+ brew install extra-cmake-modules
+
+ - name: Install system dependencies (Windows)
+ 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/actions/setup-gradle@v4
+
+ - name: Build Release APK
+ run: |
+ ./gradlew :app:assembleRelease
+ ./gradlew :assembleReleasePlugins
+
+ - name: Upload app
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-${{ matrix.os }}
+ path: app/build/outputs/apk/release/
+
+ - name: Pack plugins
+ shell: bash
+ run: |
+ mkdir plugins-to-upload
+ for i in $(ls plugin)
+ do
+ if [ -d "plugin/${i}" ]
+ then
+ mv "plugin/${i}/build/outputs/apk/release" "plugins-to-upload/${i}"
+ fi
+ done
+
+ - name: Upload plugins
+ uses: actions/upload-artifact@v4
+ with:
+ name: plugins-${{ matrix.os }}
+ path: plugins-to-upload
diff --git a/.gitignore b/.gitignore
index 49ae2eb54..0bd6d8615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,20 @@
+# 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/
+# Generated asset descriptor
+/plugin/*/src/main/assets/descriptor.json
+
# Intellij
.idea/*
!.idea/codeStyles/
+!.idea/copyright/
!.idea/dictionaries/
-!.idea/modules/
-.idea/modules/*
-# tell Andriod Stuido to exclude prebuilt dir in this file
-!.idea/modules/fcitx5-android.iml
# below are generated by Android Studio
@@ -103,3 +99,6 @@ lint/tmp/
# Android Profiling
*.hprof
+
+### Kotlin ###
+.kotlin/
diff --git a/.gitmodules b/.gitmodules
index d4b9412ca..2f3d38b6d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,18 +1,58 @@
-[submodule "app/src/main/cpp/fcitx5"]
- path = app/src/main/cpp/fcitx5
- url = git@github.com:fcitx/fcitx5.git
-[submodule "app/src/main/cpp/fcitx5-chinese-addons"]
- path = app/src/main/cpp/fcitx5-chinese-addons
- url = git@github.com:fcitx/fcitx5-chinese-addons.git
-[submodule "app/src/main/cpp/libime"]
- path = app/src/main/cpp/libime
- url = git@github.com:fcitx/libime.git
-[submodule "app/src/main/cpp/prebuilt"]
- path = app/src/main/cpp/prebuilt
- url = git@github.com:fcitx5-android/prebuilt
-[submodule "app/src/main/cpp/fcitx5-lua"]
- path = app/src/main/cpp/fcitx5-lua
- url = git@github.com:fcitx/fcitx5-lua.git
-[submodule "app/src/main/cpp/fcitx5-unikey"]
- path = app/src/main/cpp/fcitx5-unikey
- url = git@github.com:fcitx/fcitx5-unikey.git
+[submodule "lib/fcitx5/src/main/cpp/fcitx5"]
+ path = lib/fcitx5/src/main/cpp/fcitx5
+ url = https://github.com/fcitx/fcitx5.git
+[submodule "lib/fcitx5/src/main/cpp/prebuilt"]
+ path = lib/fcitx5/src/main/cpp/prebuilt
+ 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 = https://github.com/fcitx/fcitx5-lua.git
+[submodule "lib/libime/src/main/cpp/libime"]
+ path = lib/libime/src/main/cpp/libime
+ 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 = 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 = 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 = https://github.com/fcitx/fcitx5-anthy.git
+[submodule "plugin/unikey/src/main/cpp/fcitx5-unikey"]
+ path = plugin/unikey/src/main/cpp/fcitx5-unikey
+ 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 = https://github.com/fcitx/fcitx5-rime.git
+[submodule "plugin/rime/src/main/cpp/rime-prelude"]
+ path = plugin/rime/src/main/cpp/rime-prelude
+ 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 = 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 = 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 = https://github.com/rime/rime-stroke.git
+[submodule "plugin/hangul/src/main/cpp/fcitx5-hangul"]
+ path = plugin/hangul/src/main/cpp/fcitx5-hangul
+ 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 = https://github.com/fcitx/fcitx5-chewing.git
+[submodule "plugin/sayura/src/main/cpp/fcitx5-sayura"]
+ path = plugin/sayura/src/main/cpp/fcitx5-sayura
+ 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 = 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 c31322978..cf0517db7 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,6 +1,42 @@
+
+
+
+
diff --git a/.idea/copyright/fcitx5_android.xml b/.idea/copyright/fcitx5_android.xml
new file mode 100644
index 000000000..cdcc660a6
--- /dev/null
+++ b/.idea/copyright/fcitx5_android.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 000000000..08f4594e4
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 88418956f..fa009839e 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -3,18 +3,26 @@
androidfrontend
androidkeyboard
+ androidnotification
berberman
+ bopomofo
constraintlayout
cout
+ customphrase
endl
fcitx
fmtlib
icuuid
+ inputmethodservice
+ iostreams
iter
jbytes
jnicall
jobject
jstring
+ jyutping
+ kawaii
+ keypress
lgpl
libevent
libime
@@ -25,13 +33,19 @@
pkgdatadir
preedit
quickphrase
+ sayura
sdkmanager
setenv
shijienihao
shuangpin
+ sinhala
+ snackbar
spdx
stringutils
unfocus
+ unikey
+ zhuyin
+ zlib
\ No newline at end of file
diff --git a/.idea/modules/fcitx5-android.iml b/.idea/modules/fcitx5-android.iml
deleted file mode 100644
index 9b777b35b..000000000
--- a/.idea/modules/fcitx5-android.iml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
deleted file mode 100644
index b86167a14..000000000
--- a/Jenkinsfile
+++ /dev/null
@@ -1,148 +0,0 @@
-import groovy.transform.Field
-
-@Field
-def abiList = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']
-
-@Field
-def commitSha = ""
-
-def setBuildStatus(String message, String state, String ctx, String commitSha) {
- withCredentials([string(credentialsId: 'github-commit-status-token', variable: 'token')]) {
- def body = """{
- "state": "$state",
- "description": "$message",
- "context": "$ctx",
- "target_url": "$BUILD_URL"
- }
- """.toString()
- httpRequest consoleLogResponseBody: true,
- contentType: 'APPLICATION_JSON',
- httpMode: 'POST',
- requestBody: body,
- url: "https://api.github.com/repos/fcitx5-android/fcitx5-android/statuses/$commitSha".toString(),
- validResponseCodes: '201',
- customHeaders: [[name: 'Authorization', value: "token " + token]]
- }
-}
-
-def sendMessageToTelegramGroup(String message) {
- withCredentials([string(credentialsId: 'fcitx5-android-telegram-group', variable: 'chatId'),
- string(credentialsId: 'fcitx5-android-telegram-bot', variable: 'token')]) {
- def body = """{
- "chat_id": $chatId,
- "text": "${message.replace("-", "\\\\-")}",
- "parse_mode": "MarkdownV2",
- "disable_web_page_preview": true
- }
- """.toString()
- httpRequest consoleLogResponseBody: true,
- contentType: 'APPLICATION_JSON',
- httpMode: 'POST',
- requestBody: body,
- url: "https://api.telegram.org/bot$token/sendMessage".toString(),
- validResponseCodes: '200'
- }
-}
-
-def withBuildStatus(String name, Closure closure) {
- def ctx = "Jenkins Build / $name"
- stage(name) {
- try {
- setBuildStatus("...", "pending", ctx, commitSha)
- def start = System.currentTimeMillis()
- closure()
- def end = System.currentTimeMillis()
- setBuildStatus("Successful in ${(end - start) / 1000} seconds", "success", ctx, commitSha)
- } catch (Exception e) {
- setBuildStatus("Failed", "failure", ctx, commitSha)
- throw e
- }
- }
-}
-
-def forEachABI(String name, Closure closure) {
- abiList.each { String abi ->
- withBuildStatus("$name ($abi)") {
- closure(abi)
- }
- }
-}
-
-
-node("android") {
- catchError {
- timestamps {
- try {
- def buildTimestamp = System.currentTimeMillis().toString()
-
- stage("Fetching sources") {
- checkout([$class : 'GitSCM',
- branches : scm.branches,
- doGenerateSubmoduleConfigurations: false,
- extensions : [[$class : 'SubmoduleOption',
- disableSubmodules : false,
- parentCredentials : true,
- recursiveSubmodules: true,
- reference : '',
- trackingSubmodules : false],
- [$class : 'CleanBeforeCheckout',
- deleteUntrackedNestedRepositories: true]],
- submoduleCfg : [],
- userRemoteConfigs : scm.userRemoteConfigs])
- sh "git config --get remote.origin.url > .git/remote-url"
- repoUrl = readFile(".git/remote-url").trim()
- sh "git rev-parse HEAD > .git/current-commit"
- commitSha = readFile(".git/current-commit").trim()
- setBuildStatus("...", "pending", "Jenkins Build", commitSha)
- sh 'rm -rf out'
- sh 'mkdir out'
- }
-
- withBuildStatus("Clean build intermediates") {
- sh './gradlew clean'
- }
-
- withBuildStatus("Compile release kotlin") {
- sh './gradlew compileReleaseKotlin'
- }
-
- withBuildStatus("Run release unit test") {
- sh './gradlew testReleaseUnitTest'
- }
-
- forEachABI("Assemble release") { String abi ->
- withEnv(["BUILD_ABI=$abi", "BUILD_TIMESTAMP=$buildTimestamp"]) {
- sh './gradlew assembleRelease'
- sh 'mv app/build/outputs/apk/release/*.apk out/'
- }
- sh 'mv app/build/outputs/apk/build-metadata.json out/'
- }
-
- withBuildStatus("Sign and archive apks") {
- signAndroidApks(
- keyStoreId: 'fcitx5-android-sign-key',
- apksToSign: 'out/*-unsigned.apk',
- archiveSignedApks: true,
- )
- archiveArtifacts(
- artifacts: 'out/build-metadata.json',
- fingerprint: true
- )
- }
-
- stage("Post build (success)") {
- setBuildStatus("Successful", "success", "Jenkins Build", commitSha)
- sendMessageToTelegramGroup("[${JOB_NAME}-${BUILD_NUMBER}](${BUILD_URL}) succeeded")
- }
-
- } catch (Exception e) {
- stage("Post build (failure)") {
- setBuildStatus("Failed", "failure", "Jenkins Build", commitSha)
- sendMessageToTelegramGroup("[${JOB_NAME}-${BUILD_NUMBER}](${BUILD_URL}) failed")
- throw e
- }
- }
-
- }
- }
-}
diff --git a/README.md b/README.md
index 30e82190c..85f9bba0f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# fcitx5-android
-An attempt to run fcitx5 on Android.
+[Fcitx5](https://github.com/fcitx/fcitx5) input method framework and engines ported to Android.
## Download
@@ -12,36 +12,46 @@ Jenkins: [](https://github.com/fcitx5-android/fcitx5-android/releases)
-[
](https://f-droid.org/packages/org.fcitx.fcitx5.android)
-
-[
](https://play.google.com/store/apps/details?id=org.fcitx.fcitx5.android&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
+[
](https://f-droid.org/packages/org.fcitx.fcitx5.android)
+[
](https://play.google.com/store/apps/details?id=org.fcitx.fcitx5.android)
## Project status
-### Implemented
+### Supported Languages
+
+- English (with spell check)
+- Chinese
+ - Pinyin, Shuangpin, Wubi, Cangjie and custom tables (built-in, powered by [fcitx5-chinese-addons](https://github.com/fcitx/fcitx5-chinese-addons))
+ - Zhuyin/Bopomofo (via [Chewing Plugin](./plugin/chewing))
+ - Jyutping (via [Jyutping Plugin](./plugin/jyutping/), powered by [libime-jyutping](https://github.com/fcitx/libime-jyutping))
+- Vietnamese (via [UniKey Plugin](./plugin/unikey), supports Telex, VNI and VIQR)
+- Japanese (via [Anthy Plugin](./plugin/anthy))
+- 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
- Virtual Keyboard (layout not customizable yet)
- 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
+- Plugin System for loading addons from other installed apk
-### Work in progress
+### Planned Features
- Customizable keyboard layout
-- More input methods
+- More input methods (via plugin)
## Screenshots
|拼音, Material Light theme, key border enabled|自然码双拼, Pixel Dark theme, key border disabled|
|:-:|:-:|
-|
|
|
+|
|
|
|Emoji picker, Pixel Light theme, key border enabled|Symbol picker, Material Dark theme, key border disabled|
|:-:|:-:|
@@ -53,33 +63,54 @@ 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.)
### How to set up development environment
+
+Prerequisites for Windows
+
+- Enable [Developer Mode](https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development) so that symlinks can be created without administrator privilege.
+
+- Enable symlink support for `git`:
+
+ ```shell
+ git config --global core.symlinks true
+ ```
+
+
+
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
```
-Install extra-cmake-modules from your distribution software repository:
+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
+
# For Debian/Ubuntu
sudo apt install extra-cmake-modules gettext
+
+# For macOS
+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
```
Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via SDK Manager in Android Studio:
@@ -87,6 +118,9 @@ Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via
Detailed steps (screenshots)
+**Note:** These screenshots are for references and the versions in them may be out of date.
+The current recommended versions are recorded in [Versions.kt](build-logic/convention/src/main/kotlin/Versions.kt) file.
+


@@ -99,6 +133,16 @@ Install Android SDK Platform, Android SDK Build-Tools, Android NDK and cmake via
+### Trouble-shooting
+
+- Android Studio indexing takes forever to complete and cosumes a lot of memory.
+
+ 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." 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.
+
## Nix
Appropriate Android SDK with NDK is available in the development shell. The `gradlew` should work out-of-the-box, so you can install the app to your phone with `./gradlew installDebug` after applying the patch mentioned above. For development, you may want to install the unstable version of Android Studio, and point the project SDK path to `$ANDROID_SDK_ROOT` defined in the shell. Notice that Android Studio may generate wrong `local.properties` which sets the SDK location to `~/Android/SDK` (installed by SDK Manager). In such case, you need specify `sdk.dir` as the project SDK in that file manually, in case Android Studio sticks to the wrong global SDK.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index dc4924306..1f435a29b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,110 +1,40 @@
-@file:Suppress("UnstableApiUsage")
-
-import android.databinding.tool.ext.capitalizeUS
-import com.android.build.gradle.internal.tasks.factory.dependsOn
-import com.google.common.hash.Hashing
-import com.google.common.io.Files
-import groovy.json.JsonOutput
-import groovy.json.JsonSlurper
-import java.io.ByteArrayOutputStream
-import java.nio.charset.Charset
-
-fun exec(cmd: String): String = ByteArrayOutputStream().use {
- project.exec {
- commandLine = cmd.split(" ")
- standardOutput = it
- }
- it.toString().trim()
-}
-
plugins {
- id("com.android.application")
- kotlin("android")
- kotlin("plugin.parcelize")
- kotlin("plugin.serialization") version "1.8.10"
- id("com.google.devtools.ksp") version "1.8.10-1.0.9"
- id("com.mikepenz.aboutlibraries.plugin")
-}
-
-val dataDescriptorName = "descriptor.json"
-
-// NOTE: increase this value to bump version code
-val baseVersionCode = 3
-
-fun calculateVersionCode(abi: String): Int {
- val abiId = when (abi) {
- "armeabi-v7a" -> 1
- "arm64-v8a" -> 2
- "x86" -> 3
- "x86_64" -> 4
- else -> 0
- }
- return baseVersionCode * 10 + abiId
-}
-
-fun envOrDefault(env: String, default: () -> String): String {
- val v = System.getenv(env)
- return if (v.isNullOrBlank()) default() else v
-}
-
-fun propertyOrDefault(prop: String, default: () -> String): String {
- return try {
- project.property(prop)!!.toString()
- } catch (e: Exception) {
- default()
- }
-}
-
-val buildABI = envOrDefault("BUILD_ABI") {
- propertyOrDefault("buildABI") {
-// "armeabi-v7a"
- "arm64-v8a"
-// "x86"
-// "x86_64"
- }
-}
-
-val buildVersionName = envOrDefault("BUILD_VERSION_NAME") {
- propertyOrDefault("buildVersionName") {
- exec("git describe --tags --long --always")
- }
-}
-
-val buildCommitHash = envOrDefault("BUILD_COMMIT_HASH") {
- propertyOrDefault("buildCommitHash") {
- exec("git rev-parse HEAD")
- }
-}
-
-val buildTimestamp = envOrDefault("BUILD_TIMESTAMP") {
- propertyOrDefault("buildTimestamp") {
- System.currentTimeMillis().toString()
- }
+ id("org.fcitx.fcitx5.android.app-convention")
+ id("org.fcitx.fcitx5.android.native-app-convention")
+ id("org.fcitx.fcitx5.android.build-metadata")
+ id("org.fcitx.fcitx5.android.data-descriptor")
+ id("org.fcitx.fcitx5.android.fcitx-component")
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ksp)
}
android {
namespace = "org.fcitx.fcitx5.android"
- compileSdk = 33
- buildToolsVersion = "33.0.0"
- ndkVersion = System.getenv("NDK_VERSION") ?: "25.0.8775105"
defaultConfig {
applicationId = "org.fcitx.fcitx5.android"
- minSdk = 23
- targetSdk = 33
- versionCode = calculateVersionCode(buildABI)
- versionName = buildVersionName
- setProperty("archivesBaseName", "$applicationId-$buildVersionName")
- buildConfigField("String", "BUILD_GIT_HASH", "\"${buildCommitHash}\"")
- buildConfigField("long", "BUILD_TIME", buildTimestamp)
- buildConfigField("String", "DATA_DESCRIPTOR_NAME", "\"${dataDescriptorName}\"")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ @Suppress("UnstableApiUsage")
+ externalNativeBuild {
+ cmake {
+ targets(
+ // jni
+ "native-lib",
+ // copy fcitx5 built-in addon libraries
+ "copy-fcitx5-modules",
+ // android specific modules
+ "androidfrontend",
+ "androidkeyboard",
+ "androidnotification"
+ )
+ }
+ }
}
buildTypes {
- getByName("release") {
- isMinifyEnabled = true
- isShrinkResources = true
+ release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -114,325 +44,108 @@ android {
resValue("mipmap", "app_icon_round", "@mipmap/ic_launcher_round")
resValue("string", "app_name", "@string/app_name_release")
}
- getByName("debug") {
- applicationIdSuffix = ".debug"
-
+ debug {
resValue("mipmap", "app_icon", "@mipmap/ic_launcher_debug")
resValue("mipmap", "app_icon_round", "@mipmap/ic_launcher_round_debug")
resValue("string", "app_name", "@string/app_name_debug")
}
}
- splits {
- abi {
- isEnable = true
- reset()
- include(buildABI)
- isUniversalApk = false
- }
- }
-
- externalNativeBuild {
- cmake {
- version = "3.22.1"
- path("src/main/cpp/CMakeLists.txt")
- }
- }
-
buildFeatures {
viewBinding = true
}
- compileOptions {
- isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-
- kotlinOptions {
- jvmTarget = "11"
- }
-
- packagingOptions {
- jniLibs {
- useLegacyPackaging = true
- }
+ androidResources {
+ @Suppress("UnstableApiUsage")
+ generateLocaleConfig = true
}
}
kotlin {
sourceSets.configureEach {
- kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/")
+ kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/$name/kotlin"))
}
}
-aboutLibraries {
- // configPath = "app/licenses"
- excludeFields = arrayOf("generated", "developers", "organization", "scm", "funding", "content")
- fetchRemoteLicense = false
- fetchRemoteFunding = false
- includePlatform = false
-}
-
-kotlin.sourceSets.all {
- languageSettings.optIn("kotlin.RequiresOptIn")
-}
-
-val generateBuildMetadata by tasks.register("generateBuildMetadata") {
- doLast {
- val outputDir = file("build/outputs/apk")
- outputDir.mkdirs()
- val metadataFile = outputDir.resolve("build-metadata.json")
- metadataFile.writeText(
- JsonOutput.prettyPrint(
- JsonOutput.toJson(
- mapOf(
- "versionName" to buildVersionName,
- "commitHash" to buildCommitHash,
- "timestamp" to buildTimestamp
- )
- )
- )
- )
- }
-
- dependsOn(tasks.find { it.name == "compileDebugKotlin" || it.name == "compileReleaseKotlin" })
-}
-
-// This task should have depended on buildCMakeABITask
-val installFcitxComponent by tasks.register("installFcitxComponent")
-
-val generateDataDescriptor by tasks.register("generateDataDescriptor") {
- inputDir.set(file("src/main/assets"))
- outputFile.set(file("src/main/assets/${dataDescriptorName}"))
- dependsOn(installFcitxComponent)
-}
-/**
- * Note *Graph*
- * Tasks registered by [installFcitxComponent] implicitly depend .cxx dir to install generated files.
- * Since the native task `buildCMake$Variant$ABI` depend on the current variant and ABI,
- * we should have registered [installFcitxComponent] tasks for the cartesian product of $Variant and $ABI.
- * However, this would be way more tedious, as the build variant and ABI do not affect components we are going to install.
- * The essential cause of this situation is that it's impossible for gradle to handle dynamic dependencies,
- * where once the build graph was evaluated, no dependencies can be changed. So a trick is used here: when the task graph
- * is evaluated, we look into it to find out the name of the native task which will be executed, and then store its output
- * path in global variable. Tasks in [installFcitxComponent] are using the output path of the native task WITHOUT explicitly
- * depending on it.
- */
-project.gradle.taskGraph.whenReady {
- val buildCMakeABITask = allTasks
- .find { it.name.startsWith("buildCMakeDebug[") || it.name.startsWith("buildCMakeRelWithDebInfo[") }
- if (buildCMakeABITask != null) {
- val cmakeDir = buildCMakeABITask.outputs.files.first().parentFile
- ext.set("cmakeDir", cmakeDir)
+fcitxComponent {
+ 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
}
-android.applicationVariants.all {
- val variantName = name.capitalizeUS()
- tasks.findByName("merge${variantName}Assets")?.dependsOn(generateDataDescriptor)
- tasks.findByName("lintAnalyze${variantName}")?.dependsOn(generateDataDescriptor)
- tasks.findByName("lintReport${variantName}")?.dependsOn(generateDataDescriptor)
- tasks.findByName("lintVitalAnalyzeRelease")?.dependsOn(generateDataDescriptor)
- tasks.findByName("assemble${variantName}")?.dependsOn(generateBuildMetadata)
-}
-
-/**
- * DO NOT run these tasks manually. See Note *Graph* for details.
- */
-fun installFcitxComponent(targetName: String, componentName: String, destDir: File) {
- // Deliberately use doLast to wait ext be set
- val build by tasks.register("buildFcitx${componentName.capitalizeUS()}") {
- doLast {
- try {
- exec {
- workingDir = ext.get("cmakeDir") as File
- commandLine("cmake", "--build", ".", "--target", targetName)
- }
- } catch (e: Exception) {
- logger.log(LogLevel.ERROR, "Failed to build target $targetName: ${e.message}")
- logger.log(LogLevel.ERROR, "Did you run this task independently?")
- throw e
- }
- }
-
- // make sure that this task runs after than the native task
- mustRunAfter("buildCMakeDebug[$buildABI]")
- mustRunAfter("buildCMakeRelWithDebInfo[$buildABI]")
- }
-
- val install by tasks.register("installFcitx${componentName.capitalizeUS()}") {
- doLast {
- try {
- exec {
- environment("DESTDIR", destDir.absolutePath)
- workingDir = ext.get("cmakeDir") as File
- commandLine("cmake", "--install", ".", "--component", componentName)
- }
- } catch (e: Exception) {
- logger.log(
- LogLevel.ERROR,
- "Failed to install component $componentName: ${e.message}"
- )
- logger.log(LogLevel.ERROR, "Did you run this task independently?")
- throw e
- }
- }
-
- dependsOn(build)
- }
-
- installFcitxComponent.dependsOn(install)
-}
-
-installFcitxComponent("generate-desktop-file", "config", file("src/main/assets"))
-installFcitxComponent("translation-file", "translation", file("src/main/assets"))
-
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
-tasks.register("cleanGeneratedAssets") {
- delete(file("src/main/assets/usr/share/locale"))
- // delete all non symlink dirs
- delete(file("src/main/assets/usr/share/fcitx5").listFiles()?.filter {
- // https://stackoverflow.com/questions/813710/java-1-6-determine-symbolic-links
- File(it.parentFile.canonicalFile, it.name).let { s ->
- s.canonicalFile == s.absoluteFile
- }
- })
- delete(file("src/main/assets/${dataDescriptorName}"))
-}.also { tasks.clean.dependsOn(it) }
-
-tasks.register("cleanCxxIntermediates") {
- delete(file(".cxx"))
-}.also { tasks.clean.dependsOn(it) }
-
dependencies {
- implementation("androidx.autofill:autofill:1.1.0")
- implementation("org.ini4j:ini4j:0.5.4")
- implementation("com.google.android.material:material:1.8.0")
ksp(project(":codegen"))
- coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2")
- implementation("io.arrow-kt:arrow-core:1.1.5")
- implementation("androidx.activity:activity-ktx:1.6.1")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
- implementation("com.github.CanHub:Android-Image-Cropper:4.2.1")
- implementation("com.google.android.flexbox:flexbox:3.0.0")
- implementation("org.mechdancer:dependency:0.1.2")
- val roomVersion = "2.5.0"
- implementation("androidx.room:room-runtime:$roomVersion")
- implementation("androidx.room:room-paging:$roomVersion")
- ksp("androidx.room:room-compiler:$roomVersion")
- implementation("androidx.room:room-ktx:$roomVersion")
- implementation("com.jakewharton.timber:timber:5.0.1")
- implementation("androidx.core:core-ktx:1.9.0")
- implementation("androidx.appcompat:appcompat:1.6.1")
- implementation("androidx.constraintlayout:constraintlayout:2.1.4")
- val lifecycleVersion = "2.6.0"
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
- implementation("androidx.lifecycle:lifecycle-service:$lifecycleVersion")
- implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
- implementation("androidx.preference:preference-ktx:1.2.0")
- implementation("androidx.recyclerview:recyclerview:1.3.0")
- implementation("androidx.viewpager2:viewpager2:1.0.0")
- implementation("androidx.paging:paging-runtime-ktx:3.1.1")
- val navVersion = "2.5.3"
- implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
- implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
- val splittiesVersion = "3.0.0"
- implementation("com.louiscad.splitties:splitties-bitflags:$splittiesVersion")
- implementation("com.louiscad.splitties:splitties-systemservices:$splittiesVersion")
- implementation("com.louiscad.splitties:splitties-views-dsl:$splittiesVersion")
- implementation("com.louiscad.splitties:splitties-views-dsl-constraintlayout:$splittiesVersion")
- implementation("com.louiscad.splitties:splitties-views-dsl-recyclerview:$splittiesVersion")
- implementation("com.louiscad.splitties:splitties-views-recyclerview:$splittiesVersion")
- implementation("com.louiscad.splitties:splitties-views-dsl-material:$splittiesVersion")
- implementation("com.mikepenz:aboutlibraries-core:10.6.1")
- testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test:runner:1.5.2")
- androidTestImplementation("androidx.test:rules:1.5.0")
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.0")
- androidTestImplementation("junit:junit:4.13.2")
-}
-
-abstract class DataDescriptorTask : DefaultTask() {
- @get:Incremental
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- @get:InputDirectory
- abstract val inputDir: DirectoryProperty
-
- @get:OutputFile
- abstract val outputFile: RegularFileProperty
-
- private val file by lazy { outputFile.get().asFile }
-
- private fun serialize(map: Map) {
- file.deleteOnExit()
- file.writeText(
- JsonOutput.prettyPrint(
- JsonOutput.toJson(
- mapOf(
- "sha256" to Hashing.sha256()
- .hashString(
- map.entries.joinToString { it.key + it.value },
- Charset.defaultCharset()
- )
- .toString(),
- "files" to map
- )
- )
- )
- )
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun deserialize(): Map =
- ((JsonSlurper().parseText(file.readText()) as Map))["files"] as Map
-
- companion object {
- fun sha256(file: File): String =
- Files.asByteSource(file).hash(Hashing.sha256()).toString()
- }
-
- @TaskAction
- fun execute(inputChanges: InputChanges) {
- val map =
- file.exists()
- .takeIf { it }
- ?.runCatching {
- deserialize()
- // remove all old dirs
- .filterValues { it.isNotBlank() }
- .toMutableMap()
- }
- ?.getOrNull()
- ?: mutableMapOf()
-
- fun File.allParents(): List =
- if (parentFile == null || parentFile.path in map)
- listOf()
- else
- listOf(parentFile) + parentFile.allParents()
- inputChanges.getFileChanges(inputDir).forEach { change ->
- if (change.file.name == file.name)
- return@forEach
- logger.log(LogLevel.DEBUG, "${change.changeType}: ${change.normalizedPath}")
- val relativeFile = change.file.relativeTo(file.parentFile)
- val key = relativeFile.path
- if (change.changeType == ChangeType.REMOVED) {
- map.remove(key)
- } else {
- map[key] = sha256(change.file)
- }
- }
- // calculate dirs
- inputDir.asFileTree.forEach {
- it.relativeTo(file.parentFile).allParents().forEach { p ->
- map[p.path] = ""
- }
- }
- serialize(map.toSortedMap())
+ implementation(project(":lib:fcitx5"))
+ implementation(project(":lib:fcitx5-lua"))
+ implementation(project(":lib:libime"))
+ implementation(project(":lib:fcitx5-chinese-addons"))
+ implementation(project(":lib:common"))
+ implementation(libs.kotlinx.coroutines)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.autofill)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.coordinatorlayout)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel)
+ implementation(libs.androidx.lifecycle.livedata)
+ implementation(libs.androidx.lifecycle.runtime)
+ implementation(libs.androidx.lifecycle.common)
+ implementation(libs.androidx.lifecycle.service)
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.ui)
+ implementation(libs.androidx.paging)
+ implementation(libs.androidx.preference)
+ implementation(libs.androidx.recyclerview)
+ ksp(libs.androidx.room.compiler)
+ 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.core)
+ implementation(libs.arrow.functions)
+ implementation(libs.imagecropper)
+ implementation(libs.flexbox)
+ implementation(libs.dependency)
+ implementation(libs.timber)
+ implementation(libs.splitties.bitflags)
+ implementation(libs.splitties.dimensions)
+ implementation(libs.splitties.resources)
+ implementation(libs.splitties.views.dsl)
+ implementation(libs.splitties.views.dsl.appcompat)
+ implementation(libs.splitties.views.dsl.constraintlayout)
+ implementation(libs.splitties.views.dsl.coordinatorlayout)
+ implementation(libs.splitties.views.dsl.recyclerview)
+ implementation(libs.splitties.views.recyclerview)
+ implementation(libs.aboutlibraries.core)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.test.runner)
+ androidTestImplementation(libs.androidx.test.rules)
+ androidTestImplementation(libs.androidx.lifecycle.testing)
+ androidTestImplementation(libs.junit)
+}
+
+configurations {
+ all {
+ // remove Baseline Profile Installer or whatever it is...
+ exclude(group = "androidx.profileinstaller", module = "profileinstaller")
+ // remove unwanted splitties libraries...
+ exclude(group = "com.louiscad.splitties", module = "splitties-appctx")
+ exclude(group = "com.louiscad.splitties", module = "splitties-systemservices")
}
}
diff --git a/app/licenses/libraries/boost.json b/app/licenses/libraries/boost.json
index 7848e8ef2..cebc09b62 100644
--- a/app/licenses/libraries/boost.json
+++ b/app/licenses/libraries/boost.json
@@ -1,6 +1,6 @@
{
"uniqueId": "boostorg/boost",
- "artifactVersion": "1.80.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 f5799b00a..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.0.17",
+ "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 36e42980a..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.10",
+ "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 0f51e4eb9..f20f85043 100644
--- a/app/licenses/libraries/fcitx5.json
+++ b/app/licenses/libraries/fcitx5.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/fcitx5",
- "artifactVersion": "5.0.23",
+ "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 ec190743e..07f2dbdc1 100644
--- a/app/licenses/libraries/fmt.json
+++ b/app/licenses/libraries/fmt.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fmtlib/fmt",
- "artifactVersion": "8.1.1",
+ "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 df66a50f4..18507f632 100644
--- a/app/licenses/libraries/libime.json
+++ b/app/licenses/libraries/libime.json
@@ -1,6 +1,6 @@
{
"uniqueId": "fcitx/libime",
- "artifactVersion": "1.0.17",
+ "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 e75a093c1..11c9ef4c7 100644
--- a/app/licenses/libraries/lua.json
+++ b/app/licenses/libraries/lua.json
@@ -1,6 +1,6 @@
{
"uniqueId": "lua/lua",
- "artifactVersion": "5.4.4",
+ "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 d1d58905d..ed4109ee0 100644
--- a/app/licenses/libraries/opencc.json
+++ b/app/licenses/libraries/opencc.json
@@ -1,6 +1,6 @@
{
"uniqueId": "BYVoid/OpenCC",
- "artifactVersion": "1.1.6",
+ "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
new file mode 100644
index 000000000..7c2ffc2eb
--- /dev/null
+++ b/app/org.fcitx.fcitx5.android.yml
@@ -0,0 +1,55 @@
+Categories:
+ - System
+License: LGPL-2.1-only
+AuthorName: Fcitx5 for Android Contributors
+WebSite: https://fcitx5-android.github.io
+SourceCode: https://github.com/fcitx5-android/fcitx5-android
+IssueTracker: https://github.com/fcitx5-android/fcitx5-android/issues
+Translation: https://explore.transifex.com/fcitx/fcitx5-android
+
+AutoName: Fcitx5 for Android
+
+RepoType: git
+Repo: https://github.com/fcitx5-android/fcitx5-android
+
+Builds:
+ - versionName: placeholder
+ versionCode: placeholder
+ commit: placeholder
+ subdir: app
+ submodules: true
+ sudo:
+ - apt-get update
+ - 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
+ srclibs:
+ - fcitx5-android-prebuilder@master
+ rm:
+ - lib/fcitx5/src/main/cpp/prebuilt
+ prebuild:
+ - 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
+ - sed -i -e 's|https://maven.pkg.github.com|https://jitpack.io|g' ../lib/*/build.gradle.kts
+ scanignore:
+ - lib/fcitx5/src/main/cpp/fcitx5/src/modules/unicode/charselectdata
+ scandelete:
+ - build-logic/convention/build
+ build:
+ - pushd $$fcitx5-android-prebuilder$$
+ - 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: 28.0.13004108
+ gradleprops:
+ - buildABI=%abi
+ - buildTimestamp=%ts
+
+AllowedAPKSigningKeys: e4db1e9edff13629d07de4bbf8165fe9bd8557ab55092672da8e40dbe484ecd7
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index fe9fa07b3..d154724d5 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -10,60 +10,31 @@
# Keep JNI interface
-keep class org.fcitx.fcitx5.android.core.* { *; }
+-keep class org.fcitx.fcitx5.android.data.pinyin.customphrase.PinyinCustomPhrase {
+ public (...);
+}
+
+# Keep dependency magic
+-keep class ** extends org.mechdancer.dependency.Component {
+ int hashCode();
+ boolean equals(java.lang.Object);
+}
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+# remove kotlin null checks
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+ 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
# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
+-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
--dontwarn java.awt.*
--keep class com.sun.jna.* { *; }
--keepclassmembers class * extends com.sun.jna.* { public *; }
-# Keep `Companion` object fields of serializable classes.
-# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
--if @kotlinx.serialization.Serializable class **
--keepclassmembers class <1> {
- static <1>$Companion Companion;
-}
-
-# Keep `serializer()` on companion objects (both default and named) of serializable classes.
--if @kotlinx.serialization.Serializable class ** {
- static **$* *;
-}
--keepclassmembers class <2>$<3> {
- kotlinx.serialization.KSerializer serializer(...);
-}
-
-# Keep `INSTANCE.serializer()` of serializable objects.
--if @kotlinx.serialization.Serializable class ** {
- public static ** INSTANCE;
-}
--keepclassmembers class <1> {
- public static <1> INSTANCE;
- kotlinx.serialization.KSerializer serializer(...);
-}
-
-# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
--keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-
-# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
-# If you have any, uncomment and replace classes with those containing named companion objects.
-#-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
-#-if @kotlinx.serialization.Serializable class
-#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
-#com.example.myapplication.HasNamedCompanion2
-#{
-# static **$* *;
-#}
-#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
-# static <1>$$serializer INSTANCE;
-#}
\ No newline at end of file
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/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt b/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt
index 2defe76f7..14bb9601a 100644
--- a/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt
+++ b/app/src/androidTest/java/org/fcitx/fcitx5/android/FcitxTest.kt
@@ -1,57 +1,72 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
package org.fcitx.fcitx5.android
-import android.util.Log
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.platform.app.InstrumentationRegistry
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.MainScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.runBlocking
import org.fcitx.fcitx5.android.core.Fcitx
import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.core.RawConfig
-import org.junit.*
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Assert
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import timber.log.Timber
class FcitxTest {
private companion object {
lateinit var fcitx: Fcitx
- val lifeCycleOwner = TestLifecycleOwner()
val fcitxEventChannel = Channel>(capacity = Channel.CONFLATED)
-
- fun log(str: String) = Log.d("UnitTest", str)
+ val scope = MainScope()
@BeforeClass
@JvmStatic
fun setup() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
fcitx = Fcitx(context)
- lifeCycleOwner.lifecycle.addObserver(fcitx)
- lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
// forward to our channel for point to point consuming
fcitx.eventFlow
.onEach { fcitxEventChannel.send(it) }
- .launchIn(GlobalScope)
+ .launchIn(scope)
+ fcitx.start()
// wait fcitx started
- runBlocking { receiveFirst() }
- fcitx.setEnabledIme(arrayOf("pinyin"))
- fcitx.globalConfig = RawConfig(arrayOf(
- RawConfig("Behavior", arrayOf(
- RawConfig("ShowInputMethodInformation", false)
- ))
- ))
+ runBlocking {
+ receiveFirst()
+ fcitx.setEnabledIme(arrayOf("pinyin"))
+ fcitx.setGlobalConfig(
+ RawConfig(
+ arrayOf(
+ RawConfig(
+ "Behavior", arrayOf(
+ RawConfig("ShowInputMethodInformation", false)
+ )
+ )
+ )
+ )
+ )
+ }
}
@AfterClass
@JvmStatic
fun cleanup() {
- log("cleanup")
- lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ fcitx.stop()
}
private suspend fun sendString(str: String) {
@@ -61,7 +76,7 @@ class FcitxTest {
}
}
- private suspend inline fun > receiveFirst(): T? =
+ private suspend inline fun > receiveFirst(): T? =
fcitxEventChannel.receiveAsFlow().mapNotNull { it as? T }.firstOrNull()
private suspend fun receiveFirstCandidateList() =
@@ -80,12 +95,12 @@ class FcitxTest {
private var enabledIme: List = listOf()
@Before
- fun saveEnabledIME() {
+ fun saveEnabledIME() = runBlocking {
enabledIme = fcitx.enabledIme().map { it.uniqueName }
}
@After
- fun restoreEnabledIME() {
+ fun restoreEnabledIME() = runBlocking {
fcitx.setEnabledIme(enabledIme.toTypedArray())
}
@@ -96,7 +111,7 @@ class FcitxTest {
val expected = "你好"
fcitx.select(0)
val commitString = receiveFirstCommitString()?.data
- log("commitString is $commitString")
+ Timber.i("commitString is $commitString")
Assert.assertEquals(expected, commitString)
fcitx.reset()
}
@@ -108,7 +123,7 @@ class FcitxTest {
val expected = "你好世界"
fcitx.select(0)
val commitString = receiveFirstCommitString()?.data
- log("commitString is $commitString")
+ Timber.i("commitString is $commitString")
Assert.assertEquals(expected, commitString)
fcitx.reset()
}
@@ -116,19 +131,19 @@ class FcitxTest {
@Test
fun testInputPanelStatus(): Unit = runBlocking {
fcitx.reset()
- log("after first reset: ${fcitx.isEmpty()}")
+ Timber.i("after first reset: ${fcitx.isEmpty()}")
Assert.assertEquals(true, fcitx.isEmpty())
fcitx.sendKey('a')
do {
val list = receiveFirstCandidateList()
- } while (list!!.data.isEmpty())
- log("after sending 'a': ${fcitx.isEmpty()}")
+ } while (list!!.data.candidates.isNotEmpty())
+ Timber.i("after sending 'a': ${fcitx.isEmpty()}")
Assert.assertEquals(false, fcitx.isEmpty())
fcitx.reset()
do {
val list = receiveFirstCandidateList()
- } while (list!!.data.isNotEmpty())
- log("after second reset: ${fcitx.isEmpty()}")
+ } while (list!!.data.candidates.isNotEmpty())
+ Timber.i("after second reset: ${fcitx.isEmpty()}")
Assert.assertEquals(true, fcitx.isEmpty())
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7f5ce7c64..fcc089b60 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,9 +2,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:exported="false"
+ android:label="@string/edit_theme" />
+ android:name=".ui.main.CropImageActivity"
+ android:exported="false" />
+
@@ -67,12 +103,17 @@
+
+
+
+
@@ -94,6 +136,16 @@
android:resource="@xml/input_method" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/pinyin b/app/src/main/assets/usr/share/fcitx5/pinyin
deleted file mode 120000
index 28bb57a4b..000000000
--- a/app/src/main/assets/usr/share/fcitx5/pinyin
+++ /dev/null
@@ -1 +0,0 @@
-../../../../cpp/prebuilt/chinese-addons-data/pinyin
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/pinyinhelper b/app/src/main/assets/usr/share/fcitx5/pinyinhelper
deleted file mode 120000
index 6cf9ad428..000000000
--- a/app/src/main/assets/usr/share/fcitx5/pinyinhelper
+++ /dev/null
@@ -1 +0,0 @@
-../../../../cpp/prebuilt/chinese-addons-data/pinyinhelper
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/spell b/app/src/main/assets/usr/share/fcitx5/spell
deleted file mode 120000
index d89038d69..000000000
--- a/app/src/main/assets/usr/share/fcitx5/spell
+++ /dev/null
@@ -1 +0,0 @@
-../../../../cpp/prebuilt/spell-dict
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/fcitx5/table b/app/src/main/assets/usr/share/fcitx5/table
deleted file mode 120000
index 78abf2508..000000000
--- a/app/src/main/assets/usr/share/fcitx5/table
+++ /dev/null
@@ -1 +0,0 @@
-../../../../cpp/prebuilt/libime/table
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/libime b/app/src/main/assets/usr/share/libime
deleted file mode 120000
index 6680b3d89..000000000
--- a/app/src/main/assets/usr/share/libime
+++ /dev/null
@@ -1 +0,0 @@
-../../../cpp/prebuilt/libime/data
\ No newline at end of file
diff --git a/app/src/main/assets/usr/share/opencc b/app/src/main/assets/usr/share/opencc
deleted file mode 120000
index 7acee9112..000000000
--- a/app/src/main/assets/usr/share/opencc
+++ /dev/null
@@ -1 +0,0 @@
-../../../cpp/prebuilt/opencc/data
\ No newline at end of file
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 78e175890..9777b761f 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -1,114 +1,90 @@
cmake_minimum_required(VERSION 3.18)
-project(fcitx5-android VERSION 0.0.5)
-
-set(CMAKE_CXX_STANDARD 17)
+project(fcitx5-android VERSION ${VERSION_NAME})
# For reproducible build
add_link_options("LINKER:--hash-style=gnu,--build-id=none")
-set(CMAKE_INSTALL_PREFIX /usr)
-# fcitx-utils/standardpath.cpp uses FCITX_INSTALL_LIBDATADIR,
-# which is CMAKE_INSTALL_LIBDATADIR's absolute path
-set(CMAKE_INSTALL_LIBDATADIR /usr/lib)
-set(FCITX_INSTALL_PKGDATADIR /usr/share/fcitx5)
-set(FCITX_INSTALL_LOCALEDIR /usr/share/locale)
-set(LIBIME_INSTALL_PKGDATADIR table)
-
-set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
-# extra-cmake-modules
-if(DEFINED ENV{ECM_DIR})
- set(ECM_DIR $ENV{ECM_DIR})
-else()
- set(ECM_DIR /usr/share/ECM/cmake)
-endif()
+# prefab dependency
+find_package(fcitx5 REQUIRED CONFIG)
+get_target_property(FCITX5_CMAKE_MODULES fcitx5::cmake INTERFACE_INCLUDE_DIRECTORIES)
+set(CMAKE_MODULE_PATH ${FCITX5_CMAKE_MODULES} ${CMAKE_MODULE_PATH})
-set(PREBUILT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/prebuilt")
+find_package(Fcitx5Core MODULE)
+find_package(Fcitx5Module MODULE)
-# prebuilt fmt
-set(fmt_DIR "${PREBUILT_DIR}/fmt/${ANDROID_ABI}/lib/cmake/fmt")
-find_package(fmt)
+find_package(libime REQUIRED CONFIG)
+get_target_property(LIBIME_CMAKE_MODULES libime::cmake INTERFACE_INCLUDE_DIRECTORIES)
+set(CMAKE_MODULE_PATH ${LIBIME_CMAKE_MODULES} ${CMAKE_MODULE_PATH})
-# prebuilt libintl-lite
-set(LibIntl_DIR "${PREBUILT_DIR}/libintl-lite/${ANDROID_ABI}/lib/cmake")
-find_package(LibIntl)
+find_package(LibIMECore MODULE)
+find_package(LibIMEPinyin MODULE)
+find_package(LibIMETable MODULE)
-# prebuilt libevent
-set(Libevent_DIR "${PREBUILT_DIR}/libevent/${ANDROID_ABI}/lib/cmake/libevent")
-find_package(Libevent)
+find_package(fcitx5-lua REQUIRED CONFIG)
+find_package(fcitx5-chinese-addons REQUIRED CONFIG)
-option(ENABLE_TEST "" OFF)
-option(ENABLE_COVERAGE "" OFF)
-option(ENABLE_ENCHANT "" OFF)
-option(ENABLE_X11 "" OFF)
-option(ENABLE_WAYLAND "" OFF)
-option(ENABLE_DBUS "" OFF)
-option(ENABLE_DOC "" OFF)
-option(ENABLE_SERVER "" OFF)
-option(ENABLE_KEYBOARD "" OFF)
-option(USE_SYSTEMD "" OFF)
-option(ENABLE_XDGAUTOSTART "" OFF)
-option(ENABLE_EMOJI "" OFF)
-option(ENABLE_LIBUUID "" OFF)
-add_subdirectory(fcitx5)
+include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cmake")
-include(fcitx5/src/lib/fcitx-utils/Fcitx5Macros.cmake)
add_subdirectory(po)
add_subdirectory(androidfrontend)
add_subdirectory(androidkeyboard)
+add_subdirectory(androidnotification)
-# prebuilt boost
-set(BOOST_VERSION "1.80.0")
-set(BOOST_MODULES headers filesystem atomic iostreams regex)
-set(BOOST_ROOT "${PREBUILT_DIR}/boost/${ANDROID_ABI}")
-set(Boost_DIR "${BOOST_ROOT}/lib/cmake/Boost-${BOOST_VERSION}")
-foreach(mod IN LISTS BOOST_MODULES)
- set("boost_${mod}_DIR" "${BOOST_ROOT}/lib/cmake/boost_${mod}-${BOOST_VERSION}")
-endforeach()
-
-option(ENABLE_TEST "" OFF)
-set(_Fcitx5Macro_SELF_DIR ${CMAKE_CURRENT_SOURCE_DIR}/fcitx5/src/lib/fcitx-utils)
-add_subdirectory(libime)
-# kenlm/util/exception.hh uses __FILE__ macro
-target_compile_options(kenlm PRIVATE "-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.")
+# prebuilt libuv
+set(libuv_DIR "${PREBUILT_DIR}/libuv/${ANDROID_ABI}/lib/cmake/libuv")
+find_package(libuv)
-# prebuilt lua
-include("${PREBUILT_DIR}/lua/${ANDROID_ABI}/lib/cmake/LuaConfig.cmake")
-
-# we are using static linking
-option(USE_DLOPEN "" OFF)
-add_subdirectory(fcitx5-lua)
-
-# prebuilt opencc
-set(OpenCC_DIR "${PREBUILT_DIR}/opencc/${ANDROID_ABI}/lib/cmake/opencc")
-find_package(OpenCC)
-
-option(ENABLE_TEST "" OFF)
-option(ENABLE_GUI "" OFF)
-option(ENABLE_BROWSER "" OFF)
-option(USE_WEBKIT "" OFF)
-option(ENABLE_CLOUDPINYIN "" OFF)
-# prefer OpenCC_DIR rather than fcitx5-chinese-addons/cmake/FindOpenCC.cmake
-set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)
-add_subdirectory(fcitx5-chinese-addons)
-# rename to include executable in apk
-set_target_properties(scel2org5 PROPERTIES OUTPUT_NAME libscel2org5.so)
+# prebuilt boost
+list(APPEND CMAKE_FIND_ROOT_PATH "${PREBUILT_DIR}/boost/${ANDROID_ABI}/lib/cmake")
+find_package(Boost 1.86.0 REQUIRED COMPONENTS headers iostreams CONFIG)
-option(ENABLE_TEST "" OFF)
-option(ENABLE_QT "" OFF)
-add_subdirectory(fcitx5-unikey)
-# suppress "illegal character encoding in character literal" warning in unikey/data.cpp
-target_compile_options(unikey-lib PRIVATE "-Wno-invalid-source-encoding")
+set(CHINESE_ADDONS_PINYIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../lib/fcitx5-chinese-addons/src/main/cpp/fcitx5-chinese-addons/im/pinyin")
+add_library(pinyin-customphrase STATIC "${CHINESE_ADDONS_PINYIN_DIR}/customphrase.cpp")
+target_include_directories(pinyin-customphrase INTERFACE "${CHINESE_ADDONS_PINYIN_DIR}")
+target_link_libraries(pinyin-customphrase PRIVATE Fcitx5::Utils LibIME::Core)
add_library(native-lib SHARED native-lib.cpp)
target_link_libraries(native-lib
log
- libevent::core
+ libuv::uv_a
Fcitx5::Utils
Fcitx5::Config
Fcitx5::Core
Fcitx5::Module::QuickPhrase
Fcitx5::Module::Unicode
Fcitx5::Module::Clipboard
+ Boost::headers
+ Boost::iostreams
LibIME::Pinyin
- LibIME::Table)
+ LibIME::Table
+ pinyin-customphrase
+ )
+
+# copy module libraries from dependency lib
+add_custom_target(copy-fcitx5-modules
+ 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/androidfrontend/androidfrontend.cpp b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
index 654af2603..620452604 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.cpp
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.cpp
@@ -1,5 +1,10 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#include
#include
+#include
#include
#include
#include
@@ -10,13 +15,13 @@
namespace fcitx {
-class AndroidInputContext : public InputContext {
+class AndroidInputContext : public InputContextV2 {
public:
AndroidInputContext(AndroidFrontend *frontend,
InputContextManager &inputContextManager,
int uid,
const std::string &pkgName)
- : InputContext(inputContextManager, pkgName),
+ : InputContextV2(inputContextManager, pkgName),
frontend_(frontend),
uid_(uid) {
created();
@@ -30,7 +35,11 @@ class AndroidInputContext : public InputContext {
[[nodiscard]] const char *frontend() const override { return "androidfrontend"; }
void commitStringImpl(const std::string &text) override {
- frontend_->commitString(text);
+ frontend_->commitString(text, -1);
+ }
+
+ void commitStringWithCursorImpl(const std::string &text, size_t cursor) override {
+ frontend_->commitString(text, static_cast(cursor));
}
void forwardKeyImpl(const ForwardKeyEvent &key) override {
@@ -38,52 +47,91 @@ class AndroidInputContext : public InputContext {
}
void deleteSurroundingTextImpl(int offset, unsigned int size) override {
- FCITX_INFO() << "DeleteSurrounding: " << offset << " " << size;
+ 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;
+ return;
+ }
+ frontend_->deleteSurrounding(before, after);
}
void updatePreeditImpl() override {
- checkClientPreeditUpdate();
+ frontend_->updateClientPreedit(filterText(inputPanel().clientPreedit()));
}
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.
- if (!isPreeditEnabled()) {
- checkClientPreeditUpdate();
- }
- InputPanel &ip = inputPanel();
+ const InputPanel &ip = inputPanel();
frontend_->updateInputPanel(
filterText(ip.preedit()),
filterText(ip.auxUp()),
filterText(ip.auxDown())
);
+ }
+
+ void updateCandidatesBulk() {
std::vector candidates;
- const auto &list = ip.candidateList();
+ int size = 0;
+ const auto &list = inputPanel().candidateList();
if (list) {
const auto &bulk = list->toBulk();
if (bulk) {
- const int size = bulk->totalSize();
- for (int i = 0; i < size; i++) {
- 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()));
+ size = bulk->totalSize();
+ // limit candidate count to 16 (for paging)
+ const int limit = size < 0 ? 16 : std::min(size, 16);
+ for (int i = 0; i < limit; i++) {
+ try {
+ auto &candidate = bulk->candidateFromAll(i);
+ // maybe unnecessary; I don't see anywhere using `CandidateWord::setPlaceHolder`
+ // if (candidate.isPlaceHolder()) continue;
+ candidates.emplace_back(filterString(candidate.textWithComment()));
+ } catch (const std::invalid_argument &e) {
+ size = static_cast(candidates.size());
+ break;
+ }
}
} else {
- const int size = list->size();
+ 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);
+ frontend_->updateCandidateList(candidates, size);
+ }
+
+ 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 selectCandidate(int idx) {
+ bool selectCandidateBulk(int idx) {
const auto &list = inputPanel().candidateList();
if (!list) {
return false;
@@ -102,21 +150,122 @@ class AndroidInputContext : public InputContext {
return true;
}
-private:
- AndroidFrontend *frontend_;
- int uid_;
+ 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();
+ if (list) {
+ const int last = offset + limit;
+ const auto &bulk = list->toBulk();
+ if (bulk) {
+ const int totalSize = bulk->totalSize();
+ const int end = totalSize < 0 ? last : std::min(totalSize, last);
+ for (int i = offset; i < end; i++) {
+ try {
+ auto &candidate = bulk->candidateFromAll(i);
+ candidates.emplace_back(filterString(candidate.textWithComment()));
+ } catch (const std::invalid_argument &e) {
+ break;
+ }
+ }
+ } else {
+ const int end = std::min(list->size(), last);
+ for (int i = offset; i < end; i++) {
+ 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;
+ }
- bool clientPreeditEmpty_ = true;
+ 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 checkClientPreeditUpdate() {
- const auto &clientPreedit = filterText(inputPanel().clientPreedit());
- bool empty = clientPreedit.empty();
- // skip update if new and old clientPreedit are both empty
- if (empty && clientPreeditEmpty_) return;
- clientPreeditEmpty_ = empty;
- frontend_->updateClientPreedit(clientPreedit);
+ 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_;
+
inline Text filterText(const Text &orig) {
return frontend_->instance()->outputFilter(this, orig);
}
@@ -132,26 +281,34 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
activeIC_(nullptr),
icCache_(),
eventHandlers_(),
- statusAreaDefer_(),
- statusAreaUpdated_(false) {
+ pagingMode_(0) {
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextInputMethodActivated,
EventWatcherPhase::Default,
- [this](Event &event) { imChangeCallback(); }
+ [this](Event &event) {
+ FCITX_UNUSED(event);
+ imChangeCallback();
+ }
));
eventHandlers_.emplace_back(instance_->watchEvent(
- EventType::InputContextUpdateUI,
+ EventType::InputContextFlushUI,
EventWatcherPhase::Default,
[this](Event &event) {
- auto &e = static_cast(event);
+ auto &e = static_cast(event);
switch (e.component()) {
case UserInterfaceComponent::InputPanel: {
- auto *ic = dynamic_cast(activeIC_);
- if (ic) ic->updateInputPanel();
+ if (activeIC_) {
+ activeIC_->updateInputPanel();
+ if (pagingMode_ == 0) {
+ activeIC_->updateCandidatesBulk();
+ } else {
+ activeIC_->updateCandidatesPaged();
+ }
+ }
break;
}
case UserInterfaceComponent::StatusArea: {
- handleStatusAreaUpdate();
+ statusAreaUpdateCallback();
break;
}
}
@@ -160,10 +317,9 @@ AndroidFrontend::AndroidFrontend(Instance *instance)
}
void AndroidFrontend::keyEvent(const Key &key, bool isRelease, const int timestamp) {
- auto *ic = activeIC_;
- if (!ic) return;
- KeyEvent keyEvent(ic, key, isRelease);
- ic->keyEvent(keyEvent);
+ if (!activeIC_) return;
+ KeyEvent keyEvent(activeIC_, key, isRelease);
+ activeIC_->keyEvent(keyEvent);
if (!keyEvent.accepted()) {
auto sym = key.sym();
keyEventCallback(sym, key.states(), Key::keySymToUnicode(sym), isRelease, timestamp);
@@ -175,12 +331,12 @@ void AndroidFrontend::forwardKey(const Key &key, bool isRelease) {
keyEventCallback(sym, key.states(), Key::keySymToUnicode(sym), isRelease, -1);
}
-void AndroidFrontend::commitString(const std::string &str) {
- commitStringCallback(str);
+void AndroidFrontend::commitString(const std::string &str, const int cursor) {
+ commitStringCallback(str, cursor);
}
-void AndroidFrontend::updateCandidateList(const std::vector &candidates) {
- candidateListCallback(candidates);
+void AndroidFrontend::updateCandidateList(const std::vector &candidates, const int size) {
+ candidateListCallback(candidates, size);
}
void AndroidFrontend::updateClientPreedit(const Text &clientPreedit) {
@@ -188,7 +344,7 @@ void AndroidFrontend::updateClientPreedit(const Text &clientPreedit) {
}
void AndroidFrontend::updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown) {
- inputPanelAuxCallback(preedit, auxUp, auxDown);
+ inputPanelCallback(preedit, auxUp, auxDown);
}
void AndroidFrontend::releaseInputContext(const int uid) {
@@ -196,46 +352,53 @@ void AndroidFrontend::releaseInputContext(const int uid) {
}
bool AndroidFrontend::selectCandidate(int idx) {
- auto *ic = dynamic_cast(focusGroup_.focusedInputContext());
- if (!ic) return false;
- return ic->selectCandidate(idx);
+ if (!activeIC_) return false;
+ 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() {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return true;
- return ic->inputPanel().empty();
+ if (!activeIC_) return true;
+ return activeIC_->inputPanel().empty();
}
void AndroidFrontend::resetInputContext() {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return;
- ic->reset();
+ if (!activeIC_) return;
+ activeIC_->reset();
}
void AndroidFrontend::repositionCursor(int position) {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return;
- auto engine = instance_->inputMethodEngine(ic);
- InvokeActionEvent event(InvokeActionEvent::Action::LeftClick, position, ic);
- engine->invokeAction(*(instance_->inputMethodEntry(ic)), event);
+ if (!activeIC_) return;
+ InvokeActionEvent event(InvokeActionEvent::Action::LeftClick, position, activeIC_);
+ activeIC_->invokeAction(event);
}
void AndroidFrontend::focusInputContext(bool focus) {
+ if (!activeIC_) return;
if (focus) {
- if (!activeIC_) return;
activeIC_->focusIn();
} else {
- auto *ic = focusGroup_.focusedInputContext();
- if (!ic) return;
- ic->focusOut();
+ activeIC_->focusOut();
}
}
void AndroidFrontend::activateInputContext(const int uid, const std::string &pkgName) {
auto *ptr = icCache_.find(uid);
if (ptr) {
- activeIC_ = ptr->get();
+ activeIC_ = dynamic_cast(ptr->get());
} else {
auto *ic = new AndroidInputContext(this, instance_->inputContextManager(), uid, pkgName);
activeIC_ = ic;
@@ -264,6 +427,32 @@ void AndroidFrontend::setCandidateListCallback(const CandidateListCallback &call
candidateListCallback = callback;
}
+std::vector AndroidFrontend::getCandidates(const int offset, const int limit) {
+ if (!activeIC_) return {};
+ return activeIC_->getCandidates(offset, limit);
+}
+
+void AndroidFrontend::deleteSurrounding(const int before, const int after) {
+ deleteSurroundingCallback(before, after);
+}
+
+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;
}
@@ -273,7 +462,7 @@ void AndroidFrontend::setPreeditCallback(const ClientPreeditCallback &callback)
}
void AndroidFrontend::setInputPanelAuxCallback(const InputPanelCallback &callback) {
- inputPanelAuxCallback = callback;
+ inputPanelCallback = callback;
}
void AndroidFrontend::setKeyEventCallback(const KeyEventCallback &callback) {
@@ -288,15 +477,16 @@ void AndroidFrontend::setStatusAreaUpdateCallback(const StatusAreaUpdateCallback
statusAreaUpdateCallback = callback;
}
-void AndroidFrontend::handleStatusAreaUpdate() {
- if (statusAreaUpdated_) return;
- statusAreaUpdated_ = true;
- statusAreaDefer_ = instance_->eventLoop().addDeferEvent([this](EventSource *) {
- statusAreaUpdateCallback();
- statusAreaUpdated_ = false;
- statusAreaDefer_ = nullptr;
- return true;
- });
+void AndroidFrontend::setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback) {
+ deleteSurroundingCallback = callback;
+}
+
+void AndroidFrontend::setToastCallback(const ToastCallback &callback) {
+ toastCallback = callback;
+}
+
+void AndroidFrontend::setPagedCandidateCallback(const PagedCandidateCallback &callback) {
+ pagedCandidateCallback = callback;
}
class AndroidFrontendFactory : public AddonFactory {
diff --git a/app/src/main/cpp/androidfrontend/androidfrontend.h b/app/src/main/cpp/androidfrontend/androidfrontend.h
index 2c1bd1406..9b9db800b 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend.h
@@ -1,9 +1,13 @@
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_H_
+/*
+ * 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
#include
#include
-#include
#include
#include "androidfrontend_public.h"
@@ -11,17 +15,20 @@
namespace fcitx {
+class AndroidInputContext;
+
class AndroidFrontend : public AddonInstance {
public:
- AndroidFrontend(Instance *instance);
+ explicit AndroidFrontend(Instance *instance);
Instance *instance() { return instance_; }
- void updateCandidateList(const std::vector &candidates);
- void commitString(const std::string &str);
+ void updateCandidateList(const std::vector &candidates, const int size);
+ void commitString(const std::string &str, const int cursor);
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);
@@ -32,8 +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);
@@ -41,6 +55,9 @@ class AndroidFrontend : public AddonInstance {
void setKeyEventCallback(const KeyEventCallback &callback);
void setInputMethodChangeCallback(const InputMethodChangeCallback &callback);
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);
@@ -53,6 +70,12 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, activeInputContext);
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);
@@ -60,25 +83,28 @@ class AndroidFrontend : public AddonInstance {
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setKeyEventCallback);
FCITX_ADDON_EXPORT_FUNCTION(AndroidFrontend, setInputMethodChangeCallback);
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_;
- InputContext *activeIC_;
+ AndroidInputContext *activeIC_;
InputContextCache icCache_;
std::vector>> eventHandlers_;
- std::unique_ptr statusAreaDefer_;
- bool statusAreaUpdated_;
-
- void handleStatusAreaUpdate();
+ int pagingMode_;
- CandidateListCallback candidateListCallback = [](const std::vector &) {};
- CommitStringCallback commitStringCallback = [](const std::string &) {};
+ CandidateListCallback candidateListCallback = [](const std::vector &, const int) {};
+ CommitStringCallback commitStringCallback = [](const std::string &, const int) {};
ClientPreeditCallback preeditCallback = [](const Text &) {};
- InputPanelCallback inputPanelAuxCallback = [](const fcitx::Text &, const fcitx::Text &, const Text &) {};
+ InputPanelCallback inputPanelCallback = [](const fcitx::Text &, const fcitx::Text &, const Text &) {};
KeyEventCallback keyEventCallback = [](const int, const uint32_t, const uint32_t, const bool, const int) {};
InputMethodChangeCallback imChangeCallback = [] {};
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 45b8d5550..69bcbbdd5 100644
--- a/app/src/main/cpp/androidfrontend/androidfrontend_public.h
+++ b/app/src/main/cpp/androidfrontend/androidfrontend_public.h
@@ -1,16 +1,27 @@
-#ifndef _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
-#define _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+/*
+ * 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
#include
+#include
+#include
#include
-typedef std::function &)> CandidateListCallback;
-typedef std::function CommitStringCallback;
+#include "../helper-types.h"
+
+typedef std::function &, const int)> CandidateListCallback;
+typedef std::function CommitStringCallback;
typedef std::function ClientPreeditCallback;
typedef std::function InputPanelCallback;
typedef std::function KeyEventCallback;
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))
@@ -42,6 +53,24 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, deactivateInputContext,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setCapabilityFlags,
void(uint64_t))
+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 &))
@@ -63,4 +92,13 @@ FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setInputMethodChangeCallback,
FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setStatusAreaUpdateCallback,
void(const StatusAreaUpdateCallback &))
-#endif // _FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H_
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setDeleteSurroundingCallback,
+ void(const DeleteSurroundingCallback &))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setToastCallback,
+ void(const ToastCallback &))
+
+FCITX_ADDON_DECLARE_FUNCTION(AndroidFrontend, setPagedCandidateCallback,
+ void(const PagedCandidateCallback &))
+
+#endif // FCITX5_ANDROID_ANDROIDFRONTEND_PUBLIC_H
diff --git a/app/src/main/cpp/androidfrontend/inputcontextcache.h b/app/src/main/cpp/androidfrontend/inputcontextcache.h
index 058044dce..637ecf44e 100644
--- a/app/src/main/cpp/androidfrontend/inputcontextcache.h
+++ b/app/src/main/cpp/androidfrontend/inputcontextcache.h
@@ -1,5 +1,9 @@
-// modified from https://github.com/fcitx/libime/blob/1.0.14/src/libime/core/lrucache.h
-
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2017-2017 CSSlayer
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ * SPDX-FileComment: Modified from https://github.com/fcitx/libime/blob/1.0.14/src/libime/core/lrucache.h
+ */
#ifndef FCITX5_ANDROID_INPUTCONTEXTCACHE_H
#define FCITX5_ANDROID_INPUTCONTEXTCACHE_H
diff --git a/app/src/main/cpp/androidkeyboard/CMakeLists.txt b/app/src/main/cpp/androidkeyboard/CMakeLists.txt
index db54ff396..d1eda2cfc 100644
--- a/app/src/main/cpp/androidkeyboard/CMakeLists.txt
+++ b/app/src/main/cpp/androidkeyboard/CMakeLists.txt
@@ -1,7 +1,7 @@
add_definitions(-DFCITX_GETTEXT_DOMAIN=\"fcitx5-android\")
add_library(androidkeyboard MODULE androidkeyboard.cpp)
-target_link_libraries(androidkeyboard Fcitx5::Core Fcitx5::Utils)
+target_link_libraries(androidkeyboard Fcitx5::Core Fcitx5::Utils Fcitx5::Module::Spell)
configure_file(androidkeyboard.conf.in.in androidkeyboard.conf.in @ONLY)
fcitx5_translate_desktop_file(${CMAKE_CURRENT_BINARY_DIR}/androidkeyboard.conf.in androidkeyboard.conf)
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in b/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in
index 7f83929d9..03d617017 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.conf.in.in
@@ -8,5 +8,4 @@ Configurable=True
[Addon/OptionalDependencies]
0=spell
-1=quickphrase
-;2=emoji
\ No newline at end of file
+1=quickphrase
\ No newline at end of file
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
index 946146209..a84d55ab7 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.cpp
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#include
#include
#include
@@ -5,13 +9,11 @@
#include
#include
-#include "../fcitx5/src/modules/spell/spell_public.h"
-#include "../fcitx5/src/im/keyboard/chardata.h"
+#include "spell_public.h"
+#include "../../../../../lib/fcitx5/src/main/cpp/fcitx5/src/im/keyboard/chardata.h" // dirty but works
#include "androidkeyboard.h"
-#define FCITX_KEYBOARD_MAX_BUFFER 20
-
namespace fcitx {
namespace {
@@ -85,7 +87,7 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
// check if we can select candidate.
if (auto candList = inputContext->inputPanel().candidateList()) {
- int idx = key.keyListIndex(selectionKeys_);
+ const int idx = key.keyListIndex(selectionKeys_);
if (idx >= 0 && idx < candList->size()) {
event.filterAndAccept();
candList->candidate(idx).select(inputContext);
@@ -93,9 +95,9 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
}
}
- bool validSym = isValidSym(key);
+ const bool validSym = isValidSym(key);
- static KeyList FCITX_HYPHEN_APOS = {Key(FcitxKey_minus), Key(FcitxKey_apostrophe)};
+ static const KeyList FCITX_HYPHEN_APOS = {Key(FcitxKey_minus), Key(FcitxKey_apostrophe)};
// check for valid character
if (key.isSimple() || validSym) {
// prepend space before input next word
@@ -106,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();
}
}
@@ -119,7 +120,7 @@ void AndroidKeyboardEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &ev
}
return updateCandidate(entry, inputContext);
}
- } else if (key.check(FcitxKey_Delete)) {
+ } else if (key.check(FcitxKey_Delete) || key.check(FcitxKey_KP_Delete)) {
if (buffer.del()) {
event.filterAndAccept();
if (buffer.empty()) {
@@ -140,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);
}
}
@@ -173,7 +174,7 @@ std::vector AndroidKeyboardEngine::listInputMethods() {
void AndroidKeyboardEngine::reloadConfig() {
readAsIni(config_, ConfPath);
selectionKeys_.clear();
- KeySym syms[] = {
+ const std::array syms{
FcitxKey_1, FcitxKey_2, FcitxKey_3, FcitxKey_4, FcitxKey_5,
FcitxKey_6, FcitxKey_7, FcitxKey_8, FcitxKey_9, FcitxKey_0,
};
@@ -209,6 +210,7 @@ void AndroidKeyboardEngine::setConfig(const RawConfig &config) {
}
void AndroidKeyboardEngine::activate(const InputMethodEntry &entry, InputContextEvent &event) {
+ FCITX_UNUSED(entry);
auto *inputContext = event.inputContext();
wordHintAction_.setChecked(*config_.enableWordHint);
wordHintAction_.update(inputContext);
@@ -226,6 +228,7 @@ void AndroidKeyboardEngine::deactivate(const InputMethodEntry &entry, InputConte
}
void AndroidKeyboardEngine::reset(const InputMethodEntry &entry, InputContextEvent &event) {
+ FCITX_UNUSED(entry);
auto *inputContext = event.inputContext();
resetState(inputContext);
inputContext->inputPanel().reset();
@@ -245,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(),
- 20);
+ 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);
}
@@ -265,45 +276,46 @@ void AndroidKeyboardEngine::updateCandidate(const InputMethodEntry &entry, Input
}
void AndroidKeyboardEngine::updateUI(InputContext *inputContext) {
- auto *state = inputContext->propertyFor(&factory_);
- Text preedit(preeditString(inputContext), TextFormatFlag::Underline);
- preedit.setCursor(static_cast(state->buffer_.cursorByChar()));
- inputContext->inputPanel().setClientPreedit(preedit);
- // 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,
- CapabilityFlag::Sensitive};
- // 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;
}
auto &buffer = state->buffer_;
- auto preedit = preeditString(inputContext);
+ auto [preedit, cursor] = preeditWithCursor(inputContext);
if (preedit != buffer.userInput()) {
buffer.clear();
buffer.type(preedit);
}
- buffer.type(chr);
+ buffer.type(Key::keySymToUTF8(event.key().sym()));
- if (buffer.size() >= FCITX_KEYBOARD_MAX_BUFFER) {
+ if (buffer.size() >= MaxBufferSize) {
commitBuffer(inputContext);
return true;
}
@@ -313,11 +325,16 @@ bool AndroidKeyboardEngine::updateBuffer(InputContext *inputContext, const std::
}
void AndroidKeyboardEngine::commitBuffer(InputContext *inputContext) {
- auto preedit = preeditString(inputContext);
+ auto [preedit, cursor] = preeditWithCursor(inputContext);
if (preedit.empty()) {
return;
}
- inputContext->commitString(preedit);
+ auto characterCount = utf8::length(preedit, 0, cursor);
+ if (inputContext->capabilityFlags().test(CapabilityFlag::CommitStringWithCursor)) {
+ inputContext->commitStringWithCursor(preedit, characterCount);
+ } else {
+ inputContext->commitString(preedit);
+ }
resetState(inputContext);
inputContext->inputPanel().reset();
inputContext->updatePreedit();
@@ -329,22 +346,22 @@ bool AndroidKeyboardEngine::supportHint(const std::string &language) {
return hasSpell;
}
-std::string AndroidKeyboardEngine::preeditString(InputContext *inputContext) {
+std::pair AndroidKeyboardEngine::preeditWithCursor(InputContext *inputContext) {
auto *state = inputContext->propertyFor(&factory_);
- return state->buffer_.userInput();
+ return {state->buffer_.userInput(), state->buffer_.cursorByChar()};
}
void AndroidKeyboardEngine::invokeActionImpl(const InputMethodEntry &entry, InvokeActionEvent &event) {
- size_t cursor = event.cursor();
+ const int cursor = event.cursor();
auto inputContext = event.inputContext();
auto *state = inputContext->propertyFor(&factory_);
if (event.action() != InvokeActionEvent::Action::LeftClick
|| cursor < 0
- || cursor > state->buffer_.size()) {
+ || static_cast(cursor) > state->buffer_.size()) {
return InputMethodEngineV3::invokeActionImpl(entry, event);
}
event.filter();
- state->buffer_.setCursor(event.cursor());
+ state->buffer_.setCursor(static_cast(cursor));
updateUI(inputContext);
}
diff --git a/app/src/main/cpp/androidkeyboard/androidkeyboard.h b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
index 847b64fc7..b365ac527 100644
--- a/app/src/main/cpp/androidkeyboard/androidkeyboard.h
+++ b/app/src/main/cpp/androidkeyboard/androidkeyboard.h
@@ -1,5 +1,9 @@
-#ifndef _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
-#define _FCITX5_ANDROID_ANDROIDKEYBOARD_H_
+/*
+ * 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
#include
#include
@@ -26,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
@@ -50,8 +58,10 @@ struct AndroidKeyboardEngineState : public InputContextProperty {
class AndroidKeyboardEngine final : public InputMethodEngineV3 {
public:
- AndroidKeyboardEngine(Instance *instance);
- ~AndroidKeyboardEngine() = default;
+ static int constexpr MaxBufferSize = 20;
+ static int constexpr SpellCandidateSize = 20;
+
+ explicit AndroidKeyboardEngine(Instance *instance);
Instance *instance() { return instance_; }
@@ -87,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().
@@ -100,7 +110,10 @@ class AndroidKeyboardEngine final : public InputMethodEngineV3 {
private:
bool supportHint(const std::string &language);
- std::string preeditString(InputContext *inputContext);
+ /**
+ * preedit string and byte cursor
+ */
+ std::pair preeditWithCursor(InputContext *inputContext);
Instance *instance_;
AndroidKeyboardEngineConfig config_;
@@ -121,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/CMakeLists.txt b/app/src/main/cpp/androidnotification/CMakeLists.txt
new file mode 100644
index 000000000..86d583495
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/CMakeLists.txt
@@ -0,0 +1,10 @@
+add_definitions(-DFCITX_GETTEXT_DOMAIN=\"fcitx5-android\")
+
+add_library(androidnotification MODULE androidnotification.cpp)
+target_link_libraries(androidnotification Fcitx5::Core Fcitx5::Utils Fcitx5::Module::Notifications)
+
+configure_file(notifications.conf.in.in notifications.conf.in @ONLY)
+fcitx5_translate_desktop_file(${CMAKE_CURRENT_BINARY_DIR}/notifications.conf.in notifications.conf)
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/notifications.conf"
+ DESTINATION "${FCITX_INSTALL_PKGDATADIR}/addon"
+ COMPONENT config)
diff --git a/app/src/main/cpp/androidnotification/androidnotification.cpp b/app/src/main/cpp/androidnotification/androidnotification.cpp
new file mode 100644
index 000000000..b2e06b5e4
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/androidnotification.cpp
@@ -0,0 +1,100 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
+ */
+#include
+
+#include
+#include
+
+#include "../androidfrontend/androidfrontend_public.h"
+
+#include "androidnotification.h"
+
+namespace fcitx {
+
+Notifications::Notifications(Instance *instance) : instance_(instance) {
+ reloadConfig();
+}
+
+void Notifications::reloadConfig() {
+ readAsIni(config_, ConfPath);
+ updateHiddenNotifications();
+}
+
+void Notifications::save() {
+ std::vector values_;
+ values_.reserve(hiddenNotifications_.size());
+ for (const auto &id: hiddenNotifications_) {
+ values_.push_back(id);
+ }
+ config_.hiddenNotifications.setValue(std::move(values_));
+ 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,
+ const std::string &appIcon,
+ const std::string &summary,
+ const std::string &body,
+ const std::vector &actions,
+ int32_t timeout,
+ NotificationActionCallback actionCallback,
+ NotificationClosedCallback closedCallback) {
+ // TODO implement Notification
+ FCITX_UNUSED(appName);
+ FCITX_UNUSED(replaceId);
+ FCITX_UNUSED(appIcon);
+ FCITX_UNUSED(summary);
+ FCITX_UNUSED(body);
+ FCITX_UNUSED(actions);
+ FCITX_UNUSED(timeout);
+ FCITX_UNUSED(actionCallback);
+ FCITX_UNUSED(closedCallback);
+ return 0;
+}
+
+void Notifications::showTip(
+ const std::string &tipId,
+ const std::string &appName,
+ const std::string &appIcon,
+ const std::string &summary,
+ const std::string &body,
+ int32_t timeout) {
+ FCITX_UNUSED(appName);
+ FCITX_UNUSED(appIcon);
+ FCITX_UNUSED(timeout);
+ if (hiddenNotifications_.count(tipId)) {
+ return;
+ }
+ std::string const s = summary + ": " + body;
+ androidfrontend()->call(s);
+}
+
+void Notifications::closeNotification(uint64_t internalId) {
+ FCITX_UNUSED(internalId);
+}
+
+class NotificationsModuleFactory : public AddonFactory {
+ AddonInstance *create(AddonManager *manager) override {
+ return new Notifications(manager->instance());
+ }
+};
+
+}
+
+FCITX_ADDON_FACTORY(fcitx::NotificationsModuleFactory)
diff --git a/app/src/main/cpp/androidnotification/androidnotification.h b/app/src/main/cpp/androidnotification/androidnotification.h
new file mode 100644
index 000000000..242e2857b
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/androidnotification.h
@@ -0,0 +1,76 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2023 Fcitx5 for Android Contributors
+ */
+#ifndef FCITX5_ANDROID_ANDROIDNOTIFICATION_H
+#define FCITX5_ANDROID_ANDROIDNOTIFICATION_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace fcitx {
+
+FCITX_CONFIGURATION(NotificationsConfig,
+ fcitx::Option> hiddenNotifications{
+ this, "HiddenNotifications",
+ _("Hidden Notifications")};)
+
+class Notifications final : public AddonInstance {
+public:
+ explicit Notifications(Instance *instance);
+
+ Instance *instance() { return instance_; }
+
+ void reloadConfig() override;
+
+ void save() override;
+
+ const Configuration *getConfig() const override { return &config_; }
+
+ void setConfig(const RawConfig &config) override;
+
+ FCITX_ADDON_DEPENDENCY_LOADER(androidfrontend, instance_->addonManager());
+
+ uint32_t sendNotification(const std::string &appName, uint32_t replaceId,
+ const std::string &appIcon,
+ const std::string &summary,
+ const std::string &body,
+ const std::vector &actions,
+ int32_t timeout,
+ NotificationActionCallback actionCallback,
+ NotificationClosedCallback closedCallback);
+
+ void showTip(const std::string &tipId, const std::string &appName,
+ const std::string &appIcon, const std::string &summary,
+ const std::string &body, int32_t timeout);
+
+ void closeNotification(uint64_t internalId);
+
+private:
+ FCITX_ADDON_EXPORT_FUNCTION(Notifications, sendNotification);
+ FCITX_ADDON_EXPORT_FUNCTION(Notifications, showTip);
+ FCITX_ADDON_EXPORT_FUNCTION(Notifications, closeNotification);
+
+ static const inline char* ConfPath = "conf/androidnotification.conf";
+
+ NotificationsConfig config_;
+ Instance *instance_;
+
+ std::unordered_set hiddenNotifications_;
+
+ void updateHiddenNotifications();
+
+}; // class Notifications
+
+} // namespace fcitx
+
+#endif //FCITX5_ANDROID_ANDROIDNOTIFICATION_H
diff --git a/app/src/main/cpp/androidnotification/notifications.conf.in.in b/app/src/main/cpp/androidnotification/notifications.conf.in.in
new file mode 100644
index 000000000..453050f16
--- /dev/null
+++ b/app/src/main/cpp/androidnotification/notifications.conf.in.in
@@ -0,0 +1,11 @@
+[Addon]
+Name=Android Toast & Notification
+Type=SharedLibrary
+Library=libandroidnotification
+Category=Module
+Version=@PROJECT_VERSION@
+OnDemand=True
+Configurable=True
+
+[Addon/Dependencies]
+0=androidfrontend:@PROJECT_VERSION@
diff --git a/app/src/main/cpp/cmake/Fcitx5Utils/Fcitx5CompilerSettings.cmake b/app/src/main/cpp/cmake/Fcitx5Utils/Fcitx5CompilerSettings.cmake
deleted file mode 120000
index 1421520cc..000000000
--- a/app/src/main/cpp/cmake/Fcitx5Utils/Fcitx5CompilerSettings.cmake
+++ /dev/null
@@ -1 +0,0 @@
-../../../cpp/fcitx5/cmake/Fcitx5CompilerSettings.cmake
\ No newline at end of file
diff --git a/app/src/main/cpp/cmake/FindFcitx5Core.cmake b/app/src/main/cpp/cmake/FindFcitx5Core.cmake
deleted file mode 100644
index 7e28125bb..000000000
--- a/app/src/main/cpp/cmake/FindFcitx5Core.cmake
+++ /dev/null
@@ -1,11 +0,0 @@
-set(Fcitx5Core_FOUND TRUE)
-
-find_package(Fcitx5Utils REQUIRED)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(Fcitx5Core
- FOUND_VAR
- Fcitx5Core_FOUND
- REQUIRED_VARS
- Fcitx5Core_FOUND
-)
diff --git a/app/src/main/cpp/cmake/FindFcitx5Module.cmake b/app/src/main/cpp/cmake/FindFcitx5Module.cmake
deleted file mode 100644
index cdf55e3bb..000000000
--- a/app/src/main/cpp/cmake/FindFcitx5Module.cmake
+++ /dev/null
@@ -1,9 +0,0 @@
-set(Fcitx5Module_FOUND TRUE)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(Fcitx5Module
- FOUND_VAR
- Fcitx5Module_FOUND
- REQUIRED_VARS
- Fcitx5Module_FOUND
-)
diff --git a/app/src/main/cpp/cmake/FindFcitx5Utils.cmake b/app/src/main/cpp/cmake/FindFcitx5Utils.cmake
deleted file mode 100644
index ba497e42f..000000000
--- a/app/src/main/cpp/cmake/FindFcitx5Utils.cmake
+++ /dev/null
@@ -1,10 +0,0 @@
-set(Fcitx5Utils_FOUND TRUE)
-set(FCITX_INSTALL_CMAKECONFIG_DIR "${CMAKE_SOURCE_DIR}/cmake")
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(Fcitx5Utils
- FOUND_VAR
- Fcitx5Utils_FOUND
- REQUIRED_VARS
- FCITX_INSTALL_CMAKECONFIG_DIR
-)
diff --git a/app/src/main/cpp/cmake/FindLibIMEPinyin.cmake b/app/src/main/cpp/cmake/FindLibIMEPinyin.cmake
deleted file mode 100644
index 1f7685afe..000000000
--- a/app/src/main/cpp/cmake/FindLibIMEPinyin.cmake
+++ /dev/null
@@ -1,9 +0,0 @@
-set(LibIMEPinyin_FOUND TRUE)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(LibIMEPinyin
- FOUND_VAR
- LibIMEPinyin_FOUND
- REQUIRED_VARS
- LibIMEPinyin_FOUND
-)
diff --git a/app/src/main/cpp/cmake/FindLibIMETable.cmake b/app/src/main/cpp/cmake/FindLibIMETable.cmake
deleted file mode 100644
index 9a202119f..000000000
--- a/app/src/main/cpp/cmake/FindLibIMETable.cmake
+++ /dev/null
@@ -1,9 +0,0 @@
-set(LibIMETable_FOUND TRUE)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(LibIMETable
- FOUND_VAR
- LibIMETable_FOUND
- REQUIRED_VARS
- LibIMETable_FOUND
-)
diff --git a/app/src/main/cpp/fcitx5 b/app/src/main/cpp/fcitx5
deleted file mode 160000
index 3b6113c0a..000000000
--- a/app/src/main/cpp/fcitx5
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 3b6113c0afc00a6187fafc308c5e1e5de5d1614c
diff --git a/app/src/main/cpp/fcitx5-chinese-addons b/app/src/main/cpp/fcitx5-chinese-addons
deleted file mode 160000
index 96b4378e4..000000000
--- a/app/src/main/cpp/fcitx5-chinese-addons
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 96b4378e485508a05f3c3d0a1ade0852c37a3379
diff --git a/app/src/main/cpp/fcitx5-lua b/app/src/main/cpp/fcitx5-lua
deleted file mode 160000
index d8a1319c6..000000000
--- a/app/src/main/cpp/fcitx5-lua
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d8a1319c6583b0055b4bd4ea5bbd0292a8f8023a
diff --git a/app/src/main/cpp/fcitx5-unikey b/app/src/main/cpp/fcitx5-unikey
deleted file mode 160000
index a5a089d15..000000000
--- a/app/src/main/cpp/fcitx5-unikey
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a5a089d15c3fd016e170091e751147044a860923
diff --git a/app/src/main/cpp/helper-types.h b/app/src/main/cpp/helper-types.h
index fa2e9453e..1f13c9494 100644
--- a/app/src/main/cpp/helper-types.h
+++ b/app/src/main/cpp/helper-types.h
@@ -1,30 +1,51 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#ifndef FCITX5_ANDROID_HELPER_TYPES_H
#define FCITX5_ANDROID_HELPER_TYPES_H
#include
#include
#include
+#include
+#include
+#include
+#include
+
+#include
class InputMethodStatus {
public:
- const fcitx::InputMethodEntry *entry;
+ // fcitx::InputMethodEntry
+ std::string uniqueName;
+ std::string name;
+ std::string nativeName;
+ std::string icon;
+ std::string label;
+ std::string languageCode;
+ std::string addon;
+ bool configurable = false;
+ // fcitx::InputMethodEngine
std::string subMode;
std::string subModeLabel;
std::string subModeIcon;
InputMethodStatus(const fcitx::InputMethodEntry *entry,
fcitx::InputMethodEngine *engine,
- fcitx::InputContext *ic)
- : entry(entry) {
- if (engine) {
- subMode = engine->subMode(*entry, *ic);
- subModeLabel = engine->subModeLabel(*entry, *ic);
- subModeIcon = engine->subModeIcon(*entry, *ic);
- }
+ fcitx::InputContext *ic) {
+ uniqueName = entry->uniqueName();
+ name = entry->name();
+ nativeName = entry->nativeName();
+ icon = entry->icon();
+ label = entry->label();
+ languageCode = entry->languageCode();
+ addon = entry->addon();
+ configurable = entry->isConfigurable();
+ subMode = engine->subMode(*entry, *ic);
+ subModeLabel = engine->subModeLabel(*entry, *ic);
+ subModeIcon = engine->subModeIcon(*entry, *ic);
}
-
- InputMethodStatus(const fcitx::InputMethodEntry *entry)
- : entry(entry) {}
};
class AddonStatus {
@@ -68,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 5ec574728..6807b531d 100644
--- a/app/src/main/cpp/jni-utils.h
+++ b/app/src/main/cpp/jni-utils.h
@@ -1,3 +1,7 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
+ */
#ifndef FCITX5_ANDROID_JNI_UTILS_H
#define FCITX5_ANDROID_JNI_UTILS_H
@@ -73,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);
}
@@ -102,6 +106,7 @@ class GlobalRefSingleton {
jmethodID BooleanInit;
jclass Fcitx;
+ jmethodID ShowToast;
jmethodID HandleFcitxEvent;
jclass InputMethodEntry;
@@ -127,7 +132,19 @@ class GlobalRefSingleton {
jclass FormattedText;
jmethodID FormattedTextFromByteCursor;
- GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
+ jclass PinyinCustomPhrase;
+ jmethodID PinyinCustomPhraseInit;
+ jfieldID PinyinCustomPhraseKey;
+ jfieldID PinyinCustomPhraseOrder;
+ jfieldID PinyinCustomPhraseValue;
+
+ jclass CandidateAction;
+ jmethodID CandidateActionInit;
+
+ jclass Candidate;
+ jmethodID CandidateInit;
+
+ explicit GlobalRefSingleton(JavaVM *jvm_) : jvm(jvm_) {
JNIEnv *env;
jvm->AttachCurrentThread(&env, nullptr);
@@ -142,11 +159,12 @@ class GlobalRefSingleton {
BooleanInit = env->GetMethodID(Boolean, "", "(Z)V");
Fcitx = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/Fcitx")));
+ ShowToast = env->GetStaticMethodID(Fcitx, "showToast", "(Ljava/lang/String;)V");
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;");
@@ -166,9 +184,21 @@ class GlobalRefSingleton {
FormattedText = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/core/FormattedText")));
FormattedTextFromByteCursor = env->GetStaticMethodID(FormattedText, "fromByteCursor", "([Ljava/lang/String;[II)Lorg/fcitx/fcitx5/android/core/FormattedText;");
+
+ PinyinCustomPhrase = reinterpret_cast(env->NewGlobalRef(env->FindClass("org/fcitx/fcitx5/android/data/pinyin/customphrase/PinyinCustomPhrase")));
+ PinyinCustomPhraseInit = env->GetMethodID(PinyinCustomPhrase, "", "(Ljava/lang/String;ILjava/lang/String;)V");
+ 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/libime b/app/src/main/cpp/libime
deleted file mode 160000
index d5aa7da86..000000000
--- a/app/src/main/cpp/libime
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d5aa7da86ed370cff7519d6f0dfaa88befc7e2f3
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
index 90fc77a35..bdeb17191 100644
--- a/app/src/main/cpp/native-lib.cpp
+++ b/app/src/main/cpp/native-lib.cpp
@@ -1,12 +1,18 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
+ */
#include
+#include
+
#include
#include
#include
#include
-#include
+#include
#include
#include
@@ -19,7 +25,9 @@
#include
#include
#include
+#include
#include
+#include
#include
#include
@@ -28,6 +36,10 @@
#include
#include
+#include
+#include
+#include "customphrase.h"
+
#include "androidfrontend/androidfrontend_public.h"
#include "jni-utils.h"
#include "nativestreambuf.h"
@@ -59,19 +71,17 @@ 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(const std::function &setupCallback) {
- char arg0[] = "";
- char *argv[] = {arg0};
- p_instance = std::make_unique(FCITX_ARRAY_SIZE(argv), argv);
+ p_instance = std::make_unique(0, nullptr);
p_instance->addonManager().registerDefaultLoader(nullptr);
p_dispatcher = std::make_unique();
p_dispatcher->attach(&p_instance->eventLoop());
@@ -136,21 +146,23 @@ class Fcitx {
const auto *entry = imMgr.entry(ime.name());
entries.emplace_back(entry);
}
- return std::move(entries);
+ return entries;
}
- InputMethodStatus inputMethodStatus() {
+ std::unique_ptr inputMethodStatus() {
auto *ic = p_frontend->call();
- auto *engine = p_instance->inputMethodEngine(ic);
- const auto *entry = p_instance->inputMethodEntry(ic);
- if (engine) {
- return {entry, engine, ic};
- }
- return {entry};
+ if (!ic) return nullptr;
+ auto *entry = p_instance->inputMethodEntry(ic);
+ auto *engine = static_cast(p_instance->addonManager().addon(entry->addon(), true));
+ return std::make_unique(entry, engine, ic);
}
void setInputMethod(const std::string &ime) {
- p_instance->setCurrentInputMethod(ime);
+ auto *ic = p_frontend->call();
+ if (!ic) return;
+ // this method remembers input method for each InputContext,
+ // while Instance::setCurrentInputMethod(std::string) doesn't
+ p_instance->setCurrentInputMethod(ic, ime, true);
}
std::vector availableInputMethods() {
@@ -159,7 +171,7 @@ class Fcitx {
entries.emplace_back(&entry);
return true;
});
- return std::move(entries);
+ return entries;
}
void setEnabledInputMethods(std::vector &entries) {
@@ -275,9 +287,10 @@ class Fcitx {
auto &globalConfig = p_instance->globalConfig();
auto &addonManager = p_instance->addonManager();
const auto &enabledAddons = globalConfig.enabledAddons();
- std::unordered_set enabledSet(enabledAddons.begin(), enabledAddons.end());
+ const std::unordered_set enabledSet(enabledAddons.begin(), enabledAddons.end());
const auto &disabledAddons = globalConfig.disabledAddons();
- std::unordered_set disabledSet(disabledAddons.begin(), disabledAddons.end());
+ const std::unordered_set
+ disabledSet(disabledAddons.begin(), disabledAddons.end());
std::vector addons;
for (const auto category: {fcitx::AddonCategory::InputMethod,
fcitx::AddonCategory::Frontend,
@@ -296,7 +309,7 @@ class Fcitx {
} else if (enabledSet.count(info->uniqueName())) {
enabled = true;
}
- addons.emplace_back(AddonStatus(info, enabled));
+ addons.emplace_back(info, enabled);
}
}
return addons;
@@ -349,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) {
@@ -382,7 +395,7 @@ class Fcitx {
fcitx::StatusGroup::InputMethod,
fcitx::StatusGroup::AfterInputMethod}) {
for (auto act: ic->statusArea().actions(group)) {
- actions.emplace_back(ActionEntity(act, ic));
+ actions.emplace_back(act, ic);
}
}
return actions;
@@ -396,13 +409,37 @@ class Fcitx {
action->activate(ic);
}
+ std::vector getCandidates(int offset, int limit) {
+ 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.
@@ -452,6 +489,9 @@ JNI_OnLoad(JavaVM *jvm, void * /* reserved */) {
return JNI_VERSION_1_6;
}
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+
extern "C"
JNIEXPORT void JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_setupLogStream(JNIEnv *env, jclass clazz, jboolean verbose) {
@@ -462,40 +502,47 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_setupLogStream(JNIEnv *env, jclass claz
extern "C"
JNIEXPORT void JNICALL
-Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz, jstring locale, jstring appData, jstring appLib, jstring extData, jstring extCache) {
+Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(
+ JNIEnv *env, jclass clazz,
+ jstring locale,
+ jstring appData,
+ jstring appLib,
+ jstring extData,
+ jstring extCache,
+ jobjectArray extDomains) {
if (Fcitx::Instance().isRunning()) {
FCITX_ERROR() << "Fcitx is already running!";
return;
}
FCITX_INFO() << "Starting...";
- setenv("SKIP_FCITX_PATH", "true", 1);
-
auto locale_ = CString(env, locale);
auto appData_ = CString(env, appData);
auto appLib_ = CString(env, appLib);
auto extData_ = CString(env, extData);
auto extCache_ = CString(env, extCache);
- std::string lang_ = fcitx::stringutils::split(*locale_, ":")[0];
- std::string config_home = fcitx::stringutils::joinPath(*extData_, "config");
- std::string data_home = fcitx::stringutils::joinPath(*extData_, "data");
- std::string usr_share = fcitx::stringutils::joinPath(*appData_, "usr", "share");
- std::string locale_dir = fcitx::stringutils::joinPath(usr_share, "locale");
- std::string libime_data = fcitx::stringutils::joinPath(usr_share, "libime");
- std::string lua_path = fcitx::stringutils::concat(
+ const std::string lang_ = fcitx::stringutils::split(*locale_, ":")[0];
+ const std::string config_home = fcitx::stringutils::joinPath(*extData_, "config");
+ const std::string data_home = fcitx::stringutils::joinPath(*extData_, "data");
+ const std::string usr_share = fcitx::stringutils::joinPath(*appData_, "usr", "share");
+ const std::string locale_dir = fcitx::stringutils::joinPath(usr_share, "locale");
+ const std::string libime_data = fcitx::stringutils::joinPath(usr_share, "libime");
+ const std::string lua_path = fcitx::stringutils::concat(
fcitx::stringutils::joinPath(data_home, "lua", "?.lua"), ";",
fcitx::stringutils::joinPath(data_home, "lua", "?", "init.lua"), ";",
fcitx::stringutils::joinPath(usr_share, "lua", "5.4", "?.lua"), ";",
fcitx::stringutils::joinPath(usr_share, "lua", "5.4", "?", "init.lua"), ";",
";" // double semicolon, for default path defined in luaconf.h
);
- std::string lua_cpath = fcitx::stringutils::concat(
+ const std::string lua_cpath = fcitx::stringutils::concat(
fcitx::stringutils::joinPath(data_home, "lua", "?.so"), ";",
fcitx::stringutils::joinPath(usr_share, "lua", "5.4", "?.so"), ";",
";"
);
+ // prevent StandardPath from resolving it's hardcoded installation path
+ setenv("SKIP_FCITX_PATH", "1", 1);
// for fcitx default profile [DefaultInputMethod]
setenv("LANG", lang_.c_str(), 1);
// for libintl-lite loading gettext .mo translations
@@ -526,24 +573,35 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
const char *locale_dir_char = locale_dir.c_str();
fcitx::registerDomain("fcitx5", locale_dir_char);
- fcitx::registerDomain("fcitx5-chinese-addons", locale_dir_char);
fcitx::registerDomain("fcitx5-lua", locale_dir_char);
- fcitx::registerDomain("fcitx5-unikey", locale_dir_char);
+ fcitx::registerDomain("fcitx5-chinese-addons", locale_dir_char);
fcitx::registerDomain("fcitx5-android", locale_dir_char);
- auto candidateListCallback = [](const std::vector &candidateList) {
+ const int extDomainsSize = env->GetArrayLength(extDomains);
+ for (int i = 0; i < extDomainsSize; i++) {
+ auto domain = JRef(env, env->GetObjectArrayElement(extDomains, i));
+ fcitx::registerDomain(CString(env, domain), locale_dir_char);
+ }
+
+ auto candidateListCallback = [](const std::vector &candidates, const int size) {
auto env = GlobalRef->AttachEnv();
- auto vararg = JRef(env, env->NewObjectArray(static_cast(candidateList.size()), GlobalRef->String, nullptr));
+ auto candidatesArray = JRef(env, env->NewObjectArray(static_cast(candidates.size()), GlobalRef->String, nullptr));
int i = 0;
- for (const auto &s: candidateList) {
- env->SetObjectArrayElement(vararg, i++, JString(env, s));
+ for (const auto &s: candidates) {
+ env->SetObjectArrayElement(candidatesArray, i++, JString(env, s));
}
+ auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->Object, nullptr));
+ auto candidatesCount = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, size));
+ env->SetObjectArrayElement(vararg, 0, *candidatesCount);
+ env->SetObjectArrayElement(vararg, 1, *candidatesArray);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 0, *vararg);
};
- auto commitStringCallback = [](const std::string &str) {
+ auto commitStringCallback = [](const std::string &str, const int cursor) {
auto env = GlobalRef->AttachEnv();
- auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->String, nullptr));
+ auto stringCursor = JRef(env, env->NewObject(GlobalRef->Integer, GlobalRef->IntegerInit, cursor));
+ auto vararg = JRef(env, env->NewObjectArray(2, GlobalRef->Object, nullptr));
env->SetObjectArrayElement(vararg, 0, JString(env, str));
+ env->SetObjectArrayElement(vararg, 1, stringCursor);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 1, *vararg);
};
auto preeditCallback = [](const fcitx::Text &clientPreedit) {
@@ -576,24 +634,71 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 5, *vararg);
};
auto imChangeCallback = []() {
+ std::unique_ptr status = Fcitx::Instance().inputMethodStatus();
+ if (!status) return;
auto env = GlobalRef->AttachEnv();
auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
- const auto status = Fcitx::Instance().inputMethodStatus();
- auto obj = JRef(env, fcitxInputMethodStatusToJObject(env, status));
+ 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();
- auto vararg = JRef(env, env->NewObjectArray(static_cast(actions.size()), GlobalRef->Action, nullptr));
+ auto actionArray = JRef(env, env->NewObjectArray(static_cast(actions.size()), GlobalRef->Action, nullptr));
int i = 0;
for (const auto &a: actions) {
auto obj = JRef(env, fcitxActionToJObject(env, a));
- env->SetObjectArrayElement(vararg, i++, obj);
+ env->SetObjectArrayElement(actionArray, i++, obj);
}
+ env->SetObjectArrayElement(vararg, 0, actionArray);
+ auto statusObj = JRef(env, fcitxInputMethodStatusToJObject(env, *status));
+ env->SetObjectArrayElement(vararg, 1, statusObj);
env->CallStaticVoidMethod(GlobalRef->Fcitx, GlobalRef->HandleFcitxEvent, 7, *vararg);
};
+ auto deleteSurroundingCallback = [](const int before, const int after) {
+ std::array arr{before, after};
+ auto env = GlobalRef->AttachEnv();
+ auto vararg = JRef(env, env->NewObjectArray(1, GlobalRef->Object, nullptr));
+ auto intArray = JRef(env, env->NewIntArray(2));
+ env->SetIntArrayRegion(intArray, 0, 2, arr.data());
+ 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::StandardPaths::global().syncUmask();
Fcitx::Instance().startup([&](auto *androidfrontend) {
FCITX_INFO() << "Setting up callback";
@@ -605,6 +710,9 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx(JNIEnv *env, jclass clazz,
androidfrontend->template call(keyEventCallback);
androidfrontend->template call(imChangeCallback);
androidfrontend->template call(statusAreaUpdateCallback);
+ androidfrontend->template call(deleteSurroundingCallback);
+ androidfrontend->template call(pagedCandidateCallback);
+ androidfrontend->template call(toastCallback);
});
FCITX_INFO() << "Finishing startup";
}
@@ -639,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
- fcitx::Key parsedKey{fcitx::Key::keySymFromString((const char *) &c),
- fcitx::KeyStates(static_cast(state))};
+ const fcitx::Key parsedKey{fcitx::Key::keySymFromString(reinterpret_cast(&c)),
+ 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);
}
@@ -668,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);
}
@@ -719,8 +829,9 @@ extern "C"
JNIEXPORT jobject JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_inputMethodStatus(JNIEnv *env, jclass clazz) {
RETURN_VALUE_IF_NOT_RUNNING(nullptr)
- const auto &status = Fcitx::Instance().inputMethodStatus();
- return fcitxInputMethodStatusToJObject(env, status);
+ auto status = Fcitx::Instance().inputMethodStatus();
+ if (!status) return nullptr;
+ return fcitxInputMethodStatusToJObject(env, *status);
}
extern "C"
@@ -865,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"
@@ -903,8 +1014,9 @@ JNIEXPORT jobjectArray JNICALL
Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxStatusAreaActions(JNIEnv *env, jclass clazz) {
RETURN_VALUE_IF_NOT_RUNNING(nullptr)
const auto actions = Fcitx::Instance().statusAreaActions();
- jobjectArray array = env->NewObjectArray(static_cast(actions.size()), GlobalRef->Action, nullptr);
- for (int i = 0; i < actions.size(); i++) {
+ int size = static_cast(actions.size());
+ jobjectArray array = env->NewObjectArray(size, GlobalRef->Action, nullptr);
+ for (int i = 0; i < size; i++) {
auto obj = JRef(env, fcitxActionToJObject(env, actions[i]));
env->SetObjectArrayElement(array, i, obj);
}
@@ -918,6 +1030,55 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_activateUserInterfaceAction(JNIEnv *env
Fcitx::Instance().activateAction(static_cast(id));
}
+extern "C"
+JNIEXPORT jobjectArray JNICALL
+Java_org_fcitx_fcitx5_android_core_Fcitx_getFcitxCandidates(JNIEnv *env, jclass clazz, jint offset, jint limit) {
+ RETURN_VALUE_IF_NOT_RUNNING(nullptr)
+ auto candidates = Fcitx::Instance().getCandidates(static_cast(offset), static_cast(limit));
+ int size = static_cast(candidates.size());
+ jobjectArray array = env->NewObjectArray(size, GlobalRef->String, nullptr);
+ for (int i = 0; i < size; i++) {
+ auto str = JString(env, candidates[i]);
+ env->SetObjectArrayElement(array, i, str);
+ }
+ 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