Merge "Remove InputMethodManagerDelegate#startInput()"
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 761d9fa..bf02ff0 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9276,13 +9276,17 @@
   }
 
   public final class BugreportManager {
+    method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void preDumpUiData();
     method @RequiresPermission(android.Manifest.permission.DUMP) public void requestBugreport(@NonNull android.os.BugreportParams, @Nullable CharSequence, @Nullable CharSequence);
     method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void startBugreport(@NonNull android.os.ParcelFileDescriptor, @Nullable android.os.ParcelFileDescriptor, @NonNull android.os.BugreportParams, @NonNull java.util.concurrent.Executor, @NonNull android.os.BugreportManager.BugreportCallback);
   }
 
   public final class BugreportParams {
     ctor public BugreportParams(int);
+    ctor public BugreportParams(int, int);
+    method public int getFlags();
     method public int getMode();
+    field public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA = 1; // 0x1
     field public static final int BUGREPORT_MODE_FULL = 0; // 0x0
     field public static final int BUGREPORT_MODE_INTERACTIVE = 1; // 0x1
     field public static final int BUGREPORT_MODE_REMOTE = 2; // 0x2
diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java
index 73bb8d5..222e88f 100644
--- a/core/java/android/os/BugreportManager.java
+++ b/core/java/android/os/BugreportManager.java
@@ -148,6 +148,29 @@
     }
 
     /**
+     * Speculatively pre-dumps UI data for a bugreport request that might come later.
+     *
+     * <p>Triggers the dump of certain critical UI data, e.g. traces stored in short
+     * ring buffers that might get lost by the time the actual bugreport is requested.
+     *
+     * <p>{@link #startBugreport} will then pick the pre-dumped data if both of the following
+     * conditions are met:
+     * - {@link android.os.BugreportParams#BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA} is specified.
+     * - {@link #preDumpUiData} and {@link #startBugreport} were called by the same UID.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.DUMP)
+    @WorkerThread
+    public void preDumpUiData() {
+        try {
+            mBinder.preDumpUiData(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Starts a bugreport.
      *
      * <p>This starts a bugreport in the background. However the call itself can take several
@@ -198,6 +221,7 @@
                     bugreportFd.getFileDescriptor(),
                     screenshotFd.getFileDescriptor(),
                     params.getMode(),
+                    params.getFlags(),
                     dsListener,
                     isScreenshotRequested);
         } catch (RemoteException e) {
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index 279ccae..990883f 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -30,16 +30,46 @@
 @SystemApi
 public final class BugreportParams {
     private final int mMode;
+    private final int mFlags;
 
+    /**
+     * Constructs a BugreportParams object to specify what kind of bugreport should be taken.
+     *
+     * @param mode of the bugreport to request
+     */
     public BugreportParams(@BugreportMode int mode) {
         mMode = mode;
+        mFlags = 0;
     }
 
+    /**
+     * Constructs a BugreportParams object to specify what kind of bugreport should be taken.
+     *
+     * @param mode of the bugreport to request
+     * @param flags to customize the bugreport request
+     */
+    public BugreportParams(@BugreportMode int mode, @BugreportFlag int flags) {
+        mMode = mode;
+        mFlags = flags;
+    }
+
+    /**
+     * Returns the mode of the bugreport to request.
+     */
+    @BugreportMode
     public int getMode() {
         return mMode;
     }
 
     /**
+     * Returns the flags to customize the bugreport request.
+     */
+    @BugreportFlag
+    public int getFlags() {
+        return mFlags;
+    }
+
+    /**
      * Defines acceptable types of bugreports.
      * @hide
      */
@@ -88,4 +118,21 @@
      * Wifi.
      */
     public static final int BUGREPORT_MODE_WIFI = IDumpstate.BUGREPORT_MODE_WIFI;
