Merge "Apply package name size limit to packages" into main
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 554fe5e..b2838c8 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -2064,24 +2064,31 @@
         }
 
         /**
-         * Transfers to a given route for the remote session. The given route must be included in
-         * {@link RoutingSessionInfo#getTransferableRoutes()}.
+         * Attempts a transfer to a {@link RoutingSessionInfo#getTransferableRoutes() transferable
+         * route}.
          *
+         * <p>Transferring to a transferable route does not require the app to transfer the playback
+         * state from one route to the other. The route provider completely manages the transfer. An
+         * example of provider-managed transfers are the switches between the system's routes, like
+         * the built-in speakers and a BT headset.
+         *
+         * @return True if the transfer is handled by this controller, or false if a new controller
+         *     should be created instead.
          * @see RoutingSessionInfo#getSelectedRoutes()
          * @see RoutingSessionInfo#getTransferableRoutes()
          * @see ControllerCallback#onControllerUpdated
          */
-        void transferToRoute(@NonNull MediaRoute2Info route) {
+        boolean tryTransferWithinProvider(@NonNull MediaRoute2Info route) {
             Objects.requireNonNull(route, "route must not be null");
             synchronized (mControllerLock) {
                 if (isReleased()) {
                     Log.w(TAG, "transferToRoute: Called on released controller. Ignoring.");
-                    return;
+                    return true;
                 }
 
                 if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) {
                     Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route);
-                    return;
+                    return false;
                 }
             }
 
@@ -2096,6 +2103,7 @@
                     Log.e(TAG, "Unable to transfer to route for session.", ex);
                 }
             }
+            return true;
         }
 
         /**
@@ -3587,20 +3595,14 @@
             }
 
             RoutingController controller = getCurrentController();
-            if (controller
-                    .getRoutingSessionInfo()
-                    .getTransferableRoutes()
-                    .contains(route.getId())) {
-                controller.transferToRoute(route);
-                return;
+            if (!controller.tryTransferWithinProvider(route)) {
+                requestCreateController(
+                        controller,
+                        route,
+                        MANAGER_REQUEST_ID_NONE,
+                        Process.myUserHandle(),
+                        mContext.getPackageName());
             }
-
-            requestCreateController(
-                    controller,
-                    route,
-                    MANAGER_REQUEST_ID_NONE,
-                    Process.myUserHandle(),
-                    mContext.getPackageName());
         }
 
         @Override
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
index 00225fc..0f6d51d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
@@ -54,6 +54,13 @@
     override fun VolumePanelComposeScope.Content(modifier: Modifier) {
         val slice by viewModel.buttonSlice.collectAsState()
         val label = stringResource(R.string.volume_panel_noise_control_title)
+        val isClickable = viewModel.isClickable(slice)
+        val onClick =
+            if (isClickable) {
+                { ancPopup.show(null) }
+            } else {
+                null
+            }
         Column(
             modifier = modifier,
             verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -69,8 +76,9 @@
                         }
                         .clip(RoundedCornerShape(28.dp)),
                 slice = slice,
+                isEnabled = onClick != null,
                 onWidthChanged = viewModel::onButtonSliceWidthChanged,
-                onClick = { ancPopup.show(null) }
+                onClick = onClick,
             )
             Text(
                 modifier = Modifier.clearAndSetSemantics {},
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt
index 74af3ca..fc5d212 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt
@@ -32,6 +32,7 @@
 fun SliceAndroidView(
     slice: Slice?,
     modifier: Modifier = Modifier,
+    isEnabled: Boolean = true,
     onWidthChanged: ((Int) -> Unit)? = null,
     onClick: (() -> Unit)? = null,
 ) {
@@ -40,7 +41,6 @@
         factory = { context: Context ->
             ClickableSliceView(
                     ContextThemeWrapper(context, R.style.Widget_SliceView_VolumePanel),
-                    onClick,
                 )
                 .apply {
                     mode = SliceView.MODE_LARGE
@@ -50,12 +50,14 @@
                     if (onWidthChanged != null) {
                         addOnLayoutChangeListener(OnWidthChangedLayoutListener(onWidthChanged))
                     }
-                    if (onClick != null) {
-                        setOnClickListener { onClick() }
-                    }
                 }
         },
-        update = { sliceView: SliceView -> sliceView.slice = slice }
+        update = { sliceView: ClickableSliceView ->
+            sliceView.slice = slice
+            sliceView.onClick = onClick
+            sliceView.isEnabled = isEnabled
+            sliceView.isClickable = isEnabled
+        }
     )
 }
 
@@ -86,10 +88,9 @@
  * first.
  */
 @SuppressLint("ViewConstructor") // only used in this class
