Merge "Remove vkDeviceWaitIdle call when destroying VulkanSurface" into udc-dev
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 2e67225..4dea4a7 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -36,7 +36,6 @@
 import android.app.PendingIntent;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
-import android.companion.utils.FeatureUtils;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -1227,11 +1226,6 @@
     @Nullable
     public IntentSender buildPermissionTransferUserConsentIntent(int associationId)
             throws DeviceNotAssociatedException {
-        if (!FeatureUtils.isPermSyncEnabled()) {
-            throw new UnsupportedOperationException("Calling"
-                    + " buildPermissionTransferUserConsentIntent, but this API is disabled by the"
-                    + " system.");
-        }
         try {
             PendingIntent pendingIntent = mService.buildPermissionTransferUserConsentIntent(
                     mContext.getOpPackageName(),
@@ -1264,10 +1258,6 @@
     @Deprecated
     @UserHandleAware
     public void startSystemDataTransfer(int associationId) throws DeviceNotAssociatedException {
-        if (!FeatureUtils.isPermSyncEnabled()) {
-            throw new UnsupportedOperationException("Calling startSystemDataTransfer, but this API"
-                    + " is disabled by the system.");
-        }
         try {
             mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(),
                     associationId, null);
@@ -1300,10 +1290,6 @@
             @NonNull Executor executor,
             @NonNull OutcomeReceiver<Void, CompanionException> result)
             throws DeviceNotAssociatedException {
-        if (!FeatureUtils.isPermSyncEnabled()) {
-            throw new UnsupportedOperationException("Calling startSystemDataTransfer, but this API"
-                    + " is disabled by the system.");
-        }
         try {
             mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(),
                     associationId, new SystemDataTransferCallbackProxy(executor, result));
diff --git a/core/java/android/companion/utils/FeatureUtils.java b/core/java/android/companion/utils/FeatureUtils.java
index 157eef8..a382e09 100644
--- a/core/java/android/companion/utils/FeatureUtils.java
+++ b/core/java/android/companion/utils/FeatureUtils.java
@@ -16,6 +16,7 @@
 
 package android.companion.utils;
 
+import android.os.Binder;
 import android.os.Build;
 import android.provider.DeviceConfig;
 
@@ -31,8 +32,19 @@
     private static final String PROPERTY_PERM_SYNC_ENABLED = "perm_sync_enabled";
 
     public static boolean isPermSyncEnabled() {
-        return Build.isDebuggable() || DeviceConfig.getBoolean(NAMESPACE_COMPANION,
-                PROPERTY_PERM_SYNC_ENABLED, false);
+        // Permissions sync is always enabled in debuggable mode.
+        if (Build.isDebuggable()) {
+            return true;
+        }
+
+        // Clear app identity to read the device config for feature flag.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            return DeviceConfig.getBoolean(NAMESPACE_COMPANION,
+                    PROPERTY_PERM_SYNC_ENABLED, false);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
     }
 
     private FeatureUtils() {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index c221d72..06635ee 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -481,7 +481,7 @@
      * 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 @@
      * </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 ac7f2ca..7e42f43 100644
--- a/core/java/android/hardware/camera2/TotalCaptureResult.java
+++ b/core/java/android/hardware/camera2/TotalCaptureResult.java
@@ -179,7 +179,7 @@
      * @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 fdcb87f..bbaaa47 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -259,6 +259,8 @@
     }
 
     private TraceDelegate mTracer;
+    private int mTraceLastState = 0;
+    private int mTraceLastState2 = 0;
 
     /**
      * Constructor
@@ -1241,7 +1243,6 @@
      */
     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 @@
      * 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 @@
     }
 
     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 @@
                     + 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/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml
index 1276dec..65c883a 100644
--- a/core/res/res/values-bg/strings.xml
+++ b/core/res/res/values-bg/strings.xml
@@ -1256,7 +1256,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"Приложенията се стартират."</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"Зареждането завършва."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Натиснахте бутона за включване/изключване – това обикновено изключва екрана.\n\nОпитайте да докоснете леко, докато настройвате отпечатъка си."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Изключете екрана за изход от настройката"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"За изход изключете екрана"</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Изключване"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Напред с потвърждаването на отпечатъка?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Натиснахте бутона за включване/изключване – това обикновено изключва екрана.\n\nОпитайте да докоснете леко, за да потвърдите отпечатъка си."</string>
diff --git a/core/res/res/values-bn/strings.xml b/core/res/res/values-bn/strings.xml
index ee3ad19..fe686db 100644
--- a/core/res/res/values-bn/strings.xml
+++ b/core/res/res/values-bn/strings.xml
@@ -638,7 +638,7 @@
     <string name="fingerprint_acquired_immobile" msgid="1621891895241888048">"প্রতিবার আপনার আঙুলের অবস্থান সামান্য পরিবর্তন করুন"</string>
   <string-array name="fingerprint_acquired_vendor">
   </string-array>
-    <string name="fingerprint_error_not_match" msgid="4599441812893438961">"আঙ্গুলের ছাপ শনাক্ত করা যায়নি"</string>
+    <string name="fingerprint_error_not_match" msgid="4599441812893438961">"ফিঙ্গারপ্রিন্ট শনাক্ত করা যায়নি"</string>
     <string name="fingerprint_udfps_error_not_match" msgid="8236930793223158856">"ফিঙ্গারপ্রিন্ট শনাক্ত করা যায়নি"</string>
     <string name="fingerprint_authenticated" msgid="2024862866860283100">"আঙ্গুলের ছাপ যাচাই করা হয়েছে"</string>
     <string name="face_authenticated_no_confirmation_required" msgid="8867889115112348167">"ফেস যাচাই করা হয়েছে"</string>
@@ -1365,7 +1365,7 @@
     <string name="usb_midi_notification_title" msgid="7404506788950595557">"USB এর মাধ্যমে MIDI চালু করা হয়েছে"</string>
     <string name="usb_uvc_notification_title" msgid="2030032862673400008">"ওয়েবক্যাম হিসেবে কানেক্ট করা আছে"</string>
     <string name="usb_accessory_notification_title" msgid="1385394660861956980">"ইউএসবি অ্যাক্সেসরি কানেক্ট করা হয়েছে"</string>
-    <string name="usb_notification_message" msgid="4715163067192110676">"আরও বিকল্পের জন্য আলতো চাপুন৷"</string>
+    <string name="usb_notification_message" msgid="4715163067192110676">"আরও বিকল্পের জন্য ট্যাপ করুন।"</string>
     <string name="usb_power_notification_message" msgid="7284765627437897702">"সংযুক্ত ডিভাইস চার্জ করা হচ্ছে। আরও বিকল্প দেখতে ট্যাপ করুন।"</string>
     <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"অ্যানালগ অডিও অ্যাক্সেসরি শনাক্ত করা হয়েছে"</string>
     <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"সংযুক্ত ডিভাইসটি এই ফোনের সাথে ব্যবহার করা যাবে না। আরও জানতে ট্যাপ করুন।"</string>
diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml
index 5889f94..b581d49 100644
--- a/core/res/res/values-ca/strings.xml
+++ b/core/res/res/values-ca/strings.xml
@@ -703,7 +703,7 @@
     <skip />
     <string name="face_acquired_recalibrate_alt" msgid="5702674220280332115">"No es pot crear el model facial. Torna-ho a provar."</string>
     <string name="face_acquired_dark_glasses_detected_alt" msgid="4052123776406041972">"S\'han detectat ulleres fosques. La cara ha de veure\'s sencera."</string>
-    <string name="face_acquired_mouth_covering_detected_alt" msgid="1122294982850589766">"S\'ha detectat una mascareta. La cara ha de veure\'s sencera."</string>
+    <string name="face_acquired_mouth_covering_detected_alt" msgid="1122294982850589766">"Tens la cara tapada. La cara ha de veure\'s sencera."</string>
   <string-array name="face_acquired_vendor">
   </string-array>
     <string name="face_error_hw_not_available" msgid="5085202213036026288">"No es pot verificar la cara. Maquinari no disponible."</string>
diff --git a/core/res/res/values-fa/strings.xml b/core/res/res/values-fa/strings.xml
index 9bb14bc..1813fc4 100644
--- a/core/res/res/values-fa/strings.xml
+++ b/core/res/res/values-fa/strings.xml
@@ -1263,7 +1263,7 @@
     <string name="fp_power_button_bp_positive_button" msgid="728945472408552251">"خاموش کردن صفحه"</string>
     <string name="fp_power_button_bp_negative_button" msgid="3971364246496775178">"ادامه"</string>
     <string name="heavy_weight_notification" msgid="8382784283600329576">"<xliff:g id="APP">%1$s</xliff:g> در حال اجرا"</string>
-    <string name="heavy_weight_notification_detail" msgid="6802247239468404078">"برای برگشت به بازی، ضربه بزنید"</string>
+    <string name="heavy_weight_notification_detail" msgid="6802247239468404078">"برای برگشتن به بازی، ضربه بزنید"</string>
     <string name="heavy_weight_switcher_title" msgid="3861984210040100886">"انتخاب بازی"</string>
     <string name="heavy_weight_switcher_text" msgid="6814316627367160126">"برای عملکرد بهتر، هربار فقط یکی از این بازی‌ها را می‌توان باز کرد."</string>
     <string name="old_app_action" msgid="725331621042848590">"به <xliff:g id="OLD_APP">%1$s</xliff:g> برگردید"</string>
diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml
index 25addec..c162672 100644
--- a/core/res/res/values-fr/strings.xml
+++ b/core/res/res/values-fr/strings.xml
@@ -1257,7 +1257,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"Lancement des applications…"</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"Finalisation de la mise à jour."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Vous avez appuyé sur le bouton Marche/Arrêt, ce qui éteint généralement l\'écran.\n\nEssayez d\'appuyer doucement pendant la configuration de votre empreinte digitale."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Éteignez l\'écran pour achever la configuration."</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Éteignez l\'écran pour finir la config."</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Éteindre"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Continuer de valider votre empreinte ?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Vous avez appuyé sur le bouton Marche/Arrêt, ce qui éteint généralement l\'écran.\n\nPour valider votre empreinte digitale, appuyez plus doucement."</string>
diff --git a/core/res/res/values-hy/strings.xml b/core/res/res/values-hy/strings.xml
index f20230f..da1b5dc 100644
--- a/core/res/res/values-hy/strings.xml
+++ b/core/res/res/values-hy/strings.xml
@@ -1256,7 +1256,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"Հավելվածները մեկնարկում են:"</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"Բեռնումն ավարտվում է:"</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Դուք սեղմել եք սնուցման կոճակը։ Սովորաբար դրա արդյունքում էկրանն անջատվում է։\n\nՄատնահետքը ավելացնելու համար թեթևակի հպեք կոճակին։"</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Ավարտեք կարգավորումը՝ անջատելով էկրանը"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Անջատեք էկրանը և ավարտեք"</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Անջատել"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Շարունակե՞լ մատնահետքի սկանավորումը"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Դուք սեղմել եք սնուցման կոճակը։ Սովորաբար դրա արդյունքում էկրանն անջատվում է։\n\nՄատնահետքը սկանավորելու համար թեթևակի հպեք կոճակին։"</string>
diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml
index b4e0255..cc03284 100644
--- a/core/res/res/values-iw/strings.xml
+++ b/core/res/res/values-iw/strings.xml
@@ -1258,7 +1258,7 @@
     <string name="android_upgrading_complete" msgid="409800058018374746">"תהליך האתחול בשלבי סיום."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"לחצת על לחצן ההפעלה – בדרך כלל הפעולה הזו מכבה את המסך.\n\nעליך לנסות להקיש בעדינות במהלך ההגדרה של טביעת האצבע שלך."</string>
     <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"לסיום ההגדרה, יש לכבות את המסך"</string>
-    <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"השבתה"</string>
+    <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"כיבוי"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"להמשיך לאמת את טביעת האצבע שלך?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"לחצת על לחצן ההפעלה – בדרך כלל הפעולה הזו מכבה את המסך.\n\nעליך לנסות להקיש בעדינות כדי לאמת את טביעת האצבע שלך."</string>
     <string name="fp_power_button_bp_positive_button" msgid="728945472408552251">"כיבוי המסך"</string>
diff --git a/core/res/res/values-kn/strings.xml b/core/res/res/values-kn/strings.xml
index 1398b35..1f0cbfc 100644
--- a/core/res/res/values-kn/strings.xml
+++ b/core/res/res/values-kn/strings.xml
@@ -645,7 +645,7 @@
     <string name="face_authenticated_confirmation_required" msgid="6872632732508013755">"ಮುಖವನ್ನು ದೃಢೀಕರಿಸಲಾಗಿದೆ, ದೃಢೀಕರಣವನ್ನು ಒತ್ತಿ"</string>
     <string name="fingerprint_error_hw_not_available" msgid="4571700896929561202">"ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಹಾರ್ಡ್‌ವೇರ್‌ ಲಭ್ಯವಿಲ್ಲ."</string>
     <string name="fingerprint_error_no_space" msgid="7285481581905967580">"ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಅನ್ನು ಸೆಟಪ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
-    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"ಫಿಂಗರ್‌ ಪ್ರಿಂಟ್ ಸೆಟಪ್ ಮಾಡುವ ಅವಧಿ ಮುಗಿದಿದೆ. ಪುನಃ ಪ್ರಯತ್ನಿಸಿ."</string>
+    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಸೆಟಪ್ ಮಾಡುವ ಅವಧಿ ಮುಗಿದಿದೆ. ಪುನಃ ಪ್ರಯತ್ನಿಸಿ."</string>
     <string name="fingerprint_error_canceled" msgid="540026881380070750">"ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಕಾರ್ಯಾಚರಣೆಯನ್ನು ರದ್ದುಮಾಡಲಾಗಿದೆ."</string>
     <string name="fingerprint_error_user_canceled" msgid="7685676229281231614">"ಬಳಕೆದಾರರು ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಕಾರ್ಯಾಚರಣೆಯನ್ನು ರದ್ದುಪಡಿಸಿದ್ದಾರೆ."</string>
     <string name="fingerprint_error_lockout" msgid="6626753679019351368">"ಹಲವು ಬಾರಿ ಪ್ರಯತ್ನಿಸಿದ್ದೀರಿ. ಬದಲಾಗಿ ಸ್ಕ್ರೀನ್‌ಲಾಕ್ ಬಳಸಿ."</string>
diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml
index 7df606b..e6979ae 100644
--- a/core/res/res/values-ko/strings.xml
+++ b/core/res/res/values-ko/strings.xml
@@ -625,11 +625,11 @@
     <string name="biometric_error_generic" msgid="6784371929985434439">"인증 오류"</string>
     <string name="screen_lock_app_setting_name" msgid="6054944352976789228">"화면 잠금 사용"</string>
     <string name="screen_lock_dialog_default_subtitle" msgid="120359538048533695">"계속하려면 화면 잠금용 사용자 인증 정보를 입력하세요"</string>
-    <string name="fingerprint_acquired_partial" msgid="4323789264604479684">"센서 위에 손가락을 좀 더 오래 올려놓으세요"</string>
+    <string name="fingerprint_acquired_partial" msgid="4323789264604479684">"센서를 세게 누르세요"</string>
     <string name="fingerprint_acquired_insufficient" msgid="623888149088216458">"지문을 인식할 수 없습니다. 다시 시도해 주세요."</string>
     <string name="fingerprint_acquired_imager_dirty" msgid="1770676120848224250">"지문 센서를 닦은 후 다시 시도해 보세요."</string>
     <string name="fingerprint_acquired_imager_dirty_alt" msgid="9169582140486372897">"센서를 닦은 후 다시 시도해 보세요."</string>
-    <string name="fingerprint_acquired_too_fast" msgid="1628459767349116104">"센서 위에 손가락을 좀 더 오래 올려놓으세요"</string>
+    <string name="fingerprint_acquired_too_fast" msgid="1628459767349116104">"센서를 세게 누르세요"</string>
     <string name="fingerprint_acquired_too_slow" msgid="6683510291554497580">"손가락을 너무 느리게 움직였습니다. 다시 시도해 주세요."</string>
     <string name="fingerprint_acquired_already_enrolled" msgid="2285166003936206785">"다른 지문으로 시도"</string>
     <string name="fingerprint_acquired_too_bright" msgid="3863560181670915607">"너무 밝음"</string>
diff --git a/core/res/res/values-ml/strings.xml b/core/res/res/values-ml/strings.xml
index fe9c02e..c3b86e3 100644
--- a/core/res/res/values-ml/strings.xml
+++ b/core/res/res/values-ml/strings.xml
@@ -1009,7 +1009,7 @@
     <string name="lockscreen_forgot_pattern_button_text" msgid="8362442730606839031">"പാറ്റേൺ മറന്നോ?"</string>
     <string name="lockscreen_glogin_forgot_pattern" msgid="9218940117797602518">"അക്കൗണ്ട് അൺലോക്ക്"</string>
     <string name="lockscreen_glogin_too_many_attempts" msgid="3775904917743034195">"വളരെയധികം പാറ്റേൺ ശ്രമങ്ങൾ"</string>
-    <string name="lockscreen_glogin_instructions" msgid="4695162942525531700">"അൺലോക്കുചെയ്യുന്നതിന്, നിങ്ങളുടെ Google അക്കൗണ്ട് ഉപയോഗിച്ച് സൈൻ ഇൻ ചെയ്യുക."</string>
+    <string name="lockscreen_glogin_instructions" msgid="4695162942525531700">"അൺലോക്കുചെയ്യുന്നതിന്, നിങ്ങളുടെ Google Account ഉപയോഗിച്ച് സൈൻ ഇൻ ചെയ്യുക."</string>
     <string name="lockscreen_glogin_username_hint" msgid="6916101478673157045">"ഉപയോക്താവിന്റെ പേര് (ഇമെയിൽ)"</string>
     <string name="lockscreen_glogin_password_hint" msgid="3031027901286812848">"പാസ്‌വേഡ്"</string>
     <string name="lockscreen_glogin_submit_button" msgid="3590556636347843733">"സൈൻ ഇൻ ചെയ്യുക"</string>
@@ -1660,7 +1660,7 @@
     <string name="kg_invalid_puk" msgid="4809502818518963344">"ശരിയായ PUK കോഡ് വീണ്ടും നൽകുക. ആവർത്തിച്ചുള്ള ശ്രമങ്ങൾ സിം ശാശ്വതമായി പ്രവർത്തനരഹിതമാക്കും."</string>
     <string name="kg_invalid_confirm_pin_hint" product="default" msgid="4705368340409816254">"പിൻ കോഡുകൾ പൊരുത്തപ്പെടുന്നില്ല"</string>
     <string name="kg_login_too_many_attempts" msgid="699292728290654121">"വളരെയധികം പാറ്റേൺ ശ്രമങ്ങൾ"</string>
-    <string name="kg_login_instructions" msgid="3619844310339066827">"അൺലോക്കുചെയ്യുന്നതിന്, നിങ്ങളുടെ Google അക്കൗണ്ട് ഉപയോഗിച്ച് സൈൻ ഇൻ ചെയ്യുക."</string>
+    <string name="kg_login_instructions" msgid="3619844310339066827">"അൺലോക്കുചെയ്യുന്നതിന്, നിങ്ങളുടെ Google Account ഉപയോഗിച്ച് സൈൻ ഇൻ ചെയ്യുക."</string>
     <string name="kg_login_username_hint" msgid="1765453775467133251">"ഉപയോക്താവിന്റെ പേര് (ഇമെയിൽ)"</string>
     <string name="kg_login_password_hint" msgid="3330530727273164402">"പാസ്‌വേഡ്"</string>
     <string name="kg_login_submit_button" msgid="893611277617096870">"സൈൻ ഇൻ ചെയ്യുക"</string>
diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml
index 64150d0..80082c8 100644
--- a/core/res/res/values-nl/strings.xml
+++ b/core/res/res/values-nl/strings.xml
@@ -701,7 +701,7 @@
     <!-- no translation found for face_acquired_mouth_covering_detected (8219428572168642593) -->
     <skip />
     <string name="face_acquired_recalibrate_alt" msgid="5702674220280332115">"Kan gezichtsmodel niet maken. Probeer het opnieuw."</string>
-    <string name="face_acquired_dark_glasses_detected_alt" msgid="4052123776406041972">"Donkere bril waargenomen. Je gezicht moet geheel zichtbaar zijn."</string>
+    <string name="face_acquired_dark_glasses_detected_alt" msgid="4052123776406041972">"Donkere bril waargenomen. Je gezicht moet helemaal zichtbaar zijn."</string>
     <string name="face_acquired_mouth_covering_detected_alt" msgid="1122294982850589766">"Gezichtsbedekking waargenomen. Je hele gezicht moet zichtbaar zijn."</string>
   <string-array name="face_acquired_vendor">
   </string-array>
@@ -1256,7 +1256,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"Apps starten."</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"Opstarten afronden."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Je hebt op de aan/uit-knop gedrukt. Zo zet je meestal het scherm uit.\n\nRaak de knop voorzichtig aan terwijl je je vingerafdruk instelt."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Zet het scherm uit om het instellen te beëindigen"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Zet scherm uit om instellen te beëindigen"</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Uitzetten"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Doorgaan met verificatie van je vingerafdruk?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Je hebt op de aan/uit-knop gedrukt. Zo zet je meestal het scherm uit.\n\nRaak de knop voorzichtig aan om je vingerafdruk te verifiëren."</string>
diff --git a/core/res/res/values-or/strings.xml b/core/res/res/values-or/strings.xml
index 92f7c5b..81a7bb4 100644
--- a/core/res/res/values-or/strings.xml
+++ b/core/res/res/values-or/strings.xml
@@ -1365,7 +1365,7 @@
     <string name="usb_midi_notification_title" msgid="7404506788950595557">"USB ମାଧ୍ୟମରେ MIDIକୁ ଚାଲୁ କରାଗଲା"</string>
     <string name="usb_uvc_notification_title" msgid="2030032862673400008">"ୱେବକେମ ଭାବେ ଡିଭାଇସ କନେକ୍ଟ କରାଯାଇଛି"</string>
     <string name="usb_accessory_notification_title" msgid="1385394660861956980">"USB ଆକ୍ସେସୋରୀ ଯୋଡ଼ାଗଲା"</string>
-    <string name="usb_notification_message" msgid="4715163067192110676">"ଅଧିକ ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ।"</string>
+    <string name="usb_notification_message" msgid="4715163067192110676">"ଅଧିକ ବିକଳ୍ପ ପାଇଁ ଟାପ କରନ୍ତୁ।"</string>
     <string name="usb_power_notification_message" msgid="7284765627437897702">"ଯୋଡ଼ାଯାଇଥିବା ଡିଭାଇସ୍ ଚାର୍ଜ ହେଉଛି। ଅଧିକ ବିକଳ୍ପ ପାଇଁ ଟାପ୍ କରନ୍ତୁ।"</string>
     <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"ଆନାଲଗ୍‍ ଅଡିଓ ଆକ୍ସେସରୀ ଚିହ୍ନଟ ହେଲା"</string>
     <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"ଏହି ଫୋନ୍‌ରେ କନେକ୍ଟ ଥିବା ଡିଭାଇସ୍‍ କମ୍ପାଟିବଲ୍‍ ନୁହେଁ। ଅଧିକ ଜାଣିବା ପାଇଁ ଟାପ୍‌ କରନ୍ତୁ।"</string>
@@ -1430,7 +1430,7 @@
     <string name="ext_media_nomedia_notification_message" msgid="2832724384636625852">"କିଛି କାର୍ଯ୍ୟକ୍ଷମତା ଠିକ୍ ଭାବେ କାମ ନକରିପାରେ। ନୂଆ ଷ୍ଟୋରେଜ୍ ଭର୍ତ୍ତି କରନ୍ତୁ।"</string>
     <string name="ext_media_unmounting_notification_title" msgid="4147986383917892162">"<xliff:g id="NAME">%s</xliff:g>କୁ ଇଜେକ୍ଟ କରାଯାଉଛି"</string>
     <string name="ext_media_unmounting_notification_message" msgid="5717036261538754203">"କାଢ଼ନ୍ତୁ ନାହିଁ"</string>
-    <string name="ext_media_init_action" msgid="2312974060585056709">"ସେଟ୍ ଅପ୍ କରନ୍ତୁ"</string>
+    <string name="ext_media_init_action" msgid="2312974060585056709">"ସେଟ ଅପ କରନ୍ତୁ"</string>
     <string name="ext_media_unmount_action" msgid="966992232088442745">"କାଢ଼ି ଦିଅନ୍ତୁ"</string>
     <string name="ext_media_browse_action" msgid="344865351947079139">"ଖୋଜନ୍ତୁ"</string>
     <string name="ext_media_seamless_action" msgid="8837030226009268080">"ଆଉଟ୍‌ପୁଟ୍ ସ୍ୱିଚ୍‌ କରନ୍ତୁ"</string>
diff --git a/core/res/res/values-pt-rBR/strings.xml b/core/res/res/values-pt-rBR/strings.xml
index fac87ff..0a6f769 100644
--- a/core/res/res/values-pt-rBR/strings.xml
+++ b/core/res/res/values-pt-rBR/strings.xml
@@ -646,7 +646,7 @@
     <string name="face_authenticated_confirmation_required" msgid="6872632732508013755">"Rosto autenticado, pressione \"Confirmar\""</string>
     <string name="fingerprint_error_hw_not_available" msgid="4571700896929561202">"Hardware de impressão digital não disponível."</string>
     <string name="fingerprint_error_no_space" msgid="7285481581905967580">"Não foi possível configurar a impressão digital"</string>
-    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"A configuração da impressão digital expirou. Tente de novo."</string>
+    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"Tempo de configuração esgotado. Tente de novo."</string>
     <string name="fingerprint_error_canceled" msgid="540026881380070750">"Operação de impressão digital cancelada."</string>
     <string name="fingerprint_error_user_canceled" msgid="7685676229281231614">"Operação de impressão digital cancelada pelo usuário."</string>
     <string name="fingerprint_error_lockout" msgid="6626753679019351368">"Excesso de tentativas. Use o bloqueio de tela."</string>
@@ -1366,7 +1366,7 @@
     <string name="usb_midi_notification_title" msgid="7404506788950595557">"MIDI via USB ativado"</string>
     <string name="usb_uvc_notification_title" msgid="2030032862673400008">"Dispositivo conectado como Webcam"</string>
     <string name="usb_accessory_notification_title" msgid="1385394660861956980">"Acessório USB conectado"</string>
-    <string name="usb_notification_message" msgid="4715163067192110676">"Toque para ver mais opções."</string>
+    <string name="usb_notification_message" msgid="4715163067192110676">"Toque para conferir mais opções."</string>
     <string name="usb_power_notification_message" msgid="7284765627437897702">"Carregando dispositivo conectado. Toque para ver mais opções."</string>
     <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"Acessório de áudio analógico detectado"</string>
     <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"O dispositivo anexo não é compatível com esse smartphone. Toque para saber mais."</string>
diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml
index 86f2c9b..96242e1 100644
--- a/core/res/res/values-pt-rPT/strings.xml
+++ b/core/res/res/values-pt-rPT/strings.xml
@@ -1257,8 +1257,8 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"A iniciar aplicações"</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"A concluir o arranque."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Premiu o botão ligar/desligar. Geralmente, esta ação desliga o ecrã.\n\nExperimente tocar levemente ao configurar a sua impressão digital."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Termine a configuração ao desligar ecrã"</string>
-    <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Desativar"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Para terminar, desligue o ecrã"</string>
+    <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Desligar"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Continuar a validar a impressão digital?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Premiu o botão ligar/desligar. Geralmente, esta ação desliga o ecrã.\n\nExperimente tocar levemente para validar a sua impressão digital."</string>
     <string name="fp_power_button_bp_positive_button" msgid="728945472408552251">"Desligar ecrã"</string>
diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml
index fac87ff..0a6f769 100644
--- a/core/res/res/values-pt/strings.xml
+++ b/core/res/res/values-pt/strings.xml
@@ -646,7 +646,7 @@
     <string name="face_authenticated_confirmation_required" msgid="6872632732508013755">"Rosto autenticado, pressione \"Confirmar\""</string>
     <string name="fingerprint_error_hw_not_available" msgid="4571700896929561202">"Hardware de impressão digital não disponível."</string>
     <string name="fingerprint_error_no_space" msgid="7285481581905967580">"Não foi possível configurar a impressão digital"</string>
-    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"A configuração da impressão digital expirou. Tente de novo."</string>
+    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"Tempo de configuração esgotado. Tente de novo."</string>
     <string name="fingerprint_error_canceled" msgid="540026881380070750">"Operação de impressão digital cancelada."</string>
     <string name="fingerprint_error_user_canceled" msgid="7685676229281231614">"Operação de impressão digital cancelada pelo usuário."</string>
     <string name="fingerprint_error_lockout" msgid="6626753679019351368">"Excesso de tentativas. Use o bloqueio de tela."</string>
@@ -1366,7 +1366,7 @@
     <string name="usb_midi_notification_title" msgid="7404506788950595557">"MIDI via USB ativado"</string>
     <string name="usb_uvc_notification_title" msgid="2030032862673400008">"Dispositivo conectado como Webcam"</string>
     <string name="usb_accessory_notification_title" msgid="1385394660861956980">"Acessório USB conectado"</string>
-    <string name="usb_notification_message" msgid="4715163067192110676">"Toque para ver mais opções."</string>
+    <string name="usb_notification_message" msgid="4715163067192110676">"Toque para conferir mais opções."</string>
     <string name="usb_power_notification_message" msgid="7284765627437897702">"Carregando dispositivo conectado. Toque para ver mais opções."</string>
     <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"Acessório de áudio analógico detectado"</string>
     <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"O dispositivo anexo não é compatível com esse smartphone. Toque para saber mais."</string>
diff --git a/core/res/res/values-ro/strings.xml b/core/res/res/values-ro/strings.xml
index 5a5132c..5edccb4 100644
--- a/core/res/res/values-ro/strings.xml
+++ b/core/res/res/values-ro/strings.xml
@@ -1257,7 +1257,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"Se pornesc aplicațiile."</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"Se finalizează pornirea."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Ai apăsat butonul de pornire. De obicei, astfel se dezactivează ecranul.\n\nAtinge ușor când îți configurezi amprenta."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Ca să termini configurarea, dezactivează ecranul"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Termină configurarea dezactivând ecranul"</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Dezactivează"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Continui cu verificarea amprentei?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Ai apăsat butonul de pornire. De obicei, astfel se dezactivează ecranul.\n\nAtinge ușor pentru verificarea amprentei."</string>
diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml
index 1d9b729..e48c487 100644
--- a/core/res/res/values-ru/strings.xml
+++ b/core/res/res/values-ru/strings.xml
@@ -1372,7 +1372,7 @@
     <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"Обнаружено аналоговое аудиоустройство"</string>
     <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"Подсоединенное устройство несовместимо с этим телефоном. Нажмите, чтобы узнать подробности."</string>
     <string name="adb_active_notification_title" msgid="408390247354560331">"Отладка по USB активна"</string>
-    <string name="adb_active_notification_message" msgid="5617264033476778211">"Нажмите, чтобы отключить"</string>
+    <string name="adb_active_notification_message" msgid="5617264033476778211">"Нажмите, чтобы отключить."</string>
     <string name="adb_active_notification_message" product="tv" msgid="6624498401272780855">"Нажмите, чтобы запретить"</string>
     <string name="adbwifi_active_notification_title" msgid="6147343659168302473">"Отладка по Wi-Fi включена"</string>
     <string name="adbwifi_active_notification_message" msgid="930987922852867972">"Нажмите, чтобы отключить отладку по Wi-Fi."</string>
diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml
index f0c6123..c40b205 100644
--- a/core/res/res/values-sv/strings.xml
+++ b/core/res/res/values-sv/strings.xml
@@ -645,7 +645,7 @@
     <string name="face_authenticated_confirmation_required" msgid="6872632732508013755">"Ansiktet har autentiserats. Tryck på Bekräfta"</string>
     <string name="fingerprint_error_hw_not_available" msgid="4571700896929561202">"Det finns ingen maskinvara för fingeravtryck."</string>
     <string name="fingerprint_error_no_space" msgid="7285481581905967580">"Det gick inte att konfigurera fingeravtryck"</string>
-    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"Fingeravtryckskonfigurering nådde tidsgränsen. Försök igen."</string>
+    <string name="fingerprint_error_timeout" msgid="7361192266621252164">"Tiden för fingeravtrycksinställning gick ut. Försök igen."</string>
     <string name="fingerprint_error_canceled" msgid="540026881380070750">"Fingeravtrycksåtgärden avbröts."</string>
     <string name="fingerprint_error_user_canceled" msgid="7685676229281231614">"Fingeravtrycksåtgärden avbröts av användaren."</string>
     <string name="fingerprint_error_lockout" msgid="6626753679019351368">"För många försök. Använd låsskärmen i stället."</string>
diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml
index 7d28aec..8946886 100644
--- a/core/res/res/values-te/strings.xml
+++ b/core/res/res/values-te/strings.xml
@@ -1256,7 +1256,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"యాప్‌లను ప్రారంభిస్తోంది."</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"బూట్‌ను ముగిస్తోంది."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"మీరు పవర్ బటన్‌ను నొక్కారు — ఇది సాధారణంగా స్క్రీన్‌ను ఆఫ్ చేస్తుంది.\n\nమీ వేలిముద్రను సెటప్ చేస్తున్నప్పుడు తేలికగా ట్యాప్ చేయడానికి ట్రై చేయండి."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"సెటప్ ముగించడానికి, స్క్రీన్‌ను ఆఫ్ చేయండి"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"సెటప్ ముగించడానికి, స్క్రీన్ ఆఫ్ చేయండి"</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"ఆఫ్ చేయండి"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"మీ వేలిముద్ర వెరిఫై‌ను కొనసాగించాలా?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"మీరు పవర్ బటన్‌ను నొక్కారు — ఇది సాధారణంగా స్క్రీన్‌ను ఆఫ్ చేస్తుంది.\n\nమీ వేలిముద్రను వెరిఫై చేయడానికి తేలికగా ట్యాప్ చేయడం ట్రై చేయండి."</string>
@@ -1365,7 +1365,7 @@
     <string name="usb_midi_notification_title" msgid="7404506788950595557">"USB ద్వారా MIDI ఆన్ చేయబడింది"</string>
     <string name="usb_uvc_notification_title" msgid="2030032862673400008">"పరికరం వెబ్‌క్యామ్‌గా కనెక్ట్ చేయబడింది"</string>
     <string name="usb_accessory_notification_title" msgid="1385394660861956980">"USB ఉపకరణం కనెక్ట్ చేయబడింది"</string>
-    <string name="usb_notification_message" msgid="4715163067192110676">"మరిన్ని ఆప్షన్ల కోసం నొక్కండి."</string>
+    <string name="usb_notification_message" msgid="4715163067192110676">"మరిన్ని ఆప్షన్ల కోసం ట్యాప్ చేయండి."</string>
     <string name="usb_power_notification_message" msgid="7284765627437897702">"కనెక్ట్ చేయబడిన పరికరాన్ని ఛార్జ్ చేస్తోంది. మరిన్ని ఎంపికల కోసం నొక్కండి."</string>
     <string name="usb_unsupported_audio_accessory_title" msgid="2335775548086533065">"అనలాగ్ ఆడియో ఉపకరణం కనుగొనబడింది"</string>
     <string name="usb_unsupported_audio_accessory_message" msgid="1300168007129796621">"జోడించిన పరికరం ఈ ఫోన్‌కు అనుకూలంగా లేదు. మరింత తెలుసుకోవడానికి నొక్కండి."</string>
diff --git a/core/res/res/values-th/strings.xml b/core/res/res/values-th/strings.xml
index eb9b1f3..107a66a 100644
--- a/core/res/res/values-th/strings.xml
+++ b/core/res/res/values-th/strings.xml
@@ -702,7 +702,7 @@
     <skip />
     <string name="face_acquired_recalibrate_alt" msgid="5702674220280332115">"สร้างรูปแบบใบหน้าไม่ได้ โปรดลองอีกครั้ง"</string>
     <string name="face_acquired_dark_glasses_detected_alt" msgid="4052123776406041972">"ตรวจพบแว่นตาดำ ต้องมองเห็นใบหน้าของคุณทั้งหมด"</string>
-    <string name="face_acquired_mouth_covering_detected_alt" msgid="1122294982850589766">"ตรวจพบหน้ากากอนามัย ต้องมองเห็นใบหน้าของคุณทั้งหมด"</string>
+    <string name="face_acquired_mouth_covering_detected_alt" msgid="1122294982850589766">"ตรวจพบหน้ากาก ต้องมองเห็นใบหน้าของคุณทั้งหมด"</string>
   <string-array name="face_acquired_vendor">
   </string-array>
     <string name="face_error_hw_not_available" msgid="5085202213036026288">"ยืนยันใบหน้าไม่ได้ ฮาร์ดแวร์ไม่พร้อมใช้งาน"</string>
diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml
index f9f9c3a..33b66d3 100644
--- a/core/res/res/values-tr/strings.xml
+++ b/core/res/res/values-tr/strings.xml
@@ -1950,7 +1950,7 @@
     <string name="app_suspended_default_message" msgid="6451215678552004172">"<xliff:g id="APP_NAME_0">%1$s</xliff:g> uygulaması şu anda kullanılamıyor. Uygulamanın kullanım durumu <xliff:g id="APP_NAME_1">%2$s</xliff:g> tarafından yönetiliyor."</string>
     <string name="app_suspended_more_details" msgid="211260942831587014">"Daha fazla bilgi"</string>
     <string name="app_suspended_unsuspend_message" msgid="1665438589450555459">"Uygulamanın duraklatmasını kaldır"</string>
-    <string name="work_mode_off_title" msgid="6367463960165135829">"İş uygulamaları devam ettirilsin mi?"</string>
+    <string name="work_mode_off_title" msgid="6367463960165135829">"İş uygulamaları açılsın mı?"</string>
     <string name="work_mode_turn_on" msgid="5316648862401307800">"Devam ettir"</string>
     <string name="work_mode_emergency_call_button" msgid="6818855962881612322">"Acil durum"</string>
     <string name="app_blocked_title" msgid="7353262160455028160">"Uygulama kullanılamıyor"</string>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 13a2ea2..edff47a 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/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index cf38990..b14c3c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -447,7 +447,7 @@
     }
 
     /**
-     * Callback when launcher finishes swipe-pip-to-home operation.
+     * Callback when launcher finishes preparation of swipe-pip-to-home operation.
      * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
      */
     public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index e04e9f7..b8407c46 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -953,7 +953,8 @@
         if (swipePipToHomeOverlay != null) {
             // Launcher fade in the overlay on top of the fullscreen Task. It is possible we
             // reparent the PIP activity to a new PIP task (in case there are other activities
-            // in the original Task), so we should also reparent the overlay to the PIP task.
+            // in the original Task, in other words multi-activity apps), so we should also reparent
+            // the overlay to the final PIP task.
             startTransaction.reparent(swipePipToHomeOverlay, leash)
                     .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE);
             mPipOrganizer.mSwipePipToHomeOverlay = null;
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index aeaa171..4c9a23d 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -96,17 +96,33 @@
         sk_sp<SkColorSpace> decodeColorSpace =
                 mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr);
         SkBitmap bm;
