diff options
| author | 2024-08-28 11:42:40 +0000 | |
|---|---|---|
| committer | 2024-08-28 11:42:40 +0000 | |
| commit | 7fec56e0fd1378bcce5a0d237d96ee5da77e0727 (patch) | |
| tree | 7e31f2a5784a57f105087e31490b99ecc1da4352 | |
| parent | 0995966daa09f30ab40362375d7a1f51b3d6624c (diff) | |
| parent | 5cb6fe05b32cd0b1cea99779375db640d5ec2851 (diff) | |
Merge "Add listening to the IVolumeController for the volume changes in AudioRepository." into main
14 files changed, 447 insertions, 152 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt deleted file mode 100644 index 02d684d7d0ce..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.data.repository - -import android.media.AudioManager -import android.media.IVolumeController -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch - -/** Returns [AudioManager.setVolumeController] events as a [Flow] */ -fun AudioManager.volumeControllerEvents(): Flow<VolumeControllerEvent> = - callbackFlow { - volumeController = - object : IVolumeController.Stub() { - override fun displaySafeVolumeWarning(flags: Int) { - launch { send(VolumeControllerEvent.DisplaySafeVolumeWarning(flags)) } - } - - override fun volumeChanged(streamType: Int, flags: Int) { - launch { send(VolumeControllerEvent.VolumeChanged(streamType, flags)) } - } - - override fun masterMuteChanged(flags: Int) { - launch { send(VolumeControllerEvent.MasterMuteChanged(flags)) } - } - - override fun setLayoutDirection(layoutDirection: Int) { - launch { send(VolumeControllerEvent.SetLayoutDirection(layoutDirection)) } - } - - override fun dismiss() { - launch { send(VolumeControllerEvent.Dismiss) } - } - - override fun setA11yMode(mode: Int) { - launch { send(VolumeControllerEvent.SetA11yMode(mode)) } - } - - override fun displayCsdWarning( - csdWarning: Int, - displayDurationMs: Int, - ) { - launch { - send( - VolumeControllerEvent.DisplayCsdWarning( - csdWarning, - displayDurationMs, - ) - ) - } - } - } - awaitClose { volumeController = null } - } - .buffer() - -/** Models events received via [IVolumeController] */ -sealed interface VolumeControllerEvent { - - /** @see [IVolumeController.displaySafeVolumeWarning] */ - data class DisplaySafeVolumeWarning(val flags: Int) : VolumeControllerEvent - - /** @see [IVolumeController.volumeChanged] */ - data class VolumeChanged(val streamType: Int, val flags: Int) : VolumeControllerEvent - - /** @see [IVolumeController.masterMuteChanged] */ - data class MasterMuteChanged(val flags: Int) : VolumeControllerEvent - - /** @see [IVolumeController.setLayoutDirection] */ - data class SetLayoutDirection(val layoutDirection: Int) : VolumeControllerEvent - - /** @see [IVolumeController.setA11yMode] */ - data class SetA11yMode(val mode: Int) : VolumeControllerEvent - - /** @see [IVolumeController.displayCsdWarning] */ - data class DisplayCsdWarning( - val csdWarning: Int, - val displayDurationMs: Int, - ) : VolumeControllerEvent - - /** @see [IVolumeController.dismiss] */ - data object Dismiss : VolumeControllerEvent -}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/model/VolumeControllerEvent.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/VolumeControllerEvent.kt new file mode 100644 index 000000000000..0fe385b3d02a --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/VolumeControllerEvent.kt @@ -0,0 +1,47 @@ +/* + * 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.model + +import android.media.IVolumeController + +/** Models events received via [IVolumeController] */ +sealed interface VolumeControllerEvent { + + /** @see [IVolumeController.displaySafeVolumeWarning] */ + data class DisplaySafeVolumeWarning(val flags: Int) : VolumeControllerEvent + + /** @see [IVolumeController.volumeChanged] */ + data class VolumeChanged(val streamType: Int, val flags: Int) : VolumeControllerEvent + + /** @see [IVolumeController.masterMuteChanged] */ + data class MasterMuteChanged(val flags: Int) : VolumeControllerEvent + + /** @see [IVolumeController.setLayoutDirection] */ + data class SetLayoutDirection(val layoutDirection: Int) : VolumeControllerEvent + + /** @see [IVolumeController.setA11yMode] */ + data class SetA11yMode(val mode: Int) : VolumeControllerEvent + + /** @see [IVolumeController.displayCsdWarning] */ + data class DisplayCsdWarning( + val csdWarning: Int, + val displayDurationMs: Int, + ) : VolumeControllerEvent + + /** @see [IVolumeController.dismiss] */ + data object Dismiss : VolumeControllerEvent +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index 0e71116db6cc..3e2d8328f21e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -22,9 +22,12 @@ import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.AudioManager.AudioDeviceCategory import android.media.AudioManager.OnCommunicationDeviceChangedListener +import android.media.IVolumeController import android.provider.Settings +import android.util.Log import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils +import com.android.settingslib.volume.data.model.VolumeControllerEvent import com.android.settingslib.volume.shared.AudioLogger import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent @@ -36,10 +39,13 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance @@ -73,6 +79,11 @@ interface AudioRepository { */ val communicationDevice: StateFlow<AudioDeviceInfo?> + /** Events from [AudioManager.setVolumeController] */ + val volumeControllerEvents: Flow<VolumeControllerEvent> + + fun init() + /** State of the [AudioStream]. */ fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> @@ -90,8 +101,9 @@ interface AudioRepository { suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode) /** Gets audio device category. */ - @AudioDeviceCategory - suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int + @AudioDeviceCategory suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int + + suspend fun notifyVolumeControllerVisible(isVisible: Boolean) } class AudioRepositoryImpl( @@ -101,8 +113,10 @@ class AudioRepositoryImpl( private val backgroundCoroutineContext: CoroutineContext, private val coroutineScope: CoroutineScope, private val logger: AudioLogger, + shouldUseVolumeController: Boolean, ) : AudioRepository { + private val volumeController = ProducingVolumeController() private val streamSettingNames: Map<AudioStream, String> = mapOf( AudioStream(AudioManager.STREAM_VOICE_CALL) to Settings.System.VOLUME_VOICE, @@ -116,12 +130,19 @@ class AudioRepositoryImpl( AudioStream(AudioManager.STREAM_ASSISTANT) to Settings.System.VOLUME_ASSISTANT, ) + override val volumeControllerEvents: Flow<VolumeControllerEvent> = + if (shouldUseVolumeController) { + volumeController.events + } else { + emptyFlow() + } + override val mode: StateFlow<Int> = callbackFlow { - val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) } - audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) - awaitClose { audioManager.removeOnModeChangedListener(listener) } - } + val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) } + audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) + awaitClose { audioManager.removeOnModeChangedListener(listener) } + } .onStart { emit(audioManager.mode) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode) @@ -141,14 +162,14 @@ class AudioRepositoryImpl( override val communicationDevice: StateFlow<AudioDeviceInfo?> get() = callbackFlow { - val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } - audioManager.addOnCommunicationDeviceChangedListener( - ConcurrentUtils.DIRECT_EXECUTOR, - listener, - ) + val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } + audioManager.addOnCommunicationDeviceChangedListener( + ConcurrentUtils.DIRECT_EXECUTOR, + listener, + ) - awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } - } + awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } + } .filterNotNull() .map { audioManager.communicationDevice } .onStart { emit(audioManager.communicationDevice) } @@ -159,20 +180,30 @@ class AudioRepositoryImpl( audioManager.communicationDevice, ) + override fun init() { + try { + audioManager.volumeController = volumeController + } catch (error: SecurityException) { + Log.wtf("AudioManager", "Unable to set the volume controller", error) + } + } + override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { return merge( - audioManagerEventsReceiver.events.filter { - if (it is StreamAudioManagerEvent) { - it.audioStream == audioStream - } else { - true - } - }, - volumeSettingChanges(audioStream), - ) + audioManagerEventsReceiver.events.filter { + if (it is StreamAudioManagerEvent) { + it.audioStream == audioStream + } else { + true + } + }, + volumeSettingChanges(audioStream), + volumeControllerEvents.filter { it is VolumeControllerEvent.VolumeChanged }, + ) .conflate() .map { getCurrentAudioStream(audioStream) } .onStart { emit(getCurrentAudioStream(audioStream)) } + .distinctUntilChanged() .onEach { logger.onVolumeUpdateReceived(audioStream, it) } .flowOn(backgroundCoroutineContext) } @@ -228,6 +259,12 @@ class AudioRepositoryImpl( } } + override suspend fun notifyVolumeControllerVisible(isVisible: Boolean) { + withContext(backgroundCoroutineContext) { + audioManager.notifyVolumeControllerVisible(volumeController, isVisible) + } + } + private fun getMinVolume(stream: AudioStream): Int = try { audioManager.getStreamMinVolume(stream.value) @@ -253,3 +290,45 @@ class AudioRepositoryImpl( } } } + +private class ProducingVolumeController : IVolumeController.Stub() { + + private val mutableEvents = MutableSharedFlow<VolumeControllerEvent>(extraBufferCapacity = 32) + val events = mutableEvents.asSharedFlow() + + override fun displaySafeVolumeWarning(flags: Int) { + mutableEvents.tryEmit(VolumeControllerEvent.DisplaySafeVolumeWarning(flags)) + } + + override fun volumeChanged(streamType: Int, flags: Int) { + mutableEvents.tryEmit(VolumeControllerEvent.VolumeChanged(streamType, flags)) + } + + override fun masterMuteChanged(flags: Int) { + mutableEvents.tryEmit(VolumeControllerEvent.MasterMuteChanged(flags)) + } + + override fun setLayoutDirection(layoutDirection: Int) { + mutableEvents.tryEmit(VolumeControllerEvent.SetLayoutDirection(layoutDirection)) + } + + override fun dismiss() { + mutableEvents.tryEmit(VolumeControllerEvent.Dismiss) + } + + override fun setA11yMode(mode: Int) { + mutableEvents.tryEmit(VolumeControllerEvent.SetA11yMode(mode)) + } + + override fun displayCsdWarning( + csdWarning: Int, + displayDurationMs: Int, + ) { + mutableEvents.tryEmit( + VolumeControllerEvent.DisplayCsdWarning( + csdWarning, + displayDurationMs, + ) + ) + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt index 0e43acb04c91..52e639172af5 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt @@ -44,6 +44,7 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -111,6 +112,7 @@ class AudioRepositoryTest { testScope.testScheduler, testScope.backgroundScope, logger, + true, ) } @@ -261,8 +263,8 @@ class AudioRepositoryTest { @Test fun getBluetoothAudioDeviceCategory() { testScope.runTest { - `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78")).thenReturn( - AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES) + `when`(audioManager.getBluetoothAudioDeviceCategory("12:34:56:78")) + .thenReturn(AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES) val category = underTest.getBluetoothAudioDeviceCategory("12:34:56:78") runCurrent() @@ -271,6 +273,27 @@ class AudioRepositoryTest { } } + @Test + fun useVolumeControllerDisabled_setVolumeController_notCalled() { + testScope.runTest { + underTest = + AudioRepositoryImpl( + eventsReceiver, + audioManager, + contentResolver, + testScope.testScheduler, + testScope.backgroundScope, + logger, + false, + ) + + underTest.volumeControllerEvents.launchIn(backgroundScope) + runCurrent() + + verify(audioManager, never()).volumeController = any() + } + } + private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) { verify(audioManager) .addOnCommunicationDeviceChangedListener( diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryVolumeControllerEventsTest.kt index 83b612df8dc9..f5c2f0174456 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryVolumeControllerEventsTest.kt @@ -14,12 +14,15 @@ * limitations under the License. */ -package com.android.settingslib.media.data.repository +package com.android.settingslib.volume.data.repository +import android.content.ContentResolver import android.media.AudioManager import android.media.IVolumeController import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.volume.data.model.VolumeControllerEvent +import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn @@ -39,16 +42,32 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -class AudioManagerVolumeControllerExtTest { +class AudioRepositoryVolumeControllerEventsTest { private val testScope = TestScope() @Captor private lateinit var volumeControllerCaptor: ArgumentCaptor<IVolumeController> @Mock private lateinit var audioManager: AudioManager + @Mock private lateinit var contentResolver: ContentResolver + + private val logger = FakeAudioRepositoryLogger() + private val eventsReceiver = FakeAudioManagerEventsReceiver() + + private lateinit var underTest: AudioRepository @Before fun setup() { MockitoAnnotations.initMocks(this) + underTest = + AudioRepositoryImpl( + eventsReceiver, + audioManager, + contentResolver, + testScope.testScheduler, + testScope.backgroundScope, + logger, + true, + ) } @Test @@ -83,7 +102,7 @@ class AudioManagerVolumeControllerExtTest { ) = testScope.runTest { var event: VolumeControllerEvent? = null - audioManager.volumeControllerEvents().onEach { event = it }.launchIn(backgroundScope) + underTest.volumeControllerEvents.onEach { event = it }.launchIn(backgroundScope) runCurrent() verify(audioManager).volumeController = volumeControllerCaptor.capture() diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerAdapter.kt index 68591910031d..e8367315c3c9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerAdapter.kt @@ -17,7 +17,8 @@ package com.android.systemui.volume import android.media.IVolumeController -import com.android.settingslib.media.data.repository.VolumeControllerEvent +import com.android.settingslib.volume.data.model.VolumeControllerEvent +import com.android.settingslib.volume.data.repository.AudioRepository import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -29,17 +30,17 @@ import kotlinx.coroutines.launch * [com.android.settingslib.volume.data.repository.AudioRepository.volumeControllerEvents] and the * old code that uses [IVolumeController] interface directly. */ -class VolumeControllerCollector +class VolumeControllerAdapter @Inject -constructor(@Application private val coroutineScope: CoroutineScope) { +constructor( + @Application private val coroutineScope: CoroutineScope, + private val audioRepository: AudioRepository, +) { /** Collects [Flow] of [VolumeControllerEvent] into [IVolumeController]. */ - fun collectToController( - eventsFlow: Flow<VolumeControllerEvent>, - controller: IVolumeController - ) = + fun collectToController(controller: IVolumeController) { coroutineScope.launch { - eventsFlow.collect { event -> + audioRepository.volumeControllerEvents.collect { event -> when (event) { is VolumeControllerEvent.VolumeChanged -> controller.volumeChanged(event.streamType, event.flags) @@ -56,4 +57,9 @@ constructor(@Application private val coroutineScope: CoroutineScope) { } } } + } + + fun notifyVolumeControllerVisible(isVisible: Boolean) { + coroutineScope.launch { audioRepository.notifyVolumeControllerVisible(isVisible) } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 1522cc490b43..d3e8bd3d8b5b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -68,6 +68,7 @@ import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; import com.android.settingslib.volume.MediaSessions; import com.android.systemui.Dumpable; +import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; @@ -153,6 +154,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final KeyguardManager mKeyguardManager; private final ActivityManager mActivityManager; private final UserTracker mUserTracker; + private final VolumeControllerAdapter mVolumeControllerAdapter; protected C mCallbacks = new C(); private final State mState = new State(); protected final MediaSessionsCallbacks mMediaSessionsCallbacksW; @@ -197,6 +199,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa NotificationManager notificationManager, VibratorHelper vibrator, IAudioService iAudioService, + VolumeControllerAdapter volumeControllerAdapter, AccessibilityManager accessibilityManager, PackageManager packageManager, WakefulnessLifecycle wakefulnessLifecycle, @@ -233,6 +236,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mVibrator = vibrator; mHasVibrator = mVibrator.hasVibrator(); mAudioService = iAudioService; + mVolumeControllerAdapter = volumeControllerAdapter; mKeyguardManager = keyguardManager; mActivityManager = activityManager; mUserTracker = userTracker; @@ -259,10 +263,14 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } protected void setVolumeController() { - try { - mAudio.setVolumeController(mVolumeController); - } catch (SecurityException e) { - Log.w(TAG, "Unable to set the volume controller", e); + if (Flags.useVolumeController()) { + mVolumeControllerAdapter.collectToController(mVolumeController); + } else { + try { + mAudio.setVolumeController(mVolumeController); + } catch (SecurityException e) { + Log.w(TAG, "Unable to set the volume controller", e); + } } } @@ -384,7 +392,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } public void notifyVisible(boolean visible) { - mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget(); + if (Flags.useVolumeController()) { + mVolumeControllerAdapter.notifyVolumeControllerVisible(visible); + } else { + mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget(); + } } public void userActivity() { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index 68d12f69215a..536403ca970e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -20,9 +20,9 @@ import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; import android.content.Context; import android.content.res.Configuration; -import android.os.Handler; import android.util.Log; +import com.android.settingslib.volume.data.repository.AudioRepository; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.qs.tiles.DndTile; @@ -39,23 +39,26 @@ public class VolumeUI implements CoreStartable, ConfigurationController.Configur private static final String TAG = "VolumeUI"; private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); - private final Handler mHandler = new Handler(); - private boolean mEnabled; private final Context mContext; private VolumeDialogComponent mVolumeComponent; private AudioSharingInteractor mAudioSharingInteractor; + private AudioRepository mAudioRepository; @Inject - public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent, + public VolumeUI(Context context, + VolumeDialogComponent volumeDialogComponent, + AudioRepository audioRepository, AudioSharingInteractor audioSharingInteractor) { mContext = context; mVolumeComponent = volumeDialogComponent; + mAudioRepository = audioRepository; mAudioSharingInteractor = audioSharingInteractor; } @Override public void start() { + mAudioRepository.init(); boolean enableVolumeUi = mContext.getResources().getBoolean(R.bool.enable_volume_ui); boolean enableSafetyWarning = mContext.getResources().getBoolean(R.bool.enable_safety_warning); @@ -77,7 +80,8 @@ public class VolumeUI implements CoreStartable, ConfigurationController.Configur @Override public void dump(PrintWriter pw, String[] args) { - pw.print("mEnabled="); pw.println(mEnabled); + pw.print("mEnabled="); + pw.println(mEnabled); if (!mEnabled) return; mVolumeComponent.dump(pw, args); } 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 d39daafd2311..20d598a9334b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -70,6 +70,7 @@ interface AudioModule { coroutineContext, coroutineScope, volumeLogger, + com.android.systemui.Flags.useVolumeController(), ) @Provides diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerAdapterTest.kt index dd78e4a1fdaa..c1403649efdd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerAdapterTest.kt @@ -19,16 +19,17 @@ package com.android.systemui.volume import android.media.IVolumeController import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.media.data.repository.VolumeControllerEvent +import com.android.settingslib.volume.data.model.VolumeControllerEvent import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.android.systemui.volume.data.repository.audioRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.eq @@ -38,14 +39,20 @@ import org.mockito.kotlin.verify @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest -class VolumeControllerCollectorTest : SysuiTestCase() { +class VolumeControllerAdapterTest : SysuiTestCase() { private val kosmos = testKosmos() private val eventsFlow = MutableStateFlow<VolumeControllerEvent?>(null) - private val underTest = VolumeControllerCollector(kosmos.applicationCoroutineScope) + private val underTest = + with(kosmos) { VolumeControllerAdapter(applicationCoroutineScope, audioRepository) } private val volumeController = mock<IVolumeController> {} + @Before + fun setUp() { + kosmos.audioRepository.init() + } + @Test fun volumeControllerEvent_volumeChanged_callsMethod() = testEvent(VolumeControllerEvent.VolumeChanged(3, 0)) { @@ -90,7 +97,8 @@ class VolumeControllerCollectorTest : SysuiTestCase() { private fun testEvent(event: VolumeControllerEvent, verify: () -> Unit) = kosmos.testScope.runTest { - underTest.collectToController(eventsFlow.filterNotNull(), volumeController) + kosmos.audioRepository.sendVolumeControllerEvent(event) + underTest.collectToController(volumeController) eventsFlow.value = event runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index 4ea1a0ca9f2b..f62beeb16ae5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -48,9 +48,11 @@ import androidx.test.filters.SmallTest; import com.android.settingslib.flags.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestCaseExtKt; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.kosmos.Kosmos; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.VibratorHelper; @@ -78,6 +80,8 @@ import java.util.concurrent.Executor; @TestableLooper.RunWithLooper public class VolumeDialogControllerImplTest extends SysuiTestCase { + private final Kosmos mKosmos = SysuiTestCaseExtKt.testKosmos(this); + TestableVolumeDialogControllerImpl mVolumeController; VolumeDialogControllerImpl.C mCallback; @Mock @@ -146,6 +150,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mNotificationManager, mVibrator, mIAudioService, + VolumeControllerAdapterKosmosKt.getVolumeControllerAdapter(mKosmos), mAccessibilityManager, mPackageManager, mWakefullnessLifcycle, @@ -323,6 +328,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { NotificationManager notificationManager, VibratorHelper optionalVibrator, IAudioService iAudioService, + VolumeControllerAdapter volumeControllerAdapter, AccessibilityManager accessibilityManager, PackageManager packageManager, WakefulnessLifecycle wakefulnessLifecycle, @@ -342,6 +348,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { notificationManager, optionalVibrator, iAudioService, + volumeControllerAdapter, accessibilityManager, packageManager, wakefulnessLifecycle, diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt new file mode 100644 index 000000000000..98cea9d92561 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt @@ -0,0 +1,156 @@ +/* + * 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.volume + +import android.app.activityManager +import android.app.keyguardManager +import android.content.applicationContext +import android.content.packageManager +import android.media.AudioManager +import android.media.IVolumeController +import android.os.Handler +import android.os.looper +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.TestableLooper +import android.view.accessibility.accessibilityManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.volume.data.model.VolumeControllerEvent +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.wakefulnessLifecycle +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.testKosmos +import com.android.systemui.util.RingerModeLiveData +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.concurrency.FakeThreadFactory +import com.android.systemui.util.time.fakeSystemClock +import com.android.systemui.volume.data.repository.audioRepository +import com.android.systemui.volume.domain.interactor.audioSharingInteractor +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +@TestableLooper.RunWithLooper +class VolumeDialogControllerImplTestKt : SysuiTestCase() { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val kosmos: Kosmos = testKosmos() + private val audioManager: AudioManager = mock {} + private val callbacks: VolumeDialogController.Callbacks = mock {} + + private lateinit var threadFactory: FakeThreadFactory + private lateinit var underTest: VolumeDialogControllerImpl + + @Before + fun setUp() = + with(kosmos) { + audioRepository.init() + threadFactory = + FakeThreadFactory(FakeExecutor(fakeSystemClock)).apply { setLooper(looper) } + underTest = + VolumeDialogControllerImpl( + applicationContext, + mock {}, + mock { + on { ringerMode }.thenReturn(mock<RingerModeLiveData> {}) + on { ringerModeInternal }.thenReturn(mock<RingerModeLiveData> {}) + }, + threadFactory, + audioManager, + mock {}, + mock {}, + mock {}, + volumeControllerAdapter, + accessibilityManager, + packageManager, + wakefulnessLifecycle, + keyguardManager, + activityManager, + mock { on { userContext }.thenReturn(applicationContext) }, + dumpManager, + audioSharingInteractor, + mock {}, + ) + .apply { + setEnableDialogs(true, true) + addCallback(callbacks, Handler(looper)) + } + } + + @Test + @EnableFlags(Flags.FLAG_USE_VOLUME_CONTROLLER) + fun useVolumeControllerEnabled_listensToVolumeController() = + testVolumeController { stream: Int, flags: Int -> + audioRepository.sendVolumeControllerEvent( + VolumeControllerEvent.VolumeChanged(streamType = stream, flags = flags) + ) + } + + @Test + @DisableFlags(Flags.FLAG_USE_VOLUME_CONTROLLER) + fun useVolumeControllerDisabled_listensToVolumeController() = + testVolumeController { stream: Int, flags: Int -> + audioManager.emitVolumeChange(stream, flags) + } + + private fun testVolumeController( + emitVolumeChange: suspend Kosmos.(stream: Int, flags: Int) -> Unit + ) = + with(kosmos) { + testScope.runTest { + whenever(wakefulnessLifecycle.wakefulness) + .thenReturn(WakefulnessLifecycle.WAKEFULNESS_AWAKE) + underTest.setVolumeController() + runCurrent() + + emitVolumeChange(AudioManager.STREAM_SYSTEM, AudioManager.FLAG_SHOW_UI) + runCurrent() + TestableLooper.get(this@VolumeDialogControllerImplTestKt).processAllMessages() + + verify(callbacks) { 1 * { onShowRequested(any(), any(), any()) } } + } + } + + private companion object { + + private fun AudioManager.emitVolumeChange(stream: Int, flags: Int = 0) { + val captor = argumentCaptor<IVolumeController>() + verify(this) { 1 * { volumeController = captor.capture() } } + captor.firstValue.volumeChanged(stream, flags) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerAdapterKosmos.kt index d60f14cab28f..4045135b928e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerAdapterKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.data.repository.audioRepository -val Kosmos.volumeControllerCollector by - Kosmos.Fixture { VolumeControllerCollector(applicationCoroutineScope) } +val Kosmos.volumeControllerAdapter by + Kosmos.Fixture { VolumeControllerAdapter(applicationCoroutineScope, audioRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt index 135cb14a3497..1fa6c3f2327b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt @@ -18,12 +18,16 @@ package com.android.systemui.volume.data.repository import android.media.AudioDeviceInfo import android.media.AudioManager +import com.android.settingslib.volume.data.model.VolumeControllerEvent import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -39,10 +43,26 @@ class FakeAudioRepository : AudioRepository { override val communicationDevice: StateFlow<AudioDeviceInfo?> = mutableCommunicationDevice.asStateFlow() + private val mutableVolumeControllerEvents = MutableSharedFlow<VolumeControllerEvent>(replay = 1) + override val volumeControllerEvents: Flow<VolumeControllerEvent> + get() = mutableVolumeControllerEvents.asSharedFlow() + private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf() private val lastAudibleVolumes: MutableMap<AudioStream, Int> = mutableMapOf() private val deviceCategories: MutableMap<String, Int> = mutableMapOf() + private val mutableIsVolumeControllerVisible = MutableStateFlow(false) + val isVolumeControllerVisible: StateFlow<Boolean> + get() = mutableIsVolumeControllerVisible.asStateFlow() + + private var mutableIsInitialized: Boolean = false + val isInitialized: Boolean + get() = mutableIsInitialized + + override fun init() { + mutableIsInitialized = true + } + private fun getAudioStreamModelState( audioStream: AudioStream ): MutableStateFlow<AudioStreamModel> = @@ -111,4 +131,16 @@ class FakeAudioRepository : AudioRepository { override suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int { return deviceCategories[bluetoothAddress] ?: AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN } + + suspend fun sendVolumeControllerEvent(event: VolumeControllerEvent) { + if (isInitialized) { + mutableVolumeControllerEvents.emit(event) + } + } + + override suspend fun notifyVolumeControllerVisible(isVisible: Boolean) { + if (isInitialized) { + mutableIsVolumeControllerVisible.value = isVisible + } + } } |