2 Commits

Author SHA1 Message Date
e17903f40f Clear the search when the launcher is left, end keyboard search on dismiss
Some checks failed
Trigger F-Droid repository rebuild / trigger (release) Has been cancelled
Two fixes on top of the type-to-search feature:

- The search no longer survives leaving the launcher. Upstream only resets the
  search when the launcher was in the background for more than five seconds, so
  a quick app switch or a fast return from a launched app left the old query on
  screen. The search page is now always reset, whatever the pause was.
- Dismissing the search page ends the keyboard driven search. Without that the
  flag stayed set, so opening the search later (gesture or search bar) did not
  focus the text field and the soft keyboard stayed away.

Version 1.40.2-typing.2.
2026-09-10 12:03:00 +02:00
75f869c094 Add notes for this fork (type-to-search design, build, release, install) 2026-09-10 11:59:00 +02:00
5 changed files with 96 additions and 3 deletions

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
CLAUDE.md

87
CLAUDE.md Normal file
View File

@@ -0,0 +1,87 @@
# Kvaesitso (fork with type-to-search)
Fork of [Kvaesitso](https://github.com/MM2-0/Kvaesitso) that adds type-to-search for phones with a
physical keyboard. Built for, and installed on, a Unihertz Titan 2 Elite.
origin = https://gitea.haugesenspil.dk/jonas/Kvaesitso (our fork — push here)
upstream = https://github.com/MM2-0/Kvaesitso (pull upstream changes from here)
The phone also has this repository checked out in its Termux home: `~/sources/Kvaesitso`.
## The feature
Pressing a printable key on the physical keyboard while the launcher home screen is shown opens the
search page and inserts the character. Backspace deletes, enter launches the highlighted result.
Only the home screen (and the open search page) reacts; the widgets, notifications and recents
pages do not. Setting: *Settings → Search → "Search on typing"* (on by default, stored as
`searchOnTyping`).
## How it works, and why it is built this way
`SharedLauncherActivity.dispatchKeyEvent` is the only place that sees hardware key events while no
view has focus, so the handler is installed there (`launcherKeyEventHandler`, registered by
`LauncherScaffold` while it is shown). The handler itself is
`app/ui/.../ui/launcher/searchbar/TypeToSearchHandler.kt`.
Two constraints shape the design:
1. **The search bar's text field stays unfocused while the keyboard drives the search.** A focused
Compose text field makes the system show the on-screen keyboard. There is no public API to
prevent that: `View.setShowSoftInputOnFocus` is a hidden API (`TextView`'s version is public) and
reflection fails with `NoSuchMethodException`, and hiding the IME reactively makes it flash.
Leaving the field unfocused keeps the IME away completely — which is why the handler inserts and
deletes the characters itself instead of relying on the text field.
2. **No key may be lost.** Characters can arrive before the text field would have focus, so the
handler keeps handling keys until the field reports real focus
(`LauncherScaffoldState.isSearchBarTextFieldFocused`). `LauncherScaffoldState.isTypingSearch`
marks a keyboard-driven search: while it is set, `SearchComponent.onPreActivate` does not focus
the field and the handler owns the keys, including backspace and enter. Tapping the search bar
clears the flag and hands over to the normal editable field.
`SearchBar` keeps a `TextFieldValue` instead of a `String` so that changing the text from the
outside moves the cursor to the end. Without that, the next typed character landed in front of the
text.
The search is also cleared whenever the launcher is left, even for a short app switch — upstream
keeps it for returns within five seconds, which leaves a stale query on screen.
## Building
JAVA_HOME=~/jdk21 ANDROID_HOME=~/android-sdk \
./gradlew :app:app:assembleDefaultRelease
Result: `app/app/build/outputs/apk/default/release/app-default-release.apk`, applicationId
`de.mm20.launcher2.release`.
The `release` build type signs with the `local` signing config, which reads `keystore.properties`
in the repository root (git-ignored) or the `KEYSTORE_FILE`, `KEYSTORE_PASSWORD`, `KEY_ALIAS` and
`KEY_PASSWORD` environment variables. The desktop key is
`~/android-keystores/kvaesitso-release.jks`; the phone has its own copy under
`~/android-keystores/` in the Termux home. Termux itself has no Android SDK, so build there only
after installing one (or build on the desktop and copy the APK over).
## Releasing (the phone updates through Obtanium)
1. Raise `versionCode` above the installed one and set a new `versionName` in
`app/app/build.gradle.kts` (`versionName` may keep the upstream version, e.g. `1.40.2-typing.2`).
2. Build the release APK (see above) and check the signature: `apksigner verify --verbose`.
3. Tag it and push `main` and the tag to `origin` (Gitea needs a token: the desktop keeps it in
`~/.config/gitea/token`).
4. Create a Gitea release for that tag and attach the APK as `Kvaesitso-v<versionName>-Signed.apk`.
The repository is public so Obtanium can read it anonymously.
5. The phone's Obtanium entry for `de.mm20.launcher2.release` points at this Gitea repo with
`overrideSource: "Codeberg"`, `versionDetection: true` and `apkFilterRegEx: "Kvaesitso-v"`. It
picks the release up on the next check. The config file lives on the phone at
`/sdcard/Android/data/dev.imranr.obtainium.fdroid/files/app_data/de.mm20.launcher2.release.json`
(force-stop Obtanium before editing it as root).
## Installing on the phone by hand
The phone already runs this build. The official build is signed with the upstream key, so it cannot
be updated in place and Obtanium cannot install over it either. The procedure is: back up
`/data/user/0/de.mm20.launcher2.release` with root, `pm uninstall` (a `-k` uninstall keeps the
package record *and* its signature, so the next install then fails with
`INSTALL_FAILED_UPDATE_INCOMPATIBLE`), install this APK, then copy the data back — `chown` to the
new app uid and `chcon` the label that installd created for the fresh data directory. The rest of
the phone (bootloader, Magisk, microG, rescue steps) is documented in `~/titan2-elite/CLAUDE.md` on
the desktop.

View File

@@ -34,8 +34,8 @@ android {
applicationId = "de.mm20.launcher2"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091000
versionName = "1.40.2-typing.1"
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091001
versionName = "1.40.2-typing.2"
signingConfig = signingConfigs.getByName("debug")
}

View File

@@ -1132,7 +1132,9 @@ internal fun LauncherScaffold(
} else {
activity.onBackPressedDispatcher.onBackPressed()
}
} else if (activity.pauseTime > 0L && System.currentTimeMillis() - activity.pauseTime > 5000L) {
} else if (activity.pauseTime > 0L && (System.currentTimeMillis() - activity.pauseTime > 5000L || state.currentComponent is SearchComponent)) {
// The search does not survive leaving the launcher, not even for a short app
// switch: coming back should always show a fresh search.
if (!state.isLocked) {
if (state.currentComponent?.survivesPause != true) {
state.reset()

View File

@@ -115,5 +115,8 @@ internal class SearchComponent(
override fun onPreDismiss(state: LauncherScaffoldState) {
super.onPreDismiss(state)
state.isSearchBarFocused = false
// Leaving the search page ends the keyboard driven search, so that opening the search
// again focuses the text field as usual.
state.isTypingSearch = false
}
}