-        HeapAllocator heapAlloc;
-        if (!mGainmapBRD->decodeRegion(&bm, &heapAlloc, desiredSubset, sampleSize, decodeColorType,
-                                       requireUnpremul, decodeColorSpace)) {
-            ALOGE("Error decoding Gainmap region");
-            return false;
-        }
-        sk_sp<Bitmap> nativeBitmap(heapAlloc.getStorageObjAndReset());
+        // Because we must match the dimensions of the base bitmap, we always use a
+        // recycling allocator even though we are allocating a new bitmap. This is to ensure
+        // that if a recycled bitmap was used for the base image that we match the relative
+        // dimensions of that base image. The behavior of BRD here is:
+        // if inBitmap is specified -> output dimensions are always equal to the inBitmap's
+        // if no bitmap is reused   -> output dimensions are the intersect of the desiredSubset &
+        //                           the image bounds
+        // The handling of the above conditionals are baked into the desiredSubset, so we
+        // simply need to ensure that the resulting bitmap is the exact same width/height as
+        // the specified desiredSubset regardless of the intersection to the image bounds.
+        // kPremul_SkAlphaType is used just as a placeholder as it doesn't change the underlying
+        // allocation type. RecyclingClippingPixelAllocator will populate this with the
+        // actual alpha type in either allocPixelRef() or copyIfNecessary()
+        sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(
+                SkImageInfo::Make(desiredSubset.width(), desiredSubset.height(), decodeColorType,
+                                  kPremul_SkAlphaType, decodeColorSpace));
         if (!nativeBitmap) {
             ALOGE("OOM allocating Bitmap for Gainmap");
             return false;
         }
