Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef31dd38 authored by Anton Potapov's avatar Anton Potapov
Browse files

Polish domain and repository for use in UI

 - adds playback to MediaDeviceSession to change react to playback
   changes;
 - adds media output user actions handling interactor;
 - improves naming in a few places;
 - moved fakes from SettingsLib to SystemUI;

Flag: aconfig new_volume_panel DISABLED
Test: atest MediaOutputAvailabilityCriteriaTest
Test: atest MediaControllerRepositoryImplTest
Test: atest MediaOutputInteractorTest
Bug: 323538193
Change-Id: I20450f7bfb3bedcb6834d11104f0eaaae3306f50
parent ad2bf673
Loading
Loading
Loading
Loading
+44 −0
Original line number Diff line number Diff line
/*
 * 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.settingslib.media.session

import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.os.UserHandle
import androidx.concurrent.futures.DirectExecutor
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch

/** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
    get() =
        callbackFlow {
                val listener =
                    MediaSessionManager.OnActiveSessionsChangedListener { launch { send(it) } }
                addOnActiveSessionsChangedListener(
                    null,
                    UserHandle.of(UserHandle.myUserId()),
                    DirectExecutor.INSTANCE,
                    listener,
                )
                awaitClose { removeOnActiveSessionsChangedListener(listener) }
            }
            .buffer(capacity = Channel.CONFLATED)
+100 −0
Original line number Diff line number Diff line
/*
 * 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.settingslib.volume.data.repository

import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.os.Bundle
import android.os.Handler
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch

/** [MediaController.Callback] flow representation. */
fun MediaController.stateChanges(handler: Handler): Flow<MediaControllerChange> {
    return callbackFlow {
        val callback = MediaControllerCallbackProducer(this)
        registerCallback(callback, handler)
        awaitClose { unregisterCallback(callback) }
    }
}

/** Models particular change event received by [MediaController.Callback]. */
sealed interface MediaControllerChange {

    data object SessionDestroyed : MediaControllerChange

    data class SessionEvent(val event: String, val extras: Bundle?) : MediaControllerChange

    data class PlaybackStateChanged(val state: PlaybackState?) : MediaControllerChange

    data class MetadataChanged(val metadata: MediaMetadata?) : MediaControllerChange

    data class QueueChanged(val queue: MutableList<MediaSession.QueueItem>?) :
        MediaControllerChange

    data class QueueTitleChanged(val title: CharSequence?) : MediaControllerChange

    data class ExtrasChanged(val extras: Bundle?) : MediaControllerChange

    data class AudioInfoChanged(val info: MediaController.PlaybackInfo?) : MediaControllerChange
}

private class MediaControllerCallbackProducer(
    private val producingScope: ProducerScope<MediaControllerChange>
) : MediaController.Callback() {

    override fun onSessionDestroyed() {
        send(MediaControllerChange.SessionDestroyed)
    }

    override fun onSessionEvent(event: String, extras: Bundle?) {
        send(MediaControllerChange.SessionEvent(event, extras))
    }

    override fun onPlaybackStateChanged(state: PlaybackState?) {
        send(MediaControllerChange.PlaybackStateChanged(state))
    }

    override fun onMetadataChanged(metadata: MediaMetadata?) {
        send(MediaControllerChange.MetadataChanged(metadata))
    }

    override fun onQueueChanged(queue: MutableList<MediaSession.QueueItem>?) {
        send(MediaControllerChange.QueueChanged(queue))
    }

    override fun onQueueTitleChanged(title: CharSequence?) {
        send(MediaControllerChange.QueueTitleChanged(title))
    }

    override fun onExtrasChanged(extras: Bundle?) {
        send(MediaControllerChange.ExtrasChanged(extras))
    }

    override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
        send(MediaControllerChange.AudioInfoChanged(info))
    }

    private fun send(change: MediaControllerChange) {
        producingScope.launch { producingScope.send(change) }
    }
}
+20 −16
Original line number Diff line number Diff line
@@ -16,21 +16,23 @@

package com.android.settingslib.volume.data.repository

import android.content.Intent
import android.media.AudioManager
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.headsetAudioModeChanges
import com.android.settingslib.media.session.activeMediaChanges
import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn

@@ -38,7 +40,7 @@ import kotlinx.coroutines.flow.stateIn
interface MediaControllerRepository {

    /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
    val activeMediaController: StateFlow<MediaController?>
    val activeLocalMediaController: StateFlow<MediaController?>
}

class MediaControllerRepositoryImpl(
@@ -53,26 +55,28 @@ class MediaControllerRepositoryImpl(
        audioManagerIntentsReceiver.intents.filter {
            AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
        }
    override val activeMediaController: StateFlow<MediaController?> =
        buildList {
                localBluetoothManager?.headsetAudioModeChanges?.let { add(it) }
                add(devicesChanges)

    override val activeLocalMediaController: StateFlow<MediaController?> =
        combine(
                mediaSessionManager.activeMediaChanges.onStart {
                    emit(mediaSessionManager.getActiveSessions(null))
                },
                localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
                    ?: flowOf(null),
                devicesChanges.onStart { emit(Intent()) },
            ) { controllers, _, _ ->
                controllers?.let(::findLocalMediaController)
            }
            .merge()
            .onStart { emit(Unit) }
            .map { getActiveLocalMediaController() }
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)

    private fun getActiveLocalMediaController(): MediaController? {
    private fun findLocalMediaController(
        controllers: Collection<MediaController>,
    ): MediaController? {
        var localController: MediaController? = null
        val remoteMediaSessionLists: MutableList<String> = ArrayList()
        for (controller in mediaSessionManager.getActiveSessions(null)) {
        for (controller in controllers) {
            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
            val playbackState = controller.playbackState ?: continue
            if (inactivePlaybackStates.contains(playbackState.state)) {
                continue
            }
            when (playbackInfo.playbackType) {
                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
                    if (localController?.packageName.equals(controller.packageName)) {
+2 −2
Original line number Diff line number Diff line
@@ -116,7 +116,7 @@ class MediaControllerRepositoryImplTest {
                    )
                )
            var mediaController: MediaController? = null
            underTest.activeMediaController
            underTest.activeLocalMediaController
                .onEach { mediaController = it }
                .launchIn(backgroundScope)
            runCurrent()
@@ -141,7 +141,7 @@ class MediaControllerRepositoryImplTest {
                    )
                )
            var mediaController: MediaController? = null
            underTest.activeMediaController
            underTest.activeLocalMediaController
                .onEach { mediaController = it }
                .launchIn(backgroundScope)
            runCurrent()
+1 −1
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ interface BottomBarModule {
    @Binds
    @IntoMap
    @StringKey(VolumePanelComponents.BOTTOM_BAR)
    fun bindMediaVolumeSliderComponent(component: BottomBarComponent): VolumePanelUiComponent
    fun bindVolumePanelUiComponent(component: BottomBarComponent): VolumePanelUiComponent

    @Binds
    @IntoMap
Loading