Files
EasyLauncher/CLAUDE.md

6.7 KiB

Working with this EasyLauncher fork

Quick reference for agents working on this fork (Gitea: jonas/EasyLauncher, installed on the Unihertz Titan 2 Elite as app.easy.launcher).

PROJECT.md in ~/titan2-elite is the source of truth for the phone itself.

Golden rules

  1. The installed app id is app.easy.launcher — NOT upstream's com.github.droidworksstudio.launcher. Every fork build must keep this id.
  2. Release APKs are signed with ~/android-keystores/easylauncher-release.jks (alias easylauncher, pass in easylauncher-release.pass). Never commit an unsigned or differently-signed APK to dist/.
  3. adb install -r dist/...-Signed.apk updates the phone in place (same signature). A debug build cannot be installed over it (signature mismatch).
  4. After a phone reboot the phone is CE-locked until the user enters the PIN; app prefs/location are not visible to the app until then. Ask the user to unlock instead of debugging it.
  5. Never fetch weather without a real location (lat/lon 0,0 must be treated as "no location").

Build

export JAVA_HOME=~/jdk21        # Gradle 8.13, compileSdk/targetSdk 36, minSdk 24
./gradlew :app:compileWithInternetReleaseKotlin :app:compileWithoutInternetReleaseKotlin --offline
./gradlew :app:assembleWithInternetRelease :app:assembleWithoutInternetRelease --offline
  • Two flavors: withInternet (INTERNET + location permissions, "Easy Launcher") and withoutInternet.
  • Outputs: app/build/outputs/apk/{withInternet,withoutInternet}/release/.
  • weather.properties (OpenWeatherMap key) is optional and absent here — the OWM weather widget is dead without it. The home-screen current-weather element uses the MET/Yr API instead (no key needed, see below).

Release flow (see git history for the v0.3.x pattern)

  1. Bump versionCode/versionName in app/build.gradle.kts (currently 35 / 0.3.5).
  2. Build both flavors, then sign each:
PASS=$(cat ~/android-keystores/easylauncher-release.pass)
~/android-sdk/build-tools/36.0.0/apksigner sign --v4-signing-enabled true \
  --ks ~/android-keystores/easylauncher-release.jks --ks-key-alias easylauncher \
  --ks-pass "pass:$PASS" --key-pass "pass:$PASS" \
  --out dist/EasyLauncher-Internet-v0.3.5-Signed.apk \
  app/build/outputs/apk/withInternet/release/app.easy.launcher_v0.3.5-Release.apk
# repeat for withoutInternet -> dist/EasyLauncher-v0.3.5-Signed.apk
  1. apksigner verify --print-certs should show the keystore SHA-256.
  2. Commit as release build vX.Y.Z (includes the dist APKs + .idsig files), annotated tag vX.Y.Z, push main + tag to origin (Gitea), create a Gitea release with both APKs as assets.

On-device testing (Titan 2 Elite via adb)

  • Wake: input keyevent KEYCODE_WAKEUP; go home: input keyevent KEYCODE_HOME.
  • Open the app drawer: input keyevent KEYCODE_SPACE (the physical-keyboard "key press -> app list" trigger; requires MainActivity focused).
  • Scroll the drawer from the right edge: input swipe 1000 850 1000 250 250.
  • Screenshot: adb exec-out screencap -p > s.png; view text via uiautomator dump.
  • Inspect the launcher's own state with su -c '...' (root via Magisk).

Editing app prefs from the host (fragile — read carefully)

Prefs live in /data/data/app.easy.launcher/shared_prefs/. To change them:

  1. Pull the file, edit locally, push back via stdin: adb shell "su -c 'cat > <path>'" < localfile
  2. The file name must end in .xml (EasyLauncher.pref.xml, EasyWeather.pref.xml) — getSharedPreferences("name") appends .xml. A missing suffix silently reads as an EMPTY map.
  3. Run restorecon -F <file> — files created via su cat get the wrong SELinux context (s0 instead of s0:c18,c257,c512,c768) and the app can't read them (silent empty map).
  4. am force-stop app.easy.launcher then press HOME so the process restarts and re-reads the file.

Notification dots

  • Service: .service.NotificationBadgeService (NotificationListenerService). Users must grant notification access; the "Notification Dots" settings toggle opens ACTION_NOTIFICATION_LISTENER_SETTINGS when missing.
  • Grant from adb: cmd notification allow_listener app.easy.launcher/com.github.droidworksstudio.launcher.service.NotificationBadgeService
  • Test notification: cmd notification post -t 'Title' tag 'body' — posts as com.android.shell, which has no launcher icon, so no dot is visible; verify visually with a real app notification instead.
  • Counts live in NotificationBadgeService.notificationCounts (StateFlow, key "userId/packageName"); fragments rebind visible rows.

Home current-weather element (MET/Yr)

  • Source: https://api.met.no/weatherapi/locationforecast/2.0/compactno API key, but REQUIRES a descriptive User-Agent header (see MetApiService). Verified working from PC and phone network.
  • AppHelper.fetchMetWeather(context, lat, lon) returns Celsius (rounded) + MET symbol_code; 15-minute cache in met_weather_prefs.
  • The icon is a Nerd Font weather glyph (nf-weather-*, U+E300+ PUA), mapped from the symbol code in MetSymbolMapper. It renders with the bundled R.font.jetbrains_mono_nf_weather — a ~95KB fontTools subset of JetBrainsMonoNerdFont (ASCII + 38 weather glyphs + °). The phone's system font IS JetBrainsMonoNerdFont (fonts.xml default), but the bundled subset guarantees rendering even if the launcher-font setting changes. Regenerate the subset with fontTools if more glyphs are ever needed (source: /system/fonts/JetBrainsMonoNerdFont-Regular.ttf).
  • Display mirrors the daily word (uses the daily-word color/size/alignment prefs); text = "$glyph $temp°" (two spaces).
  • Needs a real saved location: EasyWeather.pref.xml keys LATITUDE/ LONGITUDE (floats). MainActivity saves the real fix there; for testing you can seed e.g. Haugesund 59.4138 / 5.2680 (remember the .xml suffix + restorecon!).

Known behavior quirks

  • The accessibility-service dialog ("Please turn on accessibility service to use double tap to lock") pops on home when ActionService isn't running. Enable the service in system settings to suppress it during testing.
  • Application.setCustomFont patches the Typeface DEFAULT/MONO/SERIF/SANS static fields when a non-System launcher font is selected.
  • With "Disable Animations" on, the drawer RecyclerView item animator is null and navigation transitions are skipped.
  • Drawer whole-screen scrolling: appListTouchArea forwards vertical drags/ flings through OnSwipeTouchListener hooks (onVerticalScroll/onVerticalFling). Signs were verified against AOSP source: scrollBy(0, distanceY) is pass-through; fling takes the NEGATED pointer velocity.