summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Dan Sandler <dsandler@android.com> 2025-02-07 00:26:15 -0500
committer Dan Sandler <dsandler@android.com> 2025-02-07 12:54:48 -0500
commit96b38375be8697f511519d4351538553079c5af6 (patch)
treed57347ab1530438922600dc27c70cc91c8e76b7d
parentca7d0c749352f76ccf27f4c405e1a37322976c4a (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
-rw-r--r--packages/EasterEgg/AndroidManifest.xml2
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_large.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_medium.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_small.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_tiny.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft.xml44
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml45
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml21
-rw-r--r--packages/EasterEgg/res/values/themes.xml23
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt20
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt46
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt10
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt208
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/Namer.kt26
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt187
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)
+ }
+ }
+}