+
+    /**
+     * Defines acceptable flags for customizing bugreport requests.
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = { "BUGREPORT_FLAG_" }, value = {
+            BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA
+    })
+    public @interface BugreportFlag {}
+
+    /**
+     * Flag for reusing pre-dumped UI data. The pre-dump and bugreport request calls must be
+     * performed by the same UID, otherwise the flag is ignored.
+     */
+    public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA =
+            IDumpstate.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA;
 }
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
new file mode 100644
index 0000000..57b9cb1
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2022 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 android.hardware.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Test;
+
+public final class ProgramSelectorTest {
+
+    private static final int FM_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_FM;
+    private static final int DAB_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_DAB;
+    private static final long FM_FREQUENCY = 88500;
+    private static final long AM_FREQUENCY = 700;
+    private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_1 =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x1000011);
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_2 =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000112);
+    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1001);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 94500);
+
+    @Test
+    public void getType_forIdentifier() {
+        assertWithMessage("Identifier type").that(FM_IDENTIFIER.getType())
+                .isEqualTo(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+    }
+
+    @Test
+    public void isCategoryType_withCategoryTypeForIdentifier() {
+        int typeChecked = ProgramSelector.IDENTIFIER_TYPE_VENDOR_START + 1;
+        ProgramSelector.Identifier fmIdentifier = new ProgramSelector.Identifier(
+                typeChecked, /* value= */ 99901);
+
+        assertWithMessage("Whether %s is a category identifier type", typeChecked)
+                .that(fmIdentifier.isCategoryType()).isTrue();
+    }
+
+    @Test
+    public void isCategoryType_withNonCategoryTypeForIdentifier() {
+        assertWithMessage("Is AMFM_FREQUENCY category identifier type")
+                .that(FM_IDENTIFIER.isCategoryType()).isFalse();
+    }
+
+    @Test
+    public void getValue_forIdentifier() {
+        assertWithMessage("Identifier value")
+                .that(FM_IDENTIFIER.getValue()).isEqualTo(FM_FREQUENCY);
+    }
+
+    @Test
+    public void equals_withDifferentTypesForIdentifiers_returnsFalse() {
+        assertWithMessage("Identifier with different identifier type")
+                .that(FM_IDENTIFIER).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1);
+    }
+
+    @Test
+    public void equals_withDifferentValuesForIdentifiers_returnsFalse() {
+        assertWithMessage("Identifier with different identifier value")
+                .that(DAB_SID_EXT_IDENTIFIER_2).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1);
+    }
+
+    @Test
+    public void equals_withSameKeyAndValueForIdentifiers_returnsTrue() {
+        ProgramSelector.Identifier fmIdentifierSame = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
+
+        assertWithMessage("Identifier of the same identifier")
+                .that(FM_IDENTIFIER).isEqualTo(fmIdentifierSame);
+    }
+
+    @Test
+    public void getProgramType() {
+        ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+        int programType = selector.getProgramType();
+
+        assertWithMessage("Program type").that(programType).isEqualTo(FM_PROGRAM_TYPE);
+    }
+
+    @Test
+    public void getPrimaryId() {
+        ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+        ProgramSelector.Identifier programId = selector.getPrimaryId();
+
+        assertWithMessage("Program Id").that(programId).isEqualTo(FM_IDENTIFIER);
+    }
+
+    @Test
+    public void getSecondaryIds_withEmptySecondaryIds() {
+        ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+        ProgramSelector.Identifier[] secondaryIds = selector.getSecondaryIds();
+
+        assertWithMessage("Secondary ids of selector initialized with empty secondary ids")
+                .that(secondaryIds).isEmpty();
+    }
+
+    @Test
+    public void getSecondaryIds_withNonEmptySecondaryIds() {
+        ProgramSelector.Identifier[] secondaryIdsExpected = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector selector = getDabSelector(secondaryIdsExpected, /* vendorIds= */ null);
+
+        ProgramSelector.Identifier[] secondaryIds = selector.getSecondaryIds();
+
+        assertWithMessage("Secondary identifier got")
+                .that(secondaryIds).isEqualTo(secondaryIdsExpected);
+    }
+
+    @Test
+    public void getFirstId_withIdInSelector() {
+        ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+        long firstIdValue = selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
+
+        assertWithMessage("Value of the first DAB_SID_EXT identifier")
+                .that(firstIdValue).isEqualTo(DAB_SID_EXT_IDENTIFIER_1.getValue());
+    }
+
+    @Test
+    public void getFirstId_withIdNotInSelector() {
+        ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2};
+        ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+        int idType = ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY;
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            selector.getFirstId(idType);
+        });
+
+        assertWithMessage("Exception for getting first identifier %s", idType)
+                .that(thrown).hasMessageThat().contains("Identifier " + idType + " not found");
+    }
+
+    @Test
+    public void getAllIds_withIdInSelector() {
+        ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector.Identifier[] allIdsExpected =
+                {DAB_SID_EXT_IDENTIFIER_1, DAB_SID_EXT_IDENTIFIER_2};
+        ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+        ProgramSelector.Identifier[] allIds =
+                selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
+
+        assertWithMessage("All DAB_SID_EXT identifiers in selector")
+                .that(allIds).isEqualTo(allIdsExpected);
+    }
+
+    @Test
+    public void getAllIds_withIdNotInSelector() {
+        ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
+
+        ProgramSelector.Identifier[] allIds =
+                selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+
+        assertWithMessage("AMFM frequency identifiers found in selector")
+                .that(allIds).isEmpty();
+    }
+
+    @Test
+    public void getVendorIds_withEmptyVendorIds() {
+        ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+
+        long[] vendorIds = selector.getVendorIds();
+
+        assertWithMessage("Vendor Ids of selector initialized with empty vendor ids")
+                .that(vendorIds).isEmpty();
+    }
+
+    @Test
+    public void getVendorIds_withNonEmptyVendorIds() {
+        long[] vendorIdsExpected = {12345, 678};
+        ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, vendorIdsExpected);
+
+        long[] vendorIds = selector.getVendorIds();
+
+        assertWithMessage("Vendor Ids of selector initialized with non-empty vendor ids")
+                .that(vendorIds).isEqualTo(vendorIdsExpected);
+    }
+
+    @Test
+    public void withSecondaryPreferred() {
+        ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+        long[] vendorIdsExpected = {12345, 678};
+        ProgramSelector selector = getDabSelector(secondaryIds, vendorIdsExpected);
+        ProgramSelector.Identifier[] secondaryIdsExpected = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_1};
+
+        ProgramSelector selectorPreferred =
+                selector.withSecondaryPreferred(DAB_SID_EXT_IDENTIFIER_1);
+
+        assertWithMessage("Program type")
+                .that(selectorPreferred.getProgramType()).isEqualTo(selector.getProgramType());
+        assertWithMessage("Primary identifiers")
+                .that(selectorPreferred.getPrimaryId()).isEqualTo(selector.getPrimaryId());
+        assertWithMessage("Secondary identifiers")
+                .that(selectorPreferred.getSecondaryIds()).isEqualTo(secondaryIdsExpected);
+        assertWithMessage("Vendor Ids")
+                .that(selectorPreferred.getVendorIds()).isEqualTo(vendorIdsExpected);
+    }
+
+    @Test
+    public void createAmFmSelector_withValidFrequencyWithoutSubChannel() {
+        int band = RadioManager.BAND_AM;
+        ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, AM_FREQUENCY);
+
+        ProgramSelector selector = ProgramSelector.createAmFmSelector(band, (int) AM_FREQUENCY);
+
+        assertWithMessage("Program type")
+                .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM);
+        assertWithMessage("Primary identifiers")
+                .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+        assertWithMessage("Secondary identifiers")
+                .that(selector.getSecondaryIds()).isEmpty();
+    }
+
+    @Test
+    public void createAmFmSelector_withoutBandAndSubChannel() {
+        ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
+
+        ProgramSelector selector = ProgramSelector.createAmFmSelector(
+                RadioManager.BAND_INVALID, (int) FM_FREQUENCY);
+
+        assertWithMessage("Program type")
+                .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_FM);
+        assertWithMessage("Primary identifiers")
+                .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+        assertWithMessage("Secondary identifiers")
+                .that(selector.getSecondaryIds()).isEmpty();
+    }
+
+    @Test
+    public void createAmFmSelector_withValidFrequencyAndSubChannel() {
+        int band = RadioManager.BAND_AM_HD;
+        int subChannel = 2;
+        ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, AM_FREQUENCY);
+        ProgramSelector.Identifier[] secondaryIdExpected = {
+                new ProgramSelector.Identifier(
+                        ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, subChannel - 1)
+        };
+
+        ProgramSelector selector = ProgramSelector.createAmFmSelector(band, (int) AM_FREQUENCY,
+                subChannel);
+
+        assertWithMessage("Program type")
+                .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM);
+        assertWithMessage("Primary identifiers")
+                .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+        assertWithMessage("Secondary identifiers")
+                .that(selector.getSecondaryIds()).isEqualTo(secondaryIdExpected);
+    }
+
+    @Test
+    public void createAmFmSelector_withInvalidFrequency_throwsIllegalArgumentException() {
+        int invalidFrequency = 50000;
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            ProgramSelector.createAmFmSelector(RadioManager.BAND_AM, invalidFrequency);
+        });
+
+        assertWithMessage("Exception for using invalid frequency %s", invalidFrequency)
+                .that(thrown).hasMessageThat().contains(
+                "Provided value is not a valid AM/FM frequency: " + invalidFrequency);
+    }
+
+    @Test
+    public void createAmFmSelector_withInvalidSubChannel_throwsIllegalArgumentException() {
+        int invalidSubChannel = 9;
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            ProgramSelector.createAmFmSelector(RadioManager.BAND_FM, (int) FM_FREQUENCY,
+                    invalidSubChannel);
+        });
+
+        assertWithMessage("Exception for using invalid subchannel %s", invalidSubChannel)
+                .that(thrown).hasMessageThat().contains(
+                "Invalid subchannel: " + invalidSubChannel);
+    }
+
+    @Test
+    public void createAmFmSelector_withSubChannelNotSupported_throwsIllegalArgumentException() {
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            ProgramSelector.createAmFmSelector(RadioManager.BAND_FM, (int) FM_FREQUENCY,
+                    /* subChannel= */ 1);
+        });
+
+        assertWithMessage("Exception for using sub-channel on radio not supporting it")
+                .that(thrown)
+                .hasMessageThat().contains("Subchannels are not supported for non-HD radio");
+    }
+
+    @Test
+    public void equals_withDifferentSecondaryIds_returnTrue() {
+        ProgramSelector.Identifier[] secondaryIds1 = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector selector1 = getDabSelector(secondaryIds1, /* vendorIds= */ null);
+        ProgramSelector selector2 = getDabSelector(
+                /* secondaryIds= */ null, /* vendorIds= */ null);
+
+        assertWithMessage("Selector with different secondary id")
+                .that(selector1).isEqualTo(selector2);
+    }
+
+    @Test
+    public void equals_withDifferentPrimaryIds_returnFalse() {
+        ProgramSelector selector1 = getFmSelector(
+                /* secondaryIds= */ null, /* vendorIds= */ null);
+        ProgramSelector.Identifier fmIdentifier2 = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 88700);
+        ProgramSelector selector2 = new ProgramSelector(FM_PROGRAM_TYPE, fmIdentifier2,
+                /* secondaryIds= */ null, /* vendorIds= */ null);
+
+        assertWithMessage("Selector with different primary id")
+                .that(selector1).isNotEqualTo(selector2);
+    }
+
+    @Test
+    public void strictEquals_withDifferentPrimaryIds_returnsFalse() {
+        ProgramSelector selector1 = getFmSelector(
+                /* secondaryIds= */ null, /* vendorIds= */ null);
+        ProgramSelector.Identifier fmIdentifier2 = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 88700);
+        ProgramSelector selector2 = new ProgramSelector(FM_PROGRAM_TYPE, fmIdentifier2,
+                /* secondaryIds= */ null, /* vendorIds= */ null);
+
+        assertWithMessage(
+                "Whether two selectors with different primary ids are strictly equal")
+                .that(selector1.strictEquals(selector2)).isFalse();
+    }
+
+    @Test
+    public void strictEquals_withDifferentSecondaryIds_returnsFalse() {
+        ProgramSelector.Identifier[] secondaryIds1 = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector selector1 = getDabSelector(secondaryIds1, /* vendorIds= */ null);
+        ProgramSelector.Identifier[] secondaryIds2 = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER};
+        ProgramSelector selector2 = getDabSelector(secondaryIds2, /* vendorIds= */ null);
+
+        assertWithMessage(
+                "Whether two selectors with different secondary ids are strictly equal")
+                .that(selector1.strictEquals(selector2)).isFalse();
+    }
+
+    @Test
+    public void strictEquals_withDifferentSecondaryIdsOrders_returnsTrue() {
+        ProgramSelector.Identifier[] secondaryIds1 = new ProgramSelector.Identifier[]{
+                DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER};
+        ProgramSelector selector1 = getDabSelector(secondaryIds1, /* vendorIds= */ null);
+        ProgramSelector.Identifier[] secondaryIds2 = new ProgramSelector.Identifier[]{
+                DAB_FREQUENCY_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER};
+        ProgramSelector selector2 = getDabSelector(secondaryIds2, /* vendorIds= */ null);
+
+        assertWithMessage(
+                "Whether two selectors with different secondary id orders are strictly equal")
+                .that(selector1.strictEquals(selector2)).isTrue();
+    }
+
+    private ProgramSelector getFmSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
+            @Nullable long[] vendorIds) {
+        return new ProgramSelector(FM_PROGRAM_TYPE, FM_IDENTIFIER, secondaryIds, vendorIds);
+    }
+
+    private ProgramSelector getDabSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
+            @Nullable long[] vendorIds) {
+        return new ProgramSelector(DAB_PROGRAM_TYPE, DAB_SID_EXT_IDENTIFIER_1, secondaryIds,
+                vendorIds);
+    }
+}
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index daae957..180a312 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -36,7 +36,6 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.StrictMode;
-import android.text.TextUtils;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -48,6 +47,9 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -57,11 +59,22 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
 
 /**
  * Tests for BugreportManager API.
@@ -74,6 +87,7 @@
     private static final String TAG = "BugreportManagerTest";
     private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
     private static final long DUMPSTATE_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
+    private static final long DUMPSTATE_TEARDOWN_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
     private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
 
 
@@ -89,6 +103,18 @@
     private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
     private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
 
+    private static final Path[] UI_TRACES_PREDUMPED = {
+            Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
+            Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
+            Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
+            Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
+            Paths.get("/data/misc/wmtrace/layers_trace.winscope"),
+            Paths.get("/data/misc/wmtrace/transactions_trace.winscope"),
+    };
+    private static final Path[] UI_TRACES_GENERATED_DURING_BUGREPORT = {
+            Paths.get("/data/misc/wmtrace/layers_trace_from_transactions.winscope"),
+    };
+
     private Handler mHandler;
     private Executor mExecutor;
     private BugreportManager mBrm;
@@ -124,7 +150,6 @@
         FileUtils.closeQuietly(mScreenshotFd);
     }
 
-
     @Test
     public void normalFlow_wifi() throws Exception {
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
@@ -175,6 +200,66 @@
         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
     }
 
+    @LargeTest
+    @Test
+    public void preDumpUiData_then_fullWithUsePreDumpFlag() throws Exception {
+        startPreDumpedUiTraces();
+
+        mBrm.preDumpUiData();
+        waitTillDumpstateExitedOrTimeout();
+        List<File> expectedPreDumpedTraceFiles = copyFilesAsRoot(UI_TRACES_PREDUMPED);
+
+        BugreportCallbackImpl callback = new BugreportCallbackImpl();
+        mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
+                callback);
+        shareConsentDialog(ConsentReply.ALLOW);
+        waitTillDoneOrTimeout(callback);
+
+        stopPreDumpedUiTraces();
+
+        assertThat(callback.isDone()).isTrue();
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
+        assertFdsAreClosed(mBugreportFd);
+
+        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
+
+        List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+        assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
+    }
+
+    @LargeTest
+    @Test
+    public void preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump() throws Exception {
+        startPreDumpedUiTraces();
+
+        // Simulate pre-dump, instead of taking a real one.
+        // In some corner cases, data dumped as part of the full bugreport could be the same as the
+        // pre-dumped data and this test would fail. Hence, here we create fake/artificial
+        // pre-dumped data that we know it won't match with the full bugreport data.
+        createFilesWithFakeDataAsRoot(UI_TRACES_PREDUMPED, "system");
+
+        List<File> preDumpedTraceFiles = copyFilesAsRoot(UI_TRACES_PREDUMPED);
+
+        BugreportCallbackImpl callback = new BugreportCallbackImpl();
+        mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
+                callback);
+        shareConsentDialog(ConsentReply.ALLOW);
+        waitTillDoneOrTimeout(callback);
+
+        stopPreDumpedUiTraces();
+
+        assertThat(callback.isDone()).isTrue();
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
+        assertFdsAreClosed(mBugreportFd);
+
+        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
+
+        List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+        assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
+    }
+
     @Test
     public void simultaneousBugreportsNotAllowed() throws Exception {
         // Start bugreport #1
@@ -384,21 +469,151 @@
         }
         return bm;
     }
-
     private static File createTempFile(String prefix, String extension) throws Exception {
         final File f = File.createTempFile(prefix, extension);
         f.setReadable(true, true);
         f.setWritable(true, true);
-
         f.deleteOnExit();
         return f;
     }
 
+    private static void startPreDumpedUiTraces() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "cmd input_method tracing start"
+        );
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "cmd window tracing start"
+        );
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "service call SurfaceFlinger 1025 i32 1"
+        );
+    }
+
+    private static void stopPreDumpedUiTraces() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "cmd input_method tracing stop"
+        );
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "cmd window tracing stop"
+        );
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "service call SurfaceFlinger 1025 i32 0"
+        );
+    }
+
+    private void assertThatBugreportContainsFiles(Path[] paths)
+            throws IOException {
+        List<Path> entries = listZipArchiveEntries(mBugreportFile);
+        for (Path pathInDevice : paths) {
+            Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
+            assertThat(entries).contains(pathInArchive);
+        }
+    }
+
+    private List<File> extractFilesFromBugreport(Path[] paths) throws Exception {
+        List<File> files = new ArrayList<File>();
+        for (Path pathInDevice : paths) {
+            Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
+            files.add(extractZipArchiveEntry(mBugreportFile, pathInArchive));
+        }
+        return files;
+    }
+
+    private static List<Path> listZipArchiveEntries(File archive) throws IOException {
+        ArrayList<Path> entries = new ArrayList<>();
+
+        ZipInputStream stream = new ZipInputStream(
+                new BufferedInputStream(new FileInputStream(archive)));
+
+        for (ZipEntry entry = stream.getNextEntry(); entry != null; entry = stream.getNextEntry()) {
+            entries.add(Paths.get(entry.toString()));
+        }
+
+        return entries;
+    }
+
+    private static File extractZipArchiveEntry(File archive, Path entryToExtract)
+            throws Exception {
+        File extractedFile = createTempFile(entryToExtract.getFileName().toString(), ".extracted");
+
+        ZipInputStream is = new ZipInputStream(new FileInputStream(archive));
+        boolean hasFoundEntry = false;
+
+        for (ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry()) {
+            if (entry.toString().equals(entryToExtract.toString())) {
+                BufferedOutputStream os =
+                        new BufferedOutputStream(new FileOutputStream(extractedFile));
+                ByteStreams.copy(is, os);
+                os.close();
+                hasFoundEntry = true;
+                break;
+            }
+
+            ByteStreams.exhaust(is); // skip entry
+        }
+
+        is.closeEntry();
+        is.close();
+
+        assertThat(hasFoundEntry).isTrue();
+
+        return extractedFile;
+    }
+
+    private static void createFilesWithFakeDataAsRoot(Path[] paths, String owner) throws Exception {
+        File src = createTempFile("fake", ".data");
+        Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
+
+        for (Path path : paths) {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "install -m 611 -o " + owner + " -g " + owner
+                    + " " + src.getAbsolutePath() + " " + path.toString()
+            );
+        }
+    }
+
+    private static List<File> copyFilesAsRoot(Path[] paths) throws Exception {
+        ArrayList<File> files = new ArrayList<File>();
+        for (Path src : paths) {
+            File dst = createTempFile(src.getFileName().toString(), ".copy");
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "cp " + src.toString() + " " + dst.getAbsolutePath()
+            );
+            files.add(dst);
+        }
+        return files;
+    }
+
     private static ParcelFileDescriptor parcelFd(File file) throws Exception {
         return ParcelFileDescriptor.open(file,
                 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
     }
 
+    private static void assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected)
+            throws IOException {
+        if (actual.size() != expected.size()) {
+            fail("File lists have different size");
+        }
+        for (int i = 0; i < actual.size(); ++i) {
+            if (!Files.equal(actual.get(i), expected.get(i))) {
+                fail("Contents of " + actual.get(i).toString()
+                        + " != " + expected.get(i).toString());
+            }
+        }
+    }
+
+    private static void assertThatAllFileContentsAreDifferent(List<File> a, List<File> b)
+            throws IOException {
+        if (a.size() != b.size()) {
+            fail("File lists have different size");
+        }
+        for (int i = 0; i < a.size(); ++i) {
+            if (Files.equal(a.get(i), b.get(i))) {
+                fail("Contents of " + a.get(i).toString() + " == " + b.get(i).toString());
+            }
+        }
+    }
+
     private static void dropPermissions() {
         InstrumentationRegistry.getInstrumentation().getUiAutomation()
                 .dropShellPermissionIdentity();
@@ -410,21 +625,16 @@
     }
 
     private static boolean isDumpstateRunning() {
-        String[] output;
+        String output;
         try {
-            output =
-                    UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-                            .executeShellCommand("ps -A -o NAME | grep dumpstate")
-                            .trim()
-                            .split("\n");
+            output = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+                    .executeShellCommand("service list | grep dumpstate");
         } catch (IOException e) {
             Log.w(TAG, "Failed to check if dumpstate is running", e);
             return false;
         }
-        for (String line : output) {
-            // Check for an exact match since there may be other things that contain "dumpstate" as
-            // a substring (e.g. the dumpstate HAL).
-            if (TextUtils.equals("dumpstate", line)) {
+        for (String line : output.trim().split("\n")) {
+            if (line.matches("^.*\\s+dumpstate:\\s+\\[.*\\]$")) {
                 return true;
             }
         }
@@ -449,6 +659,17 @@
         return System.currentTimeMillis();
     }
 
+    private static void waitTillDumpstateExitedOrTimeout() throws Exception {
+        long startTimeMs = now();
+        while (isDumpstateRunning()) {
+            Thread.sleep(500 /* .5s */);
+            if (now() - startTimeMs >= DUMPSTATE_TEARDOWN_TIMEOUT_MS) {
+                break;
+            }
+            Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to exit");
+        }
+    }
+
     private static void waitTillDumpstateRunningOrTimeout() throws Exception {
         long startTimeMs = now();
         while (!isDumpstateRunning()) {
@@ -500,6 +721,16 @@
         return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL);
     }
 
