Before this CL, the same request code will cause intent to be cached. It will cause the output switcher to display the previous media session. This CL uses the package name as an unique request code to fix this issue. Bug: 169077753 Test: make -j42 RunSettingsRoboTests Change-Id: Iebfc7904609e243d5bf0222307bae98a952a3331
413 lines
18 KiB
Java
413 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2019 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.settings.media;
|
|
|
|
import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
|
|
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
|
|
|
|
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.app.settings.SettingsEnums;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.PorterDuffColorFilter;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.net.Uri;
|
|
import android.text.SpannableString;
|
|
import android.text.TextUtils;
|
|
import android.text.style.ForegroundColorSpan;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.core.graphics.drawable.IconCompat;
|
|
import androidx.slice.Slice;
|
|
import androidx.slice.builders.ListBuilder;
|
|
import androidx.slice.builders.SliceAction;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.Utils;
|
|
import com.android.settings.bluetooth.BluetoothPairingDetail;
|
|
import com.android.settings.core.SubSettingLauncher;
|
|
import com.android.settings.slices.CustomSliceable;
|
|
import com.android.settings.slices.SliceBackgroundWorker;
|
|
import com.android.settings.slices.SliceBroadcastReceiver;
|
|
import com.android.settingslib.media.LocalMediaManager;
|
|
import com.android.settingslib.media.MediaDevice;
|
|
import com.android.settingslib.media.MediaOutputSliceConstants;
|
|
|
|
import java.util.Collection;
|
|
|
|
/**
|
|
* Show the Media device that can be transfer the media.
|
|
*/
|
|
public class MediaOutputSlice implements CustomSliceable {
|
|
|
|
private static final String TAG = "MediaOutputSlice";
|
|
private static final String MEDIA_DEVICE_ID = "media_device_id";
|
|
private static final String MEDIA_GROUP_DEVICE = "media_group_device";
|
|
private static final String MEDIA_GROUP_REQUEST = "media_group_request";
|
|
private static final int NON_SLIDER_VALUE = -1;
|
|
|
|
public static final String MEDIA_PACKAGE_NAME = "media_package_name";
|
|
|
|
private final Context mContext;
|
|
|
|
private MediaDeviceUpdateWorker mWorker;
|
|
|
|
public MediaOutputSlice(Context context) {
|
|
mContext = context;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void init(MediaDeviceUpdateWorker worker) {
|
|
mWorker = worker;
|
|
}
|
|
|
|
@Override
|
|
public Slice getSlice() {
|
|
final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
|
|
.setAccentColor(COLOR_NOT_TINTED);
|
|
if (!isVisible()) {
|
|
Log.d(TAG, "getSlice() is not visible");
|
|
return listBuilder.build();
|
|
}
|
|
|
|
final Collection<MediaDevice> devices = getMediaDevices();
|
|
final MediaDeviceUpdateWorker worker = getWorker();
|
|
|
|
if (worker.getSelectedMediaDevice().size() > 1) {
|
|
// Insert group item to the first when it is available
|
|
if (worker.getSessionVolumeMax() > 0 && !worker.hasAdjustVolumeUserRestriction()) {
|
|
listBuilder.addInputRange(getGroupSliderRow());
|
|
} else {
|
|
listBuilder.addRow(getGroupRow());
|
|
}
|
|
// Add all other devices
|
|
for (MediaDevice device : devices) {
|
|
addRow(device, null /* connectedDevice */, listBuilder);
|
|
}
|
|
} else {
|
|
final MediaDevice connectedDevice = worker.getCurrentConnectedMediaDevice();
|
|
if (devices.size() == 1) {
|
|
// Zero state
|
|
final MediaDevice device = devices.iterator().next();
|
|
addRow(device, device, listBuilder);
|
|
// 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) {
|
|
listBuilder.addRow(getPairNewRow());
|
|
}
|
|
} else {
|
|
final boolean isTouched = worker.getIsTouched();
|
|
// Fix the last top device when user press device to transfer.
|
|
final MediaDevice topDevice = isTouched ? worker.getTopDevice() : connectedDevice;
|
|
|
|
if (topDevice != null) {
|
|
addRow(topDevice, connectedDevice, listBuilder);
|
|
worker.setTopDevice(topDevice);
|
|
}
|
|
|
|
for (MediaDevice device : devices) {
|
|
if (topDevice == null || !TextUtils.equals(topDevice.getId(), device.getId())) {
|
|
addRow(device, connectedDevice, listBuilder);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return listBuilder.build();
|
|
}
|
|
|
|
private ListBuilder.RowBuilder getPairNewRow() {
|
|
final Drawable d = mContext.getDrawable(R.drawable.ic_add_24dp);
|
|
d.setColorFilter(new PorterDuffColorFilter(Utils.getColorAccentDefaultColor(mContext),
|
|
PorterDuff.Mode.SRC_IN));
|
|
final IconCompat icon = Utils.createIconWithDrawable(d);
|
|
final String title = mContext.getString(R.string.bluetooth_pairing_pref_title);
|
|
final Intent intent = new SubSettingLauncher(mContext)
|
|
.setDestination(BluetoothPairingDetail.class.getName())
|
|
.setTitleRes(R.string.bluetooth_pairing_page_title)
|
|
.setSourceMetricsCategory(SettingsEnums.PANEL_MEDIA_OUTPUT)
|
|
.toIntent();
|
|
final SliceAction primarySliceAction = SliceAction.createDeeplink(
|
|
PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */),
|
|
IconCompat.createWithResource(mContext, R.drawable.ic_add_24dp/*ic_add_blue_24dp*/),
|
|
ListBuilder.ICON_IMAGE,
|
|
mContext.getText(R.string.bluetooth_pairing_pref_title));
|
|
final ListBuilder.RowBuilder builder = new ListBuilder.RowBuilder()
|
|
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
|
.setTitle(title)
|
|
.setPrimaryAction(primarySliceAction);
|
|
return builder;
|
|
}
|
|
|
|
private ListBuilder.InputRangeBuilder getGroupSliderRow() {
|
|
final IconCompat icon = IconCompat.createWithResource(mContext,
|
|
R.drawable.ic_speaker_group_black_24dp);
|
|
final CharSequence sessionName = getWorker().getSessionName();
|
|
final CharSequence title = TextUtils.isEmpty(sessionName)
|
|
? mContext.getString(R.string.media_output_group) : sessionName;
|
|
final PendingIntent broadcastAction =
|
|
getBroadcastIntent(mContext, MEDIA_GROUP_DEVICE, MEDIA_GROUP_DEVICE.hashCode());
|
|
final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon,
|
|
ListBuilder.ICON_IMAGE, title);
|
|
final ListBuilder.InputRangeBuilder builder = new ListBuilder.InputRangeBuilder()
|
|
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
|
.setTitle(title)
|
|
.setPrimaryAction(primarySliceAction)
|
|
.setInputAction(getSliderInputAction(MEDIA_GROUP_DEVICE.hashCode(),
|
|
MEDIA_GROUP_DEVICE))
|
|
.setMax(getWorker().getSessionVolumeMax())
|
|
.setValue(getWorker().getSessionVolume())
|
|
.addEndItem(getEndItemSliceAction());
|
|
return builder;
|
|
}
|
|
|
|
private ListBuilder.RowBuilder getGroupRow() {
|
|
final IconCompat icon = IconCompat.createWithResource(mContext,
|
|
R.drawable.ic_speaker_group_black_24dp);
|
|
final CharSequence sessionName = getWorker().getSessionName();
|
|
final CharSequence title = TextUtils.isEmpty(sessionName)
|
|
? mContext.getString(R.string.media_output_group) : sessionName;
|
|
final PendingIntent broadcastAction =
|
|
getBroadcastIntent(mContext, MEDIA_GROUP_DEVICE, MEDIA_GROUP_DEVICE.hashCode());
|
|
final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon,
|
|
ListBuilder.ICON_IMAGE, title);
|
|
final ListBuilder.RowBuilder builder = new ListBuilder.RowBuilder()
|
|
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
|
.setTitle(title)
|
|
.setPrimaryAction(primarySliceAction)
|
|
.addEndItem(getEndItemSliceAction());
|
|
return builder;
|
|
}
|
|
|
|
private void addRow(MediaDevice device, MediaDevice connectedDevice, ListBuilder listBuilder) {
|
|
if (connectedDevice != null && TextUtils.equals(device.getId(), connectedDevice.getId())) {
|
|
final String title = device.getName();
|
|
final IconCompat icon = getDeviceIconCompat(device);
|
|
|
|
final PendingIntent broadcastAction =
|
|
getBroadcastIntent(mContext, device.getId(), device.hashCode());
|
|
final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon,
|
|
ListBuilder.ICON_IMAGE, title);
|
|
|
|
if (device.getMaxVolume() > 0 && !getWorker().hasAdjustVolumeUserRestriction()) {
|
|
final ListBuilder.InputRangeBuilder builder = new ListBuilder.InputRangeBuilder()
|
|
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
|
.setTitle(title)
|
|
.setPrimaryAction(primarySliceAction)
|
|
.setInputAction(getSliderInputAction(device.hashCode(), device.getId()))
|
|
.setMax(device.getMaxVolume())
|
|
.setValue(device.getCurrentVolume());
|
|
// Check end item visibility
|
|
if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE
|
|
&& !getWorker().getSelectableMediaDevice().isEmpty()) {
|
|
builder.addEndItem(getEndItemSliceAction());
|
|
}
|
|
listBuilder.addInputRange(builder);
|
|
} else {
|
|
Log.d(TAG, "addRow device = " + device.getName() + " MaxVolume = "
|
|
+ device.getMaxVolume());
|
|
final ListBuilder.RowBuilder builder = getMediaDeviceRow(device);
|
|
// Check end item visibility
|
|
if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE
|
|
&& !getWorker().getSelectableMediaDevice().isEmpty()) {
|
|
builder.addEndItem(getEndItemSliceAction());
|
|
}
|
|
listBuilder.addRow(builder);
|
|
}
|
|
} else {
|
|
if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
|
|
listBuilder.addRange(getTransferringMediaDeviceRow(device));
|
|
} else {
|
|
listBuilder.addRow(getMediaDeviceRow(device));
|
|
}
|
|
}
|
|
}
|
|
|
|
private PendingIntent getSliderInputAction(int requestCode, String id) {
|
|
final Intent intent = new Intent(getUri().toString())
|
|
.setData(getUri())
|
|
.putExtra(MEDIA_DEVICE_ID, id)
|
|
.setClass(mContext, SliceBroadcastReceiver.class);
|
|
|
|
return PendingIntent.getBroadcast(mContext, requestCode, intent, 0);
|
|
}
|
|
|
|
private SliceAction getEndItemSliceAction() {
|
|
final Intent intent = new Intent()
|
|
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT_GROUP)
|
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
|
|
getWorker().getPackageName());
|
|
final int requestCode = TextUtils.isEmpty(getWorker().getPackageName())
|
|
? 0
|
|
: getWorker().getPackageName().hashCode();
|
|
return SliceAction.createDeeplink(
|
|
PendingIntent.getActivity(mContext, requestCode, intent, 0 /* flags */),
|
|
IconCompat.createWithResource(mContext, R.drawable.ic_add_blue_24dp),
|
|
ListBuilder.ICON_IMAGE,
|
|
mContext.getText(R.string.add));
|
|
}
|
|
|
|
private IconCompat getDeviceIconCompat(MediaDevice device) {
|
|
Drawable drawable = device.getIcon();
|
|
if (drawable == null) {
|
|
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 Utils.createIconWithDrawable(drawable);
|
|
}
|
|
|
|
private MediaDeviceUpdateWorker getWorker() {
|
|
if (mWorker == null) {
|
|
mWorker = SliceBackgroundWorker.getInstance(getUri());
|
|
}
|
|
return mWorker;
|
|
}
|
|
|
|
private Collection<MediaDevice> getMediaDevices() {
|
|
final Collection<MediaDevice> devices = getWorker().getMediaDevices();
|
|
return devices;
|
|
}
|
|
|
|
private ListBuilder.RangeBuilder getTransferringMediaDeviceRow(MediaDevice device) {
|
|
final IconCompat deviceIcon = getDeviceIconCompat(device);
|
|
final SliceAction sliceAction = SliceAction.create(getBroadcastIntent(mContext,
|
|
device.getId(), device.hashCode()), deviceIcon, ListBuilder.ICON_IMAGE,
|
|
mContext.getText(R.string.media_output_switching));
|
|
|
|
return new ListBuilder.RangeBuilder()
|
|
.setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE)
|
|
.setMode(ListBuilder.RANGE_MODE_INDETERMINATE)
|
|
.setTitle(device.getName())
|
|
.setPrimaryAction(sliceAction);
|
|
}
|
|
|
|
private ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) {
|
|
final String deviceName = device.getName();
|
|
final PendingIntent broadcastAction =
|
|
getBroadcastIntent(mContext, device.getId(), device.hashCode());
|
|
final IconCompat deviceIcon = getDeviceIconCompat(device);
|
|
final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
|
|
.setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE);
|
|
|
|
if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
|
|
&& !device.isConnected()) {
|
|
final int state = device.getState();
|
|
if (state == LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED) {
|
|
rowBuilder.setTitle(deviceName);
|
|
rowBuilder.setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
|
|
ListBuilder.ICON_IMAGE, deviceName));
|
|
rowBuilder.setSubtitle(mContext.getText(R.string.bluetooth_connect_failed));
|
|
} else {
|
|
// Append status to title only for the disconnected Bluetooth device.
|
|
final SpannableString spannableTitle = new SpannableString(
|
|
mContext.getString(R.string.media_output_disconnected_status, deviceName));
|
|
spannableTitle.setSpan(new ForegroundColorSpan(
|
|
Utils.getColorAttrDefaultColor(mContext,
|
|
android.R.attr.textColorSecondary)),
|
|
deviceName.length(),
|
|
spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
rowBuilder.setTitle(spannableTitle);
|
|
rowBuilder.setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
|
|
ListBuilder.ICON_IMAGE, spannableTitle));
|
|
}
|
|
} else {
|
|
rowBuilder.setTitle(deviceName);
|
|
rowBuilder.setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
|
|
ListBuilder.ICON_IMAGE, deviceName));
|
|
if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED) {
|
|
rowBuilder.setSubtitle(mContext.getText(R.string.media_output_switch_error_text));
|
|
}
|
|
}
|
|
|
|
return rowBuilder;
|
|
}
|
|
|
|
private PendingIntent getBroadcastIntent(Context context, String id, int requestCode) {
|
|
final Intent intent = new Intent(getUri().toString());
|
|
intent.setClass(context, SliceBroadcastReceiver.class);
|
|
intent.putExtra(MEDIA_DEVICE_ID, id);
|
|
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
|
|
return PendingIntent.getBroadcast(context, requestCode, intent,
|
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
}
|
|
|
|
@Override
|
|
public Uri getUri() {
|
|
return MEDIA_OUTPUT_SLICE_URI;
|
|
}
|
|
|
|
@Override
|
|
public void onNotifyChange(Intent intent) {
|
|
final MediaDeviceUpdateWorker worker = getWorker();
|
|
final String id = intent != null ? intent.getStringExtra(MEDIA_DEVICE_ID) : "";
|
|
if (TextUtils.isEmpty(id)) {
|
|
return;
|
|
}
|
|
|
|
final int newPosition = intent.getIntExtra(EXTRA_RANGE_VALUE, NON_SLIDER_VALUE);
|
|
if (TextUtils.equals(id, MEDIA_GROUP_DEVICE)) {
|
|
// Session volume adjustment
|
|
worker.adjustSessionVolume(newPosition);
|
|
} else {
|
|
final MediaDevice device = worker.getMediaDeviceById(id);
|
|
if (device == null) {
|
|
Log.d(TAG, "onNotifyChange: Unable to get device " + id);
|
|
return;
|
|
}
|
|
|
|
if (newPosition == NON_SLIDER_VALUE) {
|
|
// Intent for device connection
|
|
Log.d(TAG, "onNotifyChange: Switch to " + device.getName());
|
|
worker.setIsTouched(true);
|
|
worker.connectDevice(device);
|
|
} else {
|
|
// Single device volume adjustment
|
|
worker.adjustVolume(device, newPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Intent getIntent() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Class getBackgroundWorkerClass() {
|
|
return MediaOutputSliceWorker.class;
|
|
}
|
|
|
|
private boolean isVisible() {
|
|
// To decide Slice's visibility.
|
|
// Return true if
|
|
// 1. AudioMode is not in on-going call
|
|
// 2. worker is not null
|
|
// 3. Available devices are more than 0
|
|
return getWorker() != null
|
|
&& !com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)
|
|
&& getWorker().getMediaDevices().size() > 0;
|
|
}
|
|
}
|