Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5f2ea0e
Merge pull request #114 from AsafMah/fix-symbols-popup
LeanBitLab May 18, 2026
d53f039
Merge pull request #115 from AsafMah/fix-export-import
LeanBitLab May 18, 2026
fdff557
chore: update gitignore
LeanBitLab May 18, 2026
6ac4059
build: disable baseline profiles to guarantee reproducible builds
LeanBitLab May 18, 2026
f403969
build: bump version to 3.8.0 (3800)
LeanBitLab May 18, 2026
12aa5a5
Fix touchpad full-screen stretch and bottom padding black space
LeanBitLab May 18, 2026
c74b31b
Change clipboard clear all toolbar key icon to recycle bin icon in Ma…
LeanBitLab May 18, 2026
8f7e461
Enable R8 Full Mode and improve JNI keep rules in proguard-rules.pro
LeanBitLab May 18, 2026
f5d5cac
Enable JNI Link-Time Optimization (LTO) and fix R8 Full Mode missing …
LeanBitLab May 18, 2026
bb9d111
Add sponsor button to About screen
LeanBitLab May 18, 2026
d306282
Add setting to persist floating keyboard
LeanBitLab May 18, 2026
c615467
Fix unresolved reference in SettingsContainer.kt
LeanBitLab May 18, 2026
c6eafcf
docs: add Obtainium link and badge to README
LeanBitLab May 19, 2026
0153d7e
docs: fix badge vertical alignment using table with middle valign
LeanBitLab May 19, 2026
cdf9797
fix: symbol view number row overlap
LeanBitLab May 19, 2026
12d0b6f
fix: symbol view number row alignment
LeanBitLab May 19, 2026
4a1931f
chore: change default for showing number row in symbols to false
LeanBitLab May 19, 2026
59ef5fa
fix: symbol view number row overlap and defaults
LeanBitLab May 19, 2026
4cadf2e
fix: preserve extra symbols in number-replaced symbol row
LeanBitLab May 19, 2026
048748c
Merge upstream main
lurebat May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fastlane/Appfile
*.keystore
keystore.properties
signing.properties
app/src/main/assets/adi-registration.properties

# Build logs
*.log
Expand Down
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,28 @@ This fork adds **optional AI-powered features** using Gemini, Groq, and OpenAI-c

## Download

<a href="https://github.com/LeanBitLab/HeliboardL/releases/latest">
<img alt="Get it on GitHub" src="docs/images/get-it-on-github.png" height="80">
</a>
<a href="https://f-droid.org/en/packages/com.leanbitlab.leantype/index.html">
<img alt="Get it on F-Droid" src="docs/images/get-it-on-fdroid.png" height="80">
</a>
<table border="0">
<tr>
<td align="center" valign="middle">
<a href="https://github.com/LeanBitLab/HeliboardL/releases/latest">
<img alt="Get it on GitHub" src="docs/images/get-it-on-github.png" height="90">
</a>
</td>
<td align="center" valign="middle">
<a href="https://f-droid.org/en/packages/com.leanbitlab.leantype/index.html">
<img alt="Get it on F-Droid" src="docs/images/get-it-on-fdroid.png" height="90">
</a>
</td>
<td align="center" valign="middle">
<a href="https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/LeanBitLab/HeliboardL">
<img alt="Get it on Obtainium" src="docs/images/get-it-on-obtainium.png" height="60">
</a>
</td>
</tr>
</table>

> [!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

Expand Down
15 changes: 13 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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"
}
}


Expand Down Expand Up @@ -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
}
}
9 changes: 7 additions & 2 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Keep native methods
-keepclassmembers class * {
# Keep classes that contain native methods
-keep class * {
native <methods>;
}

Expand Down Expand Up @@ -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.**
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
50 changes: 31 additions & 19 deletions app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class ClipboardHistoryRecyclerView @JvmOverloads constructor(
undoHandler.removeCallbacks(undoDismissRunnable)
lastDeletedEntry = entry

// Dismiss confirmation bar if active
(parent as? View)?.findViewById<View>(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<View>(R.id.clipboard_undo_bar)
undoBar = bar ?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<TextView>(R.id.clipboard_confirmation_text)?.setTextColor(colors.get(ColorType.KEY_TEXT))
bar.findViewById<TextView>(R.id.clipboard_confirmation_button)?.setTextColor(colors.get(ColorType.KEY_TEXT))
} catch (_: Exception) {}
bar.findViewById<View>(R.id.clipboard_confirmation_button)?.setOnClickListener {
clipboardHistoryManager.clearHistory()
dismissConfirmationBar()
}
}

val clipboardStrip = KeyboardSwitcher.getInstance().clipboardStrip
toolbarKeys.forEach {
clipboardStrip.addView(it)
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,20 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
}

private fun addNumberRowOrPopupKeys(baseKeys: MutableList<MutableList<KeyData>>, numberRow: MutableList<KeyData>) {
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<String>()
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
Expand Down
Loading
Loading