+    /*
+     * Returns a {@link BugreportParams} for full bugreport that reuses pre-dumped data.
+     *
+     * <p> This can take on the order of minutes to finish
+     */
+    private static BugreportParams fullWithUsePreDumpFlag() {
+        return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL,
+                BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+    }
+
     /* Allow/deny the consent dialog to sharing bugreport data or check existence only. */
     private enum ConsentReply {
         ALLOW,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index fb0a9db..7e9c418 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -41,7 +41,7 @@
     // TODO(b/241126279) Introduce constants to better version functionality
     @Override
     public int getVendorApiLevel() {
-        return 1;
+        return 2;
     }
 
     /**
diff --git a/packages/SettingsLib/Spa/TEST_MAPPING b/packages/SettingsLib/Spa/TEST_MAPPING
index b4b65d4..b7ce518 100644
--- a/packages/SettingsLib/Spa/TEST_MAPPING
+++ b/packages/SettingsLib/Spa/TEST_MAPPING
@@ -5,6 +5,9 @@
     },
     {
       "name": "SpaPrivilegedLibTests"
+    },
+    {
+      "name": "SettingsSpaUnitTests"
     }
   ]
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index aa457fe..d154dc1 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -16,11 +16,9 @@
 
 package com.android.settingslib.spa.gallery
 
-import android.os.Bundle
-import androidx.navigation.NamedNavArgument
-import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironment
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.home.HomePageProvider
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
@@ -49,19 +47,6 @@
     // Add your SPPs
 }
 
-fun createSettingsPage(
-    SppName: SettingsPageProviderEnum,
-    parameter: List<NamedNavArgument> = emptyList(),
-    arguments: Bundle? = null
-): SettingsPage {
-    return SettingsPage.create(
-        name = SppName.name,
-        displayName = SppName.displayName,
-        parameter = parameter,
-        arguments = arguments,
-    )
-}
-
 object GallerySpaEnvironment : SpaEnvironment() {
     override val pageProviderRepository = lazy {
         SettingsPageProviderRepository(
@@ -82,7 +67,7 @@
                 ActionButtonPageProvider,
             ),
             rootPages = listOf(
-                createSettingsPage(SettingsPageProviderEnum.HOME)
+                HomePageProvider.createSettingsPage(),
             )
         )
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
index 33cd5f1..e40775a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
@@ -22,11 +22,11 @@
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
-import com.android.settingslib.spa.gallery.createSettingsPage
 import com.android.settingslib.spa.gallery.page.ArgumentPageModel
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
@@ -40,9 +40,10 @@
 
 object HomePageProvider : SettingsPageProvider {
     override val name = SettingsPageProviderEnum.HOME.name
+    override val displayName = SettingsPageProviderEnum.HOME.displayName
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
-        val owner = createSettingsPage(SettingsPageProviderEnum.HOME)
         return listOf(
             PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
index 5031fb4..8207310 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
@@ -23,9 +23,9 @@
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.gallery.createSettingsPage
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
@@ -43,13 +43,13 @@
     }
 
     override val name = SettingsPageProviderEnum.ARGUMENT.name
-
+    override val displayName = SettingsPageProviderEnum.ARGUMENT.displayName
     override val parameter = ArgumentPageModel.parameter
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         if (!ArgumentPageModel.isValidArgument(arguments)) return emptyList()
 
-        val owner = createSettingsPage(SettingsPageProviderEnum.ARGUMENT, parameter, arguments)
+        val owner = createSettingsPage(arguments)
         val entryList = mutableListOf<SettingsEntry>()
         entryList.add(
             createEntry(owner, EntryEnum.STRING_PARAM)
@@ -86,7 +86,7 @@
         if (!ArgumentPageModel.isValidArgument(arguments)) return null
 
         return SettingsEntryBuilder.createInject(
-            owner = createSettingsPage(SettingsPageProviderEnum.ARGUMENT, parameter, arguments),
+            owner = createSettingsPage(arguments),
             displayName = "${name}_$stringParam",
         )
             // Set attributes
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt
index 0f99c57..165eaa0 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt
@@ -20,8 +20,8 @@
 import androidx.compose.runtime.Composable
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -31,22 +31,20 @@
 
 object PreferenceMainPageProvider : SettingsPageProvider {
     override val name = "PreferenceMain"
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         return listOf(
-            PreferencePageProvider.buildInjectEntry()
-                .setLink(fromPage = SettingsPage.create(name)).build(),
-            SwitchPreferencePageProvider.buildInjectEntry()
-                .setLink(fromPage = SettingsPage.create(name)).build(),
-            MainSwitchPreferencePageProvider.buildInjectEntry()
-                .setLink(fromPage = SettingsPage.create(name)).build(),
+            PreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            SwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            MainSwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             TwoTargetSwitchPreferencePageProvider.buildInjectEntry()
-                .setLink(fromPage = SettingsPage.create(name)).build(),
+                .setLink(fromPage = owner).build(),
         )
     }
 
     fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+        return SettingsEntryBuilder.createInject(owner = owner)
             .setIsAllowSearch(true)
             .setUiLayoutFn {
                 Preference(object : PreferenceModel {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
index f7f01ea..f19e9a3 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
@@ -30,11 +30,11 @@
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.toState
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.gallery.createSettingsPage
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY
@@ -66,7 +66,8 @@
     }
 
     override val name = SettingsPageProviderEnum.PREFERENCE.name
-    private val owner = createSettingsPage(SettingsPageProviderEnum.PREFERENCE)
+    override val displayName = SettingsPageProviderEnum.PREFERENCE.displayName
+    private val owner = createSettingsPage()
 
     private fun createEntry(entry: EntryEnum): SettingsEntryBuilder {
         return SettingsEntryBuilder.create(owner, entry.name, entry.displayName)
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 1d42e27..8b29366 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -29,6 +29,7 @@
         "androidx.compose.runtime_runtime",
         "androidx.compose.runtime_runtime-livedata",
         "androidx.compose.ui_ui-tooling-preview",
+        "androidx.lifecycle_lifecycle-livedata-ktx",
         "androidx.navigation_navigation-compose",
         "com.google.android.material_material",
         "lottie_compose",
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 362953f..7e05e75 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -59,13 +59,14 @@
 }
 
 dependencies {
-    api "androidx.appcompat:appcompat:1.6.0-rc01"
+    api "androidx.appcompat:appcompat:1.7.0-alpha01"
     api "androidx.compose.material3:material3:$jetpack_compose_material3_version"
     api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
     api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
     api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version"
-    api 'androidx.navigation:navigation-compose:2.5.0'
-    api 'com.google.android.material:material:1.6.1'
+    api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha02"
+    api "androidx.navigation:navigation-compose:2.5.0"
+    api "com.google.android.material:material:1.6.1"
     debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
-    implementation 'com.airbnb.android:lottie-compose:5.2.0'
+    implementation "com.airbnb.android:lottie-compose:5.2.0"
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
index ed4004f2..ea20233 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
@@ -55,7 +55,7 @@
             val entry = entryQueue.pop()
             val page = entry.toPage
             if (page == null || pageWithEntryMap.containsKey(page.id)) continue
-            val spp = sppRepository.getProviderOrNull(page.name) ?: continue
+            val spp = sppRepository.getProviderOrNull(page.sppName) ?: continue
             val newEntries = spp.buildEntry(page.arguments)
             pageWithEntryMap[page.id] = SettingsPageWithEntry(page, newEntries)
             for (newEntry in newEntries) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 2659c10..e7d8906 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -26,16 +26,16 @@
  * Defines data to identify a Settings page.
  */
 data class SettingsPage(
-    // The unique id of this page, which is computed by name + normalized(arguments)
+    // The unique id of this page, which is computed by sppName + normalized(arguments)
     val id: String,
 
-    // The name of the page, which is used to compute the unique id, and need to be stable.
-    val name: String,
+    // The name of the page provider, who creates this page. It is used to compute the unique id.
+    val sppName: String,
 
     // The display name of the page, for better readability.
     val displayName: String,
 
-    // Defined parameters of this page.
+    // The parameters defined in its page provider.
     val parameter: List<NamedNavArgument> = emptyList(),
 
     // The arguments of this page.
@@ -50,7 +50,7 @@
         ): SettingsPage {
             return SettingsPage(
                 id = id(name, parameter, arguments),
-                name = name,
+                sppName = name,
                 displayName = displayName ?: name,
                 parameter = parameter,
                 arguments = arguments
@@ -70,7 +70,7 @@
 
     // Returns if this Settings Page is created by the given Spp.
     fun isCreateBy(SppName: String): Boolean {
-        return name == SppName
+        return sppName == SppName
     }
 
     fun formatArguments(): String {
@@ -84,7 +84,7 @@
     }
 
     fun buildRoute(): String {
-        return name + parameter.navLink(arguments)
+        return sppName + parameter.navLink(arguments)
     }
 
     fun hasRuntimeParam(): Boolean {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 965c2b3..e8a4411 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -21,13 +21,17 @@
 import androidx.navigation.NamedNavArgument
 
 /**
- * An SettingsPageProvider represent a Settings page.
+ * An SettingsPageProvider which is used to create Settings page instances.
  */
 interface SettingsPageProvider {
 
-    /** The page name without arguments. */
+    /** The page provider name, needs to be *unique* and *stable*. */
     val name: String
 
+    /** The display name of this page provider, for better readability. */
+    val displayName: String?
+        get() = null
+
     /** The page parameters, default is no parameters. */
     val parameter: List<NamedNavArgument>
         get() = emptyList()
@@ -38,3 +42,12 @@
 
     fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
 }
+
+fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
+    return SettingsPage.create(
+        name = name,
+        displayName = displayName,
+        parameter = parameter,
+        arguments = arguments
+    )
+}
diff --git a/packages/SettingsLib/SpaPrivileged/TEST_MAPPING b/packages/SettingsLib/SpaPrivileged/TEST_MAPPING
new file mode 100644
index 0000000..ea16682
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "presubmit": [
+    {
+      "name": "SpaPrivilegedLibTests"
+    },
+    {
+      "name": "SettingsSpaUnitTests"
+    }
+  ]
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
index 93ba4f7..71cf23c 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
@@ -24,7 +24,7 @@
 import android.content.pm.ApplicationInfo
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.Transformations
+import androidx.lifecycle.map
 
 class AppOpsController(
     context: Context,
@@ -36,7 +36,7 @@
     val mode: LiveData<Int>
         get() = _mode
     val isAllowed: LiveData<Boolean>
-        get() = Transformations.map(_mode) { it == MODE_ALLOWED }
+        get() = _mode.map { it == MODE_ALLOWED }
 
     fun setAllowed(allowed: Boolean) {
         val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt
index 0615807..b1adc9d 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt
@@ -20,7 +20,7 @@
 import android.content.Context
 import android.os.UserHandle
 import android.os.UserManager
-import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
 import com.android.settingslib.RestrictedLockUtilsInternal
 import com.android.settingslib.spaprivileged.R
@@ -58,13 +58,8 @@
     private val userManager by lazy { UserManager.get(context) }
     private val enterpriseRepository by lazy { EnterpriseRepository(context) }
 
-    val restrictedMode = object : LiveData<RestrictedMode>() {
-        override fun onActive() {
-            postValue(getRestrictedMode())
-        }
-
-        override fun onInactive() {
-        }
+    val restrictedMode = liveData {
+        emit(getRestrictedMode())
     }
 
     private fun getRestrictedMode(): RestrictedMode {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
index 940a1fe..a1222a1 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
@@ -41,6 +41,6 @@
     ],
     kotlincflags: [
         "-Xjvm-default=all",
-        "-Xopt-in=kotlin.RequiresOptIn",
+        "-opt-in=kotlin.RequiresOptIn",
     ],
 }
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index 9476912..cb37c07 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -199,8 +199,8 @@
             }
             mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2));
             return null;
-        }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), any(),
-                anyBoolean());
+        }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
+                any(), anyBoolean());
 
         setWarningState(mContext, STATE_HIDE);
 
