Some checks failed
Android Main Branch CI / Build, Sign & Upload (push) Has been cancelled
Update CHANGELOG.md / changelog (push) Has been cancelled
Validate Gradle Wrapper / Validation (push) Has been cancelled
Nightly Release / release (push) Has been cancelled
Nightly Release / Build, Sign & Release (push) Has been cancelled
Delete Unused Caches / delete (push) Has been cancelled
Close Inactive Issues & Pull Requests / close-issues (push) Has been cancelled
8.5 KiB
8.5 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
- The installed app id is
app.easy.launcher— NOT upstream'scom.github.droidworksstudio.launcher. Every fork build must keep this id. - Release APKs are signed with
~/android-keystores/easylauncher-release.jks(aliaseasylauncher, pass ineasylauncher-release.pass). Never commit an unsigned or differently-signed APK todist/. adb install -r dist/...-Signed.apkupdates the phone in place (same signature). A debug build cannot be installed over it (signature mismatch).- 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.
- 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") andwithoutInternet. - 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)
- Bump
versionCode/versionNameinapp/build.gradle.kts(currently 35 / 0.3.5). - 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
apksigner verify --print-certsshould show the keystore SHA-256.- Commit as
release build vX.Y.Z(includes the dist APKs + .idsig files), annotated tagvX.Y.Z, pushmain+ tag toorigin(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 viauiautomator 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:
- Pull the file, edit locally, push back via stdin:
adb shell "su -c 'cat > <path>'" < localfile - 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. - Run
restorecon -F <file>— files created viasu catget the wrong SELinux context (s0instead ofs0:c18,c257,c512,c768) and the app can't read them (silent empty map). am force-stop app.easy.launcherthen 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 opensACTION_NOTIFICATION_LISTENER_SETTINGSwhen missing. - Badge semantics: per package the service sums
Notification.number(or 1 when unset) over COUNTED notifications — same as AOSP Launcher3. Counted = clearable (swipe-away), NON-ongoing, NON group-summary, userId >= 0. Group summaries and USER_ALL (-1) records are excluded (verified withcmd notification post, see the log tagBadgeService—publish counts=lines). Re-posts overwrite the stored entry (in-placeNotification.numberupdates, e.g. K-9 unread count), and a notification that becomes non-counted is dropped. - The dot is a plain red circle (NO white outline), drawn tangent to the
icon's top-right corner so it is never clipped (
NotificationDotHelper). - NOTE: the dot follows ACTIVE notifications (AOSP/Pixel behavior). Apps that cancel their notification when opened — K-9 Mail does this on every open — make the dot disappear even when unread mail remains. There is NO Android API to read an app's unread count (K-9 v22 exposes no provider and no AccountManager accounts; counts live in its private Room DBs). This is standard Android behavior, not a bug in this launcher.
- Pref writes:
SHOW_NOTIFICATION_DOTSmust NOT be re-pushed fromobserveUserInterfaceSettings— the othersetShowXcalls do write prefs, and adding this one silently clobbered an on-disktrueback tofalseat startup (in-memory cache won). HomeFragment only observesshowNotificationDotsLiveDatato rebind visible rows on toggle. - Grant from adb:
cmd notification allow_listener app.easy.launcher/com.github.droidworksstudio.launcher.service.NotificationBadgeService - End-to-end validation trick (the shell can't spoof other packages): run
Termux's termux-notification as the user that owns com.termux.api (has a
launcher icon + posts a clearable notification):
Expect a
adb shell "su -c 'su 10261 -c \"PATH=/data/data/com.termux/files/usr/bin:\$PATH \ termux-notification --id reddot-test --title RedDotTest --content Clearable\"'"BadgeService: publish counts={... 0/com.termux.api=1}log and a red dot on the Termux:API icon in the drawer. Verify pixels withpython3/PIL (dot = pure red, top-right of the icon box). - 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/compact— no API key, but REQUIRES a descriptiveUser-Agentheader (seeMetApiService). Verified working from PC and phone network. AppHelper.fetchMetWeather(context, lat, lon)returns Celsius (rounded) + METsymbol_code; 15-minute cache inmet_weather_prefs.- The icon is a Nerd Font weather glyph (
nf-weather-*, U+E300+ PUA), mapped from the symbol code inMetSymbolMapper. It renders with the bundledR.font.jetbrains_mono_nf_weather— a ~95KB fontTools subset of JetBrainsMonoNerdFont (ASCII + 38 weather glyphs + °). The phone's system font ISJetBrainsMonoNerdFont(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.xmlkeysLATITUDE/LONGITUDE(floats). MainActivity saves the real fix there; for testing you can seed e.g. Haugesund 59.4138 / 5.2680 (remember the.xmlsuffix +restorecon!).
Known behavior quirks
- The accessibility-service dialog ("Please turn on accessibility service to
use double tap to lock") pops on home when
ActionServiceisn't running. Enable the service in system settings to suppress it during testing. Application.setCustomFontpatches theTypefaceDEFAULT/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:
appListTouchAreaforwards vertical drags/ flings throughOnSwipeTouchListenerhooks (onVerticalScroll/onVerticalFling). Signs were verified against AOSP source:scrollBy(0, distanceY)is pass-through; fling takes the NEGATED pointer velocity.