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 - - Get it on GitHub - - - Get it on F-Droid - + + + + + + +
+ + Get it on GitHub + + + + Get it on F-Droid + + + + Get it on Obtainium + +
+ +> [!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