-private class ClickableSliceView(
-    context: Context,
-    private val onClick: (() -> Unit)?,
-) : SliceView(context) {
+private class ClickableSliceView(context: Context) : SliceView(context) {
+
+    var onClick: (() -> Unit)? = null
 
     init {
         if (onClick != null) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt
index 874c0a2..12debbc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
@@ -91,7 +92,8 @@
                         },
                     onClick = { onCheckedChange(!viewModel.isActive) },
                     shape = RoundedCornerShape(28.dp),
-                    colors = colors
+                    colors = colors,
+                    contentPadding = PaddingValues(0.dp)
                 ) {
                     Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon)
                 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt
index dc96139..dddf582 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt
@@ -61,6 +61,7 @@
                 AncSliceRepositoryImpl(
                     localMediaRepositoryFactory,
                     testScope.testScheduler,
+                    testScope.testScheduler,
                     sliceViewManager,
                 )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt b/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt
index 384acc4..dd79425 100644
--- a/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt
@@ -28,6 +28,9 @@
  * Returns updating [Slice] for a [sliceUri]. It's null when there is no slice available for the
  * provided Uri. This can change overtime because of external changes (like device being
  * connected/disconnected).
+ *
+ * The flow should be [kotlinx.coroutines.flow.flowOn] the main thread because [SliceViewManager]
+ * isn't thread-safe. An exception will be thrown otherwise.
  */
 fun SliceViewManager.sliceForUri(sliceUri: Uri): Flow<Slice?> =
     ConflatedCallbackFlow.conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index e980794..d0cebae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -169,6 +169,14 @@
                 }
             }
 
+            // On the final call to {@link #resetViewState}, the alpha is set back to 1f but
+            // ambientState.isExpansionChanging() is now false. This causes a flicker on the
+            // EmptyShadeView after the shade is collapsed. Make sure the empty shade view
+            // isn't visible unless the shade is expanded.
+            if (view instanceof EmptyShadeView && ambientState.getExpansionFraction() == 0f) {
+                viewState.setAlpha(0f);
+            }
+
             // For EmptyShadeView if on keyguard, we need to control the alpha to create
             // a nice transition when the user is dragging down the notification panel.
             if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt
index 8ce3b1f..3117abc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt
@@ -23,6 +23,7 @@
 import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.media.BluetoothMediaDevice
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.slice.sliceForUri
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import dagger.assisted.Assisted
@@ -57,6 +58,7 @@
 constructor(
     mediaRepositoryFactory: LocalMediaRepositoryFactory,
     @Background private val backgroundCoroutineContext: CoroutineContext,
+    @Main private val mainCoroutineContext: CoroutineContext,
     @Assisted private val sliceViewManager: SliceViewManager,
 ) : AncSliceRepository {
 
@@ -73,7 +75,7 @@
             .distinctUntilChanged()
             .flatMapLatest { sliceUri ->
                 sliceUri ?: return@flatMapLatest flowOf(null)
-                sliceViewManager.sliceForUri(sliceUri)
+                sliceViewManager.sliceForUri(sliceUri).flowOn(mainCoroutineContext)
             }
             .flowOn(backgroundCoroutineContext)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt
index bee79bb..c980eb4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.volume.panel.component.anc.ui.viewmodel
 
+import android.content.Intent
 import androidx.slice.Slice
+import androidx.slice.SliceItem
 import com.android.systemui.volume.panel.component.anc.domain.AncAvailabilityCriteria
 import com.android.systemui.volume.panel.component.anc.domain.interactor.AncSliceInteractor
 import com.android.systemui.volume.panel.component.anc.domain.model.AncSlices
@@ -59,6 +61,31 @@
             .map { it.buttonSlice }
             .stateIn(coroutineScope, SharingStarted.Eagerly, null)
 
+    fun isClickable(slice: Slice?): Boolean {
+        slice ?: return false
+        val slices = ArrayDeque<SliceItem>()
+        slices.addAll(slice.items)
+        while (slices.isNotEmpty()) {
+            val item: SliceItem = slices.removeFirst()
+            when (item.format) {
+                android.app.slice.SliceItem.FORMAT_ACTION -> {
+                    val itemActionIntent: Intent? = item.action?.intent
+                    if (itemActionIntent?.hasExtra(EXTRA_ANC_ENABLED) == true) {
+                        return itemActionIntent.getBooleanExtra(EXTRA_ANC_ENABLED, true)
+                    }
+                }
+                android.app.slice.SliceItem.FORMAT_SLICE -> {
+                    item.slice?.items?.let(slices::addAll)
+                }
+            }
+        }
+        return true
+    }
+
+    private companion object {
+        const val EXTRA_ANC_ENABLED = "EXTRA_ANC_ENABLED"
+    }
+
     /** Call this to update [popupSlice] width in a reaction to container size change. */
     fun onPopupSliceWidthChanged(width: Int) {
         interactor.onPopupSliceWidthChanged(width)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 82725d6..a6fb718 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -394,8 +394,20 @@
     }
 
     @Test
+    fun resetViewStates_shadeCollapsed_emptyShadeViewBecomesTransparent() {
+        ambientState.expansionFraction = 0f
+        stackScrollAlgorithm.initView(context)
+        hostView.removeAllViews()
+        hostView.addView(emptyShadeView)
+
+        stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
+
+        assertThat(emptyShadeView.viewState.alpha).isEqualTo(0f)
+    }
+
+    @Test
     fun resetViewStates_isOnKeyguard_emptyShadeViewBecomesOpaque() {
-        ambientState.setStatusBarState(StatusBarState.SHADE)
+        ambientState.setStatusBarState(StatusBarState.KEYGUARD)
         ambientState.fractionToShade = 0.25f
         stackScrollAlgorithm.initView(context)
         hostView.removeAllViews()
@@ -403,7 +415,8 @@
 
         stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
 
-        assertThat(emptyShadeView.viewState.alpha).isEqualTo(1f)
+        val expected = getContentAlpha(ambientState.fractionToShade)
+        assertThat(emptyShadeView.viewState.alpha).isEqualTo(expected)
     }
 
     @Test