+        RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false);
+        if (!mGainmapBRD->decodeRegion(&bm, &allocator, desiredSubset, sampleSize, decodeColorType,
+                                       requireUnpremul, decodeColorSpace)) {
+            ALOGE("Error decoding Gainmap region");
+            return false;
+        }
+        allocator.copyIfNecessary();
         auto gainmap = sp<uirenderer::Gainmap>::make();
         if (!gainmap) {
             ALOGE("OOM allocating Gainmap");
@@ -238,13 +254,11 @@
 
     // Recycle a bitmap if possible.
     android::Bitmap* recycledBitmap = nullptr;
-    size_t recycledBytes = 0;
     if (javaBitmap) {
         recycledBitmap = &bitmap::toBitmap(inBitmapHandle);
         if (recycledBitmap->isImmutable()) {
             ALOGW("Warning: Reusing an immutable bitmap as an image decoder target.");
         }
-        recycledBytes = recycledBitmap->getAllocationByteCount();
     }
 
     auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
@@ -263,7 +277,7 @@
 
     // Set up the pixel allocator
     skia::BRDAllocator* allocator = nullptr;
-    RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap, recycledBytes);
+    RecyclingClippingPixelAllocator recycleAlloc(recycledBitmap);
     HeapAllocator heapAlloc;
     if (javaBitmap) {
         allocator = &recycleAlloc;
@@ -277,7 +291,7 @@
             decodeColorType, colorSpace);
 
     // Decode the region.
