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

Commit 45ff6b03 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Polish domain and repository for use in UI" into main

parents 9bc7373c ef31dd38
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