Merge branch 'master' into feat/swip-clear

This commit is contained in:
Yuzhong Zhang 2025-09-23 00:32:10 +08:00 committed by GitHub
commit 06bd39eec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 222 additions and 13 deletions

View File

@ -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

View File

@ -0,0 +1,102 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer <wengxt@gmail.com>
* 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 <exception>
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <fcitx-utils/flags.h>
#include <fcitx-utils/library.h>
#include <fcitx-utils/log.h>
#include <fcitx-utils/standardpaths.h>
#include <fcitx-utils/stringutils.h>
#include <fcitx/addoninfo.h>
#include <fcitx/addoninstance.h>
#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<std::string> 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<Library> libraries;
for (std::string_view libname: libnames) {
Flags<LibraryLoadHint> 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<AndroidSharedLibraryFactory>(
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

View File

@ -0,0 +1,85 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2016-2016 CSSlayer <wengxt@gmail.com>
* 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 <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <fcitx-utils/library.h>
#include <fcitx-utils/standardpaths.h>
#include <fcitx-utils/stringutils.h>
#include <fcitx/addonfactory.h>
#include <fcitx/addoninfo.h>
#include <fcitx/addoninstance.h>
#include <fcitx/addonloader.h>
namespace fcitx {
namespace {
constexpr char FCITX_ADDON_FACTORY_ENTRY[] = "fcitx_addon_factory_instance";
}
class AndroidSharedLibraryFactory {
public:
explicit AndroidSharedLibraryFactory(const AddonInfo &info, std::vector<Library> 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<AddonFactory *()>(funcPtr);
factory_ = func();
if (!factory_) {
throw std::runtime_error("Failed to get a factory");
}
}
AddonFactory *factory() { return factory_; }
private:
std::vector<Library> 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<std::string, std::vector<std::filesystem::path>>{},
Flags<StandardPathsOption>(StandardPathsOption::SkipBuiltInPath)
);
// Android specific end
std::unordered_map<std::string, std::unique_ptr<AndroidSharedLibraryFactory>>
registry_;
};
} // namespace fcitx
#endif //FCITX5_ANDROID_ANDROIDADDONLOADER_H

View File

@ -40,6 +40,7 @@
#include <boost/iostreams/stream_buffer.hpp>
#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<void(fcitx::AddonInstance *)> &setupCallback) {
p_instance = std::make_unique<fcitx::Instance>(0, nullptr);
p_instance->addonManager().registerDefaultLoader(nullptr);
p_instance->addonManager().registerLoader(std::make_unique<fcitx::AndroidSharedLibraryLoader>());
p_dispatcher = std::make_unique<fcitx::EventDispatcher>();
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

View File

@ -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()
}
}

View File

@ -43,6 +43,13 @@ class PagedCandidatesUi(
}
private val candidatesAdapter = object : RecyclerView.Adapter<UiHolder>() {
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")

View File

@ -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))

View File

@ -21,6 +21,10 @@ import splitties.views.setPaddingDp
open class HorizontalCandidateViewAdapter(val theme: Theme) :
RecyclerView.Adapter<CandidateViewHolder>() {
init {
setHasStableIds(true)
}
var candidates: Array<String> = 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)

View File

@ -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) {

View File

@ -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) {

View File

@ -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<PinyinDiction
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@SuppressLint("UseKtx")
args.uri?.let { importFromUri(Uri.parse(it)) }
super.onViewCreated(view, savedInstanceState)
}

View File

@ -26,6 +26,9 @@ open class AndroidBaseConventionPlugin : Plugin<Project> {
sourceCompatibility = Versions.java
targetCompatibility = Versions.java
}
lint {
disable += setOf("UseKtx")
}
}
target.tasks.withType<KotlinCompile> {

View File

@ -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)))
}
}