Add controller for Media operation am: c23f9171ce
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13109602 MUST ONLY BE SUBMITTED BY AUTOMERGER Change-Id: I4e059153b87530412d1143a58a4a6791d3356944
This commit is contained in:
@@ -1402,4 +1402,11 @@
|
|||||||
<dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen>
|
<dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen>
|
||||||
<dimen name="config_rounded_mask_size_top">@*android:dimen/rounded_corner_radius_top</dimen>
|
<dimen name="config_rounded_mask_size_top">@*android:dimen/rounded_corner_radius_top</dimen>
|
||||||
<dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
|
<dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
|
||||||
|
|
||||||
|
<!-- Output switcher panel related dimensions -->
|
||||||
|
<dimen name="media_output_dialog_padding_top">11dp</dimen>
|
||||||
|
<dimen name="media_output_dialog_list_max_height">364dp</dimen>
|
||||||
|
<dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
|
||||||
|
<dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
|
||||||
|
<dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2855,4 +2855,19 @@
|
|||||||
<string name="controls_menu_add">Add controls</string>
|
<string name="controls_menu_add">Add controls</string>
|
||||||
<!-- Controls menu, edit [CHAR_LIMIT=30] -->
|
<!-- Controls menu, edit [CHAR_LIMIT=30] -->
|
||||||
<string name="controls_menu_edit">Edit controls</string>
|
<string name="controls_menu_edit">Edit controls</string>
|
||||||
|
|
||||||
|
<!-- Title for the media output group dialog with media related devices [CHAR LIMIT=50] -->
|
||||||
|
<string name="media_output_dialog_add_output">Add outputs</string>
|
||||||
|
<!-- Title for the media output slice with group devices [CHAR LIMIT=50] -->
|
||||||
|
<string name="media_output_dialog_group">Group</string>
|
||||||
|
<!-- Summary for media output group with only one device which is active [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="media_output_dialog_single_device">1 device selected</string>
|
||||||
|
<!-- Summary for media output group with the active device count [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="media_output_dialog_multiple_devices"><xliff:g id="count" example="2">%1$d</xliff:g> devices selected</string>
|
||||||
|
<!-- Summary for disconnected status [CHAR LIMIT=50] -->
|
||||||
|
<string name="media_output_dialog_disconnected"><xliff:g id="device_name" example="My device">%1$s</xliff:g> (disconnected)</string>
|
||||||
|
<!-- Summary for connecting error message [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="media_output_dialog_connect_failed">Couldn\'t connect. Try again.</string>
|
||||||
|
<!-- Title for pairing item [CHAR LIMIT=60] -->
|
||||||
|
<string name="media_output_dialog_pairing_new">Pair new device</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,445 @@
|
|||||||
|
/*
|
||||||
|
* 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.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 = false;
|
||||||
|
|
||||||
|
private final String mPackageName;
|
||||||
|
private final Context mContext;
|
||||||
|
private final MediaSessionManager mMediaSessionManager;
|
||||||
|
private final ShadeController mShadeController;
|
||||||
|
private final ActivityStarter mActivityStarter;
|
||||||
|
@VisibleForTesting
|
||||||
|
final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private MediaController mMediaController;
|
||||||
|
@VisibleForTesting
|
||||||
|
Callback mCallback;
|
||||||
|
@VisibleForTesting
|
||||||
|
LocalMediaManager mLocalMediaManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MediaOutputController(@NonNull Context context, String packageName,
|
||||||
|
MediaSessionManager mediaSessionManager, LocalBluetoothManager
|
||||||
|
lbm, ShadeController shadeController, ActivityStarter starter) {
|
||||||
|
mContext = context;
|
||||||
|
mPackageName = packageName;
|
||||||
|
mMediaSessionManager = mediaSessionManager;
|
||||||
|
mShadeController = shadeController;
|
||||||
|
mActivityStarter = starter;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDeviceIncluded(Collection<MediaDevice> deviceCollection, MediaDevice targetDevice) {
|
||||||
|
for (MediaDevice device : deviceCollection) {
|
||||||
|
if (TextUtils.equals(device.getId(), targetDevice.getId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
/*
|
||||||
|
* 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 static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.reset;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.MediaDescription;
|
||||||
|
import android.media.MediaMetadata;
|
||||||
|
import android.media.RoutingSessionInfo;
|
||||||
|
import android.media.session.MediaController;
|
||||||
|
import android.media.session.MediaSessionManager;
|
||||||
|
import android.testing.AndroidTestingRunner;
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
|
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
|
||||||
|
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||||
|
import com.android.settingslib.media.LocalMediaManager;
|
||||||
|
import com.android.settingslib.media.MediaDevice;
|
||||||
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
|
import com.android.systemui.statusbar.phone.ShadeController;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner.class)
|
||||||
|
public class MediaOutputControllerTest extends SysuiTestCase {
|
||||||
|
|
||||||
|
private static final String TEST_PACKAGE_NAME = "com.test.package.name";
|
||||||
|
private static final String TEST_DEVICE_1_ID = "test_device_1_id";
|
||||||
|
private static final String TEST_DEVICE_2_ID = "test_device_2_id";
|
||||||
|
private static final String TEST_ARTIST = "test_artist";
|
||||||
|
private static final String TEST_SONG = "test_song";
|
||||||
|
private static final String TEST_SESSION_ID = "test_session_id";
|
||||||
|
private static final String TEST_SESSION_NAME = "test_session_name";
|
||||||
|
// Mock
|
||||||
|
private MediaController mMediaController = mock(MediaController.class);
|
||||||
|
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
|
||||||
|
private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager =
|
||||||
|
mock(CachedBluetoothDeviceManager.class);
|
||||||
|
private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
|
||||||
|
private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
|
||||||
|
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
|
||||||
|
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
|
||||||
|
private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
|
||||||
|
private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
|
||||||
|
private ShadeController mShadeController = mock(ShadeController.class);
|
||||||
|
private ActivityStarter mStarter = mock(ActivityStarter.class);
|
||||||
|
|
||||||
|
private Context mSpyContext;
|
||||||
|
private MediaOutputController mMediaOutputController;
|
||||||
|
private LocalMediaManager mLocalMediaManager;
|
||||||
|
private List<MediaController> mMediaControllers = new ArrayList<>();
|
||||||
|
private List<MediaDevice> mMediaDevices = new ArrayList<>();
|
||||||
|
private MediaDescription mMediaDescription;
|
||||||
|
private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mSpyContext = spy(mContext);
|
||||||
|
when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
|
||||||
|
mMediaControllers.add(mMediaController);
|
||||||
|
when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
|
||||||
|
doReturn(mMediaSessionManager).when(mSpyContext).getSystemService(
|
||||||
|
MediaSessionManager.class);
|
||||||
|
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
|
||||||
|
mCachedBluetoothDeviceManager);
|
||||||
|
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
|
||||||
|
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
|
||||||
|
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
|
||||||
|
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
|
||||||
|
MediaDescription.Builder builder = new MediaDescription.Builder();
|
||||||
|
builder.setTitle(TEST_SONG);
|
||||||
|
builder.setSubtitle(TEST_ARTIST);
|
||||||
|
mMediaDescription = builder.build();
|
||||||
|
when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
|
||||||
|
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
|
||||||
|
when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
|
||||||
|
mMediaDevices.add(mMediaDevice1);
|
||||||
|
mMediaDevices.add(mMediaDevice2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void start_verifyLocalMediaManagerInit() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
verify(mLocalMediaManager).registerCallback(mMediaOutputController);
|
||||||
|
verify(mLocalMediaManager).startScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stop_verifyLocalMediaManagerDeinit() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
reset(mLocalMediaManager);
|
||||||
|
|
||||||
|
mMediaOutputController.stop();
|
||||||
|
|
||||||
|
verify(mLocalMediaManager).unregisterCallback(mMediaOutputController);
|
||||||
|
verify(mLocalMediaManager).stopScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void start_withPackageName_verifyMediaControllerInit() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
verify(mMediaController).registerCallback(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void start_withoutPackageName_verifyMediaControllerInit() {
|
||||||
|
mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
|
||||||
|
mLocalBluetoothManager, mShadeController, mStarter);
|
||||||
|
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
verify(mMediaController, never()).registerCallback(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stop_withPackageName_verifyMediaControllerDeinit() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
reset(mMediaController);
|
||||||
|
|
||||||
|
mMediaOutputController.stop();
|
||||||
|
|
||||||
|
verify(mMediaController).unregisterCallback(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stop_withoutPackageName_verifyMediaControllerDeinit() {
|
||||||
|
mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
|
||||||
|
mLocalBluetoothManager, mShadeController, mStarter);
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
mMediaOutputController.stop();
|
||||||
|
|
||||||
|
verify(mMediaController, never()).unregisterCallback(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onDeviceListUpdate_verifyDeviceListCallback() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
reset(mCb);
|
||||||
|
|
||||||
|
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
|
||||||
|
final List<MediaDevice> devices = new ArrayList<>(mMediaOutputController.getMediaDevices());
|
||||||
|
|
||||||
|
assertThat(devices.containsAll(mMediaDevices)).isTrue();
|
||||||
|
assertThat(devices.size()).isEqualTo(mMediaDevices.size());
|
||||||
|
verify(mCb).onRouteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onSelectedDeviceStateChanged_verifyCallback() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
reset(mCb);
|
||||||
|
|
||||||
|
mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1,
|
||||||
|
LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
|
||||||
|
|
||||||
|
verify(mCb).onRouteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onDeviceAttributesChanged_verifyCallback() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
reset(mCb);
|
||||||
|
|
||||||
|
mMediaOutputController.onDeviceAttributesChanged();
|
||||||
|
|
||||||
|
verify(mCb).onRouteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onRequestFailed_verifyCallback() {
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
reset(mCb);
|
||||||
|
|
||||||
|
mMediaOutputController.onRequestFailed(0 /* reason */);
|
||||||
|
|
||||||
|
verify(mCb).onRouteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHeaderTitle_withoutMetadata_returnDefaultString() {
|
||||||
|
when(mMediaController.getMetadata()).thenReturn(null);
|
||||||
|
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(
|
||||||
|
mContext.getText(R.string.controls_media_title));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHeaderTitle_withMetadata_returnSongName() {
|
||||||
|
when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
|
||||||
|
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHeaderSubTitle_withoutMetadata_returnNull() {
|
||||||
|
when(mMediaController.getMetadata()).thenReturn(null);
|
||||||
|
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.getHeaderSubTitle()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHeaderSubTitle_withMetadata_returnArtistName() {
|
||||||
|
when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
|
||||||
|
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void connectDevice_verifyConnect() {
|
||||||
|
mMediaOutputController.connectDevice(mMediaDevice1);
|
||||||
|
|
||||||
|
// Wait for background thread execution
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(mLocalMediaManager).connectDevice(mMediaDevice1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getActiveRemoteMediaDevice_isSystemSession_returnSession() {
|
||||||
|
when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
|
||||||
|
when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
|
||||||
|
when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
|
||||||
|
when(mRemoteSessionInfo.getVolume()).thenReturn(10);
|
||||||
|
when(mRemoteSessionInfo.isSystemSession()).thenReturn(false);
|
||||||
|
mRoutingSessionInfos.add(mRemoteSessionInfo);
|
||||||
|
when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly(
|
||||||
|
mRemoteSessionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getActiveRemoteMediaDevice_notSystemSession_returnEmpty() {
|
||||||
|
when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
|
||||||
|
when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
|
||||||
|
when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
|
||||||
|
when(mRemoteSessionInfo.getVolume()).thenReturn(10);
|
||||||
|
when(mRemoteSessionInfo.isSystemSession()).thenReturn(true);
|
||||||
|
mRoutingSessionInfos.add(mRemoteSessionInfo);
|
||||||
|
when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isZeroMode_onlyFromPhoneOutput_returnTrue() {
|
||||||
|
// Multiple available devices
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isFalse();
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
|
||||||
|
mMediaDevices.clear();
|
||||||
|
mMediaDevices.add(mMediaDevice1);
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isTrue();
|
||||||
|
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isTrue();
|
||||||
|
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isZeroMode_notFromPhoneOutput_returnFalse() {
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_UNKNOWN);
|
||||||
|
mMediaDevices.clear();
|
||||||
|
mMediaDevices.add(mMediaDevice1);
|
||||||
|
mMediaOutputController.start(mCb);
|
||||||
|
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isFalse();
|
||||||
|
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isFalse();
|
||||||
|
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isFalse();
|
||||||
|
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isFalse();
|
||||||
|
|
||||||
|
when(mMediaDevice1.getDeviceType()).thenReturn(
|
||||||
|
MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE);
|
||||||
|
|
||||||
|
assertThat(mMediaOutputController.isZeroMode()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user