@@ -543,7 +543,7 @@
         getInstrumentation().waitForIdleSync();
 
         verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(),
-                anyInt(), any(), anyBoolean());
+                anyInt(), anyInt(), any(), anyBoolean());
         sendBugreportFinished();
     }
 
@@ -608,7 +608,7 @@
         ArgumentCaptor<IDumpstateListener> listenerCap = ArgumentCaptor.forClass(
                 IDumpstateListener.class);
         verify(mMockIDumpstate, timeout(TIMEOUT)).startBugreport(anyInt(), any(), any(), any(),
-                anyInt(), listenerCap.capture(), anyBoolean());
+                anyInt(), anyInt(), listenerCap.capture(), anyBoolean());
         mIDumpstateListener = listenerCap.getValue();
         assertNotNull("Dumpstate listener should not be null", mIDumpstateListener);
         mIDumpstateListener.onProgress(0);
diff --git a/services/core/java/com/android/server/attention/TEST_MAPPING b/services/core/java/com/android/server/attention/TEST_MAPPING
index 35b8165..e5b0344 100644
--- a/services/core/java/com/android/server/attention/TEST_MAPPING
+++ b/services/core/java/com/android/server/attention/TEST_MAPPING
@@ -7,7 +7,7 @@
           "include-filter": "android.voiceinteraction.cts.AlwaysOnHotwordDetectorTest"
         },
         {
-          "include-filter": "android.voiceinteraction.cts.HotwordDetectedResultTest"
+          "include-filter": "android.voiceinteraction.cts.unittests.HotwordDetectedResultTest"
         },
         {
           "include-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest"
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index 62f94ed..1a5f31c 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -30,7 +30,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 /** Probe for ambient light. */
 final class ALSProbe implements Probe {
@@ -47,12 +50,18 @@
 
     private boolean mEnabled = false;
     private boolean mDestroyed = false;
+    private boolean mDestroyRequested = false;
+    private boolean mDisableRequested = false;
+    private volatile NextConsumer mNextConsumer = null;
     private volatile float mLastAmbientLux = -1;
 
     private final SensorEventListener mLightSensorListener = new SensorEventListener() {
         @Override
         public void onSensorChanged(SensorEvent event) {
             mLastAmbientLux = event.values[0];
+            if (mNextConsumer != null) {
+                completeNextConsumer(mLastAmbientLux);
+            }
         }
 
         @Override
@@ -102,29 +111,84 @@
 
     @Override
     public synchronized void enable() {
-        if (!mDestroyed) {
+        if (!mDestroyed && !mDestroyRequested) {
+            mDisableRequested = false;
             enableLightSensorLoggingLocked();
         }
     }
 
     @Override
     public synchronized void disable() {
-        if (!mDestroyed) {
+        mDisableRequested = true;
+
+        // if a final consumer is set it will call destroy/disable on the next value if requested
+        if (!mDestroyed && mNextConsumer == null) {
             disableLightSensorLoggingLocked();
         }
     }
 
     @Override
     public synchronized void destroy() {
-        disable();
-        mDestroyed = true;
+        mDestroyRequested = true;
+
+        // if a final consumer is set it will call destroy/disable on the next value if requested
+        if (!mDestroyed && mNextConsumer == null) {
+            disable();
+            mDestroyed = true;
+        }
     }
 
     /** The most recent lux reading. */
-    public float getCurrentLux() {
+    public float getMostRecentLux() {
         return mLastAmbientLux;
     }
 
+    /**
+     * Register a listener for the next available ALS reading, which will be reported to the given
+     * consumer even if this probe is {@link #disable()}'ed or {@link #destroy()}'ed before a value
+     * is available.
+     *
+     * This method is intended to be used for event logs that occur when the screen may be
+     * off and sampling may have been {@link #disable()}'ed. In these cases, this method will turn
+     * on the sensor (if needed), fetch & report the first value, and then destroy or disable this
+     * probe (if needed).
+     *
+     * @param consumer consumer to notify when the data is available
+     * @param handler handler for notifying the consumer, or null
+     */
+    public synchronized void awaitNextLux(@NonNull Consumer<Float> consumer,
+            @Nullable Handler handler) {
+        final NextConsumer nextConsumer = new NextConsumer(consumer, handler);
+        final float current = mLastAmbientLux;
+        if (current > 0) {
+            nextConsumer.consume(current);
+        } else if (mDestroyed) {
+            nextConsumer.consume(-1f);
+        } else if (mNextConsumer != null) {
+            mNextConsumer.add(nextConsumer);
+        } else {
+            mNextConsumer = nextConsumer;
+            enableLightSensorLoggingLocked();
+        }
+    }
+
+    private synchronized void completeNextConsumer(float value) {
+        Slog.v(TAG, "Finishing next consumer");
+
+        final NextConsumer consumer = mNextConsumer;
+        mNextConsumer = null;
+
+        if (mDestroyRequested) {
+            destroy();
+        } else if (mDisableRequested) {
+            disable();
+        }
+
+        if (consumer != null) {
+            consumer.consume(value);
+        }
+    }
+
     private void enableLightSensorLoggingLocked() {
         if (!mEnabled) {
             mEnabled = true;
@@ -160,4 +224,30 @@
                 + mLightSensorListener.hashCode());
         disable();
     }
+
+    private static class NextConsumer {
+        @NonNull private final Consumer<Float> mConsumer;
+        @Nullable private final Handler mHandler;
+        @NonNull private final List<NextConsumer> mOthers = new ArrayList<>();
+
+        private NextConsumer(@NonNull Consumer<Float> consumer, @Nullable Handler handler) {
+            mConsumer = consumer;
+            mHandler = handler;
+        }
+
+        public void consume(float value) {
+            if (mHandler != null) {
+                mHandler.post(() -> mConsumer.accept(value));
+            } else {
+                mConsumer.accept(value);
+            }
+            for (NextConsumer c : mOthers) {
+                c.consume(value);
+            }
+        }
+
+        public void add(NextConsumer consumer) {
+            mOthers.add(consumer);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
index d6ca8a6..27a70c5 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
@@ -62,8 +62,7 @@
     /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
     public void authenticate(OperationContext operationContext,
             int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
-            int authState, boolean requireConfirmation,
-            int targetUserId, float ambientLightLux) {
+            int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) {
         FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED,
                 statsModality,
                 targetUserId,
@@ -80,6 +79,16 @@
                 operationContext.isAod);
     }
 
+    /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
+    public void authenticate(OperationContext operationContext,
+            int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
+            int authState, boolean requireConfirmation, int targetUserId, ALSProbe alsProbe) {
+        alsProbe.awaitNextLux((ambientLightLux) -> {
+            authenticate(operationContext, statsModality, statsAction, statsClient, isDebug,
+                    latency, authState, requireConfirmation, targetUserId, ambientLightLux);
+        }, null /* handler */);
+    }
+
     /** {@see FrameworkStatsLog.BIOMETRIC_ENROLLED}. */
     public void enroll(int statsModality, int statsAction, int statsClient,
             int targetUserId, long latency, boolean enrollSuccessful, float ambientLightLux) {
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
index 02b350e..55fe854 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
@@ -220,7 +220,7 @@
                     + ", RequireConfirmation: " + requireConfirmation
                     + ", State: " + authState
                     + ", Latency: " + latency
-                    + ", Lux: " + mALSProbe.getCurrentLux());
+                    + ", Lux: " + mALSProbe.getMostRecentLux());
         } else {
             Slog.v(TAG, "Authentication latency: " + latency);
         }
@@ -231,7 +231,7 @@
 
         mSink.authenticate(operationContext, mStatsModality, mStatsAction, mStatsClient,
                 Utils.isDebugEnabled(context, targetUserId),
-                latency, authState, requireConfirmation, targetUserId, mALSProbe.getCurrentLux());
+                latency, authState, requireConfirmation, targetUserId, mALSProbe);
     }
 
     /** Log enrollment outcome. */
@@ -245,7 +245,7 @@
                     + ", User: " + targetUserId
                     + ", Client: " + mStatsClient
                     + ", Latency: " + latency
-                    + ", Lux: " + mALSProbe.getCurrentLux()
+                    + ", Lux: " + mALSProbe.getMostRecentLux()
                     + ", Success: " + enrollSuccessful);
         } else {
             Slog.v(TAG, "Enroll latency: " + latency);
@@ -256,7 +256,7 @@
         }
 
         mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
-                targetUserId, latency, enrollSuccessful, mALSProbe.getCurrentLux());
+                targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux());
     }
 
     /** Report unexpected enrollment reported by the HAL. */
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index e0955b7..5e6a025 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -333,6 +333,9 @@
                 mALSProbeCallback.getProbe().disable();
             }
         });
