Files
Kvaesitso/CLAUDE.md

5.1 KiB

Kvaesitso (fork with type-to-search)

Fork of 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.