blob: b9fa872521253bc6cceb4eb1af19a6d9ad47dd69 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.MediaRoute2Info;
import android.media.RoutingSessionInfo;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.ShadeController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Inject;
/**
* Controller for media output dialog
*/
public class MediaOutputController implements LocalMediaManager.DeviceCallback{
private static final String TAG = "MediaOutputController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final String mPackageName;
private final Context mContext;
private final MediaSessionManager mMediaSessionManager;
private final ShadeController mShadeController;
private final ActivityStarter mActivityStarter;
private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
private final boolean mAboveStatusbar;
@VisibleForTesting
final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
private MediaController mMediaController;
@VisibleForTesting
Callback mCallback;
@VisibleForTesting
LocalMediaManager mLocalMediaManager;
@Inject
public MediaOutputController(@NonNull Context context, String packageName,
boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
lbm, ShadeController shadeController, ActivityStarter starter) {
mContext = context;
mPackageName = packageName;
mMediaSessionManager = mediaSessionManager;
mShadeController = shadeController;
mActivityStarter = starter;
mAboveStatusbar = aboveStatusbar;
InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
}
void start(@NonNull Callback cb) {
mMediaDevices.clear();
if (!TextUtils.isEmpty(mPackageName)) {
for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
mMediaController = controller;
mMediaController.unregisterCallback(mCb);
mMediaController.registerCallback(mCb);
break;
}
}
}
if (mMediaController == null) {
if (DEBUG) {
Log.d(TAG, "No media controller for " + mPackageName);
}
}
if (mLocalMediaManager == null) {
if (DEBUG) {
Log.d(TAG, "No local media manager " + mPackageName);
}
return;
}
mCallback = cb;
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
mLocalMediaManager.registerCallback(this);
mLocalMediaManager.startScan();
}
void stop() {
if (mMediaController != null) {
mMediaController.unregisterCallback(mCb);
}
if (mLocalMediaManager != null) {
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
}
mMediaDevices.clear();
}
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
buildMediaDevices(devices);
mCallback.onRouteChanged();
}
@Override
public void onSelectedDeviceStateChanged(MediaDevice device,
@LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
}
@Override
public void onDeviceAttributesChanged() {
mCallback.onRouteChanged();
}
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
}
CharSequence getHeaderTitle() {
if (mMediaController != null) {
final MediaMetadata metadata = mMediaController.getMetadata();
if (metadata != null) {
return metadata.getDescription().getTitle();
}
}
return mContext.getText(R.string.controls_media_title);
}
CharSequence getHeaderSubTitle() {
if (mMediaController == null) {
return null;
}
final MediaMetadata metadata = mMediaController.getMetadata();
if (metadata == null) {
return null;
}
return metadata.getDescription().getSubtitle();
}
IconCompat getHeaderIcon() {
if (mMediaController == null) {
return null;
}
final MediaMetadata metadata = mMediaController.getMetadata();
if (metadata != null) {
final Bitmap bitmap = metadata.getDescription().getIconBitmap();
if (bitmap != null) {
final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap,
(float) mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_icon_corner_radius));
return IconCompat.createWithBitmap(roundBitmap);
}
}
if (DEBUG) {
Log.d(TAG, "Media meta data does not contain icon information");
}
return getPackageIcon();
}
IconCompat getDeviceIconCompat(MediaDevice device) {
Drawable drawable = device.getIcon();
if (drawable == null) {
if (DEBUG) {
Log.d(TAG, "getDeviceIconCompat() device : " + device.getName()
+ ", drawable is null");
}
// Use default Bluetooth device icon to handle getIcon() is null case.
drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
}
return BluetoothUtils.createIconWithDrawable(drawable);
}
private IconCompat getPackageIcon() {
if (TextUtils.isEmpty(mPackageName)) {
return null;
}
try {
final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName);
if (drawable instanceof BitmapDrawable) {
return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap());
}
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return IconCompat.createWithBitmap(bitmap);
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) {
Log.e(TAG, "Package is not found. Unable to get package icon.");
}
}
return null;
}
private void buildMediaDevices(List<MediaDevice> devices) {
// For the first time building list, to make sure the top device is the connected device.
if (mMediaDevices.isEmpty()) {
final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
if (connectedMediaDevice == null) {
if (DEBUG) {
Log.d(TAG, "No connected media device.");
}
mMediaDevices.addAll(devices);
return;
}
for (MediaDevice device : devices) {
if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
mMediaDevices.add(0, device);
} else {
mMediaDevices.add(device);
}
}
return;
}
// To keep the same list order
final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
for (MediaDevice originalDevice : mMediaDevices) {
for (MediaDevice newDevice : devices) {
if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
targetMediaDevices.add(newDevice);
break;
}
}
}
if (targetMediaDevices.size() != devices.size()) {
devices.removeAll(targetMediaDevices);
targetMediaDevices.addAll(devices);
}
mMediaDevices.clear();
mMediaDevices.addAll(targetMediaDevices);
}
List<MediaDevice> getGroupMediaDevices() {
final List<MediaDevice> selectedDevices = getSelectedMediaDevice();
final List<MediaDevice> selectableDevices = getSelectableMediaDevice();
if (mGroupMediaDevices.isEmpty()) {
mGroupMediaDevices.addAll(selectedDevices);
mGroupMediaDevices.addAll(selectableDevices);
return mGroupMediaDevices;
}
// To keep the same list order
final Collection<MediaDevice> sourceDevices = new ArrayList<>();
final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
sourceDevices.addAll(selectedDevices);
sourceDevices.addAll(selectableDevices);
for (MediaDevice originalDevice : mGroupMediaDevices) {
for (MediaDevice newDevice : sourceDevices) {
if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
targetMediaDevices.add(newDevice);
sourceDevices.remove(newDevice);
break;
}
}
}
// Add new devices at the end of list if necessary
if (!sourceDevices.isEmpty()) {
targetMediaDevices.addAll(sourceDevices);
}
mGroupMediaDevices.clear();
mGroupMediaDevices.addAll(targetMediaDevices);
return mGroupMediaDevices;
}
void resetGroupMediaDevices() {
mGroupMediaDevices.clear();
}
void connectDevice(MediaDevice device) {
ThreadUtils.postOnBackgroundThread(() -> {
mLocalMediaManager.connectDevice(device);
});
}
Collection<MediaDevice> getMediaDevices() {
return mMediaDevices;
}
MediaDevice getCurrentConnectedMediaDevice() {
return mLocalMediaManager.getCurrentConnectedDevice();
}
private MediaDevice getMediaDeviceById(String id) {
return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
}
boolean addDeviceToPlayMedia(MediaDevice device) {
return mLocalMediaManager.addDeviceToPlayMedia(device);
}
boolean removeDeviceFromPlayMedia(MediaDevice device) {
return mLocalMediaManager.removeDeviceFromPlayMedia(device);
}
List<MediaDevice> getSelectableMediaDevice() {
return mLocalMediaManager.getSelectableMediaDevice();
}
List<MediaDevice> getSelectedMediaDevice() {
return mLocalMediaManager.getSelectedMediaDevice();
}
List<MediaDevice> getDeselectableMediaDevice() {
return mLocalMediaManager.getDeselectableMediaDevice();
}
void adjustSessionVolume(String sessionId, int volume) {
mLocalMediaManager.adjustSessionVolume(sessionId, volume);
}
void adjustSessionVolume(int volume) {
mLocalMediaManager.adjustSessionVolume(volume);
}
int getSessionVolumeMax() {
return mLocalMediaManager.getSessionVolumeMax();
}
int getSessionVolume() {
return mLocalMediaManager.getSessionVolume();
}
CharSequence getSessionName() {
return mLocalMediaManager.getSessionName();
}
void releaseSession() {
mLocalMediaManager.releaseSession();
}
List<RoutingSessionInfo> getActiveRemoteMediaDevices() {
final List<RoutingSessionInfo> sessionInfos = new ArrayList<>();
for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) {
if (!info.isSystemSession()) {
sessionInfos.add(info);
}
}
return sessionInfos;
}
void adjustVolume(MediaDevice device, int volume) {
ThreadUtils.postOnBackgroundThread(() -> {
device.requestSetVolume(volume);
});
}
String getPackageName() {
return mPackageName;
}
boolean hasAdjustVolumeUserRestriction() {
if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
return true;
}
final UserManager um = mContext.getSystemService(UserManager.class);
return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
UserHandle.of(UserHandle.myUserId()));
}
boolean isTransferring() {
for (MediaDevice device : mMediaDevices) {
if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
return true;
}
}
return false;
}
boolean isZeroMode() {
if (mMediaDevices.size() == 1) {
final MediaDevice device = mMediaDevices.iterator().next();
// Add "pair new" only when local output device exists
final int type = device.getDeviceType();
if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
|| type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
|| type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
return true;
}
}
return false;
}
void launchBluetoothPairing() {
mCallback.dismissDialog();
final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
mContext.sendBroadcast(new Intent()
.setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING)
.setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME));
mShadeController.animateCollapsePanels();
return true;
};
mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
}
void launchMediaOutputDialog() {
mCallback.dismissDialog();
new MediaOutputDialog(mContext, mAboveStatusbar, this);
}
void launchMediaOutputGroupDialog() {
mCallback.dismissDialog();
new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
}
boolean isActiveRemoteDevice(@NonNull MediaDevice device) {
final List<String> features = device.getFeatures();
return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)
|| features.contains(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK)
|| features.contains(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK)
|| features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK));
}
private final MediaController.Callback mCb = new MediaController.Callback() {
@Override
public void onMetadataChanged(MediaMetadata metadata) {
mCallback.onMediaChanged();
}
@Override
public void onPlaybackStateChanged(PlaybackState playbackState) {
final int state = playbackState.getState();
if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) {
mCallback.onMediaStoppedOrPaused();
}
}
};
interface Callback {
/**
* Override to handle the media content updating.
*/
void onMediaChanged();
/**
* Override to handle the media state updating.
*/
void onMediaStoppedOrPaused();
/**
* Override to handle the device updating.
*/
void onRouteChanged();
/**
* Override to dismiss dialog.
*/
void dismissDialog();
}
}