Merge "Move binder calls to the background threads" into main
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt
index c3b1a7c..aeea8cb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiver.kt
@@ -24,6 +24,7 @@
 import android.util.Log
 import com.android.settingslib.volume.shared.model.AudioManagerEvent
 import com.android.settingslib.volume.shared.model.AudioStream
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.SharedFlow
@@ -31,6 +32,7 @@
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.launch
@@ -44,6 +46,7 @@
 class AudioManagerEventsReceiverImpl(
     private val context: Context,
     coroutineScope: CoroutineScope,
+    backgroundCoroutineContext: CoroutineContext,
 ) : AudioManagerEventsReceiver {
 
     private val allActions: Collection<String>
@@ -79,6 +82,7 @@
             .filterNotNull()
             .filter { intent -> allActions.contains(intent.action) }
             .mapNotNull { it.toAudioManagerEvent() }
+            .flowOn(backgroundCoroutineContext)
             .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
 
     private fun Intent.toAudioManagerEvent(): AudioManagerEvent? {
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiverTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiverTest.kt
index 35ee828..58a09fb 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiverTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/AudioManagerEventsReceiverTest.kt
@@ -60,7 +60,12 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
-        underTest = AudioManagerEventsReceiverImpl(context, testScope.backgroundScope)
+        underTest =
+            AudioManagerEventsReceiverImpl(
+                context,
+                testScope.backgroundScope,
+                testScope.testScheduler,
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
index 5414b62..39fd44a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
@@ -63,6 +63,7 @@
     private val captioningManager: StateFlow<CaptioningManager?> =
         userRepository.selectedUser
             .map { userScopedCaptioningManagerProvider.forUser(it.userInfo.userHandle) }
+            .flowOn(backgroundCoroutineContext)
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
 
     override val captioningModel: StateFlow<CaptioningModel?> =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index 617aaa7..d5b8597 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -53,7 +53,9 @@
         fun provideAudioManagerIntentsReceiver(
             @Application context: Context,
             @Application coroutineScope: CoroutineScope,
-        ): AudioManagerEventsReceiver = AudioManagerEventsReceiverImpl(context, coroutineScope)
+            @Background coroutineContext: CoroutineContext,
+        ): AudioManagerEventsReceiver =
+            AudioManagerEventsReceiverImpl(context, coroutineScope, coroutineContext)
 
         @Provides
         @SysUISingleton
@@ -82,7 +84,7 @@
             localBluetoothManager: LocalBluetoothManager?,
             @Application coroutineScope: CoroutineScope,
             @Background coroutineContext: CoroutineContext,
-            volumeLogger: VolumeLogger
+            volumeLogger: VolumeLogger,
         ): AudioSharingRepository =
             if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
                 AudioSharingRepositoryImpl(
@@ -90,7 +92,7 @@
                     localBluetoothManager,
                     coroutineScope,
                     coroutineContext,
-                    volumeLogger
+                    volumeLogger,
                 )
             } else {
                 AudioSharingRepositoryEmptyImpl()
@@ -111,8 +113,7 @@
 
         @Provides
         @SysUISingleton
-        fun provideAudioSystemRepository(
-            @Application context: Context,
-        ): AudioSystemRepository = AudioSystemRepositoryImpl(context)
+        fun provideAudioSystemRepository(@Application context: Context): AudioSystemRepository =
+            AudioSystemRepositoryImpl(context)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt
index dacd6c7..b9f47d7 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractor.kt
@@ -22,15 +22,17 @@
 import android.media.session.PlaybackState
 import android.os.Bundle
 import android.os.Handler
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaControllerChangeModel
 import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.channels.ProducerScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.flow.flowOn
 
 interface MediaControllerInteractor {
 
@@ -43,14 +45,16 @@
 @Inject
 constructor(
     @Background private val backgroundHandler: Handler,
+    @Background private val backgroundCoroutineContext: CoroutineContext,
 ) : MediaControllerInteractor {
 
     override fun stateChanges(mediaController: MediaController): Flow<MediaControllerChangeModel> {
         return conflatedCallbackFlow {
-            val callback = MediaControllerCallbackProducer(this)
-            mediaController.registerCallback(callback, backgroundHandler)
-            awaitClose { mediaController.unregisterCallback(callback) }
-        }
+                val callback = MediaControllerCallbackProducer(this)
+                mediaController.registerCallback(callback, backgroundHandler)
+                awaitClose { mediaController.unregisterCallback(callback) }
+            }
+            .flowOn(backgroundCoroutineContext)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index aa07cfd..b3848a6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -20,10 +20,12 @@
 import android.media.VolumeProvider
 import android.media.session.MediaController
 import android.util.Log
+import androidx.annotation.WorkerThread
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.concurrency.Execution
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
 import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
@@ -62,6 +64,7 @@
     @Background private val backgroundCoroutineContext: CoroutineContext,
     mediaControllerRepository: MediaControllerRepository,
     private val mediaControllerInteractor: MediaControllerInteractor,
+    private val execution: Execution,
 ) {
 
     private val activeMediaControllers: Flow<MediaControllers> =
@@ -82,9 +85,10 @@
             .map {
                 MediaDeviceSessions(
                     local = it.local?.mediaDeviceSession(),
-                    remote = it.remote?.mediaDeviceSession()
+                    remote = it.remote?.mediaDeviceSession(),
                 )
             }
+            .flowOn(backgroundCoroutineContext)
             .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null))
 
     /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
@@ -115,55 +119,43 @@
     val currentConnectedDevice: Flow<MediaDevice?> =
         localMediaRepository.flatMapLatest { it.currentConnectedDevice }.distinctUntilChanged()
 
-    private suspend fun getApplicationLabel(packageName: String): CharSequence? {
-        return try {
-            withContext(backgroundCoroutineContext) {
-                val appInfo =
-                    packageManager.getApplicationInfo(
-                        packageName,
-                        PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
-                    )
-                appInfo.loadLabel(packageManager)
-            }
-        } catch (e: PackageManager.NameNotFoundException) {
-            Log.e(TAG, "Unable to find info for package: $packageName")
-            null
-        }
-    }
-
     /** Finds local and remote media controllers. */
-    private fun getMediaControllers(
-        controllers: Collection<MediaController>,
-    ): MediaControllers {
-        var localController: MediaController? = null
-        var remoteController: MediaController? = null
-        val remoteMediaSessions: MutableSet<String> = mutableSetOf()
-        for (controller in controllers) {
-            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
-            when (playbackInfo.playbackType) {
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
-                    // MediaController can't be local if there is a remote one for the same package
-                    if (localController?.packageName.equals(controller.packageName)) {
-                        localController = null
+    private suspend fun getMediaControllers(
+        controllers: Collection<MediaController>
+    ): MediaControllers =
+        withContext(backgroundCoroutineContext) {
+            var localController: MediaController? = null
+            var remoteController: MediaController? = null
+            val remoteMediaSessions: MutableSet<String> = mutableSetOf()
+            for (controller in controllers) {
+                val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
+                when (playbackInfo.playbackType) {
+                    MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
+                        // MediaController can't be local if there is a remote one for the same
+                        // package
+                        if (localController?.packageName.equals(controller.packageName)) {
+                            localController = null
+                        }
+                        if (!remoteMediaSessions.contains(controller.packageName)) {
+                            remoteMediaSessions.add(controller.packageName)
+                            remoteController = chooseController(remoteController, controller)
+                        }
                     }
-                    if (!remoteMediaSessions.contains(controller.packageName)) {
-                        remoteMediaSessions.add(controller.packageName)
-                        remoteController = chooseController(remoteController, controller)
+                    MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
+                        if (controller.packageName in remoteMediaSessions) continue
+                        localController = chooseController(localController, controller)
                     }
                 }
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
-                    if (controller.packageName in remoteMediaSessions) continue
-                    localController = chooseController(localController, controller)
-                }
             }
+            MediaControllers(local = localController, remote = remoteController)
         }
-        return MediaControllers(local = localController, remote = remoteController)
-    }
 
+    @WorkerThread
     private fun chooseController(
         currentController: MediaController?,
         newController: MediaController,
     ): MediaController {
+        require(!execution.isMainThread())
         if (currentController == null) {
             return newController
         }
@@ -175,12 +167,26 @@
         return currentController
     }
 
-    private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
+    @WorkerThread
+    private fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
+        require(!execution.isMainThread())
+        val applicationLabel =
+            try {
+                packageManager
+                    .getApplicationInfo(
+                        packageName,
+                        PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER,
+                    )
+                    .loadLabel(packageManager)
+            } catch (e: PackageManager.NameNotFoundException) {
+                Log.e(TAG, "Unable to find info for package: $packageName")
+                null
+            } ?: return null
         return MediaDeviceSession(
             packageName = packageName,
             sessionToken = sessionToken,
             canAdjustVolume = playbackInfo.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED,
-            appLabel = getApplicationLabel(packageName) ?: return null
+            appLabel = applicationLabel,
         )
     }
 
@@ -195,10 +201,7 @@
             .onStart { emit(this@stateChanges) }
     }
 
-    private data class MediaControllers(
-        val local: MediaController?,
-        val remote: MediaController?,
-    )
+    private data class MediaControllers(val local: MediaController?, val remote: MediaController?)
 
     private companion object {
         const val TAG = "MediaOutputInteractor"
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/ExecutionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/ExecutionKosmos.kt
new file mode 100644
index 0000000..bf66cb6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/ExecutionKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeExecution: FakeExecution by
+    Kosmos.Fixture { FakeExecution().apply { simulateMainThread = false } }
+var Kosmos.execution: Execution by Kosmos.Fixture { fakeExecution }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index 1b58582..ed5322e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.mediaOutputDialogManager
+import com.android.systemui.util.concurrency.execution
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -53,6 +54,7 @@
             testScope.testScheduler,
             mediaControllerRepository,
             mediaControllerInteractor,
+            execution,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractorKosmos.kt
index 652b3ea..fdeb8ce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaControllerInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import android.os.Handler
 import android.os.looper
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
 
 var Kosmos.mediaControllerInteractor: MediaControllerInteractor by
-    Kosmos.Fixture { MediaControllerInteractorImpl(Handler(looper)) }
+    Kosmos.Fixture { MediaControllerInteractorImpl(Handler(looper), testScope.testScheduler) }