diff --git a/.gitignore b/.gitignore
index eb3db7f1c..a5dadb863 100755
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,7 @@ fastlane/Appfile
*.keystore
keystore.properties
signing.properties
+app/src/main/assets/adi-registration.properties
# Build logs
*.log
diff --git a/README.md b/README.md
index ead859876..9c915dfbe 100644
--- a/README.md
+++ b/README.md
@@ -50,12 +50,28 @@ This fork adds **optional AI-powered features** using Gemini, Groq, and OpenAI-c
## Download
-
-
-
-
-
-
+
+
+> [!NOTE]
+> F-Droid is currently stuck on v3.7.6 due to baseline profile issues. For the latest version, use GitHub or Obtainium.
### 📦 Choose Your Version
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 749812534..97a09abd7 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,8 +22,8 @@ android {
applicationId = "com.leanbitlab.leantype"
minSdk = 21
targetSdk = 35
- versionCode = 3709
- versionName = "3.7.9"
+ versionCode = 3800
+ versionName = "3.8.0"
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@@ -140,6 +140,10 @@ android {
// false is required for Android 16+ 16-KB page alignment compatibility on prebuilts.
useLegacyPackaging = false
}
+ resources {
+ excludes += "assets/dexopt/baseline.prof"
+ excludes += "assets/dexopt/baseline.profm"
+ }
}
@@ -218,3 +222,10 @@ dependencies {
testImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
+
+// Disable baseline/ART profile tasks to guarantee deterministic reproducible builds
+tasks.configureEach {
+ if (name.contains("ArtProfile", ignoreCase = true)) {
+ enabled = false
+ }
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 3daf31732..5feeb81f6 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,5 +1,5 @@
-# Keep native methods
--keepclassmembers class * {
+# Keep classes that contain native methods
+-keep class * {
native ;
}
@@ -34,3 +34,8 @@
# Fix correct service name
-keep class helium314.keyboard.latin.utils.ProofreadService { *; }
+
+# Suppress warnings for missing library dependencies in R8 Full Mode
+-dontwarn com.google.api.client.**
+-dontwarn java.lang.management.**
+-dontwarn org.joda.time.**
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
index a9e4440f0..c78bd5429 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
@@ -140,38 +140,7 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
if (PointerTracker.sPersistentTouchpadModeActive) {
val touchpadView = keyboardSwitcher.touchpadView
if (touchpadView != null) {
- touchpadView.setTouchpadListener(object : TouchpadView.TouchpadListener {
- override fun onCursorMove(keyCode: Int, isSelecting: Boolean) {
- if (isSelecting) {
- val androidKeyCode = when (keyCode) {
- KeyCode.ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
- KeyCode.ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
- KeyCode.ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
- KeyCode.ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
- else -> 0
- }
- if (androidKeyCode != 0) {
- val eventTime = android.os.SystemClock.uptimeMillis()
- // Send SHIFT down to force selection mode at the InputConnection level
- connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
-
- connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, androidKeyCode, 0, KeyEvent.META_SHIFT_ON))
- connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, androidKeyCode, 0, KeyEvent.META_SHIFT_ON))
-
- // Release SHIFT
- connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
- }
- } else {
- onCodeInput(keyCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
- }
- }
- override fun onSingleTap() {
- onCodeInput(Constants.CODE_ENTER, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
- }
- override fun onScroll(direction: Int) {
- onCodeInput(direction, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
- }
- })
+ setupTouchpadListener(touchpadView)
keyboardSwitcher.showTouchpadView()
}
} else {
@@ -563,6 +532,41 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
}
}
+ fun setupTouchpadListener(touchpadView: TouchpadView) {
+ touchpadView.setTouchpadListener(object : TouchpadView.TouchpadListener {
+ override fun onCursorMove(keyCode: Int, isSelecting: Boolean) {
+ if (isSelecting) {
+ val androidKeyCode = when (keyCode) {
+ KeyCode.ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
+ KeyCode.ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
+ KeyCode.ARROW_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
+ KeyCode.ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
+ else -> 0
+ }
+ if (androidKeyCode != 0) {
+ val eventTime = android.os.SystemClock.uptimeMillis()
+ // Send SHIFT down to force selection mode at the InputConnection level
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
+
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, androidKeyCode, 0, KeyEvent.META_SHIFT_ON))
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, androidKeyCode, 0, KeyEvent.META_SHIFT_ON))
+
+ // Release SHIFT
+ connection.sendKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0))
+ }
+ } else {
+ onCodeInput(keyCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ }
+ }
+ override fun onSingleTap() {
+ onCodeInput(Constants.CODE_ENTER, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ }
+ override fun onScroll(direction: Int) {
+ onCodeInput(direction, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
+ }
+ })
+ }
+
companion object {
private enum class MetaPressState {
UNSET, // default state, not active
diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
index a6c5b59fa..12941b6e4 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
@@ -335,7 +335,7 @@ private void setMainKeyboardFrame(
mStripContainer.setVisibility(stripVisibility);
PointerTracker.switchTo(mKeyboardView);
if (PointerTracker.sPersistentTouchpadModeActive) {
- mKeyboardView.setVisibility(View.GONE);
+ mKeyboardView.setVisibility(visibility == View.VISIBLE ? View.INVISIBLE : View.GONE);
} else {
mKeyboardView.setVisibility(visibility);
}
@@ -357,6 +357,12 @@ private void setMainKeyboardFrame(
if (mTouchpadView != null) {
mTouchpadView.setVisibility(visibility);
mTouchpadView.applyColors(Settings.getValues().mColors);
+ mTouchpadView.setPadding(
+ mKeyboardView.getPaddingLeft(),
+ mKeyboardView.getPaddingTop(),
+ mKeyboardView.getPaddingRight(),
+ mKeyboardView.getPaddingBottom()
+ );
}
} else {
if (mTouchpadView != null) mTouchpadView.setVisibility(View.GONE);
@@ -557,30 +563,20 @@ public void toggleFloatingKeyboard() {
public void showTouchpadView() {
if (mTouchpadView == null) return;
- // Get keyboard height before hiding it
- int keyboardHeight = mKeyboardView.getHeight();
- if (keyboardHeight <= 0) {
- keyboardHeight = mKeyboardView.getMeasuredHeight();
- }
- mKeyboardView.setVisibility(View.GONE);
+ mKeyboardView.setVisibility(View.INVISIBLE);
mEmojiPalettesView.setVisibility(View.GONE);
mClipboardHistoryView.setVisibility(View.GONE);
// Hide one-handed mode buttons to prevent overlap
mKeyboardViewWrapper.findViewById(R.id.btn_stop_one_handed_mode).setVisibility(View.GONE);
mKeyboardViewWrapper.findViewById(R.id.btn_switch_one_handed_mode).setVisibility(View.GONE);
mKeyboardViewWrapper.findViewById(R.id.btn_resize_one_handed_mode).setVisibility(View.GONE);
- // Set touchpad height to match the keyboard
- if (keyboardHeight > 0) {
- mTouchpadView.setLayoutParams(new android.widget.FrameLayout.LayoutParams(
- android.widget.FrameLayout.LayoutParams.MATCH_PARENT, keyboardHeight));
- // Apply bottom padding to avoid overlapping the navigation bar
- mTouchpadView.setPadding(
- mKeyboardView.getPaddingLeft(),
- mKeyboardView.getPaddingTop(),
- mKeyboardView.getPaddingRight(),
- mKeyboardView.getPaddingBottom()
- );
- }
+ // Apply bottom padding to avoid overlapping the navigation bar
+ mTouchpadView.setPadding(
+ mKeyboardView.getPaddingLeft(),
+ mKeyboardView.getPaddingTop(),
+ mKeyboardView.getPaddingRight(),
+ mKeyboardView.getPaddingBottom()
+ );
mTouchpadView.applyColors(Settings.getValues().mColors);
mTouchpadView.setVisibility(View.VISIBLE);
mMainKeyboardFrame.setVisibility(View.VISIBLE);
@@ -851,6 +847,22 @@ public View onCreateInputView(@NonNull Context displayContext, final boolean isH
PointerTracker.switchTo(mKeyboardView);
mTouchpadView = mCurrentInputView.findViewById(R.id.touchpad_view);
+ if (PointerTracker.sPersistentTouchpadModeActive && mTouchpadView != null) {
+ if (mLatinIME.mKeyboardActionListener instanceof KeyboardActionListenerImpl) {
+ ((KeyboardActionListenerImpl) mLatinIME.mKeyboardActionListener).setupTouchpadListener(mTouchpadView);
+ }
+ }
+
+ mKeyboardView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if (mTouchpadView != null && mTouchpadView.getVisibility() == View.VISIBLE) {
+ mTouchpadView.setPadding(
+ mKeyboardView.getPaddingLeft(),
+ mKeyboardView.getPaddingTop(),
+ mKeyboardView.getPaddingRight(),
+ mKeyboardView.getPaddingBottom()
+ );
+ }
+ });
return mCurrentInputView;
}
diff --git a/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java b/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java
index 377b3e587..c97b7a0d0 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java
+++ b/app/src/main/java/helium314/keyboard/keyboard/TouchpadView.java
@@ -78,6 +78,7 @@ private void init(Context context) {
// Consume all touches so nothing passes through to views behind
setClickable(true);
setFocusable(true);
+ setFitsSystemWindows(true);
LayoutInflater.from(context).inflate(R.layout.touchpad_view, this, true);
mTouchpadSurface = findViewById(R.id.touchpad_surface);
diff --git a/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryRecyclerView.kt b/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryRecyclerView.kt
index abce06697..97dd0a85e 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryRecyclerView.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryRecyclerView.kt
@@ -59,6 +59,9 @@ class ClipboardHistoryRecyclerView @JvmOverloads constructor(
undoHandler.removeCallbacks(undoDismissRunnable)
lastDeletedEntry = entry
+ // Dismiss confirmation bar if active
+ (parent as? View)?.findViewById(R.id.clipboard_confirmation_bar)?.visibility = View.GONE
+
// Find the undo bar from our parent hierarchy (it's a sibling in the FrameLayout)
val bar = undoBar ?: (parent as? View)?.findViewById(R.id.clipboard_undo_bar)
undoBar = bar ?: return
diff --git a/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryView.kt b/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryView.kt
index 677c906b4..3d6743fe7 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryView.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/clipboard/ClipboardHistoryView.kt
@@ -101,6 +101,10 @@ class ClipboardHistoryView @JvmOverloads constructor(
private lateinit var emptyViewContainer: View
private lateinit var listContainer: View
+ private var confirmationBar: View? = null
+ private val confirmationDismissRunnable = Runnable { dismissConfirmationBar() }
+ private val confirmationHandler = android.os.Handler(android.os.Looper.getMainLooper())
+
private var editorInfo: EditorInfo? = null
// We already have keyboardActionListener property
@@ -181,6 +185,20 @@ class ClipboardHistoryView @JvmOverloads constructor(
persistentDrawingCache = PERSISTENT_NO_CACHE
clipboardLayoutParams.setListProperties(this)
}
+
+ confirmationBar = findViewById(R.id.clipboard_confirmation_bar)
+ confirmationBar?.let { bar ->
+ try {
+ colors.setBackground(bar, ColorType.CLIPBOARD_SUGGESTION_BACKGROUND)
+ bar.findViewById(R.id.clipboard_confirmation_text)?.setTextColor(colors.get(ColorType.KEY_TEXT))
+ bar.findViewById(R.id.clipboard_confirmation_button)?.setTextColor(colors.get(ColorType.KEY_TEXT))
+ } catch (_: Exception) {}
+ bar.findViewById(R.id.clipboard_confirmation_button)?.setOnClickListener {
+ clipboardHistoryManager.clearHistory()
+ dismissConfirmationBar()
+ }
+ }
+
val clipboardStrip = KeyboardSwitcher.getInstance().clipboardStrip
toolbarKeys.forEach {
clipboardStrip.addView(it)
@@ -613,12 +631,26 @@ class ClipboardHistoryView @JvmOverloads constructor(
// Dismiss any active undo bar
clipboardRecyclerView.dismissUndoBar()
+ dismissConfirmationBar()
clipboardRecyclerView.adapter = null
clipboardHistoryManager.setHistoryChangeListener(null)
clipboardAdapter.clipboardHistoryManager = null
}
+ fun showClearAllConfirmationBar() {
+ clipboardRecyclerView.dismissUndoBar()
+ val bar = confirmationBar ?: return
+ confirmationHandler.removeCallbacks(confirmationDismissRunnable)
+ bar.visibility = View.VISIBLE
+ confirmationHandler.postDelayed(confirmationDismissRunnable, 5000)
+ }
+
+ fun dismissConfirmationBar() {
+ confirmationHandler.removeCallbacks(confirmationDismissRunnable)
+ confirmationBar?.visibility = View.GONE
+ }
+
override fun onClick(view: View) {
val tag = view.tag
if (tag is ToolbarKey) {
@@ -628,6 +660,10 @@ class ClipboardHistoryView @JvmOverloads constructor(
startSearchMode()
return
}
+ if (code == KeyCode.CLIPBOARD_CLEAR_HISTORY) {
+ showClearAllConfirmationBar()
+ return
+ }
if (code != KeyCode.UNSPECIFIED) {
keyboardActionListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
return
diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt
index 06fa6b2b9..d6576eab1 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt
@@ -264,16 +264,20 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
}
private fun addNumberRowOrPopupKeys(baseKeys: MutableList>, numberRow: MutableList) {
- if (!params.mId.mNumberRowEnabled && params.mId.mNumberRowInSymbols && params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) {
+ if (!params.mId.mNumberRowEnabled && params.mId.mNumberRowInSymbols && (params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED)) {
// replace first symbols row with number row, but use the labels as popupKeys
+ val symbolsRow = baseKeys.getOrNull(0) ?: return
val numberRowCopy = numberRow.toMutableList()
numberRowCopy.forEachIndexed { index, keyData ->
- val symbolKey = baseKeys[0].getOrNull(index) ?: return@forEachIndexed
+ val symbolKey = symbolsRow.getOrNull(index) ?: return@forEachIndexed
val symbols = mutableListOf()
symbolKey.label.takeIf { it.isNotEmpty() }?.let { symbols.add(it) }
symbolKey.popup.getPopupKeyLabels(params)?.let { symbols.addAll(it) }
if (symbols.isNotEmpty()) keyData.popup.symbols = symbols
}
+ if (symbolsRow.size > numberRowCopy.size) {
+ numberRowCopy.addAll(symbolsRow.subList(numberRowCopy.size, symbolsRow.size))
+ }
baseKeys[0] = numberRowCopy
} else if (!params.mId.mNumberRowEnabled && params.mId.isAlphabetKeyboard && !hasBuiltInNumbers()) {
if (baseKeys[0].any { it.popup.main != null || !it.popup.relevant.isNullOrEmpty() } // first row of baseKeys has any layout popup key
diff --git a/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt b/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
index 932518083..b8a8b8379 100644
--- a/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
@@ -18,6 +18,7 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.common.ColorType
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.utils.ResourceUtils
import helium314.keyboard.latin.utils.prefs
import kotlin.math.abs
@@ -132,6 +133,33 @@ class KeyboardWrapperView @JvmOverloads constructor(
}
}
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val keyboardView = findViewById(R.id.keyboard_view)
+ if (keyboardView == null) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ return
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+ val settingsValues = Settings.getValues()
+ val keyboardHeight = ResourceUtils.getKeyboardHeight(context.resources, settingsValues)
+ val padding = keyboardView.paddingTop + keyboardView.paddingBottom
+ val maxExpectedHeight = keyboardHeight + padding
+
+ if (measuredHeight > maxExpectedHeight && maxExpectedHeight > 0) {
+ setMeasuredDimension(measuredWidth, maxExpectedHeight)
+ // Re-measure children with the capped height
+ val exactHeightSpec = MeasureSpec.makeMeasureSpec(maxExpectedHeight, MeasureSpec.EXACTLY)
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ if (child.visibility != GONE) {
+ measureChildWithMargins(child, widthMeasureSpec, 0, exactHeightSpec, 0)
+ }
+ }
+ }
+ }
+
@SuppressLint("RtlHardcoded")
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
if (!oneHandedModeEnabled) {
diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
index 868c66b0c..b06e2fe59 100644
--- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java
+++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
@@ -846,7 +846,9 @@ public void onFinishInput() {
// Auto-dismiss floating keyboard when the input session ends
// (user navigated away from text input)
if (mFloatingKeyboardManager != null && mFloatingKeyboardManager.isFloating()) {
- mFloatingKeyboardManager.hide(false);
+ if (!Settings.getInstance().getCurrent().mPersistFloatingKeyboard) {
+ mFloatingKeyboardManager.hide(false);
+ }
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
index 9a5235383..c4f801962 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
@@ -10,6 +10,7 @@ object Links {
// LeanBitBoard fork repo
const val GITHUB = "https://github.com/LeanBitLab/HeliboardL"
const val LICENSE = "$GITHUB/blob/main/LICENSE"
+ const val SPONSOR = "https://github.com/sponsors/LeanBitLab"
// Original HeliBoard wiki and community links
const val ORIGINAL_GITHUB = "https://github.com/Helium314/HeliBoard"
const val LAYOUT_WIKI_URL = "$ORIGINAL_GITHUB/wiki/2.-Layouts"
diff --git a/app/src/main/java/helium314/keyboard/latin/database/ClipboardDao.kt b/app/src/main/java/helium314/keyboard/latin/database/ClipboardDao.kt
index 5c0bb4e83..adff05bdc 100644
--- a/app/src/main/java/helium314/keyboard/latin/database/ClipboardDao.kt
+++ b/app/src/main/java/helium314/keyboard/latin/database/ClipboardDao.kt
@@ -54,6 +54,7 @@ class ClipboardDao private constructor(private val db: Database) {
sort()
}
+ @Synchronized
fun addClip(timestamp: Long, pinned: Boolean, text: String, imageUri: String? = null) {
clearOldClips()
val existingIndex = cache.indexOfFirst { it.text == text && it.imageUri == imageUri }
@@ -66,6 +67,7 @@ class ClipboardDao private constructor(private val db: Database) {
insertNewEntry(timestamp, pinned, text, imageUri)
}
+ @Synchronized
private fun insertNewEntry(timestamp: Long, pinned: Boolean, text: String, imageUri: String?) {
val cv = ContentValues(4)
cv.put(COLUMN_TIMESTAMP, timestamp)
@@ -80,6 +82,7 @@ class ClipboardDao private constructor(private val db: Database) {
listener?.onClipInserted(cache.indexOf(entry))
}
+ @Synchronized
private fun updateTimestampAt(index: Int, timestamp: Long) {
val entry = cache[index]
entry.timeStamp = timestamp
@@ -90,16 +93,22 @@ class ClipboardDao private constructor(private val db: Database) {
db.writableDatabase.update(TABLE, cv, "$COLUMN_ID = ${entry.id}", null)
}
+ @Synchronized
fun isPinned(index: Int) = cache[index].isPinned
+ @Synchronized
fun getAt(index: Int) = cache[index]
+ @Synchronized
fun getClips(): List = cache.toList()
+ @Synchronized
fun get(id: Long) = cache.first { it.id == id }
+ @Synchronized
fun count() = cache.size
+ @Synchronized
fun sort() = cache.sort()
/**
@@ -114,6 +123,7 @@ class ClipboardDao private constructor(private val db: Database) {
* (empty text or not found). If the entry was deleted because of empty text, the
* deleted [ClipboardHistoryEntry] is returned via [deletedOut] for undo support.
*/
+ @Synchronized
fun updateText(id: Long, newText: String, deletedOut: Array? = null): Int {
val index = cache.indexOfFirst { it.id == id }
if (index < 0) return -1
@@ -172,6 +182,7 @@ class ClipboardDao private constructor(private val db: Database) {
return newPos
}
+ @Synchronized
fun togglePinned(id: Long) {
val entry = cache.first { it.id == id }
entry.isPinned = !entry.isPinned
@@ -191,6 +202,7 @@ class ClipboardDao private constructor(private val db: Database) {
}
// RecyclerView initiates this, so we don't call listener (or we'll get an IndexOutOfRangeException from RecyclerView)
+ @Synchronized
fun deleteClipAt(index: Int): ClipboardHistoryEntry? {
if (index < 0 || index >= cache.size) return null
val entry = cache[index]
@@ -200,6 +212,7 @@ class ClipboardDao private constructor(private val db: Database) {
}
/** Re-insert a previously deleted entry (for undo). */
+ @Synchronized
fun restoreClip(entry: ClipboardHistoryEntry) {
val cv = ContentValues(5)
cv.put(COLUMN_ID, entry.id)
@@ -213,6 +226,7 @@ class ClipboardDao private constructor(private val db: Database) {
listener?.onClipInserted(cache.indexOf(entry))
}
+ @Synchronized
fun clearOldClips(now: Boolean = false) {
if (listener != null)
return // never clear when clipboard is visible
@@ -229,6 +243,7 @@ class ClipboardDao private constructor(private val db: Database) {
db.writableDatabase.delete(TABLE, "$COLUMN_TIMESTAMP < $minTime AND $COLUMN_PINNED = 0", null)
}
+ @Synchronized
fun clearNonPinned() {
if (listener != null) {
val indicesToRemove = mutableListOf()
@@ -246,6 +261,7 @@ class ClipboardDao private constructor(private val db: Database) {
db.writableDatabase.delete(TABLE, "$COLUMN_PINNED = 0", null)
}
+ @Synchronized
fun clear() {
if (count() == 0) return
cache.clear()
@@ -277,6 +293,7 @@ class ClipboardDao private constructor(private val db: Database) {
private var instance: ClipboardDao? = null
/** Returns the instance or creates a new one. Returns null if instance can't be created (e.g. no access to db due to device being locked) */
+ @Synchronized
fun getInstance(context: Context): ClipboardDao? {
if (instance == null)
try {
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
index 4f7ed1340..073071296 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
@@ -85,6 +85,7 @@ object Defaults {
"hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty"
const val PREF_ENABLE_SPLIT_KEYBOARD = false
const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false
+ const val PREF_PERSIST_FLOATING_KEYBOARD = false
@JvmField
val PREF_SPLIT_SPACER_SCALE = Array(2) { DEFAULT_SIZE_SCALE }
@JvmField
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
index 96c8d1840..e99d3e22e 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
@@ -200,6 +200,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_SPACE_TO_CHANGE_LANG = "prefs_long_press_keyboard_to_change_lang";
public static final String PREF_LANGUAGE_SWIPE_DISTANCE = "language_swipe_distance";
public static final String PREF_TOUCHPAD_SENSITIVITY = "touchpad_sensitivity";
+ public static final String PREF_PERSIST_FLOATING_KEYBOARD = "persist_floating_keyboard";
public static final String PREF_FORCE_AUTO_CAPS = "force_auto_caps";
public static final String PREF_OFFLINE_TEMP = "offline_temp";
public static final String PREF_OFFLINE_TOP_P = "offline_top_p";
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
index 95a12386b..53c51266c 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
@@ -184,9 +184,10 @@ public class SettingsValues {
public final boolean mAutoCorrectionEnabledPerUserSettings;
public final boolean mAutoCorrectEnabled;
public final float mAutoCorrectionThreshold;
+ public final boolean mAutoCorrectShortcuts;
+ public final boolean mPersistFloatingKeyboard;
public final boolean mBackspaceRevertsAutocorrect;
public final int mScoreLimitForAutocorrect;
- public final boolean mAutoCorrectShortcuts;
private final boolean mSuggestionsEnabledPerUserSettings;
private final boolean mOverrideShowingSuggestions;
public final boolean mSuggestClipboardContent;
@@ -278,6 +279,8 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
: (mAutoCorrectionThreshold < 0.07 ? 800000 : 950000); // aggressive or modest
mAutoCorrectShortcuts = prefs.getBoolean(Settings.PREF_AUTOCORRECT_SHORTCUTS,
Defaults.PREF_AUTOCORRECT_SHORTCUTS);
+ mPersistFloatingKeyboard = prefs.getBoolean(Settings.PREF_PERSIST_FLOATING_KEYBOARD,
+ Defaults.PREF_PERSIST_FLOATING_KEYBOARD);
mBackspaceRevertsAutocorrect = prefs.getBoolean(Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT,
Defaults.PREF_BACKSPACE_REVERTS_AUTOCORRECT);
mBigramPredictionEnabled = prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS,
diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt b/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt
index fffd5e436..5339bd204 100644
--- a/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SettingsContainer.kt
@@ -87,9 +87,11 @@ object SettingsWithoutKey {
const val LICENSE = "license"
const val HIDDEN_FEATURES = "hidden_features"
const val GITHUB = "github"
+ const val SPONSOR = "sponsor"
const val GITHUB_WIKI = "github_wiki"
const val SAVE_LOG = "save_log"
const val BACKUP_RESTORE = "backup_restore"
+ const val PERSIST_FLOATING_KEYBOARD = "persist_floating_keyboard"
const val DEBUG_SETTINGS = "screen_debug"
const val LOAD_GESTURE_LIB = "load_gesture_library"
const val BACKGROUND_IMAGE = "background_image"
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt
index 8bb14379b..55bda2a7b 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt
@@ -58,6 +58,7 @@ fun AboutScreen(
SettingsWithoutKey.HIDDEN_FEATURES,
SettingsWithoutKey.GITHUB_WIKI,
SettingsWithoutKey.GITHUB,
+ SettingsWithoutKey.SPONSOR,
SettingsWithoutKey.SAVE_LOG,
)
SearchSettingsScreen(
@@ -160,6 +161,21 @@ fun createAboutSettings(context: Context) = listOf(
icon = R.drawable.ic_settings_about_github
)
},
+ Setting(context, SettingsWithoutKey.SPONSOR, R.string.about_sponsor_link, R.string.about_sponsor_link_description) {
+ val ctx = LocalContext.current
+ Preference(
+ name = it.title,
+ description = it.description,
+ onClick = {
+ val intent = Intent()
+ intent.data = Links.SPONSOR.toUri()
+ intent.action = Intent.ACTION_VIEW
+ ctx.startActivity(intent)
+ },
+ // Re-using the github icon or using a generic heart/sponsor icon if available. We will use ic_settings_about_github for now or a generic one. Let's see what icons we have. Let's try R.drawable.ic_keyboard_settings or similar, actually R.drawable.ic_settings_about_github might be okay if we don't have a sponsor one. Wait, let's look for a heart icon. Let's just use ic_settings_about_github for now and change it if needed.
+ icon = R.drawable.ic_settings_about_github
+ )
+ },
Setting(context, SettingsWithoutKey.SAVE_LOG, R.string.save_log) { setting ->
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt
index fe3bc7e32..b790e8609 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt
@@ -66,6 +66,7 @@ fun AppearanceScreen(
SettingsWithoutKey.BACKGROUND_IMAGE,
SettingsWithoutKey.BACKGROUND_IMAGE_LANDSCAPE,
R.string.settings_category_miscellaneous,
+ Settings.PREF_PERSIST_FLOATING_KEYBOARD,
Settings.PREF_ENABLE_SPLIT_KEYBOARD,
Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE,
if (prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE, Defaults.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE)
@@ -201,6 +202,9 @@ fun createAppearanceSettings(context: Context) = listOf(
Setting(context, Settings.PREF_ENABLE_SPLIT_KEYBOARD, R.string.enable_split_keyboard) {
SwitchPreference(it, Defaults.PREF_ENABLE_SPLIT_KEYBOARD) { KeyboardSwitcher.getInstance().reloadKeyboard() }
},
+ Setting(context, Settings.PREF_PERSIST_FLOATING_KEYBOARD, R.string.persist_floating_keyboard_title, R.string.persist_floating_keyboard_summary) {
+ SwitchPreference(it, Defaults.PREF_PERSIST_FLOATING_KEYBOARD)
+ },
Setting(context, Settings.PREF_SPLIT_SPACER_SCALE_PREFIX, R.string.split_spacer_scale) { setting ->
MultiSliderPreference(
name = setting.title,
diff --git a/app/src/main/jni/Android.mk b/app/src/main/jni/Android.mk
index bf7c430f4..522ecbc30 100755
--- a/app/src/main/jni/Android.mk
+++ b/app/src/main/jni/Android.mk
@@ -60,6 +60,10 @@ endif # FLAG_FULL_DBG
endif # FLAG_DBG
endif # FLAG_DO_PROFILE
+ifneq ($(FLAG_DBG), true)
+ LOCAL_CFLAGS += -flto
+endif
+
LOCAL_MODULE := libjni_latinime_common_static
LOCAL_MODULE_TAGS := optional
@@ -92,6 +96,11 @@ LOCAL_SDK_VERSION := 14
LOCAL_NDK_STL_VARIANT := c++_static
LOCAL_LDFLAGS += -ldl -Wl,-z,max-page-size=16384
+ifneq ($(FLAG_DBG), true)
+ LOCAL_CFLAGS += -flto
+ LOCAL_LDFLAGS += -flto
+endif
+
# Avoid issues with reproducible builds, see https://gitlab.com/fdroid/rfp/-/issues/2662
LOCAL_LDFLAGS += -Wl,--build-id=none
diff --git a/app/src/main/res/layout/clipboard_history_view.xml b/app/src/main/res/layout/clipboard_history_view.xml
index b853b47ce..50af306e5 100644
--- a/app/src/main/res/layout/clipboard_history_view.xml
+++ b/app/src/main/res/layout/clipboard_history_view.xml
@@ -137,6 +137,38 @@
android:background="?android:attr/selectableItemBackground" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f4abc9954..46e63d2d2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -334,6 +334,8 @@
Clip text
No results found
+ Are you sure you want to clear all history?
+ Clear All
Delete swipe
@@ -1085,6 +1087,10 @@ New dictionary:
Version
View on GitHub
+
+ Sponsor project
+
+ Support the development of LeanType
Go to Wiki
@@ -1302,4 +1308,6 @@ New dictionary:
Are you sure you want to clear your recently used emojis?
Drag to move keyboard
Drag to resize keyboard
+ Persist floating keyboard
+ Do not hide floating keyboard when input finishes
diff --git a/dev-log.md b/dev-log.md
index 3ae29ea94..42ec473e7 100644
--- a/dev-log.md
+++ b/dev-log.md
@@ -232,3 +232,22 @@ Review comment r3289683508 on PR #3 noted that `createToolbarStateBackground()`
### Open Questions / Next Steps
- If toolbar state updates ever move off the main thread, add `@Volatile` or synchronization to the cache variable.
+---
+
+## 2026-05-23 — Merge upstream main into origin main branch
+
+### Context
+The user asked for a new PR based on `main` that merges changes from the configured `upstream` remote. `origin/main` and `upstream/main` had diverged, with upstream containing build, README, symbol-row, floating-keyboard persistence, clipboard icon, touchpad, and release/version updates.
+
+### Actions Taken
+- Created branch `copilot/merge-upstream-main` from `origin/main`.
+- Merged `upstream/main` into the branch.
+- Resolved conflicts in `KeyboardIconsSet.kt` by preserving LeanType toolbar state icons while keeping the merge compatible with upstream's icon mapping changes.
+- Resolved conflicts in `ClipboardDao.kt` by preserving clipboard edit support and upstream synchronization around pin toggling.
+
+### Decisions Made
+- Used a merge branch instead of rebasing so the PR clearly represents an upstream sync into `main`.
+- Preserved LeanType-specific clipboard editing and toolbar state-key behavior where it overlapped with upstream changes.
+
+### Open Questions / Next Steps
+- Build validation and PR creation should complete before merging.
diff --git a/docs/images/get-it-on-obtainium.png b/docs/images/get-it-on-obtainium.png
new file mode 100644
index 000000000..a4cf4f9ca
Binary files /dev/null and b/docs/images/get-it-on-obtainium.png differ
diff --git a/fastlane/metadata/android/en-US/changelogs/3800.txt b/fastlane/metadata/android/en-US/changelogs/3800.txt
new file mode 100644
index 000000000..3d402a978
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3800.txt
@@ -0,0 +1,2 @@
+- Disable non-deterministic baseline profiles for 100% reproducible F-Droid builds
+- Improve reliability of emoji search and layout stability
diff --git a/gradle.properties b/gradle.properties
index ec2881758..85cc0abff 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,3 +11,4 @@ org.gradle.vfs.watch=true
org.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=384m -XX:+UseParallelGC
# Limit workers to reduce peak memory spikes
org.gradle.workers.max=2
+android.enableR8.fullMode=true