diff options
author | 2025-02-07 00:26:15 -0500 | |
---|---|---|
committer | 2025-02-07 12:54:48 -0500 | |
commit | 96b38375be8697f511519d4351538553079c5af6 (patch) | |
tree | d57347ab1530438922600dc27c70cc91c8e76b7d | |
parent | ca7d0c749352f76ccf27f4c405e1a37322976c4a (diff) |
Seen while cruising the solar system:
+---------------+ .
| SPACE IS BIG, | .
+---------------+
. || +---------------+
. || | SPACE IS FAR. |
^^^^ +---------------+
+-----------+ . || . .
| HOW ABOUT | || .
+-----------+ ^^^^
|| +-----------------+
. || | A PROGRESS BAR? |
^^^^ +-----------------+
. . || .
. +------------+ .
| ANDROID 16 |
+------------+
. || .
|| .
. ^^^^
Bug: 373855388
Test: adb shell am start -n com.android.egg/.landroid.MainActivity
Flag: com.android.egg.flags.flag_flag
Change-Id: I2d1e2b34d036f8b765d407087130a00cc7b7082e
15 files changed, 622 insertions, 118 deletions
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml index 96e5892f4d1d..bcc10ddde228 100644 --- a/packages/EasterEgg/AndroidManifest.xml +++ b/packages/EasterEgg/AndroidManifest.xml @@ -64,7 +64,7 @@ android:label="@string/u_egg_name" android:icon="@drawable/android16_patch_adaptive" android:configChanges="orientation|screenLayout|screenSize|density" - android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> + android:theme="@style/Theme.Landroid"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> diff --git a/packages/EasterEgg/res/drawable/ic_planet_large.xml b/packages/EasterEgg/res/drawable/ic_planet_large.xml new file mode 100644 index 000000000000..7ac7c38153f2 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_large.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_medium.xml b/packages/EasterEgg/res/drawable/ic_planet_medium.xml new file mode 100644 index 000000000000..e997b45eb6e5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_medium.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_small.xml b/packages/EasterEgg/res/drawable/ic_planet_small.xml new file mode 100644 index 000000000000..43339573207b --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_small.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_planet_tiny.xml b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml new file mode 100644 index 000000000000..c666765113da --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" + android:strokeWidth="2" + android:fillColor="#16161D" + android:strokeColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft.xml b/packages/EasterEgg/res/drawable/ic_spacecraft.xml new file mode 100644 index 000000000000..3cef4ab29192 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" android:viewportWidth="24" + > + <group android:translateX="10" android:translateY="12"> + <path + android:strokeColor="#FFFFFF" + android:strokeWidth="2" + android:pathData=" +M11.853 0 +C11.853 -4.418 8.374 -8 4.083 -8 +L-5.5 -8 +C-6.328 -8 -7 -7.328 -7 -6.5 +C-7 -5.672 -6.328 -5 -5.5 -5 +L-2.917 -5 +C-1.26 -5 0.083 -3.657 0.083 -2 +L0.083 2 +C0.083 3.657 -1.26 5 -2.917 5 +L-5.5 5 +C-6.328 5 -7 5.672 -7 6.5 +C-7 7.328 -6.328 8 -5.5 8 +L4.083 8 +C8.374 8 11.853 4.418 11.853 0 +Z + "/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml new file mode 100644 index 000000000000..7a0c70379f20 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportHeight="24" android:viewportWidth="24" + > + <group android:translateX="10" android:translateY="12"> + <path + android:strokeColor="#FFFFFF" + android:fillColor="#000000" + android:strokeWidth="2" + android:pathData=" +M11.853 0 +C11.853 -4.418 8.374 -8 4.083 -8 +L-5.5 -8 +C-6.328 -8 -7 -7.328 -7 -6.5 +C-7 -5.672 -6.328 -5 -5.5 -5 +L-2.917 -5 +C-1.26 -5 0.083 -3.657 0.083 -2 +L0.083 2 +C0.083 3.657 -1.26 5 -2.917 5 +L-5.5 5 +C-6.328 5 -7 5.672 -7 6.5 +C-7 7.328 -6.328 8 -5.5 8 +L4.083 8 +C8.374 8 11.853 4.418 11.853 0 +Z + "/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml new file mode 100644 index 000000000000..2d4ce106ef38 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<rotate xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/ic_spacecraft" + android:fromDegrees="0" + android:toDegrees="360" + />
\ No newline at end of file diff --git a/packages/EasterEgg/res/values/themes.xml b/packages/EasterEgg/res/values/themes.xml index 5b163043a356..3a87e456fc3b 100644 --- a/packages/EasterEgg/res/values/themes.xml +++ b/packages/EasterEgg/res/values/themes.xml @@ -1,7 +1,26 @@ -<resources> +<?xml version="1.0" encoding="utf-8"?><!-- +Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> <style name="ThemeOverlay.EasterEgg.AppWidgetContainer" parent=""> <item name="appWidgetBackgroundColor">@color/light_blue_600</item> <item name="appWidgetTextColor">@color/light_blue_50</item> </style> -</resources>
\ No newline at end of file + + <style name="Theme.Landroid" parent="android:Theme.Material.NoActionBar"> + <item name="android:windowLightStatusBar">false</item> + <item name="android:windowLightNavigationBar">false</item> + </style> +</resources> diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt index fb5954ec9736..8214c540304e 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt @@ -41,14 +41,16 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity { val telemetry: String get() = - listOf( - "---- AUTOPILOT ENGAGED ----", - "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."), - "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "", - ) - .joinToString("\n") - - private var strategy: String = "NONE" + if (enabled) + listOf( + "---- AUTOPILOT ENGAGED ----", + "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."), + "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "", + ) + .joinToString("\n") + else "" + + var strategy: String = "NONE" private var debug: String = "" override fun update(sim: Simulator, dt: Float) { @@ -119,7 +121,7 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity { target.pos + Vec2.makeWithAngleMag( target.velocity.angle(), - min(altitude / 2, target.velocity.mag()) + min(altitude / 2, target.velocity.mag()), ) leadingVector = leadingPos - ship.pos diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt index d040fba49fdf..e74863849efa 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt @@ -20,9 +20,19 @@ import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.Easing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlin.random.Random @Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() } @@ -36,6 +46,40 @@ val flickerFadeIn = animationSpec = tween( durationMillis = 1000, - easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random) + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random), ) ) + +fun flickerFadeInAfterDelay(delay: Int = 0) = + fadeIn( + animationSpec = + tween( + durationMillis = 1000, + delayMillis = delay, + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random), + ) + ) + +@Composable +fun ConsoleButton( + modifier: Modifier = Modifier, + textStyle: TextStyle = TextStyle.Default, + color: Color, + bgColor: Color, + borderColor: Color, + text: String, + onClick: () -> Unit, +) { + Text( + style = textStyle, + color = color, + modifier = + modifier + .clickable { onClick() } + .background(color = bgColor) + .border(width = 1.dp, color = borderColor) + .padding(6.dp) + .minimumInteractiveComponentSize(), + text = text, + ) +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt index d56e8b9e8d0e..8d4adf638bb3 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt @@ -56,6 +56,8 @@ class DreamUniverse : DreamService() { } } + private var notifier: UniverseProgressNotifier? = null + override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -76,8 +78,8 @@ class DreamUniverse : DreamService() { Random.nextFloat() * PI2f, Random.nextFloatInRange( PLANET_ORBIT_RANGE.start, - PLANET_ORBIT_RANGE.endInclusive - ) + PLANET_ORBIT_RANGE.endInclusive, + ), ) } @@ -94,9 +96,11 @@ class DreamUniverse : DreamService() { composeView.setContent { Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) DebugText(DEBUG_TEXT) - Telemetry(universe) + Telemetry(universe, showControls = false) } + notifier = UniverseProgressNotifier(this, universe) + composeView.setViewTreeLifecycleOwner(lifecycleOwner) composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner) diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt index 4f77b00b7570..95a60c7a5292 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt @@ -21,6 +21,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility @@ -34,6 +35,7 @@ import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -46,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -59,6 +62,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.TextStyle @@ -74,9 +78,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import java.lang.Float.max import java.lang.Float.min import java.util.Calendar @@ -85,11 +86,14 @@ import kotlin.math.absoluteValue import kotlin.math.floor import kotlin.math.sqrt import kotlin.random.Random +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch enum class RandomSeedType { Fixed, Daily, - Evergreen + Evergreen, } const val TEST_UNIVERSE = false @@ -138,6 +142,10 @@ fun getDessertCode(): String = else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "") } +fun getSystemDesignation(universe: Universe): String { + return "${getDessertCode()}-${universe.randomSeed % 100_000}" +} + val DEBUG_TEXT = mutableStateOf("Hello Universe") const val SHOW_DEBUG_TEXT = false @@ -150,13 +158,13 @@ fun DebugText(text: MutableState<String>) { fontWeight = FontWeight.Medium, fontSize = 9.sp, color = Color.Yellow, - text = text.value + text = text.value, ) } } @Composable -fun Telemetry(universe: Universe) { +fun Telemetry(universe: Universe, showControls: Boolean) { var topVisible by remember { mutableStateOf(false) } var bottomVisible by remember { mutableStateOf(false) } @@ -174,7 +182,6 @@ fun Telemetry(universe: Universe) { LaunchedEffect("blah") { delay(1000) bottomVisible = true - delay(1000) topVisible = true } @@ -183,13 +190,11 @@ fun Telemetry(universe: Universe) { // TODO: Narrow the scope of invalidation here to the specific data needed; // the behavior below mimics the previous implementation of a snapshot ticker value val recomposeScope = currentRecomposeScope - Telescope(universe) { - recomposeScope.invalidate() - } + Telescope(universe) { recomposeScope.invalidate() } BoxWithConstraints( modifier = - Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent), + Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent) ) { val wide = maxWidth > maxHeight Column( @@ -197,57 +202,82 @@ fun Telemetry(universe: Universe) { Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart) .fillMaxWidth(if (wide) 0.45f else 1.0f) ) { - universe.ship.autopilot?.let { autopilot -> - if (autopilot.enabled) { + val autopilotEnabled = universe.ship.autopilot?.enabled == true + if (autopilotEnabled) { + universe.ship.autopilot?.let { autopilot -> AnimatedVisibility( modifier = Modifier, visible = bottomVisible, - enter = flickerFadeIn + enter = flickerFadeIn, ) { Text( style = textStyle, color = Colors.Autopilot, modifier = Modifier.align(Left), - text = autopilot.telemetry + text = autopilot.telemetry, ) } } } - AnimatedVisibility( - modifier = Modifier, - visible = bottomVisible, - enter = flickerFadeIn - ) { - Text( - style = textStyle, - color = Colors.Console, - modifier = Modifier.align(Left), - text = - with(universe.ship) { - val closest = universe.closestPlanet() - val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt() - listOfNotNull( - landing?.let { - "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}" - } - ?: if (distToClosest < 10_000) { - "ALT: $distToClosest" - } else null, - "THR: %.0f%%".format(thrust.mag() * 100f), - "POS: %s".format(pos.str("%+7.0f")), - "VEL: %.0f".format(velocity.mag()) - ) - .joinToString("\n") + Row(modifier = Modifier.padding(top = 6.dp)) { + AnimatedVisibility( + modifier = Modifier.weight(1f), + visible = bottomVisible, + enter = flickerFadeIn, + ) { + Text( + style = textStyle, + color = Colors.Console, + text = + with(universe.ship) { + val closest = universe.closestPlanet() + val distToClosest = + ((closest.pos - pos).mag() - closest.radius).toInt() + listOfNotNull( + landing?.let { + "LND: ${it.planet.name.toUpperCase()}\n" + + "JOB: ${it.text.toUpperCase()}" + } + ?: if (distToClosest < 10_000) { + "ALT: $distToClosest" + } else null, + "THR: %.0f%%".format(thrust.mag() * 100f), + "POS: %s".format(pos.str("%+7.0f")), + "VEL: %.0f".format(velocity.mag()), + ) + .joinToString("\n") + }, + ) + } + + if (showControls) { + AnimatedVisibility( + visible = bottomVisible, + enter = flickerFadeInAfterDelay(500), + ) { + ConsoleButton( + textStyle = textStyle, + color = Colors.Console, + bgColor = if (autopilotEnabled) Colors.Autopilot else Color.Transparent, + borderColor = Colors.Console, + text = "AUTO", + ) { + universe.ship.autopilot?.let { + it.enabled = !it.enabled + DYNAMIC_ZOOM = it.enabled + if (!it.enabled) universe.ship.thrust = Vec2.Zero + } } - ) + } + } } } AnimatedVisibility( modifier = Modifier.align(Alignment.TopStart), visible = topVisible, - enter = flickerFadeIn + enter = flickerFadeInAfterDelay(1000), ) { Text( style = textStyle, @@ -263,13 +293,12 @@ fun Telemetry(universe: Universe) { text = (with(universe.star) { listOf( - " STAR: $name (${getDessertCode()}-" + - "${universe.randomSeed % 100_000})", + " STAR: $name (${getSystemDesignation(universe)})", " CLASS: ${cls.name}", "RADIUS: ${radius.toInt()}", " MASS: %.3g".format(mass), "BODIES: ${explored.size} / ${universe.planets.size}", - "" + "", ) } + explored @@ -280,11 +309,11 @@ fun Telemetry(universe: Universe) { " ATMO: ${it.atmosphere.capitalize()}", " FAUNA: ${it.fauna.capitalize()}", " FLORA: ${it.flora.capitalize()}", - "" + "", ) } .flatten()) - .joinToString("\n") + .joinToString("\n"), // TODO: different colors, highlight latest discovery ) @@ -293,6 +322,7 @@ fun Telemetry(universe: Universe) { } class MainActivity : ComponentActivity() { + private var notifier: UniverseProgressNotifier? = null private var foldState = mutableStateOf<FoldingFeature?>(null) override fun onCreate(savedInstanceState: Bundle?) { @@ -300,7 +330,7 @@ class MainActivity : ComponentActivity() { onWindowLayoutInfoChange() - enableEdgeToEdge() + enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Red.toArgb())) val universe = Universe(namer = Namer(resources), randomSeed = randomSeed()) @@ -312,12 +342,13 @@ class MainActivity : ComponentActivity() { com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext) - // for autopilot testing in the activity - // val autopilot = Autopilot(universe.ship, universe) - // universe.ship.autopilot = autopilot - // universe.add(autopilot) - // autopilot.enabled = true - // DYNAMIC_ZOOM = autopilot.enabled + // set up the autopilot in case we need it + val autopilot = Autopilot(universe.ship, universe) + universe.ship.autopilot = autopilot + universe.add(autopilot) + autopilot.enabled = false + + notifier = UniverseProgressNotifier(this, universe) setContent { Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) @@ -329,7 +360,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), minRadius = minRadius, maxRadius = maxRadius, - color = Color.Green + color = Color.Green, ) { vec -> (universe.follow as? Spacecraft)?.let { ship -> if (vec == Vec2.Zero) { @@ -346,13 +377,13 @@ class MainActivity : ComponentActivity() { ship.thrust = Vec2.makeWithAngleMag( a, - lexp(minRadius, maxRadius, m).coerceIn(0f, 1f) + lexp(minRadius, maxRadius, m).coerceIn(0f, 1f), ) } } } } - Telemetry(universe) + Telemetry(universe, true) } } @@ -382,7 +413,7 @@ fun MainActivityPreview() { Spaaaace(modifier = Modifier.fillMaxSize(), universe) DebugText(DEBUG_TEXT) - Telemetry(universe) + Telemetry(universe, true) } @Composable @@ -391,7 +422,7 @@ fun FlightStick( minRadius: Float = 0f, maxRadius: Float = 1000f, color: Color = Color.Green, - onStickChanged: (vector: Vec2) -> Unit + onStickChanged: (vector: Vec2) -> Unit, ) { val origin = remember { mutableStateOf(Vec2.Zero) } val target = remember { mutableStateOf(Vec2.Zero) } @@ -444,14 +475,14 @@ fun FlightStick( PathEffect.dashPathEffect( floatArrayOf(this.density * 1f, this.density * 2f) ) - else null - ) + else null, + ), ) drawLine( color = color, start = origin.value, end = origin.value + Vec2.makeWithAngleMag(a, mag), - strokeWidth = 2f + strokeWidth = 2f, ) } } @@ -462,15 +493,13 @@ fun FlightStick( fun Spaaaace( modifier: Modifier, u: Universe, - foldState: MutableState<FoldingFeature?> = mutableStateOf(null) + foldState: MutableState<FoldingFeature?> = mutableStateOf(null), ) { LaunchedEffect(u) { - while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> - u.step(frameTimeNanos) - } + while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> u.step(frameTimeNanos) } } - var cameraZoom by remember { mutableStateOf(1f) } + var cameraZoom by remember { mutableFloatStateOf(DEFAULT_CAMERA_ZOOM) } var cameraOffset by remember { mutableStateOf(Offset.Zero) } val transformableState = @@ -501,15 +530,16 @@ fun Spaaaace( val closest = u.closestPlanet() val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f) // val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f - if (DYNAMIC_ZOOM) { - cameraZoom = - expSmooth( - cameraZoom, - clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM), - dt = u.dt, - speed = 1.5f - ) - } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM + val targetZoom = + if (DYNAMIC_ZOOM) { + clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM) + } else { + DEFAULT_CAMERA_ZOOM + } + if (!TOUCH_CAMERA_ZOOM) { + cameraZoom = expSmooth(cameraZoom, targetZoom, dt = u.dt, speed = 1.5f) + } + if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f // cameraZoom: metersToPixels @@ -521,9 +551,9 @@ fun Spaaaace( -cameraOffset - Offset( visibleSpaceSizeMeters.width * centerFracX, - visibleSpaceSizeMeters.height * centerFracY + visibleSpaceSizeMeters.height * centerFracY, ), - visibleSpaceSizeMeters + visibleSpaceSizeMeters, ) var gridStep = 1000f @@ -537,14 +567,14 @@ fun Spaaaace( "fps: ${"%3.0f".format(1f / u.dt)} " + "dt: ${u.dt}\n" + ((u.follow as? Spacecraft)?.let { - "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format( - it.pos.str("%+7.1f"), - it.velocity.mag(), - it.angle, - it.thrust.str("%+5.2f") - ) - } - ?: "") + + "ship: p=%s v=%7.2f a=%6.3f t=%s\n" + .format( + it.pos.str("%+7.1f"), + it.velocity.mag(), + it.angle, + it.thrust.str("%+5.2f"), + ) + } ?: "") + "star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " + "class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" + "planets: ${u.planets.size}\n" + @@ -574,7 +604,7 @@ fun Spaaaace( translate( -visibleSpaceRectMeters.center.x + size.width * 0.5f, - -visibleSpaceRectMeters.center.y + size.height * 0.5f + -visibleSpaceRectMeters.center.y + size.height * 0.5f, ) { // debug outer frame // drawRect( @@ -590,7 +620,7 @@ fun Spaaaace( color = Colors.Eigengrau2, start = Offset(x, visibleSpaceRectMeters.top), end = Offset(x, visibleSpaceRectMeters.bottom), - strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom, ) x += gridStep } @@ -601,7 +631,7 @@ fun Spaaaace( color = Colors.Eigengrau2, start = Offset(visibleSpaceRectMeters.left, y), end = Offset(visibleSpaceRectMeters.right, y), - strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom, ) y += gridStep } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt index 73318077f47a..babf1328c7d4 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt @@ -16,8 +16,8 @@ package com.android.egg.landroid -import android.content.res.Resources import com.android.egg.R +import android.content.res.Resources import kotlin.random.Random const val SUFFIX_PROB = 0.75f @@ -58,7 +58,7 @@ class Namer(resources: Resources) { 1f to "*", 1f to "^", 1f to "#", - 0.1f to "(^*!%@##!!" + 0.1f to "(^*!%@##!!", ) private var activities = Bag(resources.getStringArray(R.array.activities)) @@ -101,26 +101,26 @@ class Namer(resources: Resources) { fun floraPlural(rng: Random): String { return floraGenericPlurals.pull(rng) } + fun faunaPlural(rng: Random): String { return faunaGenericPlurals.pull(rng) } + fun atmoPlural(rng: Random): String { return atmoGenericPlurals.pull(rng) } val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""") + fun describeActivity(rng: Random, target: Planet?): String { - return activities - .pull(rng) - .replace(TEMPLATE_REGEX) { - when (it.groupValues[1]) { - "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng) - "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng) - "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng) - "planet" -> (target?.description ?: "SOME BODY") // once told me - else -> "unknown template tag: ${it.groupValues[0]}" - } + return activities.pull(rng).replace(TEMPLATE_REGEX) { + when (it.groupValues[1]) { + "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng) + "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng) + "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng) + "planet" -> (target?.description ?: "SOME BODY") // once told me + else -> "unknown template tag: ${it.groupValues[0]}" } - .toUpperCase() + } } } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt new file mode 100644 index 000000000000..bb3a04df6f36 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.egg.landroid + +import com.android.egg.R + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.DisposableHandle + +const val CHANNEL_ID = "progress" +const val CHANNEL_NAME = "Spacecraft progress" +const val UPDATE_FREQUENCY_SEC = 1f + +fun lerpRange(range: ClosedFloatingPointRange<Float>, x: Float): Float = + lerp(range.start, range.endInclusive, x) + +class UniverseProgressNotifier(val context: Context, val universe: Universe) { + private val notificationId = universe.randomSeed.toInt() + private val chan = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + .apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC } + private val noman = + context.getSystemService(NotificationManager::class.java)?.apply { + createNotificationChannel(chan) + } + + private val registration: DisposableHandle = + universe.addSimulationStepListener(this::onSimulationStep) + + private val spacecraftIcon = Icon.createWithResource(context, R.drawable.ic_spacecraft_filled) + private val planetIcons = + listOf( + (lerpRange(PLANET_RADIUS_RANGE, 0.75f)) to + Icon.createWithResource(context, R.drawable.ic_planet_large), + (lerpRange(PLANET_RADIUS_RANGE, 0.5f)) to + Icon.createWithResource(context, R.drawable.ic_planet_medium), + (lerpRange(PLANET_RADIUS_RANGE, 0.25f)) to + Icon.createWithResource(context, R.drawable.ic_planet_small), + (PLANET_RADIUS_RANGE.start to + Icon.createWithResource(context, R.drawable.ic_planet_tiny)), + ) + + private fun getPlanetIcon(planet: Planet): Icon { + for ((radius, icon) in planetIcons) { + if (planet.radius > radius) return icon + } + return planetIcons.last().second + } + + private val progress = Notification.ProgressStyle().setProgressTrackerIcon(spacecraftIcon) + + private val builder = + Notification.Builder(context, CHANNEL_ID) + .setContentIntent( + PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + ) + .setPriority(Notification.PRIORITY_DEFAULT) + .setColorized(true) + .setOngoing(true) + .setColor(Colors.Eigengrau2.toArgb()) + .setStyle(progress) + + private var lastUpdate = 0f + private var initialDistToTarget = 0 + + private fun onSimulationStep() { + if (universe.now - lastUpdate >= UPDATE_FREQUENCY_SEC) { + lastUpdate = universe.now + // android.util.Log.v("Landroid", "posting notification at time ${universe.now}") + + var distToTarget = 0 + val autopilot = universe.ship.autopilot + val autopilotEnabled: Boolean = autopilot?.enabled == true + val target = autopilot?.target + val landing = universe.ship.landing + val speed = universe.ship.velocity.mag() + + if (landing != null) { + // landed + builder.setContentTitle("landed: ${landing.planet.name}") + builder.setContentText("currently: ${landing.text}") + builder.setShortCriticalText("landed") + + progress.setProgress(progress.progressMax) + progress.setProgressIndeterminate(false) + + builder.setStyle(progress) + } else if (autopilotEnabled) { + if (target != null) { + // autopilot en route + distToTarget = ((target.pos - universe.ship.pos).mag() - target.radius).toInt() + if (initialDistToTarget == 0) { + // we have a new target! + initialDistToTarget = distToTarget + progress.progressEndIcon = getPlanetIcon(target) + } + + val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???" + builder.setContentTitle("headed to: ${target.name}") + builder.setContentText( + "autopilot is ${autopilot.strategy.toLowerCase()}" + + "\ndist: ${distToTarget}u // eta: $eta" + ) + // fun fact: ProgressStyle was originally EnRouteStyle + builder.setShortCriticalText("en route") + + progress + .setProgressSegments( + listOf( + Notification.ProgressStyle.Segment(initialDistToTarget) + .setColor(Colors.Track.toArgb()) + ) + ) + .setProgress(initialDistToTarget - distToTarget) + .setProgressIndeterminate(false) + builder.setStyle(progress) + } else { + // no target + if (initialDistToTarget != 0) { + // just launched + initialDistToTarget = 0 + progress.progressStartIcon = progress.progressEndIcon + progress.progressEndIcon = null + } + + builder.setContentTitle("in space") + builder.setContentText("selecting new target...") + builder.setShortCriticalText("launched") + + progress.setProgressIndeterminate(true) + + builder.setStyle(progress) + } + } else { + // under user control + + initialDistToTarget = 0 + + builder.setContentTitle("in space") + builder.setContentText("under manual control") + builder.setShortCriticalText("adrift") + + builder.setStyle(null) + } + + builder + .setSubText(getSystemDesignation(universe)) + .setSmallIcon(R.drawable.ic_spacecraft_rotated) + + val notification = builder.build() + + // one of the silliest things about Android is that icon levels go from 0 to 10000 + notification.iconLevel = (((universe.ship.angle + PI2f) / PI2f) * 10_000f).toInt() + + noman?.notify(notificationId, notification) + } + } +} |