Sync Android Easter Egg from AOSP
Overview
This skill documents the end-to-end workflow for syncing a new Android platform
Easter egg from AOSP source into this project. The goal is to produce a module
under eggs/<VersionName>/ that matches the style of existing modules like
eggs/Baklava.
Always copy or read an existing recent egg module (e.g. eggs/Baklava or
eggs/VanillaIceCream) and mimic its package structure, resource naming, and
Compose/Hilt wiring. Do not invent new conventions.
minSdk Compatibility (CRITICAL)
The project's minSdk is 23 (Android 6.0 Marshmallow). All AOSP upstream code targets the current platform API level and contains no compatibility guards. Every file synced from upstream MUST be checked against the API reference below.
High-risk APIs requiring SDK guards
| API | Min SDK | Guard pattern |
|---|---|---|
NotificationChannel |
26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) |
WindowInsets.Type.systemBars() |
30 | Use AndroidX WindowInsetsControllerCompat instead |
getWindowInsetsController() |
30 | Use WindowCompat.getInsetsController() |
setColorMode(COLOR_MODE_HDR) |
26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) |
ColorSpace.get(EXTENDED_SRGB) |
26 | Lazy-init with @RequiresApi, fallback to packColor() SDR |
VibratorManager |
31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + null guard |
VibrationEffect.startComposition() |
31 | Same guard as VibratorManager |
Handler.createAsync() |
28 | Use HandlerCompat.createAsync() from AndroidX |
RELEASE_OR_CODENAME |
30 | OK if only in when else-branch (only reached on new devices) |
Notification.Builder(context, channelId) |
26 | Fallback to deprecated Notification.Builder(context) |
setFlag(FLAG_ONLY_ALERT_ONCE) |
30 | if (R) else setOnlyAlertOnce(true) |
setRequestPromotedOngoing() |
BAKLAVA_1 | Guard with SDK_INT_FULL check |
Compatibility verification checklist
After syncing each file, run these checks:
# Scan for unguarded high-API calls
grep -nE "NotificationChannel|ColorSpace|COLOR_MODE_HDR|setColorMode|VibratorManager|VibrationEffect|PRIMITIVE_SPIN|CombinedVibration|WindowInsets\.Type|setDecorFitsSystemWindows|getWindowInsetsController|Handler\.createAsync|RELEASE_OR_CODENAME" <file>
# Check imports for AndroidX compat (should use these, not raw framework)
grep -nE "WindowCompat|WindowInsetsControllerCompat|WindowInsetsCompat|HandlerCompat" <file>
If the file uses any high-API call without a Build.VERSION.SDK_INT >= guard,
copy the guard pattern from eggs/Baklava or eggs/VanillaIceCream.
Critical file: UniverseProgressNotifier.kt
Do NOT simply copy from the existing egg module. The upstream version may contain new features or logic changes. Follow this process:
- Fetch the upstream version as part of Phase 2.1.
- Compare it against the most recent existing egg module's
UniverseProgressNotifier.kt(e.g.eggs/Baklava):diff eggs/<ExistingEgg>/.../UniverseProgressNotifier.kt eggs/<NewEgg>/.../UniverseProgressNotifier.kt - The existing version has compatibility guards (SDK checks on
NotificationChannel,Notification.Builder,setFlag,setStyle,isProgressNotifierSupported, etc.) that are not in the upstream source. Preserve these guards while merging any NEW logic from upstream. - If the diff shows only minor upstream changes (comments, formatting), prefer the existing module's version and adapt the package/prefix.
- If the upstream has significant new features, port them into the existing version's guarded structure.
Phase 0: Determine version info
Identify the Android version name (e.g. CinnamonBun), API level, AOSP branch name (e.g.
android17-release).Decide on a module namespace (
com.android_<lowercamel>_egg), resource prefix (<lowercamel>_), and setting key (egg_mode_<lowercamel>).Confirm the branch exists on
https://android.googlesource.com/platform/frameworks/base/+/refs/heads/<branch>/If googlesource.com is unreachable or asks for login, see Fallback: source access.Scan upstream for new content: Fetch the upstream
EasterEgg/AndroidManifest.xmlandEasterEgg/src/com/android/egg/directory listing. Identify any new subdirectories or declared components beyond what the previous egg module has. This prevents missing new egg content that was added in this release.
Phase 1: Fetch and adapt PlatLogoActivity.java
Fetch the upstream source:
export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890 curl -sL "https://android.googlesource.com/platform/frameworks/base/+/refs/heads/<BRANCH>/core/java/com/android/internal/app/PlatLogoActivity.java?format=TEXT" | base64 -d > PlatLogoActivity.javaAdapt the file:
- Package:
com.android.internal.app→com.android_<name>_egg - Resource:
R.drawable.platlogo→R.drawable.<prefix>_platlogo - Starfield class: change
private static class→static class(package-private for Kotlin interop) shouldWriteSettings(): returntrue(standalone app)
- Package:
Add API compatibility guards — refer to the minSdk Compatibility table at the top of this skill for all required guards. Key points:
- Window insets: use AndroidX
WindowInsetsControllerCompatinstead of rawWindowInsets - HDR color mode: wrap in
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - VibratorManager: guard with
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - Handler: use
HandlerCompat.createAsync()instead ofHandler.createAsync() - HDR color rendering: add
@ChecksSdkIntAtLeast isSRgbExtSupportedflag withpackColorSDR fallback ColorSpace.get: lazy-init with@RequiresApi
After applying guards, run the grep scan from the compatibility section to verify.
- Window insets: use AndroidX
Remove unnecessary AOSP internals:
- Delete
measureTouchPressure/syncTouchPressuremethods and related fields - Delete
onStart()/onStop()overrides if they only call touch pressure - Remove
import org.json.JSONObject,import static java.lang.Math.max - Remove
ContentResolverusage if replaced by SpUtils
- Delete
Replace
Settings.System.putLong→SpUtils.putLong(this, key, value)fromcom.dede.basic.SpUtilsCheck the upstream PlatLogoActivity for any Heptadecagram-related fixes (e.g. commit
350b4843for the swap-to-platlogo timing fix)
Phase 2: Scan and sync EasterEgg game code
2.0 Discover ALL new content
Do NOT assume the upstream only contains landroid/. Each Android release
may add, remove, or rename subdirectories. First, scan the upstream
EasterEgg/src/com/android/egg/ directory to discover all content:
# List all subdirectories in the upstream EasterEgg source
curl -sL "$BASE/../?format=TEXT" | base64 -d | grep -oP '(?<=href=")[^"]+(?=")' | grep -E '^[a-z]+/$'
Known historical subdirectories (for reference):
| Directory | Android version | Content type |
|---|---|---|
landroid/ |
S+ | Space exploration game |
neko/ |
N | Cat collector game |
paint/ |
P | Paint widget |
quares/ |
Q | Number puzzle |
widget/ |
S | Paint chips widget |
For each NEW subdirectory not present in the previous egg module:
- Determine if it's a core feature (should be synced) or legacy relayed game (only sync if directly referenced by the new version's code).
- Check the upstream
AndroidManifest.xmlfor any declared activities or services in that package.
2.1 Sync landroid game code (if present)
Fetch all files from
EasterEgg/src/com/android/egg/landroid/:for f in MainActivity.kt Assets.kt Autopilot.kt Colors.kt ComposeTools.kt \ DreamUniverse.kt Maths.kt Namer.kt PathTools.kt Physics.kt \ Randomness.kt Universe.kt UniverseProgressNotifier.kt Vec2.kt \ VisibleUniverse.kt; do curl -sL "$BASE/$f?format=TEXT" | base64 -d > "$DEST/$f" doneNote:
Assets.ktwas added in Android 17. Files may vary by version.Adapt package names:
com.android.egg.landroid→com.android_<name>_egg.landroidcom.android.egg.flags→com.android_<name>_egg.flagscom.android.egg.R→com.android_<name>_egg.Rcom.android.egg.ComponentActivationActivity→com.android_<name>_egg.ComponentActivationActivity
Prefix ALL
R.array.*,R.string.*,R.drawable.*references with<prefix>_:R.array.planet_descriptors→R.array.<prefix>_planet_descriptorsR.drawable.ic_spacecraft_filled→R.drawable.<prefix>_ic_spacecraft_filled
Replace
getDessertCode()body with delegation tocom.dede.basic.utils.DessertUtils.getDessertCode()Copy
UniverseProgressNotifier.ktfrom the most recent existing egg (not upstream) to preserve compatibility guards for minSdk 23
2.2 Sync other new content (if discovered)
For any subdirectory found in step 2.0 that is not landroid/ and is
not present in the previous egg module:
Compare the upstream and local
AndroidManifest.xmlto identify new activities, services, or receivers declared in that package.Fetch all
.javaand.ktfiles from the new subdirectory, applying the same package/prefix adaptations as Phase 1 and 2.1.Fetch any new resources (
res/drawable/,res/layout/,res/values/,res/xml/) that are referenced by the new code.If the new content has its own unlock/activation flow, add the necessary
ComponentActivationActivityentries orComponentProviderregistrations.Check with the user whether the new content should be synced or is unnecessary legacy content carried over from the AOSP module that won't be used in this project.
Phase 3: Add support Java files
Copy from the most recent existing egg (e.g. eggs/Baklava) and adapt:
flags/Flags.java— packagecom.android_<name>_egg.flags, always returnstrueComponentActivationActivity.java— enables DreamUniverse after egg unlock
Phase 4: Resources
Fetch the upstream
platlogo.xmlfrom:frameworks/base/+/<BRANCH>/core/res/res/drawable-nodpi/platlogo.xmlSave as
res/drawable-nodpi/<prefix>_platlogo.xmlCopy planet/spacecraft icon drawables from the most recent existing egg, renaming the prefix:
ic_planet_large, ic_planet_medium, ic_planet_small, ic_planet_tiny, ic_spacecraft, ic_spacecraft_filled, ic_spacecraft_rotatedFetch the upstream
landroid_strings.xmlfrom the target branch (do NOT copy it from an existing egg module) and add the resource prefix:curl -sL "https://android.googlesource.com/platform/frameworks/base/+/<BRANCH>/packages/EasterEgg/res/values/landroid_strings.xml?format=TEXT" \ | base64 -d \ | sed "s/name=\"/name=\"<prefix>_/g" \ > res/values/landroid_strings.xmlWhy not copy from the previous egg: upstream expands this word pool in every release cycle (new descriptors, constellations, planet names). Copying from an older egg silently drops those entries — this happened with CinnamonBun, which missed upstream commit
4f583d60b6f9("IMPROVEMENTS TO SHIP SENSORS", ~28 entries) because its strings were copied from Baklava.IMPORTANT: Remove any duplicate string (e.g.
<prefix>_egg_name, historicalu_egg_name/v_egg_nameleftovers) that already exists inres/values/strings.xml.Create
res/values/themes.xmlwith<prefix>_Theme.Landroidstyle.Create
res/xml/<prefix>_landroid_dream.xmlreferencing@drawable/<prefix>_platlogo.
Phase 5: AndroidManifest.xml
Following the Baklava module pattern, declare:
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS"/>
<activity android:name=".PlatLogoActivity" ... />
<service android:name=".landroid.DreamUniverse" android:enabled="false" ... />
<activity android:name=".landroid.MainActivity" ... />
Phase 6: Register with Hilt/DI
Create Android<VersionName>EasterEgg.kt as an @Module @InstallIn(SingletonComponent::class)
object implementing both EasterEggProvider and ComponentProvider.
provideEasterEgg(): register with icon, name, API level,PlatLogoActivity::class.javaprovideComponent(): provide enable/disable logic forDreamUniverseprovideTimelineEvents(): add release timeline entriesSnapshotProvider: create a preview usingStarfield+ plat logo
Phase 7: Permission handling (optional)
Only required if the module's AndroidManifest.xml declares runtime permissions
such as POST_NOTIFICATIONS. Check the manifest first:
grep -E "uses-permission.*android:name" eggs/<VersionName>/src/main/AndroidManifest.xml
Runtime permissions (e.g. POST_NOTIFICATIONS on API 33+) need the app to
request them when the relevant Activity opens. Install-time permissions
(e.g. POST_PROMOTED_NOTIFICATIONS, VIBRATE) are granted automatically.
If runtime permissions are declared:
Add the new module as a dependency in
core/activity-actions/build.gradle.kts:implementation(project(":eggs:<VersionName>"))Add the new
MainActivityclass to thepagerslist incore/activity-actions/src/main/java/com/dede/android_eggs/util/actions/RequestNotificationPermissionAction.kt:com.android_<name>_egg.landroid.MainActivity::class, // auto pilot
Skip this phase if no runtime permissions are declared.
Phase 8: PlatLogoActivity → MainActivity navigation
Change launchNextStage() to launch MainActivity directly instead of using
the PLATLOGO category intent:
import com.android_<name>_egg.landroid.MainActivity;
// in launchNextStage:
final Intent eggActivity = new Intent(this, MainActivity.class);
startActivity(eggActivity);
Phase 9: Compile, fix, and verify compatibility
./gradlew :eggs:<VersionName>:compileDebugKotlin
./gradlew :eggs:<VersionName>:compileDebugJavaWithJavac
./gradlew :core:activity-actions:compileDebugKotlin
Common errors:
- Duplicate resources: remove duplicates from
landroid_strings.xml - Unresolved R.array.*: ensure all resource references have the
<prefix>_prefix - Unresolved
com.android.egg.*: fix any remaining package references - API compatibility: ensure
UniverseProgressNotifierhas SDK guards
After compilation succeeds, re-run the compatibility checks from the
minSdk Compatibility section on all synced
files. The Kotlin compiler will not catch runtime API level violations — only
grep-style scan or lint can detect them.
# Scan all new files for unguarded high-API calls
grep -rnE "NotificationChannel|ColorSpace|VibratorManager|VibrationEffect\.start|CombinedVibration|WindowInsets\.Type\b|setDecorFitsSystemWindows|getWindowInsetsController|Handler\.createAsync" eggs/<VersionName>/src/
Known fixes not in upstream AOSP
These fixes were applied to existing eggs and are not present in the upstream AOSP source. Apply them when syncing a new egg.
Heptadecagram swap timing fix
File: PlatLogoActivity.java
Commit: 350b4843
Issue: swapToPlatlogo() was called immediately when the 17-dot path was
complete, but the user's finger is still down (tracking). This could cause the
swap during a MOVE event before UP.
Fix: Set a isPendingSwapToPlatlogo flag when path completes, and only
call swapToPlatlogo() on ACTION_UP:
private boolean isPendingSwapToPlatlogo = false;
// In mHeptaDecaView.setOnTouchListener:
if (heptadecagram.getPathLength() > Heptadecagram.MAX_DOTS) {
isPendingSwapToPlatlogo = true;
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP && isPendingSwapToPlatlogo) {
swapToPlatlogo();
isPendingSwapToPlatlogo = false;
}
Spacecraft may enter planet interior
File: landroid/Universe.kt
Gerrit: 3743150
Issue: When landing on a planet with residual thrust, the spacecraft could
penetrate the planet's interior.
Fix: Kill thrust before creating the landing:
val landing = Landing(ship, planet, a, namer.describeActivity(rng, planet))
if (ship.thrust != Vec2.Zero) {
// kill the power
ship.thrust = Vec2.Zero
}
ship.landing = landing
DYNAMIC_ZOOM global state not reset on destroy
File: landroid/MainActivity.kt and landroid/DreamUniverse.kt
Issue: DYNAMIC_ZOOM is a top-level var that persists across Activity/DreamService
lifecycles. When autopilot is toggled ON (or screensaver starts), it is set to true,
but never reset to false on destroy. Re-entering the page causes the camera zoom
to still dynamically adjust based on distance.
Fix: Reset DYNAMIC_ZOOM = false in onDestroy() of both classes:
// In MainActivity.kt:
override fun onDestroy() {
notifier?.cancel()
DYNAMIC_ZOOM = false
super.onDestroy()
}
// In DreamUniverse.kt:
override fun onDestroy() {
notifier?.cancel()
DYNAMIC_ZOOM = false
super.onDestroy()
}
UniverseProgressNotifier live-update notification
File: landroid/UniverseProgressNotifier.kt
Commit: 85c032fd (Baklava)
Issue: Missing setRequestPromotedOngoing(true) for promoted live-update
notifications; .setColorized(true) removed to avoid conflicts.
Fix: Add in the .apply {} block:
if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1) {
setRequestPromotedOngoing(true)
}
Reference: Key files in an egg module (may vary by version)
eggs/<VersionName>/
├── build.gradle.kts # plugin: easter.eggs.compose.library
├── src/main/AndroidManifest.xml # permissions + components
├── src/main/java/com/android_<name>_egg/
│ ├── PlatLogoActivity.java # splash / starfield + Heptadecagram
│ ├── ComponentActivationActivity.java # enables unlockable components
│ ├── Android<Name>EasterEgg.kt # Hilt registration
│ ├── flags/Flags.java # feature flag (always true)
│ └── landroid/ # space exploration game
│ ├── MainActivity.kt
│ ├── Assets.kt # SVG paths (new in A17+)
│ ├── Autopilot.kt / Colors.kt / ComposeTools.kt
│ ├── DreamUniverse.kt # screensaver
│ ├── Maths.kt / Namer.kt / PathTools.kt
│ ├── Physics.kt / Randomness.kt
│ ├── Universe.kt / Vec2.kt / VisibleUniverse.kt
│ └── UniverseProgressNotifier.kt # notification progress
│ └── <other>/ # any new content discovered in Phase 2.0
└── src/main/res/
├── drawable-nodpi/<prefix>_platlogo.xml
├── drawable/<prefix>_ic_planet_*.xml # (if landroid present)
├── drawable/<prefix>_ic_spacecraft*.xml # (if landroid present)
├── values/landroid_strings.xml # (if landroid present)
├── values/strings.xml
├── values/themes.xml
└── xml/<prefix>_landroid_dream.xml # (if DreamUniverse present)
Note: The file tree varies by Android version. For example, Assets.kt was
introduced in Android 17 (CinnamonBun). Always compare with the upstream source
listing and the previous egg module to determine the correct set of files to sync.
Reference: Common AOSP source locations
| Component | Path on android.googlesource.com |
|---|---|
| PlatLogoActivity | platform/frameworks/base/+/<BRANCH>/core/java/com/android/internal/app/PlatLogoActivity.java |
| platlogo drawable | platform/frameworks/base/+/<BRANCH>/core/res/res/drawable-nodpi/platlogo.xml |
| EasterEgg module | platform/frameworks/base/+/<BRANCH>/packages/EasterEgg/ |
| landroid sources | platform/frameworks/base/+/<BRANCH>/packages/EasterEgg/src/com/android/egg/landroid/ |
| landroid resources | platform/frameworks/base/+/<BRANCH>/packages/EasterEgg/res/ |
Use curl with proxy to fetch files in base64 format, then decode with base64 -d.
Fallback: source access when googlesource.com fails
The ?format=TEXT file endpoint used throughout this skill requires no login.
The only authenticated part of android.googlesource.com is the web history UI
(/+log/... returns 403 Forbidden). Pick a fallback by what you need:
Browse code or history manually (human-readable, no login)
Use Android Code Search:
https://cs.android.com/android/platform/superproject/main/frameworks/base/packages/EasterEgg
Open this in a browser and switch the branch selector (top bar) to the target
release branch. Note that cs.android.com is a JavaScript app — it renders
nothing for curl, so it can never replace the scripted ?format=TEXT
fetches in this skill.
Fetch commit history programmatically
Partial-clone over the git protocol (no login required) and read history
locally — verified working for refs/heads/android17-release:
mkdir fb && cd fb && git init -q
git remote add origin https://android.googlesource.com/platform/frameworks/base
git config extensions.partialClone origin
git fetch --depth=300 --filter=blob:none origin refs/heads/<BRANCH>
git log --oneline FETCH_HEAD -- packages/EasterEgg/
git show <commit> --stat # blobs are fetched on demand
git show <commit> -- <path> # full diff of one file
Raw-file mirror for older eggs (works up to android16-release)
The official GitHub mirror serves raw files directly, but its branches lag behind AOSP — do not rely on it for the newest egg:
curl -sL "https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/<BRANCH>/<PATH>"