diff options
30 files changed, 2734 insertions, 73 deletions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index c221d724c5a2..06635eedeb9a 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -481,7 +481,7 @@ public abstract class Context { * If binding from a top app and its target SDK version is at or above * {@link android.os.Build.VERSION_CODES#R}, the app needs to * explicitly use BIND_INCLUDE_CAPABILITIES flag to pass all capabilities to the service so the - * other app can have while-use-use access such as location, camera, microphone from background. + * other app can have while-in-use access such as location, camera, microphone from background. * If binding from a top app and its target SDK version is below * {@link android.os.Build.VERSION_CODES#R}, BIND_INCLUDE_CAPABILITIES is implicit. */ @@ -678,7 +678,7 @@ public abstract class Context { * </p> * * <em>This flag is NOT compatible with {@link BindServiceFlags}. If you need to use - * {@link BindServiceFlags}, you must use {@link #BIND_EXTERNAL_SERVICE_LONG} instead. + * {@link BindServiceFlags}, you must use {@link #BIND_EXTERNAL_SERVICE_LONG} instead.</em> */ public static final int BIND_EXTERNAL_SERVICE = 0x80000000; diff --git a/core/java/android/hardware/camera2/TotalCaptureResult.java b/core/java/android/hardware/camera2/TotalCaptureResult.java index ac7f2ca6a427..7e42f43056e1 100644 --- a/core/java/android/hardware/camera2/TotalCaptureResult.java +++ b/core/java/android/hardware/camera2/TotalCaptureResult.java @@ -179,7 +179,7 @@ public final class TotalCaptureResult extends CaptureResult { * @return unmodifiable map between physical camera ids and their capture result metadata * * @deprecated - * <p>Please use {@link #getPhysicalCameraTotalResults() instead to get the + * <p>Please use {@link #getPhysicalCameraTotalResults()} instead to get the * physical cameras' {@code TotalCaptureResult}.</p> */ public Map<String, CaptureResult> getPhysicalCameraResults() { diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index fdcb87ff5e3f..bbaaa472cbbb 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -259,6 +259,8 @@ public class BatteryStatsHistory { } private TraceDelegate mTracer; + private int mTraceLastState = 0; + private int mTraceLastState2 = 0; /** * Constructor @@ -1241,7 +1243,6 @@ public class BatteryStatsHistory { */ private void recordTraceEvents(int code, HistoryTag tag) { if (code == HistoryItem.EVENT_NONE) return; - if (!mTracer.tracingEnabled()) return; final int idx = code & HistoryItem.EVENT_TYPE_MASK; final String prefix = (code & HistoryItem.EVENT_FLAG_START) != 0 ? "+" : @@ -1270,8 +1271,6 @@ public class BatteryStatsHistory { * Writes changes to a HistoryItem state bitmap to Atrace. */ private void recordTraceCounters(int oldval, int newval, BitDescription[] descriptions) { - if (!mTracer.tracingEnabled()) return; - int diff = oldval ^ newval; if (diff == 0) return; @@ -1324,6 +1323,16 @@ public class BatteryStatsHistory { } private void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) { + if (mTracer != null && mTracer.tracingEnabled()) { + recordTraceEvents(cur.eventCode, cur.eventTag); + recordTraceCounters(mTraceLastState, cur.states, + BatteryStats.HISTORY_STATE_DESCRIPTIONS); + recordTraceCounters(mTraceLastState2, cur.states2, + BatteryStats.HISTORY_STATE2_DESCRIPTIONS); + mTraceLastState = cur.states; + mTraceLastState2 = cur.states2; + } + if (!mHaveBatteryLevel || !mRecordingHistory) { return; } @@ -1345,12 +1354,6 @@ public class BatteryStatsHistory { + Integer.toHexString(lastDiffStates2)); } - recordTraceEvents(cur.eventCode, cur.eventTag); - recordTraceCounters(mHistoryLastWritten.states, - cur.states, BatteryStats.HISTORY_STATE_DESCRIPTIONS); - recordTraceCounters(mHistoryLastWritten.states2, - cur.states2, BatteryStats.HISTORY_STATE2_DESCRIPTIONS); - if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE && timeDiffMs < 1000 && (diffStates & lastDiffStates) == 0 && (diffStates2 & lastDiffStates2) == 0 diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index 13a2ea2db140..edff47a0be1a 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -59,7 +59,7 @@ <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"خروج از «حالت یکدستی»"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"تنظیمات برای حبابکهای <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"سرریز"</string> - <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشت به پشته"</string> + <string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"افزودن برگشتن به پشته"</string> <string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g>"</string> <string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> از <xliff:g id="APP_NAME">%2$s</xliff:g> و <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> مورد بیشتر"</string> <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"انتقال به بالا سمت راست"</string> diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp index e88410c97415..8699f59e4fba 100644 --- a/packages/EasterEgg/Android.bp +++ b/packages/EasterEgg/Android.bp @@ -26,7 +26,10 @@ package { android_app { // the build system in pi-dev can't quite handle R.java in kt // so we will have a mix of java and kotlin files - srcs: ["src/**/*.java", "src/**/*.kt"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], resource_dirs: ["res"], @@ -36,17 +39,34 @@ android_app { certificate: "platform", optimize: { + enabled: true, + optimize: true, + shrink: true, + shrink_resources: true, + proguard_compatibility: false, proguard_flags_files: ["proguard.flags"], }, - static_libs: [ - "androidx.core_core", - "androidx.recyclerview_recyclerview", + static_libs: [ + "androidx.core_core", "androidx.annotation_annotation", - "kotlinx-coroutines-android", - "kotlinx-coroutines-core", - //"kotlinx-coroutines-reactive", - ], + "androidx.recyclerview_recyclerview", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + + "androidx.core_core-ktx", + "androidx.lifecycle_lifecycle-runtime-ktx", + "androidx.activity_activity-compose", + "androidx.compose.ui_ui", + "androidx.compose.ui_ui-util", + "androidx.compose.ui_ui-tooling-preview", + "androidx.compose.material_material", + "androidx.window_window", + + "androidx.compose.runtime_runtime", + "androidx.activity_activity-compose", + "androidx.compose.ui_ui", + ], manifest: "AndroidManifest.xml", diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml index cc7bb4a3ff81..d1db237966d5 100644 --- a/packages/EasterEgg/AndroidManifest.xml +++ b/packages/EasterEgg/AndroidManifest.xml @@ -1,4 +1,19 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2023 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. +--> + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.egg" android:versionCode="12" @@ -18,8 +33,27 @@ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <application - android:icon="@drawable/icon" + android:icon="@drawable/android14_patch_adaptive" android:label="@string/app_name"> + + <!-- Android U easter egg --> + + <activity + android:name=".landroid.MainActivity" + android:exported="true" + android:label="@string/u_egg_name" + android:icon="@drawable/android14_patch_adaptive" + android:configChanges="orientation|screenLayout|screenSize|density" + android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="com.android.internal.category.PLATLOGO" /> + </intent-filter> + </activity> + + + <!-- Android Q easter egg --> <activity android:name=".quares.QuaresActivity" android:exported="true" @@ -69,7 +103,7 @@ android:exported="true" android:showOnLockScreen="true" android:theme="@android:style/Theme.Material.Light.Dialog.NoActionBar" /> - <!-- Used to enable easter egg --> + <!-- Used to enable easter egg components for earlier easter eggs. --> <activity android:name=".ComponentActivationActivity" android:excludeFromRecents="true" @@ -79,7 +113,6 @@ <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> - <category android:name="com.android.internal.category.PLATLOGO" /> </intent-filter> </activity> diff --git a/packages/EasterEgg/res/drawable/android14_patch_adaptive.xml b/packages/EasterEgg/res/drawable/android14_patch_adaptive.xml new file mode 100644 index 000000000000..423e35146c24 --- /dev/null +++ b/packages/EasterEgg/res/drawable/android14_patch_adaptive.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2023 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. +--> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/android14_patch_adaptive_background"/> + <foreground android:drawable="@drawable/android14_patch_adaptive_foreground"/> + <monochrome android:drawable="@drawable/android14_patch_monochrome"/> +</adaptive-icon>
\ No newline at end of file diff --git a/packages/EasterEgg/res/drawable/android14_patch_adaptive_background.xml b/packages/EasterEgg/res/drawable/android14_patch_adaptive_background.xml new file mode 100644 index 000000000000..c31aa7bcfac1 --- /dev/null +++ b/packages/EasterEgg/res/drawable/android14_patch_adaptive_background.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2023 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="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:pathData="M0,0 L108,0 L108,108 L0,108 z" + android:fillColor="#FF073042"/> + <path + android:pathData="M44.51,43.32L44.86,42.27C47.04,54.48 52.81,86.71 52.81,50.14C52.81,49.99 52.92,49.86 53.06,49.86H55.04C55.18,49.86 55.3,49.98 55.3,50.14C55.27,114.18 44.51,43.32 44.51,43.32Z" + android:fillColor="#3DDC84"/> + <path + android:name="planetary head" + android:pathData="M38.81,42.23L33.63,51.21C33.33,51.72 33.51,52.38 34.02,52.68C34.54,52.98 35.2,52.8 35.49,52.28L40.74,43.2C49.22,47 58.92,47 67.4,43.2L72.65,52.28C72.96,52.79 73.62,52.96 74.13,52.65C74.62,52.35 74.79,51.71 74.51,51.21L69.33,42.23C78.23,37.39 84.32,28.38 85.21,17.74H22.93C23.82,28.38 29.91,37.39 38.81,42.23Z" + android:fillColor="#ffffff"/> + <!-- yes it's an easter egg in a vector drawable --> + <path + android:name="planetary body" + android:pathData="M22.9,0 L85.1,0 L85.1,15.5 L22.9,15.5 z" + android:fillColor="#ffffff" /> + <path + android:pathData="M54.96,43.32H53.1C52.92,43.32 52.77,43.47 52.77,43.65V48.04C52.77,48.22 52.92,48.37 53.1,48.37H54.96C55.15,48.37 55.3,48.22 55.3,48.04V43.65C55.3,43.47 55.15,43.32 54.96,43.32Z" + android:fillColor="#3DDC84"/> + <path + android:pathData="M54.99,40.61H53.08C52.91,40.61 52.77,40.75 52.77,40.92V41.56C52.77,41.73 52.91,41.87 53.08,41.87H54.99C55.16,41.87 55.3,41.73 55.3,41.56V40.92C55.3,40.75 55.16,40.61 54.99,40.61Z" + android:fillColor="#3DDC84"/> + <path + android:pathData="M41.49,47.88H40.86V48.51H41.49V47.88Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M44.13,57.08H43.5V57.71H44.13V57.08Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M72.29,66.76H71.66V67.39H72.29V66.76Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M59.31,53.41H58.68V54.04H59.31V53.41Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M64.47,48.19H63.84V48.83H64.47V48.19Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M60.58,59.09H59.95V59.72H60.58V59.09Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M66.95,56.7H65.69V57.97H66.95V56.7Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M44.13,60.71H43.5V61.34H44.13V60.71Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M49.66,51.33H48.4V52.6H49.66V51.33Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M57.78,63.83H56.52V65.09H57.78V63.83Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M61.1,68.57H59.83V69.83H61.1V68.57Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M40.43,53.73H39.16V54.99H40.43V53.73Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M74.47,44H73.21V45.26H74.47V44Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M36.8,64.58H35.54V65.84H36.8V64.58Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/packages/EasterEgg/res/drawable/android14_patch_adaptive_foreground.xml b/packages/EasterEgg/res/drawable/android14_patch_adaptive_foreground.xml new file mode 100644 index 000000000000..391d5158e522 --- /dev/null +++ b/packages/EasterEgg/res/drawable/android14_patch_adaptive_foreground.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2023 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="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:pathData="M54.03,33.03C52.99,33.03 52.14,33.86 52.14,34.87V37.14C52.14,37.34 52.3,37.5 52.5,37.5C52.69,37.5 52.85,37.34 52.85,37.14V36.53C52.85,36.14 53.17,35.82 53.56,35.82H54.51C54.9,35.82 55.22,36.14 55.22,36.53V37.14C55.22,37.34 55.38,37.5 55.57,37.5C55.77,37.5 55.93,37.34 55.93,37.14V34.87C55.93,33.86 55.08,33.03 54.03,33.03H54.03Z" + android:fillColor="#3DDC84"/> + <path + android:pathData="M108,0H0V108H108V0ZM54,80.67C68.73,80.67 80.67,68.73 80.67,54C80.67,39.27 68.73,27.33 54,27.33C39.27,27.33 27.33,39.27 27.33,54C27.33,68.73 39.27,80.67 54,80.67Z" + android:fillColor="#F86734" + android:fillType="evenOdd"/> + <group> + <!-- the text doesn't look great everywhere but you can remove the clip to try it out. --> + <clip-path /> + <path + android:pathData="M28.58,32.18L29.18,31.5L33.82,33.02L33.12,33.81L32.15,33.48L30.92,34.87L31.37,35.8L30.68,36.58L28.58,32.18L28.58,32.18ZM31.25,33.18L29.87,32.71L30.51,34.02L31.25,33.18V33.18Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M38,29.76L34.61,28.79L36.23,31.04L35.42,31.62L32.8,27.99L33.5,27.48L36.88,28.45L35.26,26.21L36.08,25.62L38.7,29.25L38,29.76Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M39.23,23.87L40.63,23.27C41.79,22.77 43.13,23.28 43.62,24.43C44.11,25.57 43.56,26.89 42.4,27.39L40.99,27.99L39.23,23.87ZM42.03,26.54C42.73,26.24 42.96,25.49 42.68,24.83C42.4,24.17 41.69,23.82 41,24.11L40.51,24.32L41.55,26.75L42.03,26.54Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M45.91,21.43L47.64,21.09C48.47,20.93 49.12,21.41 49.27,22.15C49.38,22.72 49.15,23.14 48.63,23.45L50.57,25.08L49.39,25.31L47.57,23.79L47.41,23.82L47.76,25.63L46.78,25.83L45.91,21.43H45.91ZM47.87,22.86C48.16,22.8 48.34,22.59 48.29,22.34C48.24,22.07 48,21.96 47.71,22.02L47.07,22.14L47.24,22.98L47.87,22.86Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M52.17,22.69C52.19,21.41 53.24,20.39 54.52,20.41C55.8,20.43 56.82,21.49 56.8,22.76C56.78,24.04 55.72,25.06 54.45,25.04C53.17,25.02 52.15,23.96 52.17,22.69ZM55.79,22.75C55.8,22.02 55.23,21.39 54.51,21.38C53.78,21.37 53.19,21.98 53.18,22.7C53.17,23.43 53.73,24.06 54.47,24.07C55.19,24.08 55.78,23.47 55.79,22.75H55.79Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M60,21.01L60.98,21.2L60.12,25.6L59.14,25.41L60,21.01Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M64.3,22.03L65.73,22.58C66.91,23.03 67.51,24.32 67.07,25.49C66.62,26.65 65.31,27.22 64.13,26.77L62.71,26.22L64.3,22.03L64.3,22.03ZM64.46,25.9C65.17,26.17 65.86,25.8 66.12,25.12C66.37,24.45 66.11,23.71 65.4,23.44L64.91,23.25L63.97,25.72L64.46,25.9Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M73.59,27.94L72.94,27.44L73.51,26.69L74.92,27.77L72.2,31.34L71.45,30.76L73.59,27.94Z" + android:fillColor="#ffffff"/> + <path + android:pathData="M76.18,33.75L74.69,32.14L75.25,31.62L78.81,31.42L79.4,32.05L77.47,33.85L77.86,34.27L77.22,34.86L76.83,34.44L76.12,35.11L75.47,34.41L76.18,33.75ZM77.72,32.31L76.12,32.4L76.82,33.15L77.72,32.31Z" + android:fillColor="#ffffff"/> + </group> +</vector> diff --git a/packages/EasterEgg/res/drawable/android14_patch_monochrome.xml b/packages/EasterEgg/res/drawable/android14_patch_monochrome.xml new file mode 100644 index 000000000000..beef85ce3b3f --- /dev/null +++ b/packages/EasterEgg/res/drawable/android14_patch_monochrome.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2023 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="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <group> + <clip-path + android:pathData="M0,0h108v108h-108z"/> + <group> + <clip-path + android:pathData="M22,22h64v64h-64z"/> + <path + android:pathData="M54,78C67.25,78 78,67.25 78,54C78,40.75 67.25,30 54,30C40.75,30 30,40.75 30,54C30,67.25 40.75,78 54,78Z" + android:strokeWidth="5" + android:fillColor="#00000000" + android:strokeColor="#000000"/> + <group> + <clip-path + android:pathData="M77.5,54C77.5,66.98 66.98,77.5 54,77.5C41.02,77.5 30.5,66.98 30.5,54C30.5,41.02 41.02,30.5 54,30.5C66.98,30.5 77.5,41.02 77.5,54Z"/> + <path + android:pathData="M61.5,46.06C56.7,47.89 51.4,47.89 46.61,46.06L44.04,50.51C43.49,51.46 42.28,51.79 41.33,51.24C40.39,50.69 40.06,49.48 40.61,48.53L43.06,44.28C37.97,41.03 34.54,35.56 34,29.19L33.88,27.74H74.22L74.1,29.19C73.57,35.56 70.14,41.03 65.04,44.28L67.51,48.56C68.03,49.49 67.71,50.66 66.8,51.21C65.87,51.77 64.65,51.47 64.08,50.54L64.07,50.51L61.5,46.06Z" + android:fillColor="#000000"/> + </group> + <path + android:pathData="M51.33,67.33h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M48.67,62h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M56.67,70h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M56.67,62h2.67v2.67h-2.67z" + android:fillColor="#000000"/> + <path + android:pathData="M67.33,62h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M59.33,51.33h2.67v2.67h-2.67z" + android:fillColor="#000000"/> + <path + android:pathData="M62,59.33h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M70,54h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M35.33,56.67h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M35.33,48.67h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M40.67,59.33h2.67v2.67h-2.67z" + android:fillColor="#000000"/> + <path + android:pathData="M46,51.33h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M43.33,67.33h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + <path + android:pathData="M54,54h1.33v1.33h-1.33z" + android:fillColor="#000000"/> + </group> + </group> +</vector> diff --git a/packages/EasterEgg/res/values/landroid_strings.xml b/packages/EasterEgg/res/values/landroid_strings.xml new file mode 100644 index 000000000000..1394f2f55868 --- /dev/null +++ b/packages/EasterEgg/res/values/landroid_strings.xml @@ -0,0 +1,371 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2023 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> + <string name="u_egg_name" translatable="false">Android 14 Easter Egg</string> + + <string-array name="planet_descriptors" translatable="false"> + <item>earthy</item> + <item>swamp</item> + <item>frozen</item> + <item>grassy</item> + <item>arid</item> + <item>crowded</item> + <item>ancient</item> + <item>lively</item> + <item>homey</item> + <item>modern</item> + <item>boring</item> + <item>compact</item> + <item>expensive</item> + <item>polluted</item> + <item>rusty</item> + <item>sandy</item> + <item>undulating</item> + <item>verdant</item> + <item>tessellated</item> + <item>hollow</item> + <item>scalding</item> + <item>hemispherical</item> + <item>oblong</item> + <item>oblate</item> + <item>vacuum</item> + <item>high-pressure</item> + <item>low-pressure</item> + <item>plastic</item> + <item>metallic</item> + <item>burned-out</item> + <item>bucolic</item> + </string-array> + + <string-array name="life_descriptors" translatable="false"> + <item>aggressive</item> + <item>passive-aggressive</item> + <item>shy</item> + <item>timid</item> + <item>nasty</item> + <item>brutish</item> + <item>short</item> + <item>absent</item> + <item>teen-aged</item> + <item>confused</item> + <item>transparent</item> + <item>cubic</item> + <item>quadratic</item> + <item>higher-order</item> + <item>huge</item> + <item>tall</item> + <item>wary</item> + <item>loud</item> + <item>yodeling</item> + <item>purring</item> + <item>slender</item> + <item>cats</item> + <item>adorable</item> + <item>eclectic</item> + <item>electric</item> + <item>microscopic</item> + <item>trunkless</item> + <item>myriad</item> + <item>cantankerous</item> + <item>gargantuan</item> + <item>contagious</item> + <item>fungal</item> + <item>cattywampus</item> + <item>spatchcocked</item> + <item>rotisserie</item> + <item>farm-to-table</item> + <item>organic</item> + <item>synthetic</item> + <item>unfocused</item> + <item>focused</item> + <item>capitalist</item> + <item>communal</item> + <item>bossy</item> + <item>malicious</item> + <item>compliant</item> + <item>psychic</item> + <item>oblivious</item> + <item>passive</item> + <item>bonsai</item> + </string-array> + + <string-array name="any_descriptors" translatable="false"> + <item>silly</item> + <item>dangerous</item> + <item>vast</item> + <item>invisible</item> + <item>superfluous</item> + <item>superconducting</item> + <item>superior</item> + <item>alien</item> + <item>phantom</item> + <item>friendly</item> + <item>peaceful</item> + <item>lonely</item> + <item>uncomfortable</item> + <item>charming</item> + <item>fractal</item> + <item>imaginary</item> + <item>forgotten</item> + <item>tardy</item> + <item>gassy</item> + <item>fungible</item> + <item>bespoke</item> + <item>artisanal</item> + <item>exceptional</item> + <item>puffy</item> + <item>rusty</item> + <item>fresh</item> + <item>crusty</item> + <item>glossy</item> + <item>lovely</item> + <item>processed</item> + <item>macabre</item> + <item>reticulated</item> + <item>shocking</item> + <item>void</item> + <item>undefined</item> + <item>gothic</item> + <item>beige</item> + <item>mid</item> + <item>milquetoast</item> + <item>melancholy</item> + <item>unnerving</item> + <item>cheery</item> + <item>vibrant</item> + <item>heliotrope</item> + <item>psychedelic</item> + <item>nondescript</item> + <item>indescribable</item> + <item>tubular</item> + <item>toroidal</item> + <item>voxellated</item> + <item>low-poly</item> + <item>low-carb</item> + <item>100% cotton</item> + <item>synthetic</item> + <item>boot-cut</item> + <item>bell-bottom</item> + <item>bumpy</item> + <item>fluffy</item> + <item>sous-vide</item> + <item>tepid</item> + <item>upcycled</item> + <item>sous-vide</item> + <item>bedazzled</item> + <item>ancient</item> + <item>inexplicable</item> + <item>sparkling</item> + <item>still</item> + <item>lemon-scented</item> + <item>eccentric</item> + <item>tilted</item> + <item>pungent</item> + <item>pine-scented</item> + <item>corduroy</item> + <item>overengineered</item> + <item>bioengineered</item> + <item>impossible</item> + </string-array> + + <string-array name="constellations" translatable="false"> + <item>Aries</item> + <item>Taurus</item> + <item>Gemini</item> + <item>Cancer</item> + <item>Leo</item> + <item>Virgo</item> + <item>Libra</item> + <item>Scorpio</item> + <item>Sagittarius</item> + <item>Capricorn</item> + <item>Aquarius</item> + <item>Pisces</item> + <item>Andromeda</item> + <item>Cygnus</item> + <item>Draco</item> + <item>Alcor</item> + <item>Calamari</item> + <item>Cuckoo</item> + <item>Neko</item> + <item>Monoceros</item> + <item>Norma</item> + <item>Abnorma</item> + <item>Morel</item> + <item>Redlands</item> + <item>Cupcake</item> + <item>Donut</item> + <item>Eclair</item> + <item>Froyo</item> + <item>Gingerbread</item> + <item>Honeycomb</item> + <item>Icecreamsandwich</item> + <item>Jellybean</item> + <item>Kitkat</item> + <item>Lollipop</item> + <item>Marshmallow</item> + <item>Nougat</item> + <item>Oreo</item> + <item>Pie</item> + <item>Quincetart</item> + <item>Redvelvetcake</item> + <item>Snowcone</item> + <item>Tiramisu</item> + <item>Upsidedowncake</item> + <item>Vanillaicecream</item> + <item>Android</item> + <item>Binder</item> + <item>Campanile</item> + <item>Dread</item> + </string-array> + + <!-- prob: 5% --> + <string-array name="constellations_rare" translatable="false"> + <item>Jandycane</item> + <item>Zombiegingerbread</item> + <item>Astro</item> + <item>Bender</item> + <item>Flan</item> + <item>Untitled-1</item> + <item>Expedit</item> + <item>Petit Four</item> + <item>Worcester</item> + <item>Xylophone</item> + <item>Yellowpeep</item> + <item>Zebraball</item> + <item>Hutton</item> + <item>Klang</item> + <item>Frogblast</item> + <item>Exo</item> + <item>Keylimepie</item> + <item>Nat</item> + <item>Nrp</item> + </string-array> + + <!-- prob: 75% --> + <string-array name="star_suffixes" translatable="false"> + <item>Alpha</item> + <item>Beta</item> + <item>Gamma</item> + <item>Delta</item> + <item>Epsilon</item> + <item>Zeta</item> + <item>Eta</item> + <item>Theta</item> + <item>Iota</item> + <item>Kappa</item> + <item>Lambda</item> + <item>Mu</item> + <item>Nu</item> + <item>Xi</item> + <item>Omicron</item> + <item>Pi</item> + <item>Rho</item> + <item>Sigma</item> + <item>Tau</item> + <item>Upsilon</item> + <item>Phi</item> + <item>Chi</item> + <item>Psi</item> + <item>Omega</item> + + <item>Prime</item> + <item>Secundo</item> + <item>Major</item> + <item>Minor</item> + <item>Diminished</item> + <item>Augmented</item> + <item>Ultima</item> + <item>Penultima</item> + <item>Mid</item> + + <item>Proxima</item> + <item>Novis</item> + + <item>Plus</item> + </string-array> + + <!-- prob: 5% --> + <!-- more than one can be appended, with very low prob --> + <string-array name="star_suffixes_rare" translatable="false"> + <item>Serif</item> + <item>Sans</item> + <item>Oblique</item> + <item>Grotesque</item> + <item>Handtooled</item> + <item>III “Trey”</item> + <item>Alfredo</item> + <item>2.0</item> + <item>(Final)</item> + <item>(Final (Final))</item> + <item>(Draft)</item> + <item>Con Carne</item> + </string-array> + + <string-array name="planet_types" translatable="false"> + <item>planet</item> + <item>planetoid</item> + <item>moon</item> + <item>moonlet</item> + <item>centaur</item> + <item>asteroid</item> + <item>space garbage</item> + <item>detritus</item> + <item>satellite</item> + <item>core</item> + <item>giant</item> + <item>body</item> + <item>slab</item> + <item>rock</item> + <item>husk</item> + <item>planemo</item> + <item>object</item> + <item>planetesimal</item> + <item>exoplanet</item> + <item>ploonet</item> + </string-array> + + <string-array name="atmo_descriptors" translatable="false"> + <item>toxic</item> + <item>breathable</item> + <item>radioactive</item> + <item>clear</item> + <item>calm</item> + <item>peaceful</item> + <item>vacuum</item> + <item>stormy</item> + <item>freezing</item> + <item>burning</item> + <item>humid</item> + <item>tropical</item> + <item>cloudy</item> + <item>obscured</item> + <item>damp</item> + <item>dank</item> + <item>clammy</item> + <item>frozen</item> + <item>contaminated</item> + <item>temperate</item> + <item>moist</item> + <item>minty</item> + <item>relaxed</item> + <item>skunky</item> + <item>breezy</item> + <item>soup </item> + </string-array> + +</resources> diff --git a/packages/EasterEgg/res/values/strings.xml b/packages/EasterEgg/res/values/strings.xml index 743947ad281e..79957df04720 100644 --- a/packages/EasterEgg/res/values/strings.xml +++ b/packages/EasterEgg/res/values/strings.xml @@ -14,7 +14,7 @@ Copyright (C) 2018 The Android Open Source Project limitations under the License. --> <resources xmlns:android="http://schemas.android.com/apk/res/android"> - <string name="app_name" translatable="false">Android S Easter Egg</string> + <string name="app_name" translatable="false">Android Easter Egg</string> <!-- name of the Q easter egg, a nonogram-style icon puzzle --> <string name="q_egg_name" translatable="false">Icon Quiz</string> diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt new file mode 100644 index 000000000000..f5657ae6c0c3 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Colors.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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 androidx.compose.ui.graphics.Color + +/** Various UI colors. */ +object Colors { + val Eigengrau = Color(0xFF16161D) + val Eigengrau2 = Color(0xFF292936) + val Eigengrau3 = Color(0xFF3C3C4F) + val Eigengrau4 = Color(0xFFA7A7CA) + + val Console = Color(0xFFB7B7FF) +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt new file mode 100644 index 000000000000..d040fba49fdf --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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 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.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import kotlin.random.Random + +@Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() } + +operator fun Easing.times(next: Easing) = { x: Float -> next.transform(transform(x)) } + +fun flickerFadeEasing(rng: Random) = Easing { frac -> if (rng.nextFloat() < frac) 1f else 0f } + +val flickerFadeIn = + fadeIn( + animationSpec = + tween( + durationMillis = 1000, + easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random) + ) + ) diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt new file mode 100644 index 000000000000..5a9b8141bb40 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2023 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 android.content.res.Resources +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.withInfiniteAnimationFrameNanos +import androidx.compose.animation.fadeIn +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.transformable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.AbsoluteAlignment.Left +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +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.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.math.MathUtils.clamp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.window.layout.FoldingFeature +import androidx.window.layout.WindowInfoTracker +import java.lang.Float.max +import java.lang.Float.min +import java.util.Calendar +import java.util.GregorianCalendar +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 +} + +const val TEST_UNIVERSE = false + +val RANDOM_SEED_TYPE = RandomSeedType.Daily + +const val FIXED_RANDOM_SEED = 5038L +const val DEFAULT_CAMERA_ZOOM = 0.25f +const val MIN_CAMERA_ZOOM = 250f / UNIVERSE_RANGE // 0.0025f +const val MAX_CAMERA_ZOOM = 5f +const val TOUCH_CAMERA_PAN = false +const val TOUCH_CAMERA_ZOOM = true +const val DYNAMIC_ZOOM = false // @@@ FIXME + +fun dailySeed(): Long { + val today = GregorianCalendar() + return today.get(Calendar.YEAR) * 10_000L + + today.get(Calendar.MONTH) * 100L + + today.get(Calendar.DAY_OF_MONTH) +} + +fun randomSeed(): Long { + return when (RANDOM_SEED_TYPE) { + RandomSeedType.Fixed -> FIXED_RANDOM_SEED + RandomSeedType.Daily -> dailySeed() + else -> Random.Default.nextLong().mod(10_000_000).toLong() + }.absoluteValue +} + +val DEBUG_TEXT = mutableStateOf("Hello Universe") +const val SHOW_DEBUG_TEXT = false + +@Composable +fun DebugText(text: MutableState<String>) { + if (SHOW_DEBUG_TEXT) { + Text( + modifier = Modifier.fillMaxWidth().border(0.5.dp, color = Color.Yellow).padding(2.dp), + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Medium, + fontSize = 9.sp, + color = Color.Yellow, + text = text.value + ) + } +} + +@Composable +fun ColumnScope.ConsoleText( + modifier: Modifier = Modifier, + visible: Boolean = true, + random: Random = Random.Default, + text: String +) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = + fadeIn( + animationSpec = + tween( + durationMillis = 1000, + easing = flickerFadeEasing(random) * CubicBezierEasing(0f, 1f, 1f, 0f) + ) + ) + ) { + Text( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + color = Color(0xFFFF8000), + text = text + ) + } +} + +@Composable +fun Telemetry(universe: VisibleUniverse) { + var topVisible by remember { mutableStateOf(false) } + var bottomVisible by remember { mutableStateOf(false) } + + LaunchedEffect("blah") { + delay(1000) + bottomVisible = true + delay(1000) + topVisible = true + } + + Column(modifier = Modifier.fillMaxSize().padding(6.dp)) { + universe.triggerDraw.value // recompose on every frame + val explored = universe.planets.filter { it.explored } + + AnimatedVisibility(modifier = Modifier, visible = topVisible, enter = flickerFadeIn) { + Text( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + color = Colors.Console, + modifier = Modifier.align(Left), + text = + with(universe.star) { + " STAR: $name (UDC-${universe.randomSeed % 100_000})\n" + + " CLASS: ${cls.name}\n" + + "RADIUS: ${radius.toInt()}\n" + + " MASS: %.3g\n".format(mass) + + "BODIES: ${explored.size} / ${universe.planets.size}\n" + + "\n" + } + + explored + .map { + " BODY: ${it.name}\n" + + " TYPE: ${it.description.capitalize()}\n" + + " ATMO: ${it.atmosphere.capitalize()}\n" + + " FAUNA: ${it.fauna.capitalize()}\n" + + " FLORA: ${it.flora.capitalize()}\n" + } + .joinToString("\n") + + // TODO: different colors, highlight latest discovery + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + AnimatedVisibility(modifier = Modifier, visible = bottomVisible, enter = flickerFadeIn) { + Text( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + color = Colors.Console, + modifier = Modifier.align(Left), + text = + with(universe.ship) { + val closest = universe.closestPlanet() + val distToClosest = (closest.pos - pos).mag().toInt() + listOfNotNull( + landing?.let { "LND: ${it.planet.name}" } + ?: if (distToClosest < 10_000) { + "ALT: $distToClosest" + } else null, + if (thrust != Vec2.Zero) "THR: %.0f%%".format(thrust.mag() * 100f) + else null, + "POS: %s".format(pos.str("%+7.0f")), + "VEL: %.0f".format(velocity.mag()) + ) + .joinToString("\n") + } + ) + } + } +} + +class MainActivity : ComponentActivity() { + private var foldState = mutableStateOf<FoldingFeature?>(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + onWindowLayoutInfoChange() + + val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed()) + + if (TEST_UNIVERSE) { + universe.initTest() + } else { + universe.initRandom() + } + + setContent { + Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState) + DebugText(DEBUG_TEXT) + + val minRadius = 50.dp.toLocalPx() + val maxRadius = 100.dp.toLocalPx() + FlightStick( + modifier = Modifier.fillMaxSize(), + minRadius = minRadius, + maxRadius = maxRadius, + color = Color.Green + ) { vec -> + (universe.follow as? Spacecraft)?.let { ship -> + if (vec == Vec2.Zero) { + ship.thrust = Vec2.Zero + } else { + val a = vec.angle() + ship.angle = a + + val m = vec.mag() + if (m < minRadius) { + // within this radius, just reorient + ship.thrust = Vec2.Zero + } else { + ship.thrust = + Vec2.makeWithAngleMag( + a, + lexp(minRadius, maxRadius, m).coerceIn(0f, 1f) + ) + } + } + } + } + Telemetry(universe) + } + } + + private fun onWindowLayoutInfoChange() { + val windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity) + + lifecycleScope.launch(Dispatchers.Main) { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + windowInfoTracker.windowLayoutInfo(this@MainActivity).collect { layoutInfo -> + foldState.value = + layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull() + Log.v("Landroid", "fold updated: $foldState") + } + } + } + } +} + +@Preview(name = "phone", device = Devices.PHONE) +@Preview(name = "fold", device = Devices.FOLDABLE) +@Preview(name = "tablet", device = Devices.TABLET) +@Composable +fun MainActivityPreview() { + val universe = VisibleUniverse(namer = Namer(Resources.getSystem()), randomSeed = randomSeed()) + + universe.initTest() + + Spaaaace(modifier = Modifier.fillMaxSize(), universe) + DebugText(DEBUG_TEXT) + Telemetry(universe) +} + +@Composable +fun FlightStick( + modifier: Modifier, + minRadius: Float = 0f, + maxRadius: Float = 1000f, + color: Color = Color.Green, + onStickChanged: (vector: Vec2) -> Unit +) { + val origin = remember { mutableStateOf(Vec2.Zero) } + val target = remember { mutableStateOf(Vec2.Zero) } + + Box( + modifier = + modifier + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + // ACTION_DOWN + val down = awaitFirstDown(requireUnconsumed = false) + origin.value = down.position + target.value = down.position + + do { + // ACTION_MOVE + val event: PointerEvent = awaitPointerEvent() + target.value = event.changes[0].position + + onStickChanged(target.value - origin.value) + } while ( + !event.changes.any { it.isConsumed } && + event.changes.count { it.pressed } == 1 + ) + + // ACTION_UP / CANCEL + target.value = Vec2.Zero + origin.value = Vec2.Zero + + onStickChanged(Vec2.Zero) + } + } + } + .drawBehind { + if (origin.value != Vec2.Zero) { + val delta = target.value - origin.value + val mag = min(maxRadius, delta.mag()) + val r = max(minRadius, mag) + val a = delta.angle() + drawCircle( + color = color, + center = origin.value, + radius = r, + style = + Stroke( + width = 2f, + pathEffect = + if (mag < minRadius) + PathEffect.dashPathEffect( + floatArrayOf(this.density * 1f, this.density * 2f) + ) + else null + ) + ) + drawLine( + color = color, + start = origin.value, + end = origin.value + Vec2.makeWithAngleMag(a, mag), + strokeWidth = 2f + ) + } + } + ) +} + +@Composable +fun Spaaaace( + modifier: Modifier, + u: VisibleUniverse, + foldState: MutableState<FoldingFeature?> = mutableStateOf(null) +) { + LaunchedEffect(u) { + while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> + u.simulateAndDrawFrame(frameTimeNanos) + } + } + + var cameraZoom by remember { mutableStateOf(1f) } + var cameraOffset by remember { mutableStateOf(Offset.Zero) } + + val transformableState = + rememberTransformableState { zoomChange, offsetChange, rotationChange -> + if (TOUCH_CAMERA_PAN) cameraOffset += offsetChange / cameraZoom + if (TOUCH_CAMERA_ZOOM) + cameraZoom = clamp(cameraZoom * zoomChange, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM) + } + + var canvasModifier = modifier + + if (TOUCH_CAMERA_PAN || TOUCH_CAMERA_ZOOM) { + canvasModifier = canvasModifier.transformable(transformableState) + } + + val halfFolded = foldState.value?.let { it.state == FoldingFeature.State.HALF_OPENED } ?: false + val horizontalFold = + foldState.value?.let { it.orientation == FoldingFeature.Orientation.HORIZONTAL } ?: false + + val centerFracX: Float by + animateFloatAsState(if (halfFolded && !horizontalFold) 0.25f else 0.5f, label = "centerX") + val centerFracY: Float by + animateFloatAsState(if (halfFolded && horizontalFold) 0.25f else 0.5f, label = "centerY") + + Canvas(modifier = canvasModifier) { + drawRect(Colors.Eigengrau, Offset.Zero, size) + + 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 = lerp(0.1f, 5f, smooth(1f-normalizedDist)) + cameraZoom = clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM) + } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM + if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f + + // cameraZoom: metersToPixels + // visibleSpaceSizeMeters: meters + // cameraOffset: meters ≈ vector pointing from ship to (0,0) (e.g. -pos) + val visibleSpaceSizeMeters = size / cameraZoom // meters x meters + val visibleSpaceRectMeters = + Rect( + -cameraOffset - + Offset( + visibleSpaceSizeMeters.width * centerFracX, + visibleSpaceSizeMeters.height * centerFracY + ), + visibleSpaceSizeMeters + ) + + var gridStep = 1000f + while (gridStep * cameraZoom < 32.dp.toPx()) gridStep *= 10 + + DEBUG_TEXT.value = + ("SIMULATION //\n" + + // "normalizedDist=${normalizedDist} \n" + + "entities: ${u.entities.size} // " + + "zoom: ${"%.4f".format(cameraZoom)}x // " + + "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") + ) + } + ?: "") + + "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" + + u.planets.joinToString("\n") { + val range = (u.ship.pos - it.pos).mag() + val vorbit = sqrt(GRAVITATION * it.mass / range) + val vescape = sqrt(2 * GRAVITATION * it.mass / it.radius) + " * ${it.name}:\n" + + if (it.explored) { + " TYPE: ${it.description.capitalize()}\n" + + " ATMO: ${it.atmosphere.capitalize()}\n" + + " FAUNA: ${it.fauna.capitalize()}\n" + + " FLORA: ${it.flora.capitalize()}\n" + } else { + " (Unexplored)\n" + } + + " orbit=${(it.pos - it.orbitCenter).mag().toInt()}" + + " radius=${it.radius.toInt()}" + + " mass=${"%g".format(it.mass)}" + + " vel=${(it.speed).toInt()}" + + " // range=${"%.0f".format(range)}" + + " vorbit=${vorbit.toInt()} vescape=${vescape.toInt()}" + }) + + zoom(cameraZoom) { + // All coordinates are space coordinates now. + + translate( + -visibleSpaceRectMeters.center.x + size.width * 0.5f, + -visibleSpaceRectMeters.center.y + size.height * 0.5f + ) { + // debug outer frame + // drawRect( + // Colors.Eigengrau2, + // visibleSpaceRectMeters.topLeft, + // visibleSpaceRectMeters.size, + // style = Stroke(width = 10f / cameraZoom) + // ) + + var x = floor(visibleSpaceRectMeters.left / gridStep) * gridStep + while (x < visibleSpaceRectMeters.right) { + drawLine( + color = Colors.Eigengrau2, + start = Offset(x, visibleSpaceRectMeters.top), + end = Offset(x, visibleSpaceRectMeters.bottom), + strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + ) + x += gridStep + } + + var y = floor(visibleSpaceRectMeters.top / gridStep) * gridStep + while (y < visibleSpaceRectMeters.bottom) { + drawLine( + color = Colors.Eigengrau2, + start = Offset(visibleSpaceRectMeters.left, y), + end = Offset(visibleSpaceRectMeters.right, y), + strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom + ) + y += gridStep + } + + this@zoom.drawUniverse(u) + } + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt new file mode 100644 index 000000000000..fdf29f7aa948 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Maths.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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 kotlin.math.pow + +/** smoothstep. Ken Perlin's version */ +fun smooth(x: Float): Float { + return x * x * x * (x * (x * 6 - 15) + 10) +} + +/** Kind of like an inverted smoothstep, but */ +fun invsmoothish(x: Float): Float { + return 0.25f * ((2f * x - 1f).pow(5f) + 1f) + 0.5f * x +} + +/** Compute the fraction that progress represents between start and end (inverse of lerp). */ +fun lexp(start: Float, end: Float, progress: Float): Float { + return (progress - start) / (end - start) +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt new file mode 100644 index 000000000000..67d536e0aea1 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 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 android.content.res.Resources +import kotlin.random.Random + +import com.android.egg.R + +const val SUFFIX_PROB = 0.75f +const val LETTER_PROB = 0.3f +const val NUMBER_PROB = 0.3f +const val RARE_PROB = 0.05f + +class Namer(resources: Resources) { + private val planetDescriptors = Bag(resources.getStringArray(R.array.planet_descriptors)) + private val lifeDescriptors = Bag(resources.getStringArray(R.array.life_descriptors)) + private val anyDescriptors = Bag(resources.getStringArray(R.array.any_descriptors)) + private val atmoDescriptors = Bag(resources.getStringArray(R.array.atmo_descriptors)) + + private val planetTypes = Bag(resources.getStringArray(R.array.planet_types)) + private val constellations = Bag(resources.getStringArray(R.array.constellations)) + private val constellationsRare = Bag(resources.getStringArray(R.array.constellations_rare)) + private val suffixes = Bag(resources.getStringArray(R.array.star_suffixes)) + private val suffixesRare = Bag(resources.getStringArray(R.array.star_suffixes_rare)) + + private val planetTable = RandomTable(0.75f to planetDescriptors, 0.25f to anyDescriptors) + + private var lifeTable = RandomTable(0.75f to lifeDescriptors, 0.25f to anyDescriptors) + + private var constellationsTable = + RandomTable(RARE_PROB to constellationsRare, 1f - RARE_PROB to constellations) + + private var suffixesTable = RandomTable(RARE_PROB to suffixesRare, 1f - RARE_PROB to suffixes) + + private var atmoTable = RandomTable(0.75f to atmoDescriptors, 0.25f to anyDescriptors) + + private var delimiterTable = + RandomTable( + 15f to " ", + 3f to "-", + 1f to "_", + 1f to "/", + 1f to ".", + 1f to "*", + 1f to "^", + 1f to "#", + 0.1f to "(^*!%@##!!" + ) + + fun describePlanet(rng: Random): String { + return planetTable.roll(rng).pull(rng) + " " + planetTypes.pull(rng) + } + + fun describeLife(rng: Random): String { + return lifeTable.roll(rng).pull(rng) + } + + fun nameSystem(rng: Random): String { + val parts = StringBuilder() + parts.append(constellationsTable.roll(rng).pull(rng)) + if (rng.nextFloat() <= SUFFIX_PROB) { + parts.append(delimiterTable.roll(rng)) + parts.append(suffixesTable.roll(rng).pull(rng)) + if (rng.nextFloat() <= RARE_PROB) parts.append(' ').append(suffixesRare.pull(rng)) + } + if (rng.nextFloat() <= LETTER_PROB) { + parts.append(delimiterTable.roll(rng)) + parts.append('A' + rng.nextInt(0, 26)) + if (rng.nextFloat() <= RARE_PROB) parts.append(delimiterTable.roll(rng)) + } + if (rng.nextFloat() <= NUMBER_PROB) { + parts.append(delimiterTable.roll(rng)) + parts.append(rng.nextInt(2, 5039)) + } + return parts.toString() + } + + fun describeAtmo(rng: Random): String { + return atmoTable.roll(rng).pull(rng) + } +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt new file mode 100644 index 000000000000..851064063d19 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/PathTools.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 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 android.util.Log +import androidx.compose.ui.graphics.Path +import kotlin.math.cos +import kotlin.math.sin + +fun createPolygon(radius: Float, sides: Int): Path { + return Path().apply { + moveTo(radius, 0f) + val angleStep = PI2f / sides + for (i in 1 until sides) { + lineTo(radius * cos(angleStep * i), radius * sin(angleStep * i)) + } + close() + } +} + +fun createStar(radius1: Float, radius2: Float, points: Int): Path { + return Path().apply { + val angleStep = PI2f / points + moveTo(radius1, 0f) + lineTo(radius2 * cos(angleStep * (0.5f)), radius2 * sin(angleStep * (0.5f))) + for (i in 1 until points) { + lineTo(radius1 * cos(angleStep * i), radius1 * sin(angleStep * i)) + lineTo(radius2 * cos(angleStep * (i + 0.5f)), radius2 * sin(angleStep * (i + 0.5f))) + } + close() + } +} + +fun Path.parseSvgPathData(d: String) { + Regex("([A-Z])([-.,0-9e ]+)").findAll(d.trim()).forEach { + val cmd = it.groups[1]!!.value + val args = + it.groups[2]?.value?.split(Regex("\\s+"))?.map { v -> v.toFloat() } ?: emptyList() + Log.d("Landroid", "cmd = $cmd, args = " + args.joinToString(",")) + when (cmd) { + "M" -> moveTo(args[0], args[1]) + "C" -> cubicTo(args[0], args[1], args[2], args[3], args[4], args[5]) + "L" -> lineTo(args[0], args[1]) + "Z" -> close() + else -> Log.v("Landroid", "unsupported SVG command: $cmd") + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Physics.kt b/packages/EasterEgg/src/com/android/egg/landroid/Physics.kt new file mode 100644 index 000000000000..fc66ad6bc2ae --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Physics.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2023 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 android.util.ArraySet +import kotlin.random.Random + +// artificially speed up or slow down the simulation +const val TIME_SCALE = 1f + +// if it's been over 1 real second since our last timestep, don't simulate that elapsed time. +// this allows the simulation to "pause" when, for example, the activity pauses +const val MAX_VALID_DT = 1f + +interface Entity { + // Integrate. + // Compute accelerations from forces, add accelerations to velocity, save old position, + // add velocity to position. + fun update(sim: Simulator, dt: Float) + + // Post-integration step, after constraints are satisfied. + fun postUpdate(sim: Simulator, dt: Float) +} + +open class Body(var name: String = "Unknown") : Entity { + var pos = Vec2.Zero + var opos = Vec2.Zero + var velocity = Vec2.Zero + + var mass = 0f + var angle = 0f + var radius = 0f + + var collides = true + + var omega: Float + get() = angle - oangle + set(value) { + oangle = angle - value + } + + var oangle = 0f + + override fun update(sim: Simulator, dt: Float) { + if (dt <= 0) return + + // integrate velocity + val vscaled = velocity * dt + opos = pos + pos += vscaled + + // integrate angular velocity + // val wscaled = omega * timescale + // oangle = angle + // angle = (angle + wscaled) % PI2f + } + + override fun postUpdate(sim: Simulator, dt: Float) { + if (dt <= 0) return + velocity = (pos - opos) / dt + } +} + +interface Constraint { + // Solve constraints. Pick up objects and put them where they are "supposed" to be. + fun solve(sim: Simulator, dt: Float) +} + +open class Container(val radius: Float) : Constraint { + private val list = ArraySet<Body>() + private val softness = 0.0f + + override fun toString(): String { + return "Container($radius)" + } + + fun add(p: Body) { + list.add(p) + } + + fun remove(p: Body) { + list.remove(p) + } + + override fun solve(sim: Simulator, dt: Float) { + for (p in list) { + if ((p.pos.mag() + p.radius) > radius) { + p.pos = + p.pos * (softness) + + Vec2.makeWithAngleMag(p.pos.angle(), radius - p.radius) * (1f - softness) + } + } + } +} + +open class Simulator(val randomSeed: Long) { + private var wallClockNanos: Long = 0L + var now: Float = 0f + var dt: Float = 0f + val rng = Random(randomSeed) + val entities = ArraySet<Entity>(1000) + val constraints = ArraySet<Constraint>(100) + + fun add(e: Entity) = entities.add(e) + fun remove(e: Entity) = entities.remove(e) + fun add(c: Constraint) = constraints.add(c) + fun remove(c: Constraint) = constraints.remove(c) + + open fun updateAll(dt: Float, entities: ArraySet<Entity>) { + entities.forEach { it.update(this, dt) } + } + + open fun solveAll(dt: Float, constraints: ArraySet<Constraint>) { + constraints.forEach { it.solve(this, dt) } + } + + open fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) { + entities.forEach { it.postUpdate(this, dt) } + } + + fun step(nanos: Long) { + val firstFrame = (wallClockNanos == 0L) + + dt = (nanos - wallClockNanos) / 1_000_000_000f * TIME_SCALE + this.wallClockNanos = nanos + + // we start the simulation on the next frame + if (firstFrame || dt > MAX_VALID_DT) return + + // simulation is running; we start accumulating simulation time + this.now += dt + + val localEntities = ArraySet(entities) + val localConstraints = ArraySet(constraints) + + // position-based dynamics approach: + // 1. apply acceleration to velocity, save positions, apply velocity to position + updateAll(dt, localEntities) + + // 2. solve all constraints + solveAll(dt, localConstraints) + + // 3. compute new velocities from updated positions and saved positions + postUpdateAll(dt, localEntities) + } +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt new file mode 100644 index 000000000000..ebbb2bd1270c --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Randomness.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 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 kotlin.random.Random + +/** + * A bag of stones. Each time you pull one out it is not replaced, preventing duplicates. When the + * bag is exhausted, all the stones are replaced and reshuffled. + */ +class Bag<T>(items: Array<T>) { + private val remaining = items.copyOf() + private var next = remaining.size // will cause a shuffle on first pull() + + /** Return the next random item from the bag, without replacing it. */ + fun pull(rng: Random): T { + if (next >= remaining.size) { + remaining.shuffle(rng) + next = 0 + } + return remaining[next++] + } +} + +/** + * A loot table. The weight of each possibility is in the first of the pair; the value to be + * returned in the second. They need not add up to 1f (we will do that for you, free of charge). + */ +class RandomTable<T>(private vararg val pairs: Pair<Float, T>) { + private val total = pairs.map { it.first }.sum() + + /** Select a random value from the weighted table. */ + fun roll(rng: Random): T { + var x = rng.nextFloatInRange(0f, total) + for ((weight, result) in pairs) { + x -= weight + if (x < 0f) return result + } + return pairs.last().second + } +} + +/** Return a random float in the range [from, until). */ +fun Random.nextFloatInRange(from: Float, until: Float): Float = + from + ((until - from) * nextFloat()) + +/** Return a random float in the range [start, end). */ +fun Random.nextFloatInRange(fromUntil: ClosedFloatingPointRange<Float>): Float = + nextFloatInRange(fromUntil.start, fromUntil.endInclusive) +/** Return a random float in the range [first, second). */ +fun Random.nextFloatInRange(fromUntil: Pair<Float, Float>): Float = + nextFloatInRange(fromUntil.first, fromUntil.second) + +/** Choose a random element from an array. */ +fun <T> Random.choose(array: Array<T>) = array[nextInt(array.size)] diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt new file mode 100644 index 000000000000..fec3ab3877ea --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Universe.kt @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2023 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 android.util.ArraySet +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.util.lerp +import kotlin.math.absoluteValue +import kotlin.math.pow +import kotlin.math.sqrt + +const val UNIVERSE_RANGE = 200_000f + +val NUM_PLANETS_RANGE = 1..10 +val STAR_RADIUS_RANGE = (1_000f..8_000f) +val PLANET_RADIUS_RANGE = (50f..2_000f) +val PLANET_ORBIT_RANGE = (STAR_RADIUS_RANGE.endInclusive * 2f)..(UNIVERSE_RANGE * 0.75f) + +const val GRAVITATION = 1e-2f +const val KEPLER_CONSTANT = 50f // * 4f * PIf * PIf / GRAVITATION + +// m = d * r +const val PLANETARY_DENSITY = 2.5f +const val STELLAR_DENSITY = 0.5f + +const val SPACECRAFT_MASS = 10f + +const val CRAFT_SPEED_LIMIT = 5_000f +const val MAIN_ENGINE_ACCEL = 1000f // thrust effect, pixels per second squared +const val LAUNCH_MECO = 2f // how long to suspend gravity when launching + +const val SCALED_THRUST = true + +interface Removable { + fun canBeRemoved(): Boolean +} + +open class Planet( + val orbitCenter: Vec2, + radius: Float, + pos: Vec2, + var speed: Float, + var color: Color = Color.White +) : Body() { + var atmosphere = "" + var description = "" + var flora = "" + var fauna = "" + var explored = false + private val orbitRadius: Float + init { + this.radius = radius + this.pos = pos + orbitRadius = pos.distance(orbitCenter) + mass = 4 / 3 * PIf * radius.pow(3) * PLANETARY_DENSITY + } + + override fun update(sim: Simulator, dt: Float) { + val orbitAngle = (pos - orbitCenter).angle() + // constant linear velocity + velocity = Vec2.makeWithAngleMag(orbitAngle + PIf / 2f, speed) + + super.update(sim, dt) + } + + override fun postUpdate(sim: Simulator, dt: Float) { + // This is kind of like a constraint, but whatever. + val orbitAngle = (pos - orbitCenter).angle() + pos = orbitCenter + Vec2.makeWithAngleMag(orbitAngle, orbitRadius) + super.postUpdate(sim, dt) + } +} + +enum class StarClass { + O, + B, + A, + F, + G, + K, + M +} + +fun starColor(cls: StarClass) = + when (cls) { + StarClass.O -> Color(0xFF6666FF) + StarClass.B -> Color(0xFFCCCCFF) + StarClass.A -> Color(0xFFEEEEFF) + StarClass.F -> Color(0xFFFFFFFF) + StarClass.G -> Color(0xFFFFFF66) + StarClass.K -> Color(0xFFFFCC33) + StarClass.M -> Color(0xFFFF8800) + } + +class Star(val cls: StarClass, radius: Float) : + Planet(orbitCenter = Vec2.Zero, radius = radius, pos = Vec2.Zero, speed = 0f) { + init { + pos = Vec2.Zero + mass = 4 / 3 * PIf * radius.pow(3) * STELLAR_DENSITY + color = starColor(cls) + collides = false + } + var anim = 0f + override fun update(sim: Simulator, dt: Float) { + anim += dt + } +} + +open class Universe(val namer: Namer, randomSeed: Long) : Simulator(randomSeed) { + var latestDiscovery: Planet? = null + lateinit var star: Star + lateinit var ship: Spacecraft + val planets: MutableList<Planet> = mutableListOf() + var follow: Body? = null + val ringfence = Container(UNIVERSE_RANGE) + + fun initTest() { + val systemName = "TEST SYSTEM" + star = + Star( + cls = StarClass.A, + radius = STAR_RADIUS_RANGE.endInclusive, + ) + .apply { name = "TEST SYSTEM" } + + repeat(NUM_PLANETS_RANGE.last) { + val thisPlanetFrac = it.toFloat() / (NUM_PLANETS_RANGE.last - 1) + val radius = + lerp(PLANET_RADIUS_RANGE.start, PLANET_RADIUS_RANGE.endInclusive, thisPlanetFrac) + val orbitRadius = + lerp(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive, thisPlanetFrac) + + val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT + val speed = 2f * PIf * orbitRadius / period + + val p = + Planet( + orbitCenter = star.pos, + radius = radius, + pos = star.pos + Vec2.makeWithAngleMag(thisPlanetFrac * PI2f, orbitRadius), + speed = speed, + color = Colors.Eigengrau4 + ) + android.util.Log.v( + "Landroid", + "created planet $p with period $period and vel $speed" + ) + val num = it + 1 + p.description = "TEST PLANET #$num" + p.atmosphere = "radius=$radius" + p.flora = "mass=${p.mass}" + p.fauna = "speed=$speed" + planets.add(p) + add(p) + } + + planets.sortBy { it.pos.distance(star.pos) } + planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" } + add(star) + + ship = Spacecraft() + + ship.pos = star.pos + Vec2.makeWithAngleMag(PIf / 4, PLANET_ORBIT_RANGE.start) + ship.angle = 0f + add(ship) + + ringfence.add(ship) + add(ringfence) + + follow = ship + } + + fun initRandom() { + val systemName = namer.nameSystem(rng) + star = + Star( + cls = rng.choose(StarClass.values()), + radius = rng.nextFloatInRange(STAR_RADIUS_RANGE) + ) + star.name = systemName + repeat(rng.nextInt(NUM_PLANETS_RANGE.first, NUM_PLANETS_RANGE.last + 1)) { + val radius = rng.nextFloatInRange(PLANET_RADIUS_RANGE) + val orbitRadius = + lerp( + PLANET_ORBIT_RANGE.start, + PLANET_ORBIT_RANGE.endInclusive, + rng.nextFloat().pow(1f) + ) + + // Kepler's third law + val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT + val speed = 2f * PIf * orbitRadius / period + + val p = + Planet( + orbitCenter = star.pos, + radius = radius, + pos = star.pos + Vec2.makeWithAngleMag(rng.nextFloat() * PI2f, orbitRadius), + speed = speed, + color = Colors.Eigengrau4 + ) + android.util.Log.v( + "Landroid", + "created planet $p with period $period and vel $speed" + ) + p.description = namer.describePlanet(rng) + p.atmosphere = namer.describeAtmo(rng) + p.flora = namer.describeLife(rng) + p.fauna = namer.describeLife(rng) + planets.add(p) + add(p) + } + planets.sortBy { it.pos.distance(star.pos) } + planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" } + add(star) + + ship = Spacecraft() + + ship.pos = + star.pos + + Vec2.makeWithAngleMag( + rng.nextFloat() * PI2f, + rng.nextFloatInRange(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive) + ) + ship.angle = rng.nextFloat() * PI2f + add(ship) + + ringfence.add(ship) + add(ringfence) + + follow = ship + } + + override fun updateAll(dt: Float, entities: ArraySet<Entity>) { + // check for passing in front of the sun + ship.transit = false + + (planets + star).forEach { planet -> + val vector = planet.pos - ship.pos + val d = vector.mag() + if (d < planet.radius) { + if (planet is Star) ship.transit = true + } else if ( + now > ship.launchClock + LAUNCH_MECO + ) { // within MECO sec of launch, no gravity at all + // simulate gravity: $ f_g = G * m1 * m2 * 1/d^2 $ + ship.velocity = + ship.velocity + + Vec2.makeWithAngleMag( + vector.angle(), + GRAVITATION * (ship.mass * planet.mass) / d.pow(2) + ) * dt + } + } + + super.updateAll(dt, entities) + } + + fun closestPlanet(): Planet { + val bodiesByDist = + (planets + star) + .map { planet -> (planet.pos - ship.pos) to planet } + .sortedBy { it.first.mag() } + + return bodiesByDist[0].second + } + + override fun solveAll(dt: Float, constraints: ArraySet<Constraint>) { + if (ship.landing == null) { + val planet = closestPlanet() + + if (planet.collides) { + val d = (ship.pos - planet.pos).mag() - ship.radius - planet.radius + val a = (ship.pos - planet.pos).angle() + + if (d < 0) { + // landing, or impact? + + // 1. relative speed + val vDiff = (ship.velocity - planet.velocity).mag() + // 2. landing angle + val aDiff = (ship.angle - a).absoluteValue + + // landing criteria + if (aDiff < PIf / 4 + // && + // vDiff < 100f + ) { + val landing = Landing(ship, planet, a) + ship.landing = landing + ship.velocity = planet.velocity + add(landing) + + planet.explored = true + latestDiscovery = planet + } else { + val impact = planet.pos + Vec2.makeWithAngleMag(a, planet.radius) + ship.pos = + planet.pos + Vec2.makeWithAngleMag(a, planet.radius + ship.radius - d) + + // add(Spark( + // lifetime = 1f, + // style = Spark.Style.DOT, + // color = Color.Yellow, + // size = 10f + // ).apply { + // pos = impact + // opos = impact + // velocity = Vec2.Zero + // }) + // + (1..10).forEach { + Spark( + lifetime = rng.nextFloatInRange(0.5f, 2f), + style = Spark.Style.DOT, + color = Color.White, + size = 1f + ) + .apply { + pos = + impact + + Vec2.makeWithAngleMag( + rng.nextFloatInRange(0f, 2 * PIf), + rng.nextFloatInRange(0.1f, 0.5f) + ) + opos = pos + velocity = + ship.velocity * 0.8f + + Vec2.makeWithAngleMag( + // a + + // rng.nextFloatInRange(-PIf, PIf), + rng.nextFloatInRange(0f, 2 * PIf), + rng.nextFloatInRange(0.1f, 0.5f) + ) + add(this) + } + } + } + } + } + } + + super.solveAll(dt, constraints) + } + + override fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) { + super.postUpdateAll(dt, entities) + + entities + .filterIsInstance<Removable>() + .filter(predicate = Removable::canBeRemoved) + .filterIsInstance<Entity>() + .forEach { remove(it) } + } +} + +class Landing(val ship: Spacecraft, val planet: Planet, val angle: Float) : Constraint { + private val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius) + override fun solve(sim: Simulator, dt: Float) { + val desiredPos = planet.pos + landingVector + ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME + ship.angle = angle + } +} + +class Spark( + var lifetime: Float, + collides: Boolean = false, + mass: Float = 0f, + val style: Style = Style.LINE, + val color: Color = Color.Gray, + val size: Float = 2f +) : Removable, Body() { + enum class Style { + LINE, + LINE_ABSOLUTE, + DOT, + DOT_ABSOLUTE, + RING + } + + init { + this.collides = collides + this.mass = mass + } + override fun update(sim: Simulator, dt: Float) { + super.update(sim, dt) + lifetime -= dt + } + override fun canBeRemoved(): Boolean { + return lifetime < 0 + } +} + +const val TRACK_LENGTH = 10_000 +const val SIMPLE_TRACK_DRAWING = true + +class Track { + val positions = ArrayDeque<Vec2>(TRACK_LENGTH) + private val angles = ArrayDeque<Float>(TRACK_LENGTH) + fun add(x: Float, y: Float, a: Float) { + if (positions.size >= (TRACK_LENGTH - 1)) { + positions.removeFirst() + angles.removeFirst() + positions.removeFirst() + angles.removeFirst() + } + positions.addLast(Vec2(x, y)) + angles.addLast(a) + } +} + +class Spacecraft : Body() { + var thrust = Vec2.Zero + var launchClock = 0f + + var transit = false + + val track = Track() + + var landing: Landing? = null + + init { + mass = SPACECRAFT_MASS + radius = 12f + } + + override fun update(sim: Simulator, dt: Float) { + // check for thrusters + val thrustMag = thrust.mag() + if (thrustMag > 0) { + var deltaV = MAIN_ENGINE_ACCEL * dt + if (SCALED_THRUST) deltaV *= thrustMag.coerceIn(0f, 1f) + + if (landing == null) { + // we are free in space, so we attempt to pivot toward the desired direction + // NOTE: no longer required thanks to FlightStick + // angle = thrust.angle() + } else + landing?.let { landing -> + if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */ + + if (sim.now > launchClock) { + // first-stage to orbit has 1000x power + // deltaV *= 1000f + sim.remove(landing) + this.landing = null + } else { + deltaV = 0f + } + } + + // this is it. impart thrust to the ship. + // note that we always thrust in the forward direction + velocity += Vec2.makeWithAngleMag(angle, deltaV) + } else { + if (launchClock != 0f) launchClock = 0f + } + + // apply global speed limit + if (velocity.mag() > CRAFT_SPEED_LIMIT) + velocity = Vec2.makeWithAngleMag(velocity.angle(), CRAFT_SPEED_LIMIT) + + super.update(sim, dt) + } + + override fun postUpdate(sim: Simulator, dt: Float) { + super.postUpdate(sim, dt) + + // special effects all need to be added after the simulation step so they have + // the correct position of the ship. + track.add(pos.x, pos.y, angle) + + val mag = thrust.mag() + if (sim.rng.nextFloat() < mag) { + // exhaust + sim.add( + Spark( + lifetime = sim.rng.nextFloatInRange(0.5f, 1f), + collides = true, + mass = 1f, + style = Spark.Style.RING, + size = 3f, + color = Color(0x40FFFFFF) + ) + .also { spark -> + spark.pos = pos + spark.opos = pos + spark.velocity = + velocity + + Vec2.makeWithAngleMag( + angle + sim.rng.nextFloatInRange(-0.2f, 0.2f), + -MAIN_ENGINE_ACCEL * mag * 10f * dt + ) + } + ) + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt b/packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt new file mode 100644 index 000000000000..82bae759e84e --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/Vec2.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 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 androidx.compose.ui.geometry.Offset +import kotlin.math.PI +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin + +const val PIf = PI.toFloat() +const val PI2f = (2 * PI).toFloat() + +typealias Vec2 = Offset + +fun Vec2.str(fmt: String = "%+.2f"): String = "<$fmt,$fmt>".format(x, y) + +fun Vec2(x: Float, y: Float): Vec2 = Offset(x, y) + +fun Vec2.mag(): Float { + return getDistance() +} + +fun Vec2.distance(other: Vec2): Float { + return (this - other).mag() +} + +fun Vec2.angle(): Float { + return atan2(y, x) +} + +fun Vec2.dot(o: Vec2): Float { + return x * o.x + y * o.y +} + +fun Vec2.product(f: Float): Vec2 { + return Vec2(x * f, y * f) +} + +fun Offset.Companion.makeWithAngleMag(a: Float, m: Float): Vec2 { + return Vec2(m * cos(a), m * sin(a)) +} + +fun Vec2.rotate(angle: Float, origin: Vec2 = Vec2.Zero): Offset { + val translated = this - origin + return origin + + Offset( + (translated.x * cos(angle) - translated.y * sin(angle)), + (translated.x * sin(angle) + translated.y * cos(angle)) + ) +} diff --git a/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt new file mode 100644 index 000000000000..24b9c6a283c2 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/landroid/VisibleUniverse.kt @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2023 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 androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PointMode +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotateRad +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.util.lerp +import androidx.core.math.MathUtils.clamp +import java.lang.Float.max +import kotlin.math.sqrt + +const val DRAW_ORBITS = true +const val DRAW_GRAVITATIONAL_FIELDS = true +const val DRAW_STAR_GRAVITATIONAL_FIELDS = true + +val STAR_POINTS = android.os.Build.VERSION.SDK_INT.takeIf { it in 1..99 } ?: 31 + +/** + * A zoomedDrawScope is one that is scaled, but remembers its zoom level, so you can correct for it + * if you want to draw single-pixel lines. Which we do. + */ +interface ZoomedDrawScope : DrawScope { + val zoom: Float +} + +fun DrawScope.zoom(zoom: Float, block: ZoomedDrawScope.() -> Unit) { + val ds = + object : ZoomedDrawScope, DrawScope by this { + override var zoom = zoom + } + ds.scale(zoom) { block(ds) } +} + +class VisibleUniverse(namer: Namer, randomSeed: Long) : Universe(namer, randomSeed) { + // Magic variable. Every time we update it, Compose will notice and redraw the universe. + val triggerDraw = mutableStateOf(0L) + + fun simulateAndDrawFrame(nanos: Long) { + // By writing this value, Compose will look for functions that read it (like drawZoomed). + triggerDraw.value = nanos + + step(nanos) + } +} + +fun ZoomedDrawScope.drawUniverse(universe: VisibleUniverse) { + with(universe) { + triggerDraw.value // Please recompose when this value changes. + + // star.drawZoomed(ds, zoom) + // planets.forEach { p -> + // p.drawZoomed(ds, zoom) + // if (p == follow) { + // drawCircle(Color.Red, 20f / zoom, p.pos) + // } + // } + // + // ship.drawZoomed(ds, zoom) + + constraints.forEach { + when (it) { + is Landing -> drawLanding(it) + is Container -> drawContainer(it) + } + } + drawStar(star) + entities.forEach { + if (it === ship || it === star) return@forEach // draw the ship last + when (it) { + is Spacecraft -> drawSpacecraft(it) + is Spark -> drawSpark(it) + is Planet -> drawPlanet(it) + } + } + drawSpacecraft(ship) + } +} + +fun ZoomedDrawScope.drawContainer(container: Container) { + drawCircle( + color = Color(0xFF800000), + radius = container.radius, + center = Vec2.Zero, + style = + Stroke( + width = 1f / zoom, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f / zoom, 8f / zoom), 0f) + ) + ) + // val path = Path().apply { + // fillType = PathFillType.EvenOdd + // addOval(Rect(center = Vec2.Zero, radius = container.radius)) + // addOval(Rect(center = Vec2.Zero, radius = container.radius + 10_000)) + // } + // drawPath( + // path = path, + // + // ) +} + +fun ZoomedDrawScope.drawGravitationalField(planet: Planet) { + val rings = 8 + for (i in 0 until rings) { + val force = + lerp( + 200f, + 0.01f, + i.toFloat() / rings + ) // first rings at force = 1N, dropping off after that + val r = sqrt(GRAVITATION * planet.mass * SPACECRAFT_MASS / force) + drawCircle( + color = Color(1f, 0f, 0f, lerp(0.5f, 0.1f, i.toFloat() / rings)), + center = planet.pos, + style = Stroke(2f / zoom), + radius = r + ) + } +} + +fun ZoomedDrawScope.drawPlanet(planet: Planet) { + with(planet) { + if (DRAW_ORBITS) + drawCircle( + color = Color(0x8000FFFF), + radius = pos.distance(orbitCenter), + center = orbitCenter, + style = + Stroke( + width = 1f / zoom, + ) + ) + + if (DRAW_GRAVITATIONAL_FIELDS) { + drawGravitationalField(this) + } + + drawCircle(color = Colors.Eigengrau, radius = radius, center = pos) + drawCircle(color = color, radius = radius, center = pos, style = Stroke(2f / zoom)) + } +} + +fun ZoomedDrawScope.drawStar(star: Star) { + translate(star.pos.x, star.pos.y) { + drawCircle(color = star.color, radius = star.radius, center = Vec2.Zero) + + if (DRAW_STAR_GRAVITATIONAL_FIELDS) this@drawStar.drawGravitationalField(star) + + rotateRad(radians = star.anim / 23f * PI2f, pivot = Vec2.Zero) { + drawPath( + path = + createStar( + radius1 = star.radius + 80, + radius2 = star.radius + 250, + points = STAR_POINTS + ), + color = star.color, + style = + Stroke( + width = 3f / this@drawStar.zoom, + pathEffect = PathEffect.cornerPathEffect(radius = 200f) + ) + ) + } + rotateRad(radians = star.anim / -19f * PI2f, pivot = Vec2.Zero) { + drawPath( + path = + createStar( + radius1 = star.radius + 20, + radius2 = star.radius + 200, + points = STAR_POINTS + 1 + ), + color = star.color, + style = + Stroke( + width = 3f / this@drawStar.zoom, + pathEffect = PathEffect.cornerPathEffect(radius = 200f) + ) + ) + } + } +} + +val spaceshipPath = + Path().apply { + parseSvgPathData( + """ +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 +""" + ) + } +val thrustPath = createPolygon(-3f, 3).also { it.translate(Vec2(-4f, 0f)) } + +fun ZoomedDrawScope.drawSpacecraft(ship: Spacecraft) { + with(ship) { + rotateRad(angle, pivot = pos) { + translate(pos.x, pos.y) { + // drawPath( + // path = createStar(200f, 100f, 3), + // color = Color.White, + // style = Stroke(width = 2f / zoom) + // ) + drawPath(path = spaceshipPath, color = Colors.Eigengrau) // fauxpaque + drawPath( + path = spaceshipPath, + color = if (transit) Color.Black else Color.White, + style = Stroke(width = 2f / this@drawSpacecraft.zoom) + ) + if (thrust != Vec2.Zero) { + drawPath( + path = thrustPath, + color = Color(0xFFFF8800), + style = + Stroke( + width = 2f / this@drawSpacecraft.zoom, + pathEffect = PathEffect.cornerPathEffect(radius = 1f) + ) + ) + } + // drawRect( + // topLeft = Offset(-1f, -1f), + // size = Size(2f, 2f), + // color = Color.Cyan, + // style = Stroke(width = 2f / zoom) + // ) + // drawLine( + // start = Vec2.Zero, + // end = Vec2(20f, 0f), + // color = Color.Cyan, + // strokeWidth = 2f / zoom + // ) + } + } + // // DEBUG: draw velocity vector + // drawLine( + // start = pos, + // end = pos + velocity, + // color = Color.Red, + // strokeWidth = 3f / zoom + // ) + drawTrack(track) + } +} + +fun ZoomedDrawScope.drawLanding(landing: Landing) { + val v = landing.planet.pos + Vec2.makeWithAngleMag(landing.angle, landing.planet.radius) + drawLine(Color.Red, v + Vec2(-5f, -5f), v + Vec2(5f, 5f), strokeWidth = 1f / zoom) + drawLine(Color.Red, v + Vec2(5f, -5f), v + Vec2(-5f, 5f), strokeWidth = 1f / zoom) +} + +fun ZoomedDrawScope.drawSpark(spark: Spark) { + with(spark) { + if (lifetime < 0) return + when (style) { + Spark.Style.LINE -> + if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size) + Spark.Style.LINE_ABSOLUTE -> + if (opos != Vec2.Zero) drawLine(color, opos, pos, strokeWidth = size / zoom) + Spark.Style.DOT -> drawCircle(color, size, pos) + Spark.Style.DOT_ABSOLUTE -> drawCircle(color, size, pos / zoom) + Spark.Style.RING -> drawCircle(color, size, pos, style = Stroke(width = 1f / zoom)) + // drawPoints(listOf(pos), PointMode.Points, color, strokeWidth = 2f/zoom) + // drawCircle(color, 2f/zoom, pos) + } + // drawCircle(Color.Gray, center = pos, radius = 1.5f / zoom) + } +} + +fun ZoomedDrawScope.drawTrack(track: Track) { + with(track) { + if (SIMPLE_TRACK_DRAWING) { + drawPoints( + positions, + pointMode = PointMode.Lines, + color = Color.Green, + strokeWidth = 1f / zoom + ) + // if (positions.size < 2) return + // drawPath(Path() + // .apply { + // val p = positions[positions.size - 1] + // moveTo(p.x, p.y) + // positions.reversed().subList(1, positions.size).forEach { p -> + // lineTo(p.x, p.y) + // } + // }, + // color = Color.Green, style = Stroke(1f/zoom)) + } else { + if (positions.size < 2) return + var prev: Vec2 = positions[positions.size - 1] + var a = 0.5f + positions.reversed().subList(1, positions.size).forEach { pos -> + drawLine(Color(0f, 1f, 0f, a), prev, pos, strokeWidth = max(1f, 1f / zoom)) + prev = pos + a = clamp((a - 1f / TRACK_LENGTH), 0f, 1f) + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java index 5a3db4b18a1a..3cb9ac823bac 100644 --- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java +++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java @@ -130,6 +130,7 @@ public class SecureChannel { if (DEBUG) { Slog.d(TAG, "Starting secure channel."); } + mStopped = false; new Thread(() -> { try { // 1. Wait for the next handshake message and process it. @@ -185,6 +186,17 @@ public class SecureChannel { } /** + * Return true if the channel is currently inactive. + * The channel could have been stopped by either {@link SecureChannel#stop()} or by + * encountering a fatal error. + * + * @return true if the channel is currently inactive. + */ + public boolean isStopped() { + return mStopped; + } + + /** * Start exchanging handshakes to create a secure layer asynchronously. When the handshake is * completed successfully, then the {@link Callback#onSecureConnection()} will trigger. Any * error that occurs during the handshake will be passed by {@link Callback#onError(Throwable)}. @@ -290,6 +302,7 @@ public class SecureChannel { try { data = new byte[length]; } catch (OutOfMemoryError error) { + Streams.skipByReading(mInput, Long.MAX_VALUE); throw new SecureChannelException("Payload is too large.", error); } diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 0f00f5f1c3a5..9498108b35dc 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -137,13 +137,7 @@ public class CompanionTransportManager { synchronized (mTransports) { for (int i = 0; i < associationIds.length; i++) { if (mTransports.contains(associationIds[i])) { - try { - mTransports.get(associationIds[i]).sendMessage(message, data); - } catch (IOException e) { - Slog.e(TAG, "Failed to send message 0x" + Integer.toHexString(message) - + " data length " + data.length + " to association " - + associationIds[i]); - } + mTransports.get(associationIds[i]).requestForResponse(message, data); } } } @@ -244,6 +238,7 @@ public class CompanionTransportManager { } addMessageListenersToTransport(transport); + transport.setOnTransportClosedListener(this::detachSystemDataTransport); transport.start(); synchronized (mTransports) { mTransports.put(associationId, transport); @@ -321,4 +316,14 @@ public class CompanionTransportManager { transport.addListener(mMessageListeners.keyAt(i), mMessageListeners.valueAt(i)); } } + + void detachSystemDataTransport(Transport transport) { + int associationId = transport.mAssociationId; + AssociationInfo association = mAssociationStore.getAssociationById(associationId); + if (association != null) { + detachSystemDataTransport(association.getPackageName(), + association.getUserId(), + association.getId()); + } + } } diff --git a/services/companion/java/com/android/server/companion/transport/RawTransport.java b/services/companion/java/com/android/server/companion/transport/RawTransport.java index e64509facbb4..ca169aac6a37 100644 --- a/services/companion/java/com/android/server/companion/transport/RawTransport.java +++ b/services/companion/java/com/android/server/companion/transport/RawTransport.java @@ -70,6 +70,8 @@ class RawTransport extends Transport { } IoUtils.closeQuietly(mRemoteIn); IoUtils.closeQuietly(mRemoteOut); + + super.close(); } @Override diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index 2d856b9614cb..a0301a920d96 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -21,7 +21,6 @@ import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Slog; -import com.android.internal.annotations.GuardedBy; import com.android.server.companion.securechannel.AttestationVerifier; import com.android.server.companion.securechannel.SecureChannel; @@ -35,7 +34,6 @@ class SecureTransport extends Transport implements SecureChannel.Callback { private volatile boolean mShouldProcessRequests = false; - @GuardedBy("mRequestQueue") private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(100); SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) { @@ -64,6 +62,8 @@ class SecureTransport extends Transport implements SecureChannel.Callback { void close() { mSecureChannel.close(); mShouldProcessRequests = false; + + super.close(); } @Override @@ -81,13 +81,19 @@ class SecureTransport extends Transport implements SecureChannel.Callback { } // Queue up a message to send - synchronized (mRequestQueue) { + try { mRequestQueue.add(ByteBuffer.allocate(HEADER_LENGTH + data.length) .putInt(message) .putInt(sequence) .putInt(data.length) .put(data) .array()); + } catch (IllegalStateException e) { + // Request buffer can only be full if too many requests are being added or + // the request processing thread is dead. Assume latter and detach the transport. + Slog.w(TAG, "Failed to queue message 0x" + Integer.toHexString(message) + + " . Request buffer is full; detaching transport.", e); + close(); } } @@ -96,8 +102,8 @@ class SecureTransport extends Transport implements SecureChannel.Callback { try { mSecureChannel.establishSecureConnection(); } catch (Exception e) { - Slog.w(TAG, "Failed to initiate secure channel handshake.", e); - onError(e); + Slog.e(TAG, "Failed to initiate secure channel handshake.", e); + close(); } } @@ -108,17 +114,14 @@ class SecureTransport extends Transport implements SecureChannel.Callback { // TODO: find a better way to handle incoming requests than a dedicated thread. new Thread(() -> { - try { - while (mShouldProcessRequests) { - synchronized (mRequestQueue) { - byte[] request = mRequestQueue.poll(); - if (request != null) { - mSecureChannel.sendSecureMessage(request); - } - } + while (mShouldProcessRequests) { + try { + byte[] request = mRequestQueue.take(); + mSecureChannel.sendSecureMessage(request); + } catch (Exception e) { + Slog.e(TAG, "Failed to send secure message.", e); + close(); } - } catch (IOException e) { - onError(e); } }).start(); } @@ -135,13 +138,18 @@ class SecureTransport extends Transport implements SecureChannel.Callback { try { handleMessage(message, sequence, content); } catch (IOException error) { - onError(error); + // IOException won't be thrown here because a separate thread is handling + // the write operations inside onSecureConnection(). } } @Override public void onError(Throwable error) { - mShouldProcessRequests = false; - Slog.e(TAG, error.getMessage(), error); + Slog.e(TAG, "Secure transport encountered an error.", error); + + // If the channel was stopped as a result of the error, then detach itself. + if (mSecureChannel.isStopped()) { + close(); + } } } diff --git a/services/companion/java/com/android/server/companion/transport/Transport.java b/services/companion/java/com/android/server/companion/transport/Transport.java index 6ad6d3a4aa72..bc9c8694ece5 100644 --- a/services/companion/java/com/android/server/companion/transport/Transport.java +++ b/services/companion/java/com/android/server/companion/transport/Transport.java @@ -70,6 +70,8 @@ public abstract class Transport { */ private final Map<Integer, IOnMessageReceivedListener> mListeners; + private OnTransportClosedListener mOnTransportClosed; + private static boolean isRequest(int message) { return (message & 0xFF000000) == 0x63000000; } @@ -120,20 +122,18 @@ public abstract class Transport { abstract void stop(); /** - * Stop listening to the incoming data and close the streams. + * Stop listening to the incoming data and close the streams. If a listener for closed event + * is set, then trigger it to assist with its clean-up. */ - abstract void close(); + void close() { + if (mOnTransportClosed != null) { + mOnTransportClosed.onClosed(this); + } + } protected abstract void sendMessage(int message, int sequence, @NonNull byte[] data) throws IOException; - /** - * Send a message. - */ - public void sendMessage(int message, @NonNull byte[] data) throws IOException { - sendMessage(message, mNextSequence.incrementAndGet(), data); - } - public Future<byte[]> requestForResponse(int message, byte[] data) { if (DEBUG) Slog.d(TAG, "Requesting for response"); final int sequence = mNextSequence.incrementAndGet(); @@ -247,4 +247,14 @@ public abstract class Transport { } } } + + void setOnTransportClosedListener(OnTransportClosedListener callback) { + this.mOnTransportClosed = callback; + } + + // Interface to pass transport to the transport manager to assist with detachment. + @FunctionalInterface + interface OnTransportClosedListener { + void onClosed(Transport transport); + } } diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index e65229f188fc..1ba14623f04e 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -93,8 +93,6 @@ public class BatteryStatsHistoryTest { @Test public void testAtraceBinaryState1() { - mHistory.forceRecordAllHistory(); - InOrder inOrder = Mockito.inOrder(mTracer); Mockito.when(mTracer.tracingEnabled()).thenReturn(true); @@ -112,8 +110,6 @@ public class BatteryStatsHistoryTest { @Test public void testAtraceBinaryState2() { - mHistory.forceRecordAllHistory(); - InOrder inOrder = Mockito.inOrder(mTracer); Mockito.when(mTracer.tracingEnabled()).thenReturn(true); @@ -131,8 +127,6 @@ public class BatteryStatsHistoryTest { @Test public void testAtraceNumericalState() { - mHistory.forceRecordAllHistory(); - InOrder inOrder = Mockito.inOrder(mTracer); Mockito.when(mTracer.tracingEnabled()).thenReturn(true); @@ -150,8 +144,6 @@ public class BatteryStatsHistoryTest { @Test public void testAtraceInstantEvent() { - mHistory.forceRecordAllHistory(); - InOrder inOrder = Mockito.inOrder(mTracer); Mockito.when(mTracer.tracingEnabled()).thenReturn(true); diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 26590c4704c3..a72f7806d3ea 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -2097,7 +2097,10 @@ public class TelecomManager { * For a self-managed {@link ConnectionService}, a {@link SecurityException} will be thrown if * the {@link PhoneAccount} has {@link PhoneAccount#CAPABILITY_SELF_MANAGED} and the calling app * does not have {@link android.Manifest.permission#MANAGE_OWN_CALLS}. - * + * <p> + * <p> + * <b>Note</b>: {@link android.app.Notification.CallStyle} notifications should be posted after + * the call is added to Telecom in order for the notification to be non-dismissible. * @param phoneAccount A {@link PhoneAccountHandle} registered with * {@link #registerPhoneAccount}. * @param extras A bundle that will be passed through to @@ -2345,7 +2348,10 @@ public class TelecomManager { * {@link PhoneAccount} with the {@link PhoneAccount#CAPABILITY_PLACE_EMERGENCY_CALLS} * capability, depending on external factors, such as network conditions and Modem/SIM status. * </p> - * + * <p> + * <p> + * <b>Note</b>: {@link android.app.Notification.CallStyle} notifications should be posted after + * the call is placed in order for the notification to be non-dismissible. * @param address The address to make the call to. * @param extras Bundle of extras to use with the call. */ @@ -2679,9 +2685,11 @@ public class TelecomManager { /** * Add a call to the Android system service Telecom. This allows the system to start tracking an - * incoming or outgoing call with the specified {@link CallAttributes}. Once the call is ready - * to be disconnected, use the {@link CallControl#disconnect(DisconnectCause, Executor, - * OutcomeReceiver)} which is provided by the {@code pendingControl#onResult(CallControl)}. + * incoming or outgoing call with the specified {@link CallAttributes}. Once a call is added, + * a {@link android.app.Notification.CallStyle} notification should be posted and when the + * call is ready to be disconnected, use {@link CallControl#disconnect(DisconnectCause, + * Executor, OutcomeReceiver)} which is provided by the + * {@code pendingControl#onResult(CallControl)}. * <p> * <p> * <p> |