+        if (getBiometricContext().isAwake()) {
+            mALSProbeCallback.getProbe().enable();
+        }
 
         if (session.hasContextMethods()) {
             return session.getSession().authenticateWithContext(mOperationId, opContext);
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 346f311..58428ca 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -43,6 +43,7 @@
 
 import java.io.FileDescriptor;
 import java.util.Objects;
+import java.util.OptionalInt;
 
 /**
  * Implementation of the service that provides a privileged API to capture and consume bugreports.
@@ -60,6 +61,9 @@
     private final TelephonyManager mTelephonyManager;
     private final ArraySet<String> mBugreportWhitelistedPackages;
 
+    @GuardedBy("mLock")
+    private OptionalInt mPreDumpedDataUid = OptionalInt.empty();
+
     BugreportManagerServiceImpl(Context context) {
         mContext = context;
         mAppOps = context.getSystemService(AppOpsManager.class);
@@ -70,13 +74,25 @@
 
     @Override
     @RequiresPermission(android.Manifest.permission.DUMP)
+    public void preDumpUiData(String callingPackage) {
+        enforcePermission(callingPackage, Binder.getCallingUid(), true);
+
+        synchronized (mLock) {
+            preDumpUiDataLocked(callingPackage);
+        }
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.DUMP)
     public void startBugreport(int callingUidUnused, String callingPackage,
             FileDescriptor bugreportFd, FileDescriptor screenshotFd,
-            int bugreportMode, IDumpstateListener listener, boolean isScreenshotRequested) {
+            int bugreportMode, int bugreportFlags, IDumpstateListener listener,
+            boolean isScreenshotRequested) {
         Objects.requireNonNull(callingPackage);
         Objects.requireNonNull(bugreportFd);
         Objects.requireNonNull(listener);
         validateBugreportMode(bugreportMode);
+        validateBugreportFlags(bugreportFlags);
 
         int callingUid = Binder.getCallingUid();
         enforcePermission(callingPackage, callingUid, bugreportMode
@@ -90,7 +106,7 @@
 
         synchronized (mLock) {
             startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd,
-                    bugreportMode, listener, isScreenshotRequested);
+                    bugreportMode, bugreportFlags, listener, isScreenshotRequested);
         }
     }
 
@@ -108,17 +124,14 @@
             }
             try {
                 // Note: this may throw SecurityException back out to the caller if they aren't
-                // allowed to cancel the report, in which case we should NOT be setting ctl.stop,
-                // since that would unintentionally kill some other app's bugreport, which we
-                // specifically disallow.
+                // allowed to cancel the report, in which case we should NOT stop the dumpstate
+                // service, since that would unintentionally kill some other app's bugreport, which
+                // we specifically disallow.
                 ds.cancelBugreport(callingUid, callingPackage);
             } catch (RemoteException e) {
                 Slog.e(TAG, "RemoteException in cancelBugreport", e);
             }
-            // This tells init to cancel bugreportd service. Note that this is achieved through
-            // setting a system property which is not thread-safe. So the lock here offers
-            // thread-safety only among callers of the API.
-            SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
+            stopDumpstateBinderServiceLocked();
         }
     }
 
@@ -134,6 +147,14 @@
         }
     }
 
+    private void validateBugreportFlags(int flags) {
+        flags = clearBugreportFlag(flags, BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+        if (flags != 0) {
+            Slog.w(TAG, "Unknown bugreport flags: " + flags);
+            throw new IllegalArgumentException("Unknown bugreport flags: " + flags);
+        }
+    }
+
     private void enforcePermission(
             String callingPackage, int callingUid, boolean checkCarrierPrivileges) {
         mAppOps.checkPackage(callingUid, callingPackage);
@@ -224,17 +245,62 @@
     }
 
     @GuardedBy("mLock")
+    private void preDumpUiDataLocked(String callingPackage) {
+        mPreDumpedDataUid = OptionalInt.empty();
+
+        if (isDumpstateBinderServiceRunningLocked()) {
+            Slog.e(TAG, "'dumpstate' is already running. "
+                    + "Cannot pre-dump data while another operation is currently in progress.");
+            return;
+        }
+
+        IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
+        if (ds == null) {
+            Slog.e(TAG, "Unable to get bugreport service");
+            return;
+        }
+
+        try {
+            ds.preDumpUiData(callingPackage);
+        } catch (RemoteException e) {
+            return;
+        } finally {
+            // dumpstate service is already started now. We need to kill it to manage the
+            // lifecycle correctly. If we don't subsequent callers will get
+            // BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error.
+            stopDumpstateBinderServiceLocked();
+        }
+
+
+        mPreDumpedDataUid = OptionalInt.of(Binder.getCallingUid());
+    }
+
+    @GuardedBy("mLock")
     private void startBugreportLocked(int callingUid, String callingPackage,
             FileDescriptor bugreportFd, FileDescriptor screenshotFd,
-            int bugreportMode, IDumpstateListener listener, boolean isScreenshotRequested) {
+            int bugreportMode, int bugreportFlags, IDumpstateListener listener,
+            boolean isScreenshotRequested) {
         if (isDumpstateBinderServiceRunningLocked()) {
             Slog.w(TAG, "'dumpstate' is already running. Cannot start a new bugreport"
-                    + " while another one is currently in progress.");
-            reportError(listener,
-                    IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
+                    + " while another operation is currently in progress.");
+            reportError(listener, IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
             return;
         }
 
+        if ((bugreportFlags & BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA) != 0) {
+            if (mPreDumpedDataUid.isEmpty()) {
+                bugreportFlags = clearBugreportFlag(bugreportFlags,
+                        BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+                Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA."
+                        + " No pre-dumped data is available.");
+            } else if (mPreDumpedDataUid.getAsInt() != callingUid) {
+                bugreportFlags = clearBugreportFlag(bugreportFlags,
+                        BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
+                Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA."
+                        + " Data was pre-dumped by a different UID.");
+            }
+        }
+
         IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
         if (ds == null) {
             Slog.w(TAG, "Unable to get bugreport service");
@@ -245,10 +311,10 @@
         // Wrap the listener so we can intercept binder events directly.
         IDumpstateListener myListener = new DumpstateListener(listener, ds);
         try {
-            ds.startBugreport(callingUid, callingPackage,
-                    bugreportFd, screenshotFd, bugreportMode, myListener, isScreenshotRequested);
+            ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode,
+                    bugreportFlags, myListener, isScreenshotRequested);
         } catch (RemoteException e) {
-            // bugreportd service is already started now. We need to kill it to manage the
+            // dumpstate service is already started now. We need to kill it to manage the
             // lifecycle correctly. If we don't subsequent callers will get
             // BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error.
             // Note that listener will be notified by the death recipient below.
@@ -309,6 +375,19 @@
         return ds;
     }
 
+    @GuardedBy("mLock")
+    private void stopDumpstateBinderServiceLocked() {
+        // This tells init to cancel bugreportd service. Note that this is achieved through
+        // setting a system property which is not thread-safe. So the lock here offers
+        // thread-safety only among callers of the API.
+        SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
+    }
+
+    private int clearBugreportFlag(int flags, @BugreportParams.BugreportFlag int flag) {
+        flags &= ~flag;
+        return flags;
+    }
+
     private void reportError(IDumpstateListener listener, int errorCode) {
         try {
             listener.onError(errorCode);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index 10f0a5c..68c9ce4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -50,6 +51,9 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
 @Presubmit
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -93,7 +97,7 @@
         mSensorEventListenerCaptor.getValue().onSensorChanged(
                 new SensorEvent(mLightSensor, 1, 2, new float[]{value}));
 
-        assertThat(mProbe.getCurrentLux()).isEqualTo(value);
+        assertThat(mProbe.getMostRecentLux()).isEqualTo(value);
     }
 
     @Test
@@ -121,13 +125,17 @@
         mProbe.destroy();
         mProbe.enable();
 
+        AtomicInteger lux = new AtomicInteger(10);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
         verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
         verifyNoMoreInteractions(mSensorManager);
+        assertThat(lux.get()).isLessThan(0);
     }
 
     @Test
     public void testDisabledReportsNegativeValue() {
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
 
         mProbe.enable();
         verify(mSensorManager).registerListener(
@@ -136,7 +144,7 @@
                 new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
         mProbe.disable();
 
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
     }
 
     @Test
@@ -150,7 +158,7 @@
 
         verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
         verifyNoMoreInteractions(mSensorManager);
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
     }
 
     @Test
@@ -166,7 +174,148 @@
 
         verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
         verifyNoMoreInteractions(mSensorManager);
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
+    }
+
+    @Test
+    public void testNextLuxWhenAlreadyEnabledAndNotAvailable() {
+        testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */);
+    }
+
+    @Test
+    public void testNextLuxWhenAlreadyEnabledAndAvailable() {
+        testNextLuxWhenAlreadyEnabled(true /* dataIsAvailable */);
+    }
+
+    private void testNextLuxWhenAlreadyEnabled(boolean dataIsAvailable) {
+        final List<Integer> values = List.of(1, 2, 3, 4, 6);
+        mProbe.enable();
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        if (dataIsAvailable) {
+            for (int v : values) {
+                mSensorEventListenerCaptor.getValue().onSensorChanged(
+                        new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+            }
+        }
+        AtomicInteger lux = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+        if (!dataIsAvailable) {
+            for (int v : values) {
+                mSensorEventListenerCaptor.getValue().onSensorChanged(
+                        new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+            }
+        }
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{200f}));
+
+        // should remain enabled
+        assertThat(lux.get()).isEqualTo(values.get(dataIsAvailable ? values.size() - 1 : 0));
+        verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+
+        final int anotherValue = 12;
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{12}));
+        assertThat(mProbe.getMostRecentLux()).isEqualTo(anotherValue);
+    }
+
+    @Test
+    public void testNextLuxWhenNotEnabled() {
+        testNextLuxWhenNotEnabled(false /* enableWhileWaiting */);
+    }
+
+    @Test
+    public void testNextLuxWhenNotEnabledButEnabledLater() {
+        testNextLuxWhenNotEnabled(true /* enableWhileWaiting */);
+    }
+
+    private void testNextLuxWhenNotEnabled(boolean enableWhileWaiting) {
+        final List<Integer> values = List.of(1, 2, 3, 4, 6);
+        mProbe.disable();
+
+        AtomicInteger lux = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        if (enableWhileWaiting) {
+            mProbe.enable();
+        }
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        for (int v : values) {
+            mSensorEventListenerCaptor.getValue().onSensorChanged(
+                    new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+        }
+
+        // should restore the disabled state
+        assertThat(lux.get()).isEqualTo(values.get(0));
+        verify(mSensorManager, enableWhileWaiting ? never() : times(1)).unregisterListener(
+                any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testNextLuxIsNotCanceledByDisableOrDestroy() {
+        final int value = 7;
+        AtomicInteger lux = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        mProbe.destroy();
+        mProbe.disable();
+
+        assertThat(lux.get()).isEqualTo(-1);
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{value}));
+
+        assertThat(lux.get()).isEqualTo(value);
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{value + 1}));
+
+        // should remain destroyed
+        mProbe.enable();
+
+        assertThat(lux.get()).isEqualTo(value);
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testMultipleNextConsumers() {
+        final int value = 7;
+        AtomicInteger lux = new AtomicInteger(-1);
+        AtomicInteger lux2 = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+        mProbe.awaitNextLux((v) -> lux2.set(Math.round(v)), null /* handler */);
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{value}));
+
+        assertThat(lux.get()).isEqualTo(value);
+        assertThat(lux2.get()).isEqualTo(value);
+    }
+
+    @Test
+    public void testNoNextLuxWhenDestroyed() {
+        mProbe.destroy();
+
+        AtomicInteger lux = new AtomicInteger(-20);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        assertThat(lux.get()).isEqualTo(-1);
+        verify(mSensorManager, never()).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        verifyNoMoreInteractions(mSensorManager);
     }
 
     private void moveTimeBy(long millis) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
