/* * Copyright 2018 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 static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; import static android.media.MediaRoute2Info.TYPE_DOCK; import static android.media.MediaRoute2Info.TYPE_GROUP; import static android.media.MediaRoute2Info.TYPE_HDMI; import static android.media.MediaRoute2Info.TYPE_HEARING_AID; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; import static android.media.MediaRoute2Info.TYPE_UNKNOWN; import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; import android.annotation.TargetApi; import android.app.Notification; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; import android.media.RoutingSessionInfo; import android.os.Build; import android.text.TextUtils; import android.util.Log; import androidx.annotation.RequiresApi; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * InfoMediaManager provide interface to get InfoMediaDevice list. */ @RequiresApi(Build.VERSION_CODES.R) public class InfoMediaManager extends MediaManager { private static final String TAG = "InfoMediaManager"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @VisibleForTesting final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback(); @VisibleForTesting final Executor mExecutor = Executors.newSingleThreadExecutor(); @VisibleForTesting MediaRouter2Manager mRouterManager; @VisibleForTesting String mPackageName; private final boolean mVolumeAdjustmentForRemoteGroupSessions; private MediaDevice mCurrentConnectedDevice; private LocalBluetoothManager mBluetoothManager; public InfoMediaManager(Context context, String packageName, Notification notification, LocalBluetoothManager localBluetoothManager) { super(context, notification); mRouterManager = MediaRouter2Manager.getInstance(context); mBluetoothManager = localBluetoothManager; if (!TextUtils.isEmpty(packageName)) { mPackageName = packageName; } mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean( com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); } @Override public void startScan() { mMediaDevices.clear(); mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); mRouterManager.startScan(); refreshDevices(); } @Override public void stopScan() { mRouterManager.unregisterCallback(mMediaRouterCallback); mRouterManager.stopScan(); } /** * Get current device that played media. * @return MediaDevice */ MediaDevice getCurrentConnectedDevice() { return mCurrentConnectedDevice; } /** * Transfer MediaDevice for media without package name. */ boolean connectDeviceWithoutPackageName(MediaDevice device) { boolean isConnected = false; final List infos = mRouterManager.getActiveSessions(); if (infos.size() > 0) { final RoutingSessionInfo info = infos.get(0); mRouterManager.transfer(info, device.mRouteInfo); isConnected = true; } return isConnected; } /** * Add a MediaDevice to let it play current media. * * @param device MediaDevice * @return If add device successful return {@code true}, otherwise return {@code false} */ boolean addDeviceToPlayMedia(MediaDevice device) { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!"); return false; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null && info.getSelectableRoutes().contains(device.mRouteInfo.getId())) { mRouterManager.selectRoute(info, device.mRouteInfo); return true; } Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : " + device.getName()); return false; } private RoutingSessionInfo getRoutingSessionInfo() { return getRoutingSessionInfo(mPackageName); } private RoutingSessionInfo getRoutingSessionInfo(String packageName) { final List sessionInfos = mRouterManager.getRoutingSessions(packageName); if (sessionInfos == null || sessionInfos.isEmpty()) { return null; } return sessionInfos.get(sessionInfos.size() - 1); } /** * Remove a {@code device} from current media. * * @param device MediaDevice * @return If device stop successful return {@code true}, otherwise return {@code false} */ boolean removeDeviceFromPlayMedia(MediaDevice device) { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!"); return false; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null && info.getSelectedRoutes().contains(device.mRouteInfo.getId())) { mRouterManager.deselectRoute(info, device.mRouteInfo); return true; } Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : " + device.getName()); return false; } /** * Release session to stop playing media on MediaDevice. */ boolean releaseSession() { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "releaseSession() package name is null or empty!"); return false; } final RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); if (sessionInfo != null) { mRouterManager.releaseSession(sessionInfo); return true; } Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName); return false; } /** * Get the MediaDevice list that can be added to current media. * * @return list of MediaDevice */ List getSelectableMediaDevice() { final List deviceList = new ArrayList<>(); if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "getSelectableMediaDevice() package name is null or empty!"); return deviceList; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) { deviceList.add(new InfoMediaDevice(mContext, mRouterManager, route, mPackageName)); } return deviceList; } Log.w(TAG, "getSelectableMediaDevice() cannot found selectable MediaDevice from : " + mPackageName); return deviceList; } /** * Get the MediaDevice list that can be removed from current media session. * * @return list of MediaDevice */ List getDeselectableMediaDevice() { final List deviceList = new ArrayList<>(); if (TextUtils.isEmpty(mPackageName)) { Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!"); return deviceList; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) { deviceList.add(new InfoMediaDevice(mContext, mRouterManager, route, mPackageName)); Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); } return deviceList; } Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : " + mPackageName); return deviceList; } /** * Get the MediaDevice list that has been selected to current media. * * @return list of MediaDevice */ List getSelectedMediaDevice() { final List deviceList = new ArrayList<>(); if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!"); return deviceList; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) { deviceList.add(new InfoMediaDevice(mContext, mRouterManager, route, mPackageName)); } return deviceList; } Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : " + mPackageName); return deviceList; } void adjustSessionVolume(RoutingSessionInfo info, int volume) { if (info == null) { Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty"); return; } mRouterManager.setSessionVolume(info, volume); } /** * Adjust the volume of {@link android.media.RoutingSessionInfo}. * * @param volume the value of volume */ void adjustSessionVolume(int volume) { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "adjustSessionVolume() package name is null or empty!"); return; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { Log.d(TAG, "adjustSessionVolume() adjust volume : " + volume + ", with : " + mPackageName); mRouterManager.setSessionVolume(info, volume); return; } Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : " + mPackageName); } /** * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. * * @return maximum volume of the session, and return -1 if not found. */ public int getSessionVolumeMax() { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "getSessionVolumeMax() package name is null or empty!"); return -1; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { return info.getVolumeMax(); } Log.w(TAG, "getSessionVolumeMax() can't found corresponding RoutingSession with : " + mPackageName); return -1; } /** * Gets the current volume of the {@link android.media.RoutingSessionInfo}. * * @return current volume of the session, and return -1 if not found. */ public int getSessionVolume() { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "getSessionVolume() package name is null or empty!"); return -1; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { return info.getVolume(); } Log.w(TAG, "getSessionVolume() can't found corresponding RoutingSession with : " + mPackageName); return -1; } CharSequence getSessionName() { if (TextUtils.isEmpty(mPackageName)) { Log.w(TAG, "Unable to get session name. The package name is null or empty!"); return null; } final RoutingSessionInfo info = getRoutingSessionInfo(); if (info != null) { return info.getName(); } Log.w(TAG, "Unable to get session name for package: " + mPackageName); return null; } boolean shouldDisableMediaOutput(String packageName) { if (TextUtils.isEmpty(packageName)) { Log.w(TAG, "shouldDisableMediaOutput() package name is null or empty!"); return true; } // Disable when there is no transferable route return mRouterManager.getTransferableRoutes(packageName).isEmpty(); } @TargetApi(Build.VERSION_CODES.R) boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) { return sessionInfo.isSystemSession() // System sessions are not remote || mVolumeAdjustmentForRemoteGroupSessions || sessionInfo.getSelectedRoutes().size() <= 1; } private void refreshDevices() { mMediaDevices.clear(); mCurrentConnectedDevice = null; if (TextUtils.isEmpty(mPackageName)) { buildAllRoutes(); } else { buildAvailableRoutes(); } dispatchDeviceListAdded(); } private void buildAllRoutes() { for (MediaRoute2Info route : mRouterManager.getAllRoutes()) { if (DEBUG) { Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : " + route.getVolume() + ", type : " + route.getType()); } if (route.isSystemRoute()) { addMediaDevice(route); } } } List getActiveMediaSession() { return mRouterManager.getActiveSessions(); } private void buildAvailableRoutes() { for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) { if (DEBUG) { Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " + route.getVolume() + ", type : " + route.getType()); } addMediaDevice(route); } } private List getAvailableRoutes(String packageName) { final List infos = new ArrayList<>(); RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(packageName); if (routingSessionInfo != null) { infos.addAll(mRouterManager.getSelectedRoutes(routingSessionInfo)); } final List transferableRoutes = mRouterManager.getTransferableRoutes(packageName); for (MediaRoute2Info transferableRoute : transferableRoutes) { boolean alreadyAdded = false; for (MediaRoute2Info mediaRoute2Info : infos) { if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) { alreadyAdded = true; break; } } if (!alreadyAdded) { infos.add(transferableRoute); } } return infos; } @VisibleForTesting void addMediaDevice(MediaRoute2Info route) { final int deviceType = route.getType(); MediaDevice mediaDevice = null; switch (deviceType) { case TYPE_UNKNOWN: case TYPE_REMOTE_TV: case TYPE_REMOTE_SPEAKER: case TYPE_GROUP: //TODO(b/148765806): use correct device type once api is ready. mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route, mPackageName); if (!TextUtils.isEmpty(mPackageName) && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId()) && mCurrentConnectedDevice == null) { mCurrentConnectedDevice = mediaDevice; } break; case TYPE_BUILTIN_SPEAKER: case TYPE_USB_DEVICE: case TYPE_USB_HEADSET: case TYPE_USB_ACCESSORY: case TYPE_DOCK: case TYPE_HDMI: case TYPE_WIRED_HEADSET: case TYPE_WIRED_HEADPHONES: mediaDevice = new PhoneMediaDevice(mContext, mRouterManager, route, mPackageName); break; case TYPE_HEARING_AID: case TYPE_BLUETOOTH_A2DP: final BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(route.getAddress()); final CachedBluetoothDevice cachedDevice = mBluetoothManager.getCachedDeviceManager().findDevice(device); if (cachedDevice != null) { mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager, route, mPackageName); } break; default: Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType); break; } if (mediaDevice != null) { mMediaDevices.add(mediaDevice); } } class RouterManagerCallback implements MediaRouter2Manager.Callback { @Override public void onRoutesAdded(List routes) { refreshDevices(); } @Override public void onPreferredFeaturesChanged(String packageName, List preferredFeatures) { if (TextUtils.equals(mPackageName, packageName)) { refreshDevices(); } } @Override public void onRoutesChanged(List routes) { refreshDevices(); } @Override public void onRoutesRemoved(List routes) { refreshDevices(); } @Override public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { if (DEBUG) { Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() + ", newSession : " + newSession.getName()); } mMediaDevices.clear(); mCurrentConnectedDevice = null; if (TextUtils.isEmpty(mPackageName)) { buildAllRoutes(); } else { buildAvailableRoutes(); } final String id = mCurrentConnectedDevice != null ? mCurrentConnectedDevice.getId() : null; dispatchConnectedDeviceChanged(id); } @Override public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) { dispatchOnRequestFailed(REASON_UNKNOWN_ERROR); } @Override public void onRequestFailed(int reason) { dispatchOnRequestFailed(reason); } @Override public void onSessionUpdated(RoutingSessionInfo sessionInfo) { dispatchDataChanged(); } } }