-    SkIRect subset = SkIRect::MakeXYWH(inputX, inputY, inputWidth, inputHeight);
+    const SkIRect subset = SkIRect::MakeXYWH(inputX, inputY, inputWidth, inputHeight);
     SkBitmap bitmap;
     if (!brd->decodeRegion(&bitmap, allocator, subset, sampleSize,
             decodeColorType, requireUnpremul, decodeColorSpace)) {
@@ -307,10 +321,27 @@
                 GraphicsJNI::getColorSpace(env, decodeColorSpace.get(), decodeColorType));
     }
 
+    if (javaBitmap) {
+        recycleAlloc.copyIfNecessary();
+    }
+
     sp<uirenderer::Gainmap> gainmap;
     bool hasGainmap = brd->hasGainmap();
     if (hasGainmap) {
-        SkIRect gainmapSubset = brd->calculateGainmapRegion(subset);
+        SkIRect adjustedSubset{};
+        if (javaBitmap) {
+            // Clamp to the width/height of the recycled bitmap in case the reused bitmap
+            // was too small for the specified rectangle, in which case we need to clip
+            adjustedSubset = SkIRect::MakeXYWH(inputX, inputY,
+                                               std::min(subset.width(), recycledBitmap->width()),
+                                               std::min(subset.height(), recycledBitmap->height()));
+        } else {
+            // We are not recycling, so use the decoded width/height for calculating the gainmap
+            // subset instead to ensure the gainmap region proportionally matches
+            adjustedSubset = SkIRect::MakeXYWH(std::max(0, inputX), std::max(0, inputY),
+                                               bitmap.width(), bitmap.height());
+        }
+        SkIRect gainmapSubset = brd->calculateGainmapRegion(adjustedSubset);
         if (!brd->decodeGainmapRegion(&gainmap, gainmapSubset, sampleSize, requireUnpremul)) {
             // If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap
             hasGainmap = false;
@@ -319,7 +350,6 @@
 
     // If we may have reused a bitmap, we need to indicate that the pixels have changed.
     if (javaBitmap) {
-        recycleAlloc.copyIfNecessary();
         if (hasGainmap) {
             recycledBitmap->setGainmap(std::move(gainmap));
         }
@@ -331,6 +361,7 @@
     if (!requireUnpremul) {
         bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Premultiplied;
     }
+
     if (isHardware) {
         sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(bitmap);
         if (hasGainmap) {
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 914266d..78b4f7b 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -620,13 +620,13 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 
-RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(
-        android::Bitmap* recycledBitmap, size_t recycledBytes)
-    : mRecycledBitmap(recycledBitmap)
-    , mRecycledBytes(recycledBytes)
-    , mSkiaBitmap(nullptr)
-    , mNeedsCopy(false)
-{}
+RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
+                                                                 bool mustMatchColorType)
+        : mRecycledBitmap(recycledBitmap)
+        , mRecycledBytes(recycledBitmap ? recycledBitmap->getAllocationByteCount() : 0)
+        , mSkiaBitmap(nullptr)
+        , mNeedsCopy(false)
+        , mMustMatchColorType(mustMatchColorType) {}
 
 RecyclingClippingPixelAllocator::~RecyclingClippingPixelAllocator() {}
 
@@ -637,10 +637,16 @@
     LOG_ALWAYS_FATAL_IF(!bitmap);
     mSkiaBitmap = bitmap;
 
-    // This behaves differently than the RecyclingPixelAllocator.  For backwards
-    // compatibility, the original color type of the recycled bitmap must be maintained.
-    if (mRecycledBitmap->info().colorType() != bitmap->colorType()) {
-        return false;
+    if (mMustMatchColorType) {
+        // This behaves differently than the RecyclingPixelAllocator.  For backwards
+        // compatibility, the original color type of the recycled bitmap must be maintained.
+        if (mRecycledBitmap->info().colorType() != bitmap->colorType()) {
+            ALOGW("recycled color type %d != bitmap color type %d",
+                  mRecycledBitmap->info().colorType(), bitmap->colorType());
+            return false;
+        }
+    } else {
+        mRecycledBitmap->reconfigure(mRecycledBitmap->info().makeColorType(bitmap->colorType()));
     }
 
     // The Skia bitmap specifies the width and height needed by the decoder.
@@ -695,7 +701,7 @@
 void RecyclingClippingPixelAllocator::copyIfNecessary() {
     if (mNeedsCopy) {
         mRecycledBitmap->ref();
-        SkPixelRef* recycledPixels = mRecycledBitmap;
+        android::Bitmap* recycledPixels = mRecycledBitmap;
         void* dst = recycledPixels->pixels();
         const size_t dstRowBytes = mRecycledBitmap->rowBytes();
         const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
@@ -708,6 +714,8 @@
             dst = reinterpret_cast<void*>(
                     reinterpret_cast<uint8_t*>(dst) + dstRowBytes);
         }
+        recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
+        recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
         recycledPixels->notifyPixelsChanged();
         recycledPixels->unref();
     }
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index 24f9e82..23ab5dd 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -222,9 +222,8 @@
  */
 class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator {
 public:
-
     RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
-            size_t recycledBytes);
+                                    bool mustMatchColorType = true);
 
     ~RecyclingClippingPixelAllocator();
 
@@ -252,6 +251,7 @@
     const size_t     mRecycledBytes;
     SkBitmap*        mSkiaBitmap;
     bool             mNeedsCopy;
+    const bool mMustMatchColorType;
 };
 
 class AshmemPixelAllocator : public SkBitmap::Allocator {
diff --git a/packages/CarrierDefaultApp/res/values-ur/strings.xml b/packages/CarrierDefaultApp/res/values-ur/strings.xml
index 0c7cdc5..d6225c2 100644
--- a/packages/CarrierDefaultApp/res/values-ur/strings.xml
+++ b/packages/CarrierDefaultApp/res/values-ur/strings.xml
@@ -16,9 +16,7 @@
     <string name="ssl_error_continue" msgid="1138548463994095584">"براؤزر کے ذریعے بہرحال جاری رکھیں"</string>
     <string name="performance_boost_notification_channel" msgid="3475440855635538592">"پرفارمینس بوسٹ"</string>
     <string name="performance_boost_notification_title" msgid="3126203390685781861">"‏آپ کے کیریئر سے 5G کے اختیارات"</string>
-    <!-- String.format failed for translation -->
-    <!-- no translation found for performance_boost_notification_detail (216569851036236346) -->
-    <skip />
+    <string name="performance_boost_notification_detail" msgid="216569851036236346">"‏اپنی ایپ کے تجربے کے اختیارات دیکھنے کے لیے %s کی ویب سائٹ ملاحظہ کریں"</string>
     <string name="performance_boost_notification_button_not_now" msgid="6459755324243683785">"ابھی نہیں"</string>
     <string name="performance_boost_notification_button_manage" msgid="4976836444046497973">"نظم کریں"</string>
     <string name="slice_purchase_app_label" msgid="7170191659233241166">"پرفارمینس بوسٹ خریدیں۔"</string>
diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index e88410c..8699f59 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -26,7 +26,10 @@
 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 @@
     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 cc7bb4a..d1db237 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 0000000..423e351
--- /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 0000000..c31aa7b
--- /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 0000000..391d515
--- /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 0000000..beef85c
--- /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 0000000..1394f2f
--- /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 743947a..79957df 100644
--- a/packages/EasterEgg/res/values/strings.xml
+++ b/packages/EasterEgg/res/values/strings.xml
@@ -14,7 +14,7 @@
     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 0000000..f5657ae
--- /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 0000000..d040fba
--- /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 0000000..5a9b814
--- /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 0000000..fdf29f7
--- /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 0000000..67d536e
--- /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 0000000..8510640
--- /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 0000000..fc66ad6
--- /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 0000000..ebbb2bd
--- /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 0000000..fec3ab3
--- /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 0000000..82bae75
--- /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 0000000..24b9c6a
--- /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/packages/PackageInstaller/res/values-ko/strings.xml b/packages/PackageInstaller/res/values-ko/strings.xml
index 14f9513..ef39eef 100644
--- a/packages/PackageInstaller/res/values-ko/strings.xml
+++ b/packages/PackageInstaller/res/values-ko/strings.xml
@@ -70,7 +70,7 @@
     <string name="uninstalling_app" msgid="8866082646836981397">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 제거 중…"</string>
     <string name="uninstall_done" msgid="439354138387969269">"제거를 완료했습니다."</string>
     <string name="uninstall_done_app" msgid="4588850984473605768">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g>를 제거했습니다."</string>
-    <string name="uninstall_done_clone_app" msgid="5578308154544195413">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 클론 삭제됨"</string>
+    <string name="uninstall_done_clone_app" msgid="5578308154544195413">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 복제 삭제됨"</string>
     <string name="uninstall_failed" msgid="1847750968168364332">"제거하지 못했습니다."</string>
     <string name="uninstall_failed_app" msgid="5506028705017601412">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g>을(를) 제거하지 못했습니다."</string>
     <string name="uninstalling_cloned_app" msgid="1826380164974984870">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 클론 삭제 중…"</string>
@@ -94,7 +94,7 @@
     <string name="anonymous_source_warning" product="default" msgid="2784902545920822500">"휴대전화와 개인 데이터는 알 수 없는 앱의 공격에 더욱 취약합니다. 이 앱을 설치하면 앱 사용으로 인해 발생할 수 있는 모든 휴대전화 손상이나 데이터 손실에 사용자가 책임을 진다는 것에 동의하게 됩니다."</string>
     <string name="anonymous_source_warning" product="tablet" msgid="3939101621438855516">"태블릿과 개인 데이터는 알 수 없는 앱의 공격에 더욱 취약합니다. 이 앱을 설치하면 앱 사용으로 인해 발생할 수 있는 모든 태블릿 손상이나 데이터 손실에 사용자가 책임을 진다는 것에 동의하게 됩니다."</string>
     <string name="anonymous_source_warning" product="tv" msgid="5599483539528168566">"TV와 개인 데이터는 알 수 없는 앱의 공격에 더욱 취약합니다. 이 앱을 설치하면 앱 사용으로 인해 발생할 수 있는 모든 TV 손상이나 데이터 손실에 사용자가 책임을 진다는 것에 동의하게 됩니다."</string>
-    <string name="cloned_app_label" msgid="7503612829833756160">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 클론"</string>
+    <string name="cloned_app_label" msgid="7503612829833756160">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 복제"</string>
     <string name="anonymous_source_continue" msgid="4375745439457209366">"계속"</string>
     <string name="external_sources_settings" msgid="4046964413071713807">"설정"</string>
     <string name="wear_app_channel" msgid="1960809674709107850">"Wear 앱 설치/제거"</string>
diff --git a/packages/SettingsLib/SpaPrivileged/res/values-ko/strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-ko/strings.xml
index ef4ee0d..3e1b837 100644
--- a/packages/SettingsLib/SpaPrivileged/res/values-ko/strings.xml
+++ b/packages/SettingsLib/SpaPrivileged/res/values-ko/strings.xml
@@ -23,5 +23,5 @@
     <string name="app_permission_summary_allowed" msgid="6115213465364138103">"허용됨"</string>
     <string name="app_permission_summary_not_allowed" msgid="58396132188553920">"허용되지 않음"</string>
     <string name="version_text" msgid="4001669804596458577">"버전 <xliff:g id="VERSION_NUM">%1$s</xliff:g>"</string>
-    <string name="cloned_app_info_label" msgid="1765651167024478391">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 클론"</string>
+    <string name="cloned_app_info_label" msgid="1765651167024478391">"<xliff:g id="PACKAGE_LABEL">%1$s</xliff:g> 복제"</string>
 </resources>
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index 23e3a01..1a03ede 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -34,7 +34,6 @@
 import android.view.ViewGroupOverlay
 import android.widget.FrameLayout
 import com.android.internal.jank.InteractionJankMonitor
-import java.lang.IllegalArgumentException
 import java.util.LinkedList
 import kotlin.math.min
 import kotlin.math.roundToInt
@@ -240,7 +239,7 @@
         val ghostView = this.ghostView ?: return
         val backgroundView = this.backgroundView!!
 
-        if (!state.visible) {
+        if (!state.visible || !ghostedView.isAttachedToWindow) {
             if (ghostView.visibility == View.VISIBLE) {
                 // Making the ghost view invisible will make the ghosted view visible, so order is
                 // important here.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index 1e9a466..70b5e75 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -21,6 +21,8 @@
 import android.content.Intent
 import android.content.res.ColorStateList
 import android.content.res.Configuration
+import android.database.ContentObserver
+import android.provider.Settings
 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
 import android.util.Log
 import android.util.MathUtils
@@ -64,6 +66,7 @@
 import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.animation.requiresRemeasuring
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
 import com.android.systemui.util.traceSection
 import java.io.PrintWriter
@@ -105,6 +108,7 @@
     private val mediaFlags: MediaFlags,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val globalSettings: GlobalSettings,
 ) : Dumpable {
     /** The current width of the carousel */
     var currentCarouselWidth: Int = 0
@@ -169,6 +173,13 @@
 
     private var carouselLocale: Locale? = null
 
+    private val animationScaleObserver: ContentObserver =
+        object : ContentObserver(null) {
+            override fun onChange(selfChange: Boolean) {
+                MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
+            }
+        }
+
     /** Whether the media card currently has the "expanded" layout */
     @VisibleForTesting
     var currentlyExpanded = true
@@ -529,6 +540,12 @@
                 listenForAnyStateToGoneKeyguardTransition(this)
             }
         }
+
+        // Notifies all active players about animation scale changes.
+        globalSettings.registerContentObserver(
+            Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+            animationScaleObserver
+        )
     }
 
     private fun inflateSettingsButton() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 0819d0d..35082fd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -34,7 +34,6 @@
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
 import android.graphics.Color;
@@ -252,13 +251,6 @@
     private boolean mWasPlaying = false;
     private boolean mButtonClicked = false;
 
-    private ContentObserver mAnimationScaleObserver = new ContentObserver(null) {
-        @Override
-        public void onChange(boolean selfChange) {
-            updateAnimatorDurationScale();
-        }
-    };
-
     /**
      * Initialize a new control panel
      *
@@ -318,10 +310,6 @@
         mFeatureFlags = featureFlags;
 
         mGlobalSettings = globalSettings;
-        mGlobalSettings.registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
-                mAnimationScaleObserver
-        );
         updateAnimatorDurationScale();
     }
 
@@ -405,7 +393,10 @@
         updateSeekBarVisibility();
     }
 
-    private void updateAnimatorDurationScale() {
+    /**
+     * Reloads animator duration scale.
+     */
+    void updateAnimatorDurationScale() {
         if (mSeekBarObserver != null) {
             mSeekBarObserver.setAnimationEnabled(
                     mGlobalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 0a5b4b3..7712690 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -105,6 +105,7 @@
     private WallpaperColors mWallpaperColors;
     private boolean mShouldLaunchLeBroadcastDialog;
     private boolean mIsLeBroadcastCallbackRegistered;
+    private boolean mDismissing;
 
     MediaOutputBaseAdapter mAdapter;
 
@@ -265,13 +266,22 @@
         mDevicesRecyclerView.setHasFixedSize(false);
         // Init bottom buttons
         mDoneButton.setOnClickListener(v -> dismiss());
-        mStopButton.setOnClickListener(v -> {
-            mMediaOutputController.releaseSession();
-            dismiss();
-        });
+        mStopButton.setOnClickListener(v -> onStopButtonClick());
         mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication);
         mMediaMetadataSectionLayout.setOnClickListener(
                 mMediaOutputController::tryToLaunchMediaApplication);
+
+        mDismissing = false;
+    }
+
+    @Override
+    public void dismiss() {
+        // TODO(287191450): remove this once expensive binder calls are removed from refresh().
+        // Due to these binder calls on the UI thread, calling refresh() during dismissal causes
+        // significant frame drops for the dismissal animation. Since the dialog is going away
+        // anyway, we use this state to turn refresh() into a no-op.
+        mDismissing = true;
+        super.dismiss();
     }
 
     @Override
@@ -299,7 +309,9 @@
     }
 
     void refresh(boolean deviceSetChanged) {
-        if (mMediaOutputController.isRefreshing()) {
+        // TODO(287191450): remove binder calls in this method from the UI thread.
+        // If the dialog is going away or is already refreshing, do nothing.
+        if (mDismissing || mMediaOutputController.isRefreshing()) {
             return;
         }
         mMediaOutputController.setRefreshing(true);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 19b32e9..f3865f5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -28,6 +28,7 @@
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
 
@@ -36,11 +37,14 @@
  */
 @SysUISingleton
 public class MediaOutputDialog extends MediaOutputBaseDialog {
-    final UiEventLogger mUiEventLogger;
+    private final DialogLaunchAnimator mDialogLaunchAnimator;
+    private final UiEventLogger mUiEventLogger;
 
     MediaOutputDialog(Context context, boolean aboveStatusbar, BroadcastSender broadcastSender,
-            MediaOutputController mediaOutputController, UiEventLogger uiEventLogger) {
+            MediaOutputController mediaOutputController, DialogLaunchAnimator dialogLaunchAnimator,
+            UiEventLogger uiEventLogger) {
         super(context, broadcastSender, mediaOutputController);
+        mDialogLaunchAnimator = dialogLaunchAnimator;
         mUiEventLogger = uiEventLogger;
         mAdapter = new MediaOutputAdapter(mMediaOutputController);
         if (!aboveStatusbar) {
@@ -138,6 +142,7 @@
             }
         } else {
             mMediaOutputController.releaseSession();
+            mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
             dismiss();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 8024886..4c168ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -28,11 +28,11 @@
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager
 import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import java.util.Optional
 import javax.inject.Inject
 
@@ -71,7 +71,8 @@
             dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
             powerExemptionManager, keyGuardManager, featureFlags, userTracker)
         val dialog =
-            MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger)
+            MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller,
+                    dialogLaunchAnimator, uiEventLogger)
         mediaOutputDialog = dialog
 
         // Show the dialog.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index 2aff90c..5b8272b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -19,7 +19,9 @@
 import android.app.PendingIntent
 import android.content.res.ColorStateList
 import android.content.res.Configuration
+import android.database.ContentObserver
 import android.os.LocaleList
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.util.MathUtils.abs
@@ -56,6 +58,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.FakeSystemClock
 import java.util.Locale
 import javax.inject.Provider
@@ -113,6 +116,7 @@
     @Mock lateinit var mediaFlags: MediaFlags
     @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    @Mock lateinit var globalSettings: GlobalSettings
     private lateinit var transitionRepository: FakeKeyguardTransitionRepository
     @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
     @Captor
@@ -120,6 +124,7 @@
     @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
     @Captor lateinit var keyguardCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
     @Captor lateinit var hostStateCallback: ArgumentCaptor<MediaHostStatesManager.Callback>
+    @Captor lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
 
     private val clock = FakeSystemClock()
     private lateinit var mediaCarouselController: MediaCarouselController
@@ -148,6 +153,7 @@
                 mediaFlags,
                 keyguardUpdateMonitor,
                 KeyguardTransitionInteractor(transitionRepository, TestScope().backgroundScope),
+                globalSettings
             )
         verify(configurationController).addCallback(capture(configListener))
         verify(mediaDataManager).addListener(capture(listener))
@@ -160,6 +166,11 @@
         whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         MediaPlayerData.clear()
+        verify(globalSettings)
+            .registerContentObserver(
+                eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
+                settingsObserverCaptor.capture()
+            )
     }
 
     @Test
@@ -873,6 +884,15 @@
         assertTrue(stateUpdated)
     }
 
+    @Test
+    fun testAnimationScaleChanged_mediaControlPanelsNotified() {
+        MediaPlayerData.addMediaPlayer("key", DATA, panel, clock, isSsReactivated = false)
+
+        globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f)
+        settingsObserverCaptor.value!!.onChange(false)
+        verify(panel).updateAnimatorDurationScale()
+    }
+
     /**
      * Helper method when a configuration change occurs.
      *
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index f6075ad..f902be3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -25,7 +25,6 @@
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.content.res.Configuration
-import android.database.ContentObserver
 import android.graphics.Bitmap
 import android.graphics.Canvas
 import android.graphics.Color
@@ -113,7 +112,6 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.mock
@@ -239,7 +237,6 @@
             this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
         }
     @Mock private lateinit var globalSettings: GlobalSettings
-    @Captor private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
@@ -281,7 +278,7 @@
                     lockscreenUserManager,
                     broadcastDialogController,
                     fakeFeatureFlag,
-                    globalSettings,
+                    globalSettings
                 ) {
                 override fun loadAnimator(
                     animId: Int,
@@ -292,12 +289,6 @@
                 }
             }
 
-        verify(globalSettings)
-            .registerContentObserver(
-                eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
-                settingsObserverCaptor.capture()
-            )
-
         initGutsViewHolderMocks()
         initMediaViewHolderMocks()
 
@@ -986,7 +977,7 @@
 
         // When the setting changes,
         globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f)
-        settingsObserverCaptor.value!!.onChange(false)
+        player.updateAnimatorDurationScale()
 
         // Then the seekbar is set to not animate
         assertThat(seekBarObserver.animationEnabled).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index f3aee48..a14ff2f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -356,6 +356,7 @@
         });
 
         verify(mockMediaOutputController).releaseSession();
+        verify(mDialogLaunchAnimator).disableAllCurrentDialogsExitAnimations();
     }
 
     @Test
@@ -371,7 +372,7 @@
     @NonNull
     private MediaOutputDialog makeTestDialog(MediaOutputController controller) {
         return new MediaOutputDialog(mContext, false, mBroadcastSender,
-                controller, mUiEventLogger);
+                controller, mDialogLaunchAnimator, mUiEventLogger);
     }
 
     private void withTestDialog(MediaOutputController controller, Consumer<MediaOutputDialog> c) {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index a1ccade..611541f 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -64,6 +64,7 @@
 import android.companion.IOnMessageReceivedListener;
 import android.companion.IOnTransportsChangedListener;
 import android.companion.ISystemDataTransferCallback;
+import android.companion.utils.FeatureUtils;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -746,6 +747,11 @@
         @Override
         public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
                 int userId, int associationId) {
+            if (!FeatureUtils.isPermSyncEnabled()) {
+                throw new UnsupportedOperationException("Calling"
+                        + " buildPermissionTransferUserConsentIntent, but this API is disabled by"
+                        + " the system.");
+            }
             return mSystemDataTransferProcessor.buildPermissionTransferUserConsentIntent(
                     packageName, userId, associationId);
         }
@@ -753,6 +759,10 @@
         @Override
         public void startSystemDataTransfer(String packageName, int userId, int associationId,
                 ISystemDataTransferCallback callback) {
+            if (!FeatureUtils.isPermSyncEnabled()) {
+                throw new UnsupportedOperationException("Calling startSystemDataTransfer, but this"
+                        + " API is disabled by the system.");
+            }
             mSystemDataTransferProcessor.startSystemDataTransfer(packageName, userId,
                     associationId, callback);
         }
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 5a3db4b..3cb9ac8 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 @@
         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 @@
     }
 
     /**
+     * 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 @@
             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 0f00f5f..a49021a 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -22,14 +22,10 @@
 
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
-import android.app.ActivityManagerInternal;
 import android.companion.AssociationInfo;
 import android.companion.IOnMessageReceivedListener;
 import android.companion.IOnTransportsChangedListener;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Binder;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteCallbackList;
@@ -38,7 +34,6 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.LocalServices;
 import com.android.server.companion.AssociationStore;
 
 import java.io.FileDescriptor;
@@ -137,44 +132,15 @@
         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);
                 }
             }
         }
     }
 
-    /**
-     * For the moment, we only offer transporting of system data to built-in
-     * companion apps; future work will improve the security model to support
-     * third-party companion apps.
-     */
-    private void enforceCallerCanTransportSystemData(String packageName, int userId) {
-        mContext.enforceCallingOrSelfPermission(DELIVER_COMPANION_MESSAGES, TAG);
-
-        try {
-            final ApplicationInfo info = mContext.getPackageManager().getApplicationInfoAsUser(
-                    packageName, 0, userId);
-            final int instrumentationUid = LocalServices.getService(ActivityManagerInternal.class)
-                    .getInstrumentationSourceUid(Binder.getCallingUid());
-            if (!Build.isDebuggable() && !info.isSystemApp()
-                    && instrumentationUid == android.os.Process.INVALID_UID) {
-                throw new SecurityException("Transporting of system data currently only available "
-                        + "to built-in companion apps or tests");
-            }
-        } catch (NameNotFoundException e) {
-            throw new IllegalArgumentException(e);
-        }
-    }
-
     public void attachSystemDataTransport(String packageName, int userId, int associationId,
             ParcelFileDescriptor fd) {
-        enforceCallerCanTransportSystemData(packageName, userId);
+        mContext.enforceCallingOrSelfPermission(DELIVER_COMPANION_MESSAGES, TAG);
         synchronized (mTransports) {
             if (mTransports.contains(associationId)) {
                 detachSystemDataTransport(packageName, userId, associationId);
@@ -188,7 +154,7 @@
     }
 
     public void detachSystemDataTransport(String packageName, int userId, int associationId) {
-        enforceCallerCanTransportSystemData(packageName, userId);
+        mContext.enforceCallingOrSelfPermission(DELIVER_COMPANION_MESSAGES, TAG);
         synchronized (mTransports) {
             final Transport transport = mTransports.get(associationId);
             if (transport != null) {
@@ -244,6 +210,7 @@
         }
 
         addMessageListenersToTransport(transport);
+        transport.setOnTransportClosedListener(this::detachSystemDataTransport);
         transport.start();
         synchronized (mTransports) {
             mTransports.put(associationId, transport);
@@ -321,4 +288,14 @@
             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 e64509f..ca169aac 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 @@
         }
         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 2d856b9..a0301a9 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.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 @@
 
     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 @@
     void close() {
         mSecureChannel.close();
         mShouldProcessRequests = false;
+
+        super.close();
     }
 
     @Override
@@ -81,13 +81,19 @@
         }
 
         // 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 @@
         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 @@
 
         // 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 @@
         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 6ad6d3a..5af3b98 100644
--- a/services/companion/java/com/android/server/companion/transport/Transport.java
+++ b/services/companion/java/com/android/server/companion/transport/Transport.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.companion.IOnMessageReceivedListener;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -70,6 +69,8 @@
      */
     private final Map<Integer, IOnMessageReceivedListener> mListeners;
 
+    private OnTransportClosedListener mOnTransportClosed;
+
     private static boolean isRequest(int message) {
         return (message & 0xFF000000) == 0x63000000;
     }
@@ -120,20 +121,18 @@
     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();
@@ -188,12 +187,6 @@
                 break;
             }
             case MESSAGE_REQUEST_PERMISSION_RESTORE: {
-                if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
-                        && !Build.isDebuggable()) {
-                    Slog.w(TAG, "Restoring permissions only supported on watches");
-                    sendMessage(MESSAGE_RESPONSE_FAILURE, sequence, EmptyArray.BYTE);
-                    break;
-                }
                 try {
                     callback(message, data);
                     sendMessage(MESSAGE_RESPONSE_SUCCESS, sequence, EmptyArray.BYTE);
@@ -247,4 +240,14 @@
             }
         }
     }
+
+    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/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 47f6485..9185a00 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -16,6 +16,7 @@
 
 package com.android.server.media;
 
+import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
 import static android.media.VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
 import static android.media.VolumeProvider.VOLUME_CONTROL_FIXED;
 import static android.media.VolumeProvider.VOLUME_CONTROL_RELATIVE;
@@ -44,7 +45,9 @@
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.MediaMetadata;
+import android.media.MediaRouter2Manager;
 import android.media.Rating;
+import android.media.RoutingSessionInfo;
 import android.media.VolumeProvider;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
@@ -510,7 +513,33 @@
 
     @Override
     public boolean canHandleVolumeKey() {
-        return mVolumeControlType != VOLUME_CONTROL_FIXED;
+        if (isPlaybackTypeLocal()) {
+            return true;
+        }
+        if (mVolumeControlType == VOLUME_CONTROL_FIXED) {
+            return false;
+        }
+        if (mVolumeAdjustmentForRemoteGroupSessions) {
+            return true;
+        }
+        // See b/228021646 for details.
+        MediaRouter2Manager mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
+        List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
+        boolean foundNonSystemSession = false;
+        boolean remoteSessionAllowVolumeAdjustment = true;
+        for (RoutingSessionInfo session : sessions) {
+            if (!session.isSystemSession()) {
+                foundNonSystemSession = true;
+                if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
+                    remoteSessionAllowVolumeAdjustment = false;
+                }
+            }
+        }
+        if (!foundNonSystemSession) {
+            Log.d(TAG, "Package " + mPackageName
+                    + " has a remote media session but no associated routing session");
+        }
+        return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 3d9edca..5f2d3ca8 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -979,7 +979,7 @@
         }
 
         if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