index 60dc2eb..88a9646 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
@@ -121,7 +121,7 @@
         verify(mSink).authenticate(eq(mOpContext),
                 eq(DEFAULT_MODALITY), eq(DEFAULT_ACTION), eq(DEFAULT_CLIENT), anyBoolean(),
                 anyLong(), anyInt(), eq(requireConfirmation),
-                eq(targetUserId), anyFloat());
+                eq(targetUserId), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index dea4d4f..a5c181d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -215,7 +216,7 @@
 
     @Test
     public void luxProbeWhenAwake() throws RemoteException {
-        when(mBiometricContext.isAwake()).thenReturn(false, true, false);
+        when(mBiometricContext.isAwake()).thenReturn(false);
         when(mBiometricContext.isAod()).thenReturn(false);
         final FingerprintAuthenticationClient client = createClient();
         client.start(mCallback);
@@ -228,15 +229,38 @@
         verify(mLuxProbe, never()).enable();
 
         reset(mLuxProbe);
+        when(mBiometricContext.isAwake()).thenReturn(true);
+
         mContextInjector.getValue().accept(opContext);
         verify(mLuxProbe).enable();
         verify(mLuxProbe, never()).disable();
 
+        when(mBiometricContext.isAwake()).thenReturn(false);
+
         mContextInjector.getValue().accept(opContext);
         verify(mLuxProbe).disable();
     }
 
     @Test
+    public void luxProbeEnabledOnStartWhenWake() throws RemoteException {
+        luxProbeEnabledOnStart(true /* isAwake */);
+    }
+
+    @Test
+    public void luxProbeNotEnabledOnStartWhenNotWake() throws RemoteException {
+        luxProbeEnabledOnStart(false /* isAwake */);
+    }
+
+    private void luxProbeEnabledOnStart(boolean isAwake) throws RemoteException {
+        when(mBiometricContext.isAwake()).thenReturn(isAwake);
+        when(mBiometricContext.isAod()).thenReturn(false);
+        final FingerprintAuthenticationClient client = createClient();
+        client.start(mCallback);
+
+        verify(mLuxProbe, isAwake ? times(1) : never()).enable();
+    }
+
+    @Test
     public void luxProbeDisabledOnAod() throws RemoteException {
         when(mBiometricContext.isAwake()).thenReturn(false);
         when(mBiometricContext.isAod()).thenReturn(true);