From cf4fb49edcf5bb7faf9550979a9c789cdf26b949 Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 21 Sep 2025 14:15:16 +0800 Subject: [PATCH 1/4] Disable UseKtx lint rule globally --- .../java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt | 2 -- .../main/java/org/fcitx/fcitx5/android/ui/main/MainActivity.kt | 2 -- .../android/ui/main/settings/PinyinDictionaryFragment.kt | 2 -- .../convention/src/main/kotlin/AndroidBaseConventionPlugin.kt | 3 +++ .../org/fcitx/fcitx5/android/lib/plugin_base/AboutActivity.kt | 1 - 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt index 0cced119..67e56b05 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/AboutFragment.kt @@ -4,7 +4,6 @@ */ package org.fcitx.fcitx5.android.ui.main -import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.os.Bundle @@ -20,7 +19,6 @@ import org.fcitx.fcitx5.android.utils.navigateWithAnim class AboutFragment : PaddingPreferenceFragment() { - @SuppressLint("UseKtx") override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()).apply { addPreference(R.string.privacy_policy) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainActivity.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainActivity.kt index 20fdf18e..e7650d03 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainActivity.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/MainActivity.kt @@ -5,7 +5,6 @@ package org.fcitx.fcitx5.android.ui.main import android.Manifest -import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -137,7 +136,6 @@ class MainActivity : AppCompatActivity() { } val aboutMenuItems = listOf( menu.item(R.string.faq) { - @SuppressLint("UseKtx") startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Const.faqUrl))) }, menu.item(R.string.developer) { diff --git a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PinyinDictionaryFragment.kt b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PinyinDictionaryFragment.kt index 4d60266a..1749220f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PinyinDictionaryFragment.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/ui/main/settings/PinyinDictionaryFragment.kt @@ -4,7 +4,6 @@ */ package org.fcitx.fcitx5.android.ui.main.settings -import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager import android.net.Uri @@ -108,7 +107,6 @@ class PinyinDictionaryFragment : Fragment(), OnItemChangedListener { sourceCompatibility = Versions.java targetCompatibility = Versions.java } + lint { + disable += setOf("UseKtx") + } } target.tasks.withType { diff --git a/lib/plugin-base/src/main/java/org/fcitx/fcitx5/android/lib/plugin_base/AboutActivity.kt b/lib/plugin-base/src/main/java/org/fcitx/fcitx5/android/lib/plugin_base/AboutActivity.kt index db465c1b..f13cc5cd 100644 --- a/lib/plugin-base/src/main/java/org/fcitx/fcitx5/android/lib/plugin_base/AboutActivity.kt +++ b/lib/plugin-base/src/main/java/org/fcitx/fcitx5/android/lib/plugin_base/AboutActivity.kt @@ -221,7 +221,6 @@ class AboutActivity : PreferenceActivity() { private fun showLicenseContent(license: License) { if (license.url?.isNotBlank() == true) { - @SuppressLint("UseKtx") startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(license.url))) } } From bc2d6735f51d15277dc6dd4d4154ddfd26559f69 Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 21 Sep 2025 18:19:20 +0800 Subject: [PATCH 2/4] Implement "stable ID" for candidate adapters prevent crash in FlexboxLayoutManager and hopefully improves performance ref: https://github.com/google/flexbox-layout/issues/363#issuecomment-382949953 --- .../input/candidates/floating/PagedCandidatesUi.kt | 8 ++++++++ .../candidates/horizontal/HorizontalCandidateComponent.kt | 1 + .../horizontal/HorizontalCandidateViewAdapter.kt | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt index f0684d18..746266bf 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/floating/PagedCandidatesUi.kt @@ -43,6 +43,13 @@ class PagedCandidatesUi( } private val candidatesAdapter = object : RecyclerView.Adapter() { + init { + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long = + data.candidates[position].hashCode().toLong() + override fun getItemCount() = data.candidates.size + (if (data.hasPrev || data.hasNext) 1 else 0) @@ -98,6 +105,7 @@ class PagedCandidatesUi( adapter = candidatesAdapter layoutManager = candidatesLayoutManager overScrollMode = View.OVER_SCROLL_NEVER + itemAnimator = null } @SuppressLint("NotifyDataSetChanged") diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt index 8ee14370..29719e4b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt @@ -163,6 +163,7 @@ class HorizontalCandidateComponent : } }.apply { id = R.id.candidate_view + itemAnimator = null adapter = this@HorizontalCandidateComponent.adapter layoutManager = this@HorizontalCandidateComponent.layoutManager addItemDecoration(FlexboxVerticalDecoration(dividerDrawable)) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateViewAdapter.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateViewAdapter.kt index f2d4c78b..054bea9f 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateViewAdapter.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateViewAdapter.kt @@ -21,6 +21,10 @@ import splitties.views.setPaddingDp open class HorizontalCandidateViewAdapter(val theme: Theme) : RecyclerView.Adapter() { + init { + setHasStableIds(true) + } + var candidates: Array = arrayOf() private set @@ -36,6 +40,10 @@ open class HorizontalCandidateViewAdapter(val theme: Theme) : override fun getItemCount() = candidates.size + override fun getItemId(position: Int): Long { + return candidates[position].hashCode().toLong() + } + @CallSuper override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CandidateViewHolder { val ui = CandidateItemUi(parent.context, theme) From e48d07125c854e07f6997de53fcb7cb92c825551 Mon Sep 17 00:00:00 2001 From: Rocka Date: Sun, 21 Sep 2025 22:23:24 +0800 Subject: [PATCH 3/4] Implement getBaseline for AutoScaleTextView --- .../org/fcitx/fcitx5/android/input/AutoScaleTextView.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt index 0fb52735..37ada23b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/AutoScaleTextView.kt @@ -17,6 +17,7 @@ import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt @SuppressLint("AppCompatCustomView") class AutoScaleTextView @JvmOverloads constructor( @@ -129,7 +130,7 @@ class AutoScaleTextView @JvmOverloads constructor( @SuppressLint("RtlHardcoded") val shouldAlignLeft = gravity and Gravity.HORIZONTAL_GRAVITY_MASK == Gravity.LEFT - if (textWidth >= contentWidth) { + if (textWidth > contentWidth) { when (scaleMode) { Mode.None -> { textScaleX = 1.0f @@ -149,9 +150,9 @@ class AutoScaleTextView @JvmOverloads constructor( } } } else { - translateX = if (shouldAlignLeft) leftAlignOffset else centerAlignOffset textScaleX = 1.0f textScaleY = 1.0f + translateX = if (shouldAlignLeft) leftAlignOffset else centerAlignOffset } val fontHeight = (fontMetrics.bottom - fontMetrics.top) * textScaleY val fontOffsetY = fontMetrics.top * textScaleY @@ -176,4 +177,8 @@ class AutoScaleTextView @JvmOverloads constructor( override fun getTextScaleX(): Float { return textScaleX } + + override fun getBaseline(): Int { + return (-fontMetrics.top * textScaleY).roundToInt() + } } From 7d408e61cae78b264c413004a920061cb4102083 Mon Sep 17 00:00:00 2001 From: Rocka Date: Mon, 22 Sep 2025 23:26:24 +0800 Subject: [PATCH 4/4] Fix loading addon library from plugins partially reverts c57e8b4618d1380c9d1bac3d70bb1a331961daf0 --- app/src/main/cpp/CMakeLists.txt | 2 +- .../androidaddonloader/androidaddonloader.cpp | 102 ++++++++++++++++++ .../androidaddonloader/androidaddonloader.h | 85 +++++++++++++++ app/src/main/cpp/native-lib.cpp | 10 +- 4 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 app/src/main/cpp/androidaddonloader/androidaddonloader.cpp create mode 100644 app/src/main/cpp/androidaddonloader/androidaddonloader.h diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 9777b761..96454f12 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -44,7 +44,7 @@ add_library(pinyin-customphrase STATIC "${CHINESE_ADDONS_PINYIN_DIR}/customphras 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) +add_library(native-lib SHARED native-lib.cpp androidaddonloader/androidaddonloader.cpp) target_link_libraries(native-lib log libuv::uv_a diff --git a/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp b/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp new file mode 100644 index 00000000..500d9b23 --- /dev/null +++ b/app/src/main/cpp/androidaddonloader/androidaddonloader.cpp @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer + * SPDX-FileCopyrightText: Copyright 2023-2025 Fcitx5 for Android Contributors + * SPDX-FileComment: Modified from https://github.com/fcitx/fcitx5/blob/5.1.14/src/lib/fcitx/addonloader.cpp + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define FCITX_LIBRARY_SUFFIX ".so" + +#include "androidaddonloader.h" + +namespace fcitx { + +AddonInstance *AndroidSharedLibraryLoader::load(const AddonInfo &info, + AddonManager *manager) { + auto iter = registry_.find(info.uniqueName()); + if (iter == registry_.end()) { + std::vector libnames = + stringutils::split(info.library(), ";"); + + if (libnames.empty()) { + FCITX_ERROR() << "Failed to parse Library field: " << info.library() + << " for addon " << info.uniqueName(); + return nullptr; + } + + std::vector libraries; + for (std::string_view libname: libnames) { + Flags flag = LibraryLoadHint::DefaultHint; + if (stringutils::consumePrefix(libname, "export:")) { + flag |= LibraryLoadHint::ExportExternalSymbolsHint; + } + const auto file = + stringutils::concat(libname, FCITX_LIBRARY_SUFFIX); + const auto libraryPaths = standardPaths_.locateAll( + StandardPathsType::Addon, file); + if (libraryPaths.empty()) { + FCITX_ERROR() << "Could not locate library " << file + << " for addon " << info.uniqueName() << "."; + } + bool loaded = false; + for (const auto &libraryPath: libraryPaths) { + Library library(libraryPath); + if (library.load(flag)) { + libraries.push_back(std::move(library)); + loaded = true; + break; + } + FCITX_ERROR() + << "Failed to load library for addon " << info.uniqueName() + << " on " << libraryPath << ". Error: " << library.error(); + } + if (!loaded) { + break; + } + } + + if (libraries.size() == libnames.size()) { + try { + registry_.emplace(info.uniqueName(), + std::make_unique( + info, std::move(libraries))); + } catch (const std::exception &e) { + FCITX_ERROR() << "Failed to initialize addon factory for addon " + << info.uniqueName() << ". Error: " << e.what(); + } + iter = registry_.find(info.uniqueName()); + } + } + + if (iter == registry_.end()) { + return nullptr; + } + + try { + return iter->second->factory()->create(manager); + } catch (const std::exception &e) { + FCITX_ERROR() << "Failed to create addon: " << info.uniqueName() << " " + << e.what(); + } catch (...) { + FCITX_ERROR() << "Failed to create addon: " << info.uniqueName(); + } + return nullptr; +} + +} // namespace fcitx \ No newline at end of file diff --git a/app/src/main/cpp/androidaddonloader/androidaddonloader.h b/app/src/main/cpp/androidaddonloader/androidaddonloader.h new file mode 100644 index 00000000..82e90edc --- /dev/null +++ b/app/src/main/cpp/androidaddonloader/androidaddonloader.h @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer + * SPDX-FileCopyrightText: Copyright 2023-2025 Fcitx5 for Android Contributors + * SPDX-FileComment: Modified from https://github.com/fcitx/fcitx5/blob/5.1.14/src/lib/fcitx/addonloader_p.h + */ + +#ifndef FCITX5_ANDROID_ANDROIDADDONLOADER_H +#define FCITX5_ANDROID_ANDROIDADDONLOADER_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace fcitx { + +namespace { +constexpr char FCITX_ADDON_FACTORY_ENTRY[] = "fcitx_addon_factory_instance"; +} + +class AndroidSharedLibraryFactory { +public: + explicit AndroidSharedLibraryFactory(const AddonInfo &info, std::vector libraries) + : libraries_(std::move(libraries)) { + std::string v2Name = stringutils::concat(FCITX_ADDON_FACTORY_ENTRY, "_", + info.uniqueName()); + if (libraries_.empty()) { + throw std::runtime_error("Got empty libraries."); + } + + // Only resolve with last library. + auto &library = libraries_.back(); + auto *funcPtr = library.resolve(v2Name.data()); + if (!funcPtr) { + funcPtr = library.resolve(FCITX_ADDON_FACTORY_ENTRY); + } + if (!funcPtr) { + throw std::runtime_error(library.error()); + } + auto func = Library::toFunction(funcPtr); + factory_ = func(); + if (!factory_) { + throw std::runtime_error("Failed to get a factory"); + } + } + + AddonFactory *factory() { return factory_; } + +private: + std::vector libraries_; + AddonFactory *factory_; +}; + +class AndroidSharedLibraryLoader : public AddonLoader { +public: + [[nodiscard]] std::string type() const override { return "SharedLibrary"; } + + AddonInstance *load(const AddonInfo &info, AddonManager *manager) override; + +private: + // Android specific: create a new StandardPaths instance in case FCITX_ADDON_DIRS changes + StandardPaths standardPaths_ = StandardPaths( + "fcitx5", + std::unordered_map>{}, + Flags(StandardPathsOption::SkipBuiltInPath) + ); + // Android specific end + std::unordered_map> + registry_; +}; + +} // namespace fcitx + +#endif //FCITX5_ANDROID_ANDROIDADDONLOADER_H diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index bdeb1719..6ab758b0 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -40,6 +40,7 @@ #include #include "customphrase.h" +#include "androidaddonloader/androidaddonloader.h" #include "androidfrontend/androidfrontend_public.h" #include "jni-utils.h" #include "nativestreambuf.h" @@ -82,7 +83,7 @@ public: void startup(const std::function &setupCallback) { p_instance = std::make_unique(0, nullptr); - p_instance->addonManager().registerDefaultLoader(nullptr); + p_instance->addonManager().registerLoader(std::make_unique()); p_dispatcher = std::make_unique(); p_dispatcher->attach(&p_instance->eventLoop()); p_instance->initialize(); @@ -527,7 +528,10 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx( 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 libime_data = fcitx::stringutils::concat( + fcitx::stringutils::joinPath(*extData_, "data", "libime"), ":", + 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"), ";", @@ -542,7 +546,7 @@ Java_org_fcitx_fcitx5_android_core_Fcitx_startupFcitx( ); // prevent StandardPath from resolving it's hardcoded installation path - setenv("SKIP_FCITX_PATH", "1", 1); + // setenv("SKIP_FCITX_PATH", "1", 1); // for fcitx default profile [DefaultInputMethod] setenv("LANG", lang_.c_str(), 1); // for libintl-lite loading gettext .mo translations