diff options
9 files changed, 102 insertions, 58 deletions
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 c3b1a7cb16e3..aeea8cb6df1b 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.media.AudioManager 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.SharingStarted 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 @@ interface AudioManagerEventsReceiver { class AudioManagerEventsReceiverImpl( private val context: Context, coroutineScope: CoroutineScope, + backgroundCoroutineContext: CoroutineContext, ) : AudioManagerEventsReceiver { private val allActions: Collection<String> @@ -79,6 +82,7 @@ class AudioManagerEventsReceiverImpl( .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 35ee8287d52f..58a09fbacc59 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 @@ class AudioManagerEventsReceiverTest { 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 5414b623ff97..39fd44a83c58 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 @@ constructor( 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 617aaa71d2d3..d5b8597e36ed 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 @@ interface AudioModule { 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 @@ interface AudioModule { localBluetoothManager: LocalBluetoothManager?, @Application coroutineScope: CoroutineScope, @Background coroutineContext: CoroutineContext, - volumeLogger: VolumeLogger + volumeLogger: VolumeLogger, ): AudioSharingRepository = if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { AudioSharingRepositoryImpl( @@ -90,7 +92,7 @@ interface AudioModule { localBluetoothManager, coroutineScope, coroutineContext, - volumeLogger + volumeLogger, ) } else { AudioSharingRepositoryEmptyImpl() @@ -111,8 +113,7 @@ interface AudioModule { @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 dacd6c78b034..b9f47d7ad110 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.MediaSession 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 @@ class MediaControllerInteractorImpl @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 aa07cfd26bdb..b3848a6d7817 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.content.pm.PackageManager 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 @@ constructor( @Background private val backgroundCoroutineContext: CoroutineContext, mediaControllerRepository: MediaControllerRepository, private val mediaControllerInteractor: MediaControllerInteractor, + private val execution: Execution, ) { private val activeMediaControllers: Flow<MediaControllers> = @@ -82,9 +85,10 @@ constructor( .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 @@ constructor( 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 @@ constructor( 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 @@ constructor( .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 000000000000..bf66cb6e8ecc --- /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 1b58582a806f..ed5322ed098e 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 android.content.pm.ApplicationInfo 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 @@ val Kosmos.mediaOutputInteractor by 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 652b3ea984e7..fdeb8cef02b9 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 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto 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) } |