Loading packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +167 −0 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ import android.content.Context; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.media.session.MediaController; import android.media.session.MediaSession; import android.os.Build; Loading @@ -79,6 +80,7 @@ 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.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; Loading Loading @@ -124,6 +126,59 @@ public abstract class InfoMediaManager { * android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, */ void onRequestFailed(int reason); /** Callback for notifying that the suggested device has been updated. */ default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState suggestedDevice) {} ; } /** * Wrapper class around SuggestedDeviceInfo and the corresponsing connection state of the * suggestion. */ public class SuggestedDeviceState { private final SuggestedDeviceInfo mSuggestedDeviceInfo; private final @LocalMediaManager.MediaDeviceState int mConnectionState; private SuggestedDeviceState(@NonNull SuggestedDeviceInfo suggestedDeviceInfo) { mSuggestedDeviceInfo = suggestedDeviceInfo; mConnectionState = LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED; } private SuggestedDeviceState( @NonNull SuggestedDeviceInfo suggestedDeviceInfo, @LocalMediaManager.MediaDeviceState int state) { mSuggestedDeviceInfo = suggestedDeviceInfo; mConnectionState = state; } @NonNull public SuggestedDeviceInfo getSuggestedDeviceInfo() { return mSuggestedDeviceInfo; } public @LocalMediaManager.MediaDeviceState int getConnectionState() { return mConnectionState; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof SuggestedDeviceState)) { return false; } return Objects.equals( mSuggestedDeviceInfo, ((SuggestedDeviceState) obj).mSuggestedDeviceInfo) && Objects.equals( mConnectionState, ((SuggestedDeviceState) obj).mConnectionState); } @Override public int hashCode() { return Objects.hash(mSuggestedDeviceInfo, mConnectionState); } } /** Checked exception that signals the specified package is not present in the system. */ Loading @@ -146,6 +201,9 @@ public abstract class InfoMediaManager { private final LocalBluetoothManager mBluetoothManager; private final Map<String, RouteListingPreference.Item> mPreferenceItemMap = new ConcurrentHashMap<>(); private final Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceMap = new ConcurrentHashMap<>(); @Nullable private SuggestedDeviceState mSuggestedDeviceState; private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); Loading Loading @@ -629,6 +687,114 @@ public abstract class InfoMediaManager { || sessionInfo.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED; } protected void updateDeviceSuggestion( String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestions) { if (suggestions == null) { mSuggestedDeviceMap.remove(suggestingPackageName); } else { mSuggestedDeviceMap.put(suggestingPackageName, suggestions); } updateDeviceSuggestion(); } protected final void updateDeviceSuggestion() { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return; } SuggestedDeviceInfo topSuggestion = null; SuggestedDeviceState newSuggestedDeviceState = null; SuggestedDeviceState previousState = mSuggestedDeviceState; List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null && !suggestions.isEmpty()) { topSuggestion = suggestions.get(0); } if (topSuggestion != null) { for (MediaDevice device : mMediaDevices) { if (Objects.equals(device.getId(), topSuggestion.getRouteId())) { newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion, device.getState()); break; } } if (newSuggestedDeviceState == null) { newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion); } } if (updateMediaDevicesSuggestionState()) { dispatchDeviceListAdded(mMediaDevices); } if (!Objects.equals(previousState, newSuggestedDeviceState)) { mSuggestedDeviceState = newSuggestedDeviceState; for (MediaDeviceCallback callback : getCallbacks()) { callback.onSuggestedDeviceUpdated(mSuggestedDeviceState); } } } @Nullable private 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 // 3. No suggestion at all if multiple proxy routers are providing suggestions. List<SuggestedDeviceInfo> suggestions = mSuggestedDeviceMap.get(mPackageName); if (suggestions != null) { return suggestions; } if (mSuggestedDeviceMap.size() == 1) { for (List<SuggestedDeviceInfo> packageSuggestions : mSuggestedDeviceMap.values()) { if (packageSuggestions != null) { return packageSuggestions; } } } return null; } // Go through all current MediaDevices, and update the ones that are suggested. private boolean updateMediaDevicesSuggestionState() { Set<String> suggestedDevices = new HashSet<>(); // Prioritize suggestions from the package, otherwise pick any. List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null) { for (SuggestedDeviceInfo suggestion : suggestions) { suggestedDevices.add(suggestion.getRouteId()); } } boolean didUpdate = false; for (MediaDevice device : mMediaDevices) { if (device.isSuggestedDevice()) { if (!suggestedDevices.contains(device.getId())) { device.setIsSuggested(false); // Case 1: Device was suggested only by setDeviceSuggestions(), and has been // updated to no longer be suggested. if (!device.isSuggestedByRouteListingPreferences()) { didUpdate = true; } // Case 2: Device was suggested by both setDeviceSuggestions() and RLP. Since // it's still suggested by RLP, no update. } else { // Case 3: Device was suggested (either by RLP or by setDeviceSuggestions()), // and should still be suggested. device.setIsSuggested(true); } } else { if (suggestedDevices.contains(device.getId())) { // Case 4: Device was not suggested by either RLP or setDeviceSuggestions() but // is now suggested. device.setIsSuggested(true); didUpdate = true; } else { // Case 5: Device was not suggested by either RLP or setDeviceSuggestions() and // is still not suggested. device.setIsSuggested(false); } } } return didUpdate; } protected final synchronized void refreshDevices() { rebuildDeviceList(); dispatchDeviceListAdded(mMediaDevices); Loading @@ -653,6 +819,7 @@ public abstract class InfoMediaManager { // First device on the list is always the first selected route. mCurrentConnectedDevice = mMediaDevices.get(0); } updateDeviceSuggestion(); } private synchronized List<MediaRoute2Info> getAvailableRoutes( Loading packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +22 −1 Original line number Diff line number Diff line Loading @@ -126,6 +126,7 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { protected final Context mContext; protected final MediaRoute2Info mRouteInfo; protected final RouteListingPreference.Item mItem; private boolean mIsSuggested; MediaDevice( @NonNull Context context, Loading Loading @@ -313,11 +314,26 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { } /** * Checks if device is suggested device from application * Checks if device is suggested device from application. A device can be suggested through * either RouteListingPreferences or through MediaRouter2#setDeviceSuggestions. * * <p>Prioritization and conflict resolution between the two APIs is as follows: - Suggestions * from both RLP and the new API will be visible in OSw - Only suggestions from the new API will * be visible in both OSw and new UI surfaces such as UMO - If suggestions are provided from * local and proxy routers, priority will be given to the local router * * @return true if device is suggested device */ public boolean isSuggestedDevice() { return mIsSuggested || isSuggestedByRouteListingPreferences(); } /** * Checks if the device is suggested from the application's RouteListingPreferences * * @return true if the device is suggested */ public boolean isSuggestedByRouteListingPreferences() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && Api34Impl.isSuggestedDevice(mItem); } Loading Loading @@ -434,6 +450,11 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { return mState; } /** Sets whether the current device is suggested. */ public void setIsSuggested(boolean suggested) { mIsSuggested = suggested; } /** * Rules: * 1. If there is one of the connected devices identified as a carkit or fast pair device, Loading packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +31 −0 Original line number Diff line number Diff line Loading @@ -20,11 +20,13 @@ import android.annotation.SuppressLint; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2; import android.media.MediaRouter2.DeviceSuggestionsCallback; import android.media.MediaRouter2.RoutingController; import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.media.session.MediaController; import android.os.UserHandle; import android.text.TextUtils; Loading @@ -33,6 +35,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.media.flags.Flags; import com.android.settingslib.bluetooth.LocalBluetoothManager; Loading Loading @@ -64,6 +67,18 @@ public final class RouterInfoMediaManager extends InfoMediaManager { notifyRouteListingPreferenceUpdated(preference); refreshDevices(); }; private final DeviceSuggestionsCallback mDeviceSuggestionsCallback = new DeviceSuggestionsCallback() { @Override public void onSuggestionUpdated( String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) { updateDeviceSuggestion(suggestingPackageName, suggestedDeviceInfo); } @Override public void onSuggestionRequested() {} // no-op }; @GuardedBy("this") @Nullable Loading Loading @@ -100,6 +115,20 @@ public final class RouterInfoMediaManager extends InfoMediaManager { mRouterManager = MediaRouter2Manager.getInstance(context); } @VisibleForTesting RouterInfoMediaManager( Context context, @NonNull String packageName, @NonNull UserHandle userHandle, LocalBluetoothManager localBluetoothManager, @Nullable MediaController mediaController, MediaRouter2 mediaRouter2, MediaRouter2Manager mediaRouter2Manager) { super(context, packageName, userHandle, localBluetoothManager, mediaController); mRouter = mediaRouter2; mRouterManager = mediaRouter2Manager; } @Override protected void startScanOnRouter() { if (Flags.enableScreenOffScanning()) { Loading @@ -120,6 +149,7 @@ public final class RouterInfoMediaManager extends InfoMediaManager { mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY); mRouter.registerRouteListingPreferenceUpdatedCallback( mExecutor, mRouteListingPreferenceCallback); mRouter.registerDeviceSuggestionsCallback(mExecutor, mDeviceSuggestionsCallback); mRouter.registerTransferCallback(mExecutor, mTransferCallback); mRouter.registerControllerCallback(mExecutor, mControllerCallback); } Loading @@ -143,6 +173,7 @@ public final class RouterInfoMediaManager extends InfoMediaManager { mRouter.unregisterControllerCallback(mControllerCallback); mRouter.unregisterTransferCallback(mTransferCallback); mRouter.unregisterRouteListingPreferenceUpdatedCallback(mRouteListingPreferenceCallback); mRouter.unregisterDeviceSuggestionsCallback(mDeviceSuggestionsCallback); mRouter.unregisterRouteCallback(mRouteCallback); } Loading packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +203 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; Loading @@ -43,9 +44,13 @@ import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2; import android.media.MediaRouter2.DeviceSuggestionsCallback; import android.media.MediaRouter2.RoutingController; import android.media.MediaRouter2Manager; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.media.session.MediaSessionManager; import android.os.Build; import android.platform.test.annotations.EnableFlags; Loading @@ -64,6 +69,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; Loading Loading @@ -141,6 +148,12 @@ public class InfoMediaManagerTest { private MediaSessionManager mMediaSessionManager; @Mock private ComponentName mComponentName; @Mock private MediaRouter2 mRouter2; @Mock private RoutingController mRoutingController; @Captor private ArgumentCaptor<DeviceSuggestionsCallback> mDeviceSuggestionsCallbackCaptor; @Captor private ArgumentCaptor<InfoMediaManager.SuggestedDeviceState> mSuggestedDeviceStateCaptor; private ManagerInfoMediaManager mInfoMediaManager; private Context mContext; Loading @@ -161,6 +174,9 @@ public class InfoMediaManagerTest { /* mediaController */ null); mShadowRouter2Manager = ShadowRouter2Manager.getShadow(); mInfoMediaManager.mRouterManager = MediaRouter2Manager.getInstance(mContext); when(mRouter2.getController(any())).thenReturn(mRoutingController); when(mRouter2.getControllers()).thenReturn(List.of(mRoutingController)); when(mRoutingController.getRoutingSessionInfo()).thenReturn(TEST_SYSTEM_ROUTING_SESSION); } @Test Loading Loading @@ -445,6 +461,7 @@ public class InfoMediaManagerTest { availableRoutes.add(availableInfo4); when(mRouterManager.getAvailableRoutes(packageName)).thenReturn(availableRoutes); when(mRoutingController.getSelectableRoutes()).thenReturn(availableRoutes); return availableRoutes; } Loading Loading @@ -942,6 +959,192 @@ public class InfoMediaManagerTest { assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device); } private RouterInfoMediaManager createRouterInfoMediaManager() { return new RouterInfoMediaManager( mContext, TEST_PACKAGE_NAME, mContext.getUser(), mLocalBluetoothManager, /* mediaController */ null, mRouter2, mRouterManager); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_listenersNotified() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", List.of(suggestedDeviceInfo)); verify(mCallback).onDeviceListAdded(any()); verify(mCallback).onSuggestedDeviceUpdated(mSuggestedDeviceStateCaptor.capture()); assertThat(mSuggestedDeviceStateCaptor.getValue().getSuggestedDeviceInfo()) .isEqualTo(suggestedDeviceInfo); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_mediaDeviceIsSuggested() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", List.of(suggestedDeviceInfo)); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isTrue(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_noSuggestedDevices_noSuggestedMediaDevices() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", List.of(suggestedDeviceInfo)); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", null); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_multipleProviders_noSuggestedMediaDevices() { SuggestedDeviceInfo suggestedDeviceInfo1 = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); SuggestedDeviceInfo suggestedDeviceInfo2 = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name_2") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name_1", List.of(suggestedDeviceInfo1)); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name_2", List.of(suggestedDeviceInfo2)); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_firstSuggestionFromSamePackage_suggestionIsFromSamePackage() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated(TEST_PACKAGE_NAME, List.of(suggestedDeviceInfo)); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", null); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isTrue(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_laterSuggestionFromSamePackage_suggestionIsFromSamePackage() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", null); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated(TEST_PACKAGE_NAME, List.of(suggestedDeviceInfo)); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isTrue(); } @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void composePreferenceRouteListing_useSystemOrderingIsFalse() { Loading Loading
packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +167 −0 Original line number Diff line number Diff line Loading @@ -55,6 +55,7 @@ import android.content.Context; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.media.session.MediaController; import android.media.session.MediaSession; import android.os.Build; Loading @@ -79,6 +80,7 @@ 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.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; Loading Loading @@ -124,6 +126,59 @@ public abstract class InfoMediaManager { * android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, */ void onRequestFailed(int reason); /** Callback for notifying that the suggested device has been updated. */ default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState suggestedDevice) {} ; } /** * Wrapper class around SuggestedDeviceInfo and the corresponsing connection state of the * suggestion. */ public class SuggestedDeviceState { private final SuggestedDeviceInfo mSuggestedDeviceInfo; private final @LocalMediaManager.MediaDeviceState int mConnectionState; private SuggestedDeviceState(@NonNull SuggestedDeviceInfo suggestedDeviceInfo) { mSuggestedDeviceInfo = suggestedDeviceInfo; mConnectionState = LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED; } private SuggestedDeviceState( @NonNull SuggestedDeviceInfo suggestedDeviceInfo, @LocalMediaManager.MediaDeviceState int state) { mSuggestedDeviceInfo = suggestedDeviceInfo; mConnectionState = state; } @NonNull public SuggestedDeviceInfo getSuggestedDeviceInfo() { return mSuggestedDeviceInfo; } public @LocalMediaManager.MediaDeviceState int getConnectionState() { return mConnectionState; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof SuggestedDeviceState)) { return false; } return Objects.equals( mSuggestedDeviceInfo, ((SuggestedDeviceState) obj).mSuggestedDeviceInfo) && Objects.equals( mConnectionState, ((SuggestedDeviceState) obj).mConnectionState); } @Override public int hashCode() { return Objects.hash(mSuggestedDeviceInfo, mConnectionState); } } /** Checked exception that signals the specified package is not present in the system. */ Loading @@ -146,6 +201,9 @@ public abstract class InfoMediaManager { private final LocalBluetoothManager mBluetoothManager; private final Map<String, RouteListingPreference.Item> mPreferenceItemMap = new ConcurrentHashMap<>(); private final Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceMap = new ConcurrentHashMap<>(); @Nullable private SuggestedDeviceState mSuggestedDeviceState; private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); Loading Loading @@ -629,6 +687,114 @@ public abstract class InfoMediaManager { || sessionInfo.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED; } protected void updateDeviceSuggestion( String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestions) { if (suggestions == null) { mSuggestedDeviceMap.remove(suggestingPackageName); } else { mSuggestedDeviceMap.put(suggestingPackageName, suggestions); } updateDeviceSuggestion(); } protected final void updateDeviceSuggestion() { if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) { return; } SuggestedDeviceInfo topSuggestion = null; SuggestedDeviceState newSuggestedDeviceState = null; SuggestedDeviceState previousState = mSuggestedDeviceState; List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null && !suggestions.isEmpty()) { topSuggestion = suggestions.get(0); } if (topSuggestion != null) { for (MediaDevice device : mMediaDevices) { if (Objects.equals(device.getId(), topSuggestion.getRouteId())) { newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion, device.getState()); break; } } if (newSuggestedDeviceState == null) { newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion); } } if (updateMediaDevicesSuggestionState()) { dispatchDeviceListAdded(mMediaDevices); } if (!Objects.equals(previousState, newSuggestedDeviceState)) { mSuggestedDeviceState = newSuggestedDeviceState; for (MediaDeviceCallback callback : getCallbacks()) { callback.onSuggestedDeviceUpdated(mSuggestedDeviceState); } } } @Nullable private 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 // 3. No suggestion at all if multiple proxy routers are providing suggestions. List<SuggestedDeviceInfo> suggestions = mSuggestedDeviceMap.get(mPackageName); if (suggestions != null) { return suggestions; } if (mSuggestedDeviceMap.size() == 1) { for (List<SuggestedDeviceInfo> packageSuggestions : mSuggestedDeviceMap.values()) { if (packageSuggestions != null) { return packageSuggestions; } } } return null; } // Go through all current MediaDevices, and update the ones that are suggested. private boolean updateMediaDevicesSuggestionState() { Set<String> suggestedDevices = new HashSet<>(); // Prioritize suggestions from the package, otherwise pick any. List<SuggestedDeviceInfo> suggestions = getSuggestions(); if (suggestions != null) { for (SuggestedDeviceInfo suggestion : suggestions) { suggestedDevices.add(suggestion.getRouteId()); } } boolean didUpdate = false; for (MediaDevice device : mMediaDevices) { if (device.isSuggestedDevice()) { if (!suggestedDevices.contains(device.getId())) { device.setIsSuggested(false); // Case 1: Device was suggested only by setDeviceSuggestions(), and has been // updated to no longer be suggested. if (!device.isSuggestedByRouteListingPreferences()) { didUpdate = true; } // Case 2: Device was suggested by both setDeviceSuggestions() and RLP. Since // it's still suggested by RLP, no update. } else { // Case 3: Device was suggested (either by RLP or by setDeviceSuggestions()), // and should still be suggested. device.setIsSuggested(true); } } else { if (suggestedDevices.contains(device.getId())) { // Case 4: Device was not suggested by either RLP or setDeviceSuggestions() but // is now suggested. device.setIsSuggested(true); didUpdate = true; } else { // Case 5: Device was not suggested by either RLP or setDeviceSuggestions() and // is still not suggested. device.setIsSuggested(false); } } } return didUpdate; } protected final synchronized void refreshDevices() { rebuildDeviceList(); dispatchDeviceListAdded(mMediaDevices); Loading @@ -653,6 +819,7 @@ public abstract class InfoMediaManager { // First device on the list is always the first selected route. mCurrentConnectedDevice = mMediaDevices.get(0); } updateDeviceSuggestion(); } private synchronized List<MediaRoute2Info> getAvailableRoutes( Loading
packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +22 −1 Original line number Diff line number Diff line Loading @@ -126,6 +126,7 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { protected final Context mContext; protected final MediaRoute2Info mRouteInfo; protected final RouteListingPreference.Item mItem; private boolean mIsSuggested; MediaDevice( @NonNull Context context, Loading Loading @@ -313,11 +314,26 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { } /** * Checks if device is suggested device from application * Checks if device is suggested device from application. A device can be suggested through * either RouteListingPreferences or through MediaRouter2#setDeviceSuggestions. * * <p>Prioritization and conflict resolution between the two APIs is as follows: - Suggestions * from both RLP and the new API will be visible in OSw - Only suggestions from the new API will * be visible in both OSw and new UI surfaces such as UMO - If suggestions are provided from * local and proxy routers, priority will be given to the local router * * @return true if device is suggested device */ public boolean isSuggestedDevice() { return mIsSuggested || isSuggestedByRouteListingPreferences(); } /** * Checks if the device is suggested from the application's RouteListingPreferences * * @return true if the device is suggested */ public boolean isSuggestedByRouteListingPreferences() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && Api34Impl.isSuggestedDevice(mItem); } Loading Loading @@ -434,6 +450,11 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { return mState; } /** Sets whether the current device is suggested. */ public void setIsSuggested(boolean suggested) { mIsSuggested = suggested; } /** * Rules: * 1. If there is one of the connected devices identified as a carkit or fast pair device, Loading
packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +31 −0 Original line number Diff line number Diff line Loading @@ -20,11 +20,13 @@ import android.annotation.SuppressLint; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2; import android.media.MediaRouter2.DeviceSuggestionsCallback; import android.media.MediaRouter2.RoutingController; import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.media.session.MediaController; import android.os.UserHandle; import android.text.TextUtils; Loading @@ -33,6 +35,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.media.flags.Flags; import com.android.settingslib.bluetooth.LocalBluetoothManager; Loading Loading @@ -64,6 +67,18 @@ public final class RouterInfoMediaManager extends InfoMediaManager { notifyRouteListingPreferenceUpdated(preference); refreshDevices(); }; private final DeviceSuggestionsCallback mDeviceSuggestionsCallback = new DeviceSuggestionsCallback() { @Override public void onSuggestionUpdated( String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) { updateDeviceSuggestion(suggestingPackageName, suggestedDeviceInfo); } @Override public void onSuggestionRequested() {} // no-op }; @GuardedBy("this") @Nullable Loading Loading @@ -100,6 +115,20 @@ public final class RouterInfoMediaManager extends InfoMediaManager { mRouterManager = MediaRouter2Manager.getInstance(context); } @VisibleForTesting RouterInfoMediaManager( Context context, @NonNull String packageName, @NonNull UserHandle userHandle, LocalBluetoothManager localBluetoothManager, @Nullable MediaController mediaController, MediaRouter2 mediaRouter2, MediaRouter2Manager mediaRouter2Manager) { super(context, packageName, userHandle, localBluetoothManager, mediaController); mRouter = mediaRouter2; mRouterManager = mediaRouter2Manager; } @Override protected void startScanOnRouter() { if (Flags.enableScreenOffScanning()) { Loading @@ -120,6 +149,7 @@ public final class RouterInfoMediaManager extends InfoMediaManager { mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY); mRouter.registerRouteListingPreferenceUpdatedCallback( mExecutor, mRouteListingPreferenceCallback); mRouter.registerDeviceSuggestionsCallback(mExecutor, mDeviceSuggestionsCallback); mRouter.registerTransferCallback(mExecutor, mTransferCallback); mRouter.registerControllerCallback(mExecutor, mControllerCallback); } Loading @@ -143,6 +173,7 @@ public final class RouterInfoMediaManager extends InfoMediaManager { mRouter.unregisterControllerCallback(mControllerCallback); mRouter.unregisterTransferCallback(mTransferCallback); mRouter.unregisterRouteListingPreferenceUpdatedCallback(mRouteListingPreferenceCallback); mRouter.unregisterDeviceSuggestionsCallback(mDeviceSuggestionsCallback); mRouter.unregisterRouteCallback(mRouteCallback); } Loading
packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +203 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; Loading @@ -43,9 +44,13 @@ import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2; import android.media.MediaRouter2.DeviceSuggestionsCallback; import android.media.MediaRouter2.RoutingController; import android.media.MediaRouter2Manager; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.media.SuggestedDeviceInfo; import android.media.session.MediaSessionManager; import android.os.Build; import android.platform.test.annotations.EnableFlags; Loading @@ -64,6 +69,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; Loading Loading @@ -141,6 +148,12 @@ public class InfoMediaManagerTest { private MediaSessionManager mMediaSessionManager; @Mock private ComponentName mComponentName; @Mock private MediaRouter2 mRouter2; @Mock private RoutingController mRoutingController; @Captor private ArgumentCaptor<DeviceSuggestionsCallback> mDeviceSuggestionsCallbackCaptor; @Captor private ArgumentCaptor<InfoMediaManager.SuggestedDeviceState> mSuggestedDeviceStateCaptor; private ManagerInfoMediaManager mInfoMediaManager; private Context mContext; Loading @@ -161,6 +174,9 @@ public class InfoMediaManagerTest { /* mediaController */ null); mShadowRouter2Manager = ShadowRouter2Manager.getShadow(); mInfoMediaManager.mRouterManager = MediaRouter2Manager.getInstance(mContext); when(mRouter2.getController(any())).thenReturn(mRoutingController); when(mRouter2.getControllers()).thenReturn(List.of(mRoutingController)); when(mRoutingController.getRoutingSessionInfo()).thenReturn(TEST_SYSTEM_ROUTING_SESSION); } @Test Loading Loading @@ -445,6 +461,7 @@ public class InfoMediaManagerTest { availableRoutes.add(availableInfo4); when(mRouterManager.getAvailableRoutes(packageName)).thenReturn(availableRoutes); when(mRoutingController.getSelectableRoutes()).thenReturn(availableRoutes); return availableRoutes; } Loading Loading @@ -942,6 +959,192 @@ public class InfoMediaManagerTest { assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device); } private RouterInfoMediaManager createRouterInfoMediaManager() { return new RouterInfoMediaManager( mContext, TEST_PACKAGE_NAME, mContext.getUser(), mLocalBluetoothManager, /* mediaController */ null, mRouter2, mRouterManager); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_listenersNotified() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", List.of(suggestedDeviceInfo)); verify(mCallback).onDeviceListAdded(any()); verify(mCallback).onSuggestedDeviceUpdated(mSuggestedDeviceStateCaptor.capture()); assertThat(mSuggestedDeviceStateCaptor.getValue().getSuggestedDeviceInfo()) .isEqualTo(suggestedDeviceInfo); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_mediaDeviceIsSuggested() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", List.of(suggestedDeviceInfo)); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isTrue(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_noSuggestedDevices_noSuggestedMediaDevices() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", List.of(suggestedDeviceInfo)); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", null); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_multipleProviders_noSuggestedMediaDevices() { SuggestedDeviceInfo suggestedDeviceInfo1 = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); SuggestedDeviceInfo suggestedDeviceInfo2 = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name_2") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name_1", List.of(suggestedDeviceInfo1)); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name_2", List.of(suggestedDeviceInfo2)); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_firstSuggestionFromSamePackage_suggestionIsFromSamePackage() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated(TEST_PACKAGE_NAME, List.of(suggestedDeviceInfo)); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", null); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isTrue(); } @EnableFlags(Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API) @Test public void onSuggestionUpdated_laterSuggestionFromSamePackage_suggestionIsFromSamePackage() { SuggestedDeviceInfo suggestedDeviceInfo = new SuggestedDeviceInfo.Builder() .setDeviceDisplayName("device_name") .setRouteId(TEST_ID_3) .setType(0) .build(); RouterInfoMediaManager mediaManager = createRouterInfoMediaManager(); setAvailableRoutesList(TEST_PACKAGE_NAME); mediaManager.registerCallback(mCallback); clearInvocations(mCallback); verify(mRouter2) .registerDeviceSuggestionsCallback( any(), mDeviceSuggestionsCallbackCaptor.capture()); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated("random_package_name", null); mDeviceSuggestionsCallbackCaptor .getValue() .onSuggestionUpdated(TEST_PACKAGE_NAME, List.of(suggestedDeviceInfo)); MediaDevice mediaDevice = mediaManager.mMediaDevices.get(1); assertThat(mediaDevice.getId()).isEqualTo(TEST_ID_3); assertThat(mediaDevice.isSuggestedDevice()).isTrue(); } @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void composePreferenceRouteListing_useSystemOrderingIsFalse() { Loading