Files
app_Settings/src/com/android/settings/media/MediaOutputSlice.java
Hugh Chen c3fe8d87d6 Fix output switcher will display previous media session
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
2020-09-23 10:20:19 +08:00

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;
}
}