Loading packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +21 −129 Original line number Diff line number Diff line Loading @@ -18,10 +18,7 @@ package com.android.settingslib.media; import static android.media.MediaRoute2Info.CONNECTION_STATE_CONNECTING; import static android.media.session.MediaController.PlaybackInfo; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; import static com.android.settingslib.media.MediaDeviceUtilKt.isBluetoothMediaDevice; import static com.android.settingslib.media.MediaDeviceUtilKt.isComplexMediaDevice; Loading Loading @@ -64,7 +61,6 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; Loading Loading @@ -112,9 +108,14 @@ public abstract class InfoMediaManager { */ void onRequestFailed(int reason); /** Callback for notifying that the suggested device has been updated. */ default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState suggestedDevice) {} ; /** * Callback for changes to the suggested device list. * * @param deviceSuggestions the list of suggested devices. */ default void onDeviceSuggestionsUpdated( @NonNull List<SuggestedDeviceInfo> deviceSuggestions) { } } Loading @@ -141,8 +142,6 @@ public abstract class InfoMediaManager { private final LocalBluetoothManager mBluetoothManager; @GuardedBy("mLock") private final Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceMap = new HashMap<>(); @GuardedBy("mLock") @Nullable private SuggestedDeviceState mSuggestedDeviceState; private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); Loading Loading @@ -286,7 +285,7 @@ public abstract class InfoMediaManager { protected final void rebuildDeviceList() { buildAvailableRoutes(); updateDeviceSuggestion(); updateMediaDevicesSuggestionState(); } protected final void notifyCurrentConnectedDeviceChanged() { Loading Loading @@ -608,13 +607,6 @@ public abstract class InfoMediaManager { return getActiveRoutingSession().getName(); } @Nullable public SuggestedDeviceState getSuggestedDevice() { synchronized (mLock) { return mSuggestedDeviceState; } } /** Requests a suggestion from other routers. */ public abstract void requestDeviceSuggestion(); Loading @@ -626,6 +618,9 @@ public abstract class InfoMediaManager { protected void notifyDeviceSuggestionUpdated( String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestions) { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return; } synchronized (mLock) { if (suggestions == null) { mSuggestedDeviceMap.remove(suggestingPackageName); Loading @@ -633,116 +628,21 @@ public abstract class InfoMediaManager { mSuggestedDeviceMap.put(suggestingPackageName, suggestions); } } updateDeviceSuggestion(); } private void updateDeviceSuggestion() { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return; } if (updateSuggestedDeviceState()) { dispatchOnSuggestedDeviceUpdated(); } if (updateMediaDevicesSuggestionState()) { dispatchDeviceListAdded(getMediaDevices()); } dispatchOnDeviceSuggestionsUpdated(); } private boolean updateSuggestedDeviceState() { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return false; } SuggestedDeviceInfo topSuggestion = null; SuggestedDeviceState newSuggestedDeviceState = null; SuggestedDeviceState previousState = getSuggestedDevice(); List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null && !suggestions.isEmpty()) { topSuggestion = suggestions.get(0); } if (topSuggestion != null) { synchronized (mLock) { for (MediaDevice device : mMediaDevices) { if (Objects.equals(device.getId(), topSuggestion.getRouteId())) { newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion, device.getState()); break; } } } if (newSuggestedDeviceState == null) { if (previousState != null && topSuggestion .getRouteId() .equals(previousState.getSuggestedDeviceInfo().getRouteId())) { return false; } newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion); } } if (newSuggestedDeviceState != null && isSuggestedDeviceSelected(newSuggestedDeviceState)) { newSuggestedDeviceState = null; } if (!Objects.equals(previousState, newSuggestedDeviceState)) { synchronized (mLock) { mSuggestedDeviceState = newSuggestedDeviceState; } return true; } return false; } private boolean isSuggestedDeviceSelected( @NonNull SuggestedDeviceState newSuggestedDeviceState) { synchronized (mLock) { return mMediaDevices.stream().anyMatch(device -> device.isSelected() && Objects.equals( device.getId(), newSuggestedDeviceState .getSuggestedDeviceInfo() .getRouteId())); } } final void onConnectionAttemptedForSuggestion(@NonNull SuggestedDeviceState suggestion) { synchronized (mLock) { if (!Objects.equals(suggestion, mSuggestedDeviceState)) { return; } if (mSuggestedDeviceState.getConnectionState() != STATE_DISCONNECTED && mSuggestedDeviceState.getConnectionState() != STATE_CONNECTING_FAILED) { return; } mSuggestedDeviceState = new SuggestedDeviceState( mSuggestedDeviceState.getSuggestedDeviceInfo(), STATE_CONNECTING); } dispatchOnSuggestedDeviceUpdated(); } final void onConnectionAttemptCompletedForSuggestion( @NonNull SuggestedDeviceState suggestion, boolean success) { synchronized (mLock) { if (!Objects.equals(suggestion, mSuggestedDeviceState)) { return; } int state = success ? STATE_CONNECTED : STATE_CONNECTING_FAILED; mSuggestedDeviceState = new SuggestedDeviceState(mSuggestedDeviceState.getSuggestedDeviceInfo(), state); } dispatchOnSuggestedDeviceUpdated(); } private void dispatchOnSuggestedDeviceUpdated() { SuggestedDeviceState state = getSuggestedDevice(); Log.i(TAG, "dispatchOnSuggestedDeviceUpdated(), state: " + state); private void dispatchOnDeviceSuggestionsUpdated() { Log.i(TAG, "dispatchDeviceSuggestionsUpdated()"); for (MediaDeviceCallback callback : mCallbacks) { callback.onSuggestedDeviceUpdated(state); callback.onDeviceSuggestionsUpdated(getSuggestions()); } } @Nullable private List<SuggestedDeviceInfo> getSuggestions() { @NonNull List<SuggestedDeviceInfo> getSuggestions() { // Give suggestions in the following order // 1. Suggestions from the local router // 2. Suggestions from the proxy router if only one proxy router is providing suggestions Loading @@ -760,7 +660,7 @@ public abstract class InfoMediaManager { } } } return null; return List.of(); } // Go through all current MediaDevices, and update the ones that are suggested. Loading @@ -770,12 +670,9 @@ public abstract class InfoMediaManager { } Set<String> suggestedDevices = new HashSet<>(); // Prioritize suggestions from the package, otherwise pick any. List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null) { for (SuggestedDeviceInfo suggestion : suggestions) { for (SuggestedDeviceInfo suggestion : getSuggestions()) { suggestedDevices.add(suggestion.getRouteId()); } } boolean didUpdate = false; synchronized (mLock) { for (MediaDevice device : mMediaDevices) { Loading Loading @@ -947,11 +844,6 @@ public abstract class InfoMediaManager { return; } device.setState(state); if (device.isSuggestedDevice()) { if (updateSuggestedDeviceState()) { dispatchOnSuggestedDeviceUpdated(); } } } @RequiresApi(34) Loading packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +50 −19 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import android.media.AudioManager; import android.media.RoutingChangeInfo; import android.media.RoutingChangeInfo.EntryPoint; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.os.Build; import android.os.Handler; import android.text.TextUtils; Loading @@ -55,7 +56,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; /** Loading Loading @@ -255,11 +255,6 @@ public class LocalMediaManager implements BluetoothCallback { if (mConnectingSuggestedDeviceState != null) { return; } SuggestedDeviceState currentSuggestion = mInfoMediaManager.getSuggestedDevice(); if (!Objects.equals(suggestion, currentSuggestion)) { Log.w(TAG, "Suggestion got changed, aborting connection."); return; } for (MediaDevice device : mMediaDevices) { if (suggestion.getSuggestedDeviceInfo().getRouteId().equals(device.getId())) { Log.i(TAG, "Suggestion: device is available, connecting. deviceId = " Loading @@ -270,7 +265,7 @@ public class LocalMediaManager implements BluetoothCallback { } mConnectingSuggestedDeviceState = new ConnectingSuggestedDeviceState( currentSuggestion, routingChangeInfo.getEntryPoint()); suggestion, routingChangeInfo.getEntryPoint()); mConnectingSuggestedDeviceState.tryConnect(); } } Loading @@ -292,9 +287,9 @@ public class LocalMediaManager implements BluetoothCallback { } } @Nullable public SuggestedDeviceState getSuggestedDevice() { return mInfoMediaManager.getSuggestedDevice(); @NonNull public List<SuggestedDeviceInfo> getSuggestions() { return mInfoMediaManager.getSuggestions(); } void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { Loading Loading @@ -356,9 +351,21 @@ public class LocalMediaManager implements BluetoothCallback { } } void dispatchOnSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) { void dispatchDeviceSuggestionsUpdated(List<SuggestedDeviceInfo> deviceSuggestions) { for (DeviceCallback callback : getCallbacks()) { callback.onDeviceSuggestionsUpdated(deviceSuggestions); } } void dispatchConnectSuggestedDeviceFinished(SuggestedDeviceState state, boolean success) { for (DeviceCallback callback : getCallbacks()) { callback.onConnectSuggestedDeviceFinished(state, success); } } void dispatchConnectionAttemptedForSuggestion(SuggestedDeviceState state) { for (DeviceCallback callback : getCallbacks()) { callback.onSuggestedDeviceUpdated(device); callback.onConnectionAttemptedForSuggestion(state); } } Loading Loading @@ -410,6 +417,17 @@ public class LocalMediaManager implements BluetoothCallback { return null; } /** * Returns a list of MediaDevice objects. * * @return a list of media devices */ public List<MediaDevice> getMediaDevices() { synchronized (mMediaDevicesLock) { return new ArrayList<>(mMediaDevices); } } /** * Find the current connected MediaDevice. * Loading Loading @@ -769,8 +787,9 @@ public class LocalMediaManager implements BluetoothCallback { } @Override public void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) { dispatchOnSuggestedDeviceUpdated(device); public void onDeviceSuggestionsUpdated( @NonNull List<SuggestedDeviceInfo> deviceSuggestions) { dispatchDeviceSuggestionsUpdated(deviceSuggestions); } } Loading Loading @@ -850,8 +869,20 @@ public class LocalMediaManager implements BluetoothCallback { */ default void onAboutToConnectDeviceRemoved() {} /** Callback for notifying that the suggested device has been updated. */ default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {} /** Callback for notifying that the suggested device list has been updated. */ default void onDeviceSuggestionsUpdated( @NonNull List<SuggestedDeviceInfo> deviceSuggestions) { } /** Callback for notifying that connection to suggested device is finished. */ default void onConnectSuggestedDeviceFinished( @NonNull SuggestedDeviceState suggestedDeviceState, boolean success) { } /** Callback for notifying that connection to suggested device is started. */ default void onConnectionAttemptedForSuggestion( @NonNull SuggestedDeviceState suggestedDeviceState) { } } /** Loading Loading @@ -951,8 +982,8 @@ public class LocalMediaManager implements BluetoothCallback { stopScan(); Log.i(TAG, "Suggestion: scan stopped. success = " + mDidAttemptCompleteSuccessfully); mInfoMediaManager.onConnectionAttemptCompletedForSuggestion( mSuggestedDeviceState, mDidAttemptCompleteSuccessfully); dispatchConnectSuggestedDeviceFinished(mSuggestedDeviceState, mDidAttemptCompleteSuccessfully); }; } Loading @@ -968,7 +999,7 @@ public class LocalMediaManager implements BluetoothCallback { startScan(); mConnectSuggestedDeviceHandler.postDelayed( mConnectionAttemptFinishedRunnable, SCAN_DURATION_MS); mInfoMediaManager.onConnectionAttemptedForSuggestion(mSuggestedDeviceState); dispatchConnectionAttemptedForSuggestion(mSuggestedDeviceState); } } } packages/SettingsLib/src/com/android/settingslib/media/SuggestedDeviceManager.kt 0 → 100644 +213 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 import android.media.RoutingChangeInfo import android.media.SuggestedDeviceInfo import android.util.Log import androidx.annotation.GuardedBy import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED import java.util.concurrent.CopyOnWriteArraySet private const val TAG = "SuggestedDeviceManager" /** * Provides data to render and handles user interactions for the suggested device chip within the * Android Media Controls. * * This class exposes the [SuggestedDeviceState] which is calculated based on: * - Lists of device suggestions and media routes (media devices) provided by the Media Router. * - User interactions with the suggested device chip. * - The results of user-initiated connection attempts to these devices. */ class SuggestedDeviceManager(private val localMediaManager: LocalMediaManager) { private val lock: Any = Object() private val listeners = CopyOnWriteArraySet<Listener>() @GuardedBy("lock") private var mediaDevices: List<MediaDevice> = listOf() @GuardedBy("lock") private var suggestions: List<SuggestedDeviceInfo> = listOf() @GuardedBy("lock") private var suggestedDeviceState: SuggestedDeviceState? = null private val localMediaManagerDeviceCallback = object : LocalMediaManager.DeviceCallback { override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) { val stateChanged = synchronized(lock) { mediaDevices = newDevices?.toList() ?: listOf() updateSuggestedDeviceStateLocked() } if (stateChanged) { dispatchOnSuggestedDeviceUpdated() } } override fun onDeviceSuggestionsUpdated(newSuggestions: List<SuggestedDeviceInfo>) { val stateChanged = synchronized(lock) { suggestions = newSuggestions updateSuggestedDeviceStateLocked() } if (stateChanged) { dispatchOnSuggestedDeviceUpdated() } } override fun onConnectionAttemptedForSuggestion( newSuggestedDeviceState: SuggestedDeviceState ) { synchronized(lock) { if (!isCurrentSuggestion(newSuggestedDeviceState.suggestedDeviceInfo)) { Log.w(TAG, "onConnectionAttemptedForSuggestion. Suggestion got changed.") return } if ( suggestedDeviceState?.connectionState != STATE_DISCONNECTED && suggestedDeviceState?.connectionState != STATE_CONNECTING_FAILED ) { return } suggestedDeviceState = suggestedDeviceState?.copy(connectionState = STATE_CONNECTING) } dispatchOnSuggestedDeviceUpdated() } override fun onConnectSuggestedDeviceFinished( newSuggestedDeviceState: SuggestedDeviceState, success: Boolean, ) { if (!isCurrentSuggestion(newSuggestedDeviceState.suggestedDeviceInfo)) { Log.w(TAG, "onConnectSuggestedDeviceFinished. Suggestion got changed.") return } synchronized(lock) { val connectionState = if (success) STATE_CONNECTED else STATE_CONNECTING_FAILED suggestedDeviceState = suggestedDeviceState?.copy(connectionState = connectionState) } dispatchOnSuggestedDeviceUpdated() } } fun addListener(listener: Listener) { val shouldRegisterCallback = synchronized(lock) { val wasSetEmpty = listeners.isEmpty() listeners.add(listener) wasSetEmpty } if (shouldRegisterCallback) { eagerlyUpdateState() localMediaManager.registerCallback(localMediaManagerDeviceCallback) } } fun removeListener(listener: Listener) { val shouldUnregisterCallback = synchronized(lock) { listeners.remove(listener) listeners.isEmpty() } if (shouldUnregisterCallback) { localMediaManager.unregisterCallback(localMediaManagerDeviceCallback) } } fun requestDeviceSuggestion() { localMediaManager.requestDeviceSuggestion() } fun getSuggestedDevice(): SuggestedDeviceState? { if (listeners.isEmpty()) { // If there were no callbacks set, recalculate the state before returning the result. eagerlyUpdateState() } return suggestedDeviceState } fun connectSuggestedDevice( suggestedDeviceState: SuggestedDeviceState, routingChangeInfo: RoutingChangeInfo, ) { if (!isCurrentSuggestion(suggestedDeviceState.suggestedDeviceInfo)) { Log.w(TAG, "Suggestion got changed, aborting connection.") return } localMediaManager.connectSuggestedDevice(suggestedDeviceState, routingChangeInfo) } private fun eagerlyUpdateState() { synchronized(lock) { mediaDevices = localMediaManager.mediaDevices suggestions = localMediaManager.suggestions updateSuggestedDeviceStateLocked() } } @GuardedBy("lock") private fun updateSuggestedDeviceStateLocked(): Boolean { var newSuggestedDeviceState: SuggestedDeviceState? = null val previousState = suggestedDeviceState val topSuggestion = suggestions.firstOrNull() if (topSuggestion != null) { val matchedDevice = getDeviceById(mediaDevices, topSuggestion.routeId) if (matchedDevice != null) { newSuggestedDeviceState = SuggestedDeviceState(topSuggestion, matchedDevice.state) } if (newSuggestedDeviceState == null) { if (previousState != null && (topSuggestion.routeId == previousState.suggestedDeviceInfo.routeId)) { return false } newSuggestedDeviceState = SuggestedDeviceState(topSuggestion) } } if (newSuggestedDeviceState != null && isSuggestedDeviceSelected(newSuggestedDeviceState)) { newSuggestedDeviceState = null } if (previousState != newSuggestedDeviceState) { synchronized(lock) { suggestedDeviceState = newSuggestedDeviceState } return true } return false } private fun isSuggestedDeviceSelected(newSuggestedDeviceState: SuggestedDeviceState): Boolean { synchronized(lock) { return mediaDevices.any { device -> device.isSelected() && device.getId() == newSuggestedDeviceState.suggestedDeviceInfo.routeId } } } private fun getDeviceById(mediaDevices: List<MediaDevice>, routeId: String): MediaDevice? = mediaDevices.find { it.id == routeId } private fun isCurrentSuggestion(suggestedDeviceInfo: SuggestedDeviceInfo) = synchronized(lock) { suggestedDeviceState?.suggestedDeviceInfo?.routeId == suggestedDeviceInfo.routeId } private fun dispatchOnSuggestedDeviceUpdated() { val state = synchronized(lock) { suggestedDeviceState } Log.i(TAG, "dispatchOnSuggestedDeviceUpdated(), state: $state") listeners.forEach { it.onSuggestedDeviceStateUpdated(state) } } interface Listener { fun onSuggestedDeviceStateUpdated(state: SuggestedDeviceState?) } } packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +7 −263 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +2 −9 Original line number Diff line number Diff line Loading @@ -643,7 +643,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_deviceIsDiscovered_immediatelyConnects() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1); Loading @@ -657,7 +656,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_deviceIsNotDiscovered_scanStarted() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_2); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1); Loading @@ -671,7 +669,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_deviceDiscoveredAfter_connects() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2); Loading @@ -687,7 +684,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_handlerTimesOut_completesConnectionAttempt() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2); Loading @@ -701,13 +697,11 @@ public class LocalMediaManagerTest { ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); verify(mInfoMediaManager) .onConnectionAttemptCompletedForSuggestion(mSuggestedDeviceState, false); verify(mCallback).onConnectSuggestedDeviceFinished(mSuggestedDeviceState, false); } @Test public void connectSuggestedDevice_connectionSuccess_completesConnectionAttempt() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2); Loading @@ -721,8 +715,7 @@ public class LocalMediaManagerTest { mLocalMediaManager.dispatchSelectedDeviceStateChanged(mInfoMediaDevice1, LocalMediaManager.MediaDeviceState.STATE_CONNECTED); verify(mInfoMediaManager) .onConnectionAttemptCompletedForSuggestion(mSuggestedDeviceState, true); verify(mCallback).onConnectSuggestedDeviceFinished(mSuggestedDeviceState, true); } @Test Loading Loading
packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +21 −129 Original line number Diff line number Diff line Loading @@ -18,10 +18,7 @@ package com.android.settingslib.media; import static android.media.MediaRoute2Info.CONNECTION_STATE_CONNECTING; import static android.media.session.MediaController.PlaybackInfo; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED; import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; import static com.android.settingslib.media.MediaDeviceUtilKt.isBluetoothMediaDevice; import static com.android.settingslib.media.MediaDeviceUtilKt.isComplexMediaDevice; Loading Loading @@ -64,7 +61,6 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; Loading Loading @@ -112,9 +108,14 @@ public abstract class InfoMediaManager { */ void onRequestFailed(int reason); /** Callback for notifying that the suggested device has been updated. */ default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState suggestedDevice) {} ; /** * Callback for changes to the suggested device list. * * @param deviceSuggestions the list of suggested devices. */ default void onDeviceSuggestionsUpdated( @NonNull List<SuggestedDeviceInfo> deviceSuggestions) { } } Loading @@ -141,8 +142,6 @@ public abstract class InfoMediaManager { private final LocalBluetoothManager mBluetoothManager; @GuardedBy("mLock") private final Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceMap = new HashMap<>(); @GuardedBy("mLock") @Nullable private SuggestedDeviceState mSuggestedDeviceState; private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); Loading Loading @@ -286,7 +285,7 @@ public abstract class InfoMediaManager { protected final void rebuildDeviceList() { buildAvailableRoutes(); updateDeviceSuggestion(); updateMediaDevicesSuggestionState(); } protected final void notifyCurrentConnectedDeviceChanged() { Loading Loading @@ -608,13 +607,6 @@ public abstract class InfoMediaManager { return getActiveRoutingSession().getName(); } @Nullable public SuggestedDeviceState getSuggestedDevice() { synchronized (mLock) { return mSuggestedDeviceState; } } /** Requests a suggestion from other routers. */ public abstract void requestDeviceSuggestion(); Loading @@ -626,6 +618,9 @@ public abstract class InfoMediaManager { protected void notifyDeviceSuggestionUpdated( String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestions) { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return; } synchronized (mLock) { if (suggestions == null) { mSuggestedDeviceMap.remove(suggestingPackageName); Loading @@ -633,116 +628,21 @@ public abstract class InfoMediaManager { mSuggestedDeviceMap.put(suggestingPackageName, suggestions); } } updateDeviceSuggestion(); } private void updateDeviceSuggestion() { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return; } if (updateSuggestedDeviceState()) { dispatchOnSuggestedDeviceUpdated(); } if (updateMediaDevicesSuggestionState()) { dispatchDeviceListAdded(getMediaDevices()); } dispatchOnDeviceSuggestionsUpdated(); } private boolean updateSuggestedDeviceState() { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return false; } SuggestedDeviceInfo topSuggestion = null; SuggestedDeviceState newSuggestedDeviceState = null; SuggestedDeviceState previousState = getSuggestedDevice(); List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null && !suggestions.isEmpty()) { topSuggestion = suggestions.get(0); } if (topSuggestion != null) { synchronized (mLock) { for (MediaDevice device : mMediaDevices) { if (Objects.equals(device.getId(), topSuggestion.getRouteId())) { newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion, device.getState()); break; } } } if (newSuggestedDeviceState == null) { if (previousState != null && topSuggestion .getRouteId() .equals(previousState.getSuggestedDeviceInfo().getRouteId())) { return false; } newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion); } } if (newSuggestedDeviceState != null && isSuggestedDeviceSelected(newSuggestedDeviceState)) { newSuggestedDeviceState = null; } if (!Objects.equals(previousState, newSuggestedDeviceState)) { synchronized (mLock) { mSuggestedDeviceState = newSuggestedDeviceState; } return true; } return false; } private boolean isSuggestedDeviceSelected( @NonNull SuggestedDeviceState newSuggestedDeviceState) { synchronized (mLock) { return mMediaDevices.stream().anyMatch(device -> device.isSelected() && Objects.equals( device.getId(), newSuggestedDeviceState .getSuggestedDeviceInfo() .getRouteId())); } } final void onConnectionAttemptedForSuggestion(@NonNull SuggestedDeviceState suggestion) { synchronized (mLock) { if (!Objects.equals(suggestion, mSuggestedDeviceState)) { return; } if (mSuggestedDeviceState.getConnectionState() != STATE_DISCONNECTED && mSuggestedDeviceState.getConnectionState() != STATE_CONNECTING_FAILED) { return; } mSuggestedDeviceState = new SuggestedDeviceState( mSuggestedDeviceState.getSuggestedDeviceInfo(), STATE_CONNECTING); } dispatchOnSuggestedDeviceUpdated(); } final void onConnectionAttemptCompletedForSuggestion( @NonNull SuggestedDeviceState suggestion, boolean success) { synchronized (mLock) { if (!Objects.equals(suggestion, mSuggestedDeviceState)) { return; } int state = success ? STATE_CONNECTED : STATE_CONNECTING_FAILED; mSuggestedDeviceState = new SuggestedDeviceState(mSuggestedDeviceState.getSuggestedDeviceInfo(), state); } dispatchOnSuggestedDeviceUpdated(); } private void dispatchOnSuggestedDeviceUpdated() { SuggestedDeviceState state = getSuggestedDevice(); Log.i(TAG, "dispatchOnSuggestedDeviceUpdated(), state: " + state); private void dispatchOnDeviceSuggestionsUpdated() { Log.i(TAG, "dispatchDeviceSuggestionsUpdated()"); for (MediaDeviceCallback callback : mCallbacks) { callback.onSuggestedDeviceUpdated(state); callback.onDeviceSuggestionsUpdated(getSuggestions()); } } @Nullable private List<SuggestedDeviceInfo> getSuggestions() { @NonNull List<SuggestedDeviceInfo> getSuggestions() { // Give suggestions in the following order // 1. Suggestions from the local router // 2. Suggestions from the proxy router if only one proxy router is providing suggestions Loading @@ -760,7 +660,7 @@ public abstract class InfoMediaManager { } } } return null; return List.of(); } // Go through all current MediaDevices, and update the ones that are suggested. Loading @@ -770,12 +670,9 @@ public abstract class InfoMediaManager { } Set<String> suggestedDevices = new HashSet<>(); // Prioritize suggestions from the package, otherwise pick any. List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null) { for (SuggestedDeviceInfo suggestion : suggestions) { for (SuggestedDeviceInfo suggestion : getSuggestions()) { suggestedDevices.add(suggestion.getRouteId()); } } boolean didUpdate = false; synchronized (mLock) { for (MediaDevice device : mMediaDevices) { Loading Loading @@ -947,11 +844,6 @@ public abstract class InfoMediaManager { return; } device.setState(state); if (device.isSuggestedDevice()) { if (updateSuggestedDeviceState()) { dispatchOnSuggestedDeviceUpdated(); } } } @RequiresApi(34) Loading
packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +50 −19 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import android.media.AudioManager; import android.media.RoutingChangeInfo; import android.media.RoutingChangeInfo.EntryPoint; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.os.Build; import android.os.Handler; import android.text.TextUtils; Loading @@ -55,7 +56,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; /** Loading Loading @@ -255,11 +255,6 @@ public class LocalMediaManager implements BluetoothCallback { if (mConnectingSuggestedDeviceState != null) { return; } SuggestedDeviceState currentSuggestion = mInfoMediaManager.getSuggestedDevice(); if (!Objects.equals(suggestion, currentSuggestion)) { Log.w(TAG, "Suggestion got changed, aborting connection."); return; } for (MediaDevice device : mMediaDevices) { if (suggestion.getSuggestedDeviceInfo().getRouteId().equals(device.getId())) { Log.i(TAG, "Suggestion: device is available, connecting. deviceId = " Loading @@ -270,7 +265,7 @@ public class LocalMediaManager implements BluetoothCallback { } mConnectingSuggestedDeviceState = new ConnectingSuggestedDeviceState( currentSuggestion, routingChangeInfo.getEntryPoint()); suggestion, routingChangeInfo.getEntryPoint()); mConnectingSuggestedDeviceState.tryConnect(); } } Loading @@ -292,9 +287,9 @@ public class LocalMediaManager implements BluetoothCallback { } } @Nullable public SuggestedDeviceState getSuggestedDevice() { return mInfoMediaManager.getSuggestedDevice(); @NonNull public List<SuggestedDeviceInfo> getSuggestions() { return mInfoMediaManager.getSuggestions(); } void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { Loading Loading @@ -356,9 +351,21 @@ public class LocalMediaManager implements BluetoothCallback { } } void dispatchOnSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) { void dispatchDeviceSuggestionsUpdated(List<SuggestedDeviceInfo> deviceSuggestions) { for (DeviceCallback callback : getCallbacks()) { callback.onDeviceSuggestionsUpdated(deviceSuggestions); } } void dispatchConnectSuggestedDeviceFinished(SuggestedDeviceState state, boolean success) { for (DeviceCallback callback : getCallbacks()) { callback.onConnectSuggestedDeviceFinished(state, success); } } void dispatchConnectionAttemptedForSuggestion(SuggestedDeviceState state) { for (DeviceCallback callback : getCallbacks()) { callback.onSuggestedDeviceUpdated(device); callback.onConnectionAttemptedForSuggestion(state); } } Loading Loading @@ -410,6 +417,17 @@ public class LocalMediaManager implements BluetoothCallback { return null; } /** * Returns a list of MediaDevice objects. * * @return a list of media devices */ public List<MediaDevice> getMediaDevices() { synchronized (mMediaDevicesLock) { return new ArrayList<>(mMediaDevices); } } /** * Find the current connected MediaDevice. * Loading Loading @@ -769,8 +787,9 @@ public class LocalMediaManager implements BluetoothCallback { } @Override public void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) { dispatchOnSuggestedDeviceUpdated(device); public void onDeviceSuggestionsUpdated( @NonNull List<SuggestedDeviceInfo> deviceSuggestions) { dispatchDeviceSuggestionsUpdated(deviceSuggestions); } } Loading Loading @@ -850,8 +869,20 @@ public class LocalMediaManager implements BluetoothCallback { */ default void onAboutToConnectDeviceRemoved() {} /** Callback for notifying that the suggested device has been updated. */ default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {} /** Callback for notifying that the suggested device list has been updated. */ default void onDeviceSuggestionsUpdated( @NonNull List<SuggestedDeviceInfo> deviceSuggestions) { } /** Callback for notifying that connection to suggested device is finished. */ default void onConnectSuggestedDeviceFinished( @NonNull SuggestedDeviceState suggestedDeviceState, boolean success) { } /** Callback for notifying that connection to suggested device is started. */ default void onConnectionAttemptedForSuggestion( @NonNull SuggestedDeviceState suggestedDeviceState) { } } /** Loading Loading @@ -951,8 +982,8 @@ public class LocalMediaManager implements BluetoothCallback { stopScan(); Log.i(TAG, "Suggestion: scan stopped. success = " + mDidAttemptCompleteSuccessfully); mInfoMediaManager.onConnectionAttemptCompletedForSuggestion( mSuggestedDeviceState, mDidAttemptCompleteSuccessfully); dispatchConnectSuggestedDeviceFinished(mSuggestedDeviceState, mDidAttemptCompleteSuccessfully); }; } Loading @@ -968,7 +999,7 @@ public class LocalMediaManager implements BluetoothCallback { startScan(); mConnectSuggestedDeviceHandler.postDelayed( mConnectionAttemptFinishedRunnable, SCAN_DURATION_MS); mInfoMediaManager.onConnectionAttemptedForSuggestion(mSuggestedDeviceState); dispatchConnectionAttemptedForSuggestion(mSuggestedDeviceState); } } }
packages/SettingsLib/src/com/android/settingslib/media/SuggestedDeviceManager.kt 0 → 100644 +213 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 import android.media.RoutingChangeInfo import android.media.SuggestedDeviceInfo import android.util.Log import androidx.annotation.GuardedBy import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED import java.util.concurrent.CopyOnWriteArraySet private const val TAG = "SuggestedDeviceManager" /** * Provides data to render and handles user interactions for the suggested device chip within the * Android Media Controls. * * This class exposes the [SuggestedDeviceState] which is calculated based on: * - Lists of device suggestions and media routes (media devices) provided by the Media Router. * - User interactions with the suggested device chip. * - The results of user-initiated connection attempts to these devices. */ class SuggestedDeviceManager(private val localMediaManager: LocalMediaManager) { private val lock: Any = Object() private val listeners = CopyOnWriteArraySet<Listener>() @GuardedBy("lock") private var mediaDevices: List<MediaDevice> = listOf() @GuardedBy("lock") private var suggestions: List<SuggestedDeviceInfo> = listOf() @GuardedBy("lock") private var suggestedDeviceState: SuggestedDeviceState? = null private val localMediaManagerDeviceCallback = object : LocalMediaManager.DeviceCallback { override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) { val stateChanged = synchronized(lock) { mediaDevices = newDevices?.toList() ?: listOf() updateSuggestedDeviceStateLocked() } if (stateChanged) { dispatchOnSuggestedDeviceUpdated() } } override fun onDeviceSuggestionsUpdated(newSuggestions: List<SuggestedDeviceInfo>) { val stateChanged = synchronized(lock) { suggestions = newSuggestions updateSuggestedDeviceStateLocked() } if (stateChanged) { dispatchOnSuggestedDeviceUpdated() } } override fun onConnectionAttemptedForSuggestion( newSuggestedDeviceState: SuggestedDeviceState ) { synchronized(lock) { if (!isCurrentSuggestion(newSuggestedDeviceState.suggestedDeviceInfo)) { Log.w(TAG, "onConnectionAttemptedForSuggestion. Suggestion got changed.") return } if ( suggestedDeviceState?.connectionState != STATE_DISCONNECTED && suggestedDeviceState?.connectionState != STATE_CONNECTING_FAILED ) { return } suggestedDeviceState = suggestedDeviceState?.copy(connectionState = STATE_CONNECTING) } dispatchOnSuggestedDeviceUpdated() } override fun onConnectSuggestedDeviceFinished( newSuggestedDeviceState: SuggestedDeviceState, success: Boolean, ) { if (!isCurrentSuggestion(newSuggestedDeviceState.suggestedDeviceInfo)) { Log.w(TAG, "onConnectSuggestedDeviceFinished. Suggestion got changed.") return } synchronized(lock) { val connectionState = if (success) STATE_CONNECTED else STATE_CONNECTING_FAILED suggestedDeviceState = suggestedDeviceState?.copy(connectionState = connectionState) } dispatchOnSuggestedDeviceUpdated() } } fun addListener(listener: Listener) { val shouldRegisterCallback = synchronized(lock) { val wasSetEmpty = listeners.isEmpty() listeners.add(listener) wasSetEmpty } if (shouldRegisterCallback) { eagerlyUpdateState() localMediaManager.registerCallback(localMediaManagerDeviceCallback) } } fun removeListener(listener: Listener) { val shouldUnregisterCallback = synchronized(lock) { listeners.remove(listener) listeners.isEmpty() } if (shouldUnregisterCallback) { localMediaManager.unregisterCallback(localMediaManagerDeviceCallback) } } fun requestDeviceSuggestion() { localMediaManager.requestDeviceSuggestion() } fun getSuggestedDevice(): SuggestedDeviceState? { if (listeners.isEmpty()) { // If there were no callbacks set, recalculate the state before returning the result. eagerlyUpdateState() } return suggestedDeviceState } fun connectSuggestedDevice( suggestedDeviceState: SuggestedDeviceState, routingChangeInfo: RoutingChangeInfo, ) { if (!isCurrentSuggestion(suggestedDeviceState.suggestedDeviceInfo)) { Log.w(TAG, "Suggestion got changed, aborting connection.") return } localMediaManager.connectSuggestedDevice(suggestedDeviceState, routingChangeInfo) } private fun eagerlyUpdateState() { synchronized(lock) { mediaDevices = localMediaManager.mediaDevices suggestions = localMediaManager.suggestions updateSuggestedDeviceStateLocked() } } @GuardedBy("lock") private fun updateSuggestedDeviceStateLocked(): Boolean { var newSuggestedDeviceState: SuggestedDeviceState? = null val previousState = suggestedDeviceState val topSuggestion = suggestions.firstOrNull() if (topSuggestion != null) { val matchedDevice = getDeviceById(mediaDevices, topSuggestion.routeId) if (matchedDevice != null) { newSuggestedDeviceState = SuggestedDeviceState(topSuggestion, matchedDevice.state) } if (newSuggestedDeviceState == null) { if (previousState != null && (topSuggestion.routeId == previousState.suggestedDeviceInfo.routeId)) { return false } newSuggestedDeviceState = SuggestedDeviceState(topSuggestion) } } if (newSuggestedDeviceState != null && isSuggestedDeviceSelected(newSuggestedDeviceState)) { newSuggestedDeviceState = null } if (previousState != newSuggestedDeviceState) { synchronized(lock) { suggestedDeviceState = newSuggestedDeviceState } return true } return false } private fun isSuggestedDeviceSelected(newSuggestedDeviceState: SuggestedDeviceState): Boolean { synchronized(lock) { return mediaDevices.any { device -> device.isSelected() && device.getId() == newSuggestedDeviceState.suggestedDeviceInfo.routeId } } } private fun getDeviceById(mediaDevices: List<MediaDevice>, routeId: String): MediaDevice? = mediaDevices.find { it.id == routeId } private fun isCurrentSuggestion(suggestedDeviceInfo: SuggestedDeviceInfo) = synchronized(lock) { suggestedDeviceState?.suggestedDeviceInfo?.routeId == suggestedDeviceInfo.routeId } private fun dispatchOnSuggestedDeviceUpdated() { val state = synchronized(lock) { suggestedDeviceState } Log.i(TAG, "dispatchOnSuggestedDeviceUpdated(), state: $state") listeners.forEach { it.onSuggestedDeviceStateUpdated(state) } } interface Listener { fun onSuggestedDeviceStateUpdated(state: SuggestedDeviceState?) } }
packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +7 −263 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +2 −9 Original line number Diff line number Diff line Loading @@ -643,7 +643,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_deviceIsDiscovered_immediatelyConnects() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1); Loading @@ -657,7 +656,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_deviceIsNotDiscovered_scanStarted() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_2); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1); Loading @@ -671,7 +669,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_deviceDiscoveredAfter_connects() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2); Loading @@ -687,7 +684,6 @@ public class LocalMediaManagerTest { @Test public void connectSuggestedDevice_handlerTimesOut_completesConnectionAttempt() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2); Loading @@ -701,13 +697,11 @@ public class LocalMediaManagerTest { ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); verify(mInfoMediaManager) .onConnectionAttemptCompletedForSuggestion(mSuggestedDeviceState, false); verify(mCallback).onConnectSuggestedDeviceFinished(mSuggestedDeviceState, false); } @Test public void connectSuggestedDevice_connectionSuccess_completesConnectionAttempt() { when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState); when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1); mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2); Loading @@ -721,8 +715,7 @@ public class LocalMediaManagerTest { mLocalMediaManager.dispatchSelectedDeviceStateChanged(mInfoMediaDevice1, LocalMediaManager.MediaDeviceState.STATE_CONNECTED); verify(mInfoMediaManager) .onConnectionAttemptCompletedForSuggestion(mSuggestedDeviceState, true); verify(mCallback).onConnectSuggestedDeviceFinished(mSuggestedDeviceState, true); } @Test Loading