-            if (didCommitTransientLaunch()) {
+            if (!ar.getTask().isVisibleRequested() || didCommitTransientLaunch()) {
                 // force enable pip-on-task-switch now that we've committed to actually launching
                 // to the transient activity.
                 ar.supportsEnterPipOnTaskSwitch = true;
@@ -1008,7 +1008,8 @@
         }
 
         // Legacy pip-entry (not via isAutoEnterEnabled).
-        if (didCommitTransientLaunch() && ar.supportsPictureInPicture()) {
+        if ((!ar.getTask().isVisibleRequested() || didCommitTransientLaunch())
+                && ar.supportsPictureInPicture()) {
             // force enable pip-on-task-switch now that we've committed to actually launching to the
             // transient activity, and then recalculate whether we can attempt pip.
             ar.supportsEnterPipOnTaskSwitch = true;
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index aee4f58..c9e691e 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -28,6 +28,7 @@
 import android.credentials.IGetCredentialCallback;
 import android.credentials.ui.ProviderData;
 import android.credentials.ui.RequestInfo;
+import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.RemoteException;
 import android.service.credentials.CallingAppInfo;
@@ -98,8 +99,9 @@
     protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
         mRequestSessionMetric.collectUiCallStartTime(System.nanoTime());
         mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION);
-        cancelExistingPendingIntent();
+        Binder.withCleanCallingIdentity(()-> {
         try {
+                cancelExistingPendingIntent();
             mPendingIntent = mCredentialManagerUi.createPendingIntent(
                     RequestInfo.newGetRequestInfo(
                             mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
@@ -112,9 +114,9 @@
             mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED);
             String exception = GetCredentialException.TYPE_UNKNOWN;
             mRequestSessionMetric.collectFrameworkException(exception);
-            respondToClientWithErrorAndFinish(
-                    exception, "Unable to instantiate selector");
-        }
+                respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector");
+            }
+        });
     }
 
     @Override
diff --git a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java
index b0b72bc..46c90b4 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java
@@ -38,11 +38,13 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -116,7 +118,7 @@
             @NonNull String servicePackageName,
             @NonNull CredentialOption requestOption) {
         super(context, requestOption, session,
-                new ComponentName(servicePackageName, servicePackageName),
+                new ComponentName(servicePackageName, UUID.randomUUID().toString()),
                 userId, null);
         mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId);
         mCallingAppInfo = callingAppInfo;
@@ -133,7 +135,7 @@
             @NonNull String servicePackageName,
             @NonNull CredentialOption requestOption) {
         super(context, requestOption, session,
-                new ComponentName(servicePackageName, servicePackageName),
+                new ComponentName(servicePackageName, UUID.randomUUID().toString()),
                 userId, null);
         mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId);
         mCallingAppInfo = callingAppInfo;
@@ -179,7 +181,9 @@
             return null;
         }
         return new GetCredentialProviderData.Builder(
-                mComponentName.flattenToString()).setActionChips(null)
+                mComponentName.flattenToString())
+                .setActionChips(Collections.EMPTY_LIST)
+                .setAuthenticationEntries(Collections.EMPTY_LIST)
                 .setCredentialEntries(prepareUiCredentialEntries(
                         mProviderResponse.stream().flatMap((Function<CredentialDescriptionRegistry
                                         .FilterResult,
@@ -261,12 +265,12 @@
                 .getFilteredResultForProvider(mCredentialProviderPackageName,
                         mElementKeys);
         mCredentialEntries = mProviderResponse.stream().flatMap(
-                (Function<CredentialDescriptionRegistry.FilterResult,
-                        Stream<CredentialEntry>>) filterResult
-                        -> filterResult.mCredentialEntries.stream())
-                .collect(Collectors.toList());
+                            (Function<CredentialDescriptionRegistry.FilterResult,
+                                    Stream<CredentialEntry>>)
+                filterResult -> filterResult.mCredentialEntries.stream())
+                    .collect(Collectors.toList());
         updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED,
-                /*source=*/ CredentialsSource.REGISTRY);
+                    /*source=*/ CredentialsSource.REGISTRY);
         mProviderSessionMetric.collectCandidateEntryMetrics(mCredentialEntries);
     }
 
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 e65229f..1ba1462 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 @@
 
     @Test
     public void testAtraceBinaryState1() {
-        mHistory.forceRecordAllHistory();
-
         InOrder inOrder = Mockito.inOrder(mTracer);
         Mockito.when(mTracer.tracingEnabled()).thenReturn(true);
 
@@ -112,8 +110,6 @@
 
     @Test
     public void testAtraceBinaryState2() {
-        mHistory.forceRecordAllHistory();
-
         InOrder inOrder = Mockito.inOrder(mTracer);
         Mockito.when(mTracer.tracingEnabled()).thenReturn(true);
 
@@ -131,8 +127,6 @@
 
     @Test
     public void testAtraceNumericalState() {
-        mHistory.forceRecordAllHistory();
-
         InOrder inOrder = Mockito.inOrder(mTracer);
         Mockito.when(mTracer.tracingEnabled()).thenReturn(true);
 
@@ -150,8 +144,6 @@
 
     @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 26590c4..a72f780 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -2097,7 +2097,10 @@
      * 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 @@
      * {@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 @@
 
     /**
      * 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>