blob: 316c903eed5bc700628155ff08177ff89cb0c983 [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 static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.widget.CompoundButtonCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Adapter for media output dialog.
*/
public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private static final String TAG = "MediaOutputAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final float DEVICE_DISCONNECTED_ALPHA = 0.5f;
private static final float DEVICE_CONNECTED_ALPHA = 1f;
protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
public MediaOutputAdapter(MediaOutputController controller) {
super(controller);
setHasStableIds(true);
}
@Override
public void updateItems() {
mMediaItemList.clear();
mMediaItemList.addAll(mController.getMediaItemList());
notifyDataSetChanged();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
super.onCreateViewHolder(viewGroup, viewType);
switch (viewType) {
case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
return new MediaGroupDividerViewHolder(mHolderView);
case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
case MediaItem.MediaItemType.TYPE_DEVICE:
default:
return new MediaDeviceViewHolder(mHolderView);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (position >= mMediaItemList.size()) {
if (DEBUG) {
Log.d(TAG, "Incorrect position: " + position + " list size: "
+ mMediaItemList.size());
}
return;
}
MediaItem currentMediaItem = mMediaItemList.get(position);
switch (currentMediaItem.getMediaItemType()) {
case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle());
break;
case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE:
((MediaDeviceViewHolder) viewHolder).onBind(CUSTOMIZED_ITEM_PAIR_NEW);
break;
case MediaItem.MediaItemType.TYPE_DEVICE:
((MediaDeviceViewHolder) viewHolder).onBind(
currentMediaItem.getMediaDevice().get(),
position);
break;
default:
Log.d(TAG, "Incorrect position: " + position);
}
}
@Override
public long getItemId(int position) {
if (position >= mMediaItemList.size()) {
Log.d(TAG, "Incorrect position for item id: " + position);
return position;
}
MediaItem currentMediaItem = mMediaItemList.get(position);
return currentMediaItem.getMediaDevice().isPresent()
? currentMediaItem.getMediaDevice().get().getId().hashCode()
: position;
}
@Override
public int getItemViewType(int position) {
if (position >= mMediaItemList.size()) {
Log.d(TAG, "Incorrect position for item type: " + position);
return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER;
}
return mMediaItemList.get(position).getMediaItemType();
}
@Override
public int getItemCount() {
return mMediaItemList.size();
}
class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
MediaDeviceViewHolder(View view) {
super(view);
}
@Override
void onBind(MediaDevice device, int position) {
super.onBind(device, position);
boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
final boolean currentlyConnected = isCurrentlyConnected(device);
boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
if (mCurrentActivePosition == position) {
mCurrentActivePosition = -1;
}
mStatusIcon.setVisibility(View.GONE);
if (mController.isAnyDeviceTransferring()) {
if (device.getState() == MediaDeviceState.STATE_CONNECTING
&& !mController.hasAdjustVolumeUserRestriction()) {
setUpDeviceIcon(device);
updateProgressBarColor();
setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/,
true /* showProgressBar */, false /* showCheckBox */,
false /* showEndTouchArea */);
} else {
setUpDeviceIcon(device);
setSingleLineLayout(getItemTitle(device));
}
} else {
// Set different layout for each device
if (device.isMutingExpectedDevice()
&& !mController.isCurrentConnectedDeviceRemote()) {
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
mCurrentActivePosition = position;
updateFullItemClickListener(v -> onItemClick(v, device));
setSingleLineLayout(getItemTitle(device));
initMutingExpectedDevice();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& device.hasSubtext()) {
boolean isActiveWithOngoingSession =
(device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded(
mController.getSelectedMediaDevice(), device)));
boolean isHost = device.isHostForOngoingSession()
&& isActiveWithOngoingSession;
if (isHost) {
mCurrentActivePosition = position;
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
mSubTitleText.setText(device.getSubtextString());
updateTwoLineLayoutContentAlpha(DEVICE_CONNECTED_ALPHA);
updateEndClickAreaAsSessionEditing(device);
setTwoLineLayout(device, null /* title */, true /* bFocused */,
true /* showSeekBar */, false /* showProgressBar */,
true /* showSubtitle */, false /* showStatus */,
true /* showEndTouchArea */, false /* isFakeActive */);
initSeekbar(device, isCurrentSeekbarInvisible);
} else {
if (isActiveWithOngoingSession) {
//Selected device which has ongoing session, disable seekbar since we
//only allow volume control on Host
mCurrentActivePosition = position;
}
boolean showSeekbar =
(!device.hasOngoingSession() && currentlyConnected);
if (showSeekbar) {
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
initSeekbar(device, isCurrentSeekbarInvisible);
} else {
setUpDeviceIcon(device);
}
mSubTitleText.setText(device.getSubtextString());
Drawable deviceStatusIcon =
device.hasOngoingSession() ? mContext.getDrawable(
R.drawable.ic_sound_bars_anim)
: Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(
device,
mContext);
if (deviceStatusIcon != null) {
updateDeviceStatusIcon(deviceStatusIcon);
}
updateTwoLineLayoutContentAlpha(
updateClickActionBasedOnSelectionBehavior(device)
? DEVICE_CONNECTED_ALPHA : DEVICE_DISCONNECTED_ALPHA);
setTwoLineLayout(device, currentlyConnected /* bFocused */,
showSeekbar /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
deviceStatusIcon != null /* showStatus */,
isActiveWithOngoingSession /* isFakeActive */);
}
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
updateConnectionFailedStatusIcon();
mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
updateFullItemClickListener(v -> onItemClick(v, device));
setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
true /* showStatus */, false /*isFakeActive*/);
} else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
setUpDeviceIcon(device);
updateProgressBarColor();
setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/,
true /* showProgressBar */, false /* showCheckBox */,
false /* showEndTouchArea */);
} else if (mController.getSelectedMediaDevice().size() > 1
&& isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
// selected device in group
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
updateGroupableCheckBox(true, isDeviceDeselectable, device);
updateEndClickArea(device, isDeviceDeselectable);
setUpContentDescriptionForView(mContainerLayout, false, device);
setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
false /* showProgressBar */, true /* showCheckBox */,
true /* showEndTouchArea */);
initSeekbar(device, isCurrentSeekbarInvisible);
} else if (!mController.hasAdjustVolumeUserRestriction()
&& currentlyConnected) {
// single selected device
if (isMutingExpectedDeviceExist
&& !mController.isCurrentConnectedDeviceRemote()) {
// mark as disconnected and set special click listener
setUpDeviceIcon(device);
updateFullItemClickListener(v -> cancelMuteAwaitConnection());
setSingleLineLayout(getItemTitle(device));
} else if (mController.isCurrentConnectedDeviceRemote()
&& !mController.getSelectableMediaDevice().isEmpty()) {
//If device is connected and there's other selectable devices, layout as
// one of selected devices.
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
updateGroupableCheckBox(true, isDeviceDeselectable, device);
updateEndClickArea(device, isDeviceDeselectable);
setUpContentDescriptionForView(mContainerLayout, false, device);
setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
false /* showProgressBar */, true /* showCheckBox */,
true /* showEndTouchArea */);
initSeekbar(device, isCurrentSeekbarInvisible);
} else {
updateTitleIcon(R.drawable.media_output_icon_volume,
mController.getColorItemContent());
setUpContentDescriptionForView(mContainerLayout, false, device);
mCurrentActivePosition = position;
setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
false /* showProgressBar */, false /* showCheckBox */,
false /* showEndTouchArea */);
initSeekbar(device, isCurrentSeekbarInvisible);
}
} else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
//groupable device
setUpDeviceIcon(device);
updateGroupableCheckBox(false, true, device);
updateEndClickArea(device, true);
updateFullItemClickListener(v -> onItemClick(v, device));
setSingleLineLayout(getItemTitle(device), false /* showSeekBar */,
false /* showProgressBar */, true /* showCheckBox */,
true /* showEndTouchArea */);
} else {
setUpDeviceIcon(device);
setSingleLineLayout(getItemTitle(device));
Drawable deviceStatusIcon =
device.hasOngoingSession() ? mContext.getDrawable(
R.drawable.ic_sound_bars_anim)
: Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(
device,
mContext);
if (deviceStatusIcon != null) {
updateDeviceStatusIcon(deviceStatusIcon);
mStatusIcon.setVisibility(View.VISIBLE);
}
updateSingleLineLayoutContentAlpha(
updateClickActionBasedOnSelectionBehavior(device)
? DEVICE_CONNECTED_ALPHA : DEVICE_DISCONNECTED_ALPHA);
}
}
}
public void setCheckBoxColor(CheckBox checkBox, int color) {
int[][] states = {{android.R.attr.state_checked}, {}};
int[] colors = {color, color};
CompoundButtonCompat.setButtonTintList(checkBox, new
ColorStateList(states, colors));
}
private void updateTwoLineLayoutContentAlpha(float alphaValue) {
mSubTitleText.setAlpha(alphaValue);
mTitleIcon.setAlpha(alphaValue);
mTwoLineTitleText.setAlpha(alphaValue);
mStatusIcon.setAlpha(alphaValue);
}
private void updateSingleLineLayoutContentAlpha(float alphaValue) {
mTitleIcon.setAlpha(alphaValue);
mTitleText.setAlpha(alphaValue);
mStatusIcon.setAlpha(alphaValue);
}
private void updateEndClickAreaAsSessionEditing(MediaDevice device) {
mEndClickIcon.setOnClickListener(null);
mEndTouchArea.setOnClickListener(null);
updateEndClickAreaColor(mController.getColorSeekbarProgress());
mEndClickIcon.setImageTintList(
ColorStateList.valueOf(mController.getColorItemContent()));
mEndClickIcon.setOnClickListener(
v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v));
mEndTouchArea.setOnClickListener(v -> mCheckBox.performClick());
}
public void updateEndClickAreaColor(int color) {
mEndTouchArea.setBackgroundTintList(
ColorStateList.valueOf(color));
}
private boolean updateClickActionBasedOnSelectionBehavior(MediaDevice device) {
View.OnClickListener clickListener = Api34Impl.getClickListenerBasedOnSelectionBehavior(
device, mController, v -> onItemClick(v, device));
updateFullItemClickListener(clickListener);
return clickListener != null;
}
private void updateConnectionFailedStatusIcon() {
mStatusIcon.setImageDrawable(
mContext.getDrawable(R.drawable.media_output_status_failed));
mStatusIcon.setImageTintList(
ColorStateList.valueOf(mController.getColorItemContent()));
}
private void updateDeviceStatusIcon(Drawable drawable) {
mStatusIcon.setImageDrawable(drawable);
mStatusIcon.setImageTintList(
ColorStateList.valueOf(mController.getColorItemContent()));
if (drawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) drawable).start();
}
}
private void updateProgressBarColor() {
mProgressBar.getIndeterminateDrawable().setTintList(
ColorStateList.valueOf(mController.getColorItemContent()));
}
public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) {
mEndTouchArea.setOnClickListener(null);
mEndTouchArea.setOnClickListener(
isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
mEndTouchArea.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_YES);
mEndTouchArea.setBackgroundTintList(
ColorStateList.valueOf(mController.getColorItemBackground()));
setUpContentDescriptionForView(mEndTouchArea, true, device);
}
private void updateGroupableCheckBox(boolean isSelected, boolean isGroupable,
MediaDevice device) {
mCheckBox.setOnCheckedChangeListener(null);
mCheckBox.setChecked(isSelected);
mCheckBox.setOnCheckedChangeListener(
isGroupable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected,
device) : null);
mCheckBox.setEnabled(isGroupable);
setCheckBoxColor(mCheckBox, mController.getColorItemContent());
}
private void updateFullItemClickListener(View.OnClickListener listener) {
mContainerLayout.setOnClickListener(listener);
updateIconAreaClickListener(listener);
}
@Override
void onBind(int customizedItem) {
if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
mTitleText.setTextColor(mController.getColorItemContent());
mCheckBox.setVisibility(View.GONE);
setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new));
final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
mTitleIcon.setImageDrawable(addDrawable);
mTitleIcon.setImageTintList(
ColorStateList.valueOf(mController.getColorItemContent()));
mIconAreaLayout.setBackgroundTintList(
ColorStateList.valueOf(mController.getColorItemBackground()));
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
}
private void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
disableSeekBar();
if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
mController.addDeviceToPlayMedia(device);
} else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
device)) {
mController.removeDeviceFromPlayMedia(device);
}
}
private void onItemClick(View view, MediaDevice device) {
if (mController.isCurrentOutputDeviceHasSessionOngoing()) {
showCustomEndSessionDialog(device);
} else {
transferOutput(device);
}
}
private void transferOutput(MediaDevice device) {
if (mController.isAnyDeviceTransferring()) {
return;
}
if (isCurrentlyConnected(device)) {
Log.d(TAG, "This device is already connected! : " + device.getName());
return;
}
mController.setTemporaryAllowListExceptionIfNeeded(device);
mCurrentActivePosition = -1;
mController.connectDevice(device);
device.setState(MediaDeviceState.STATE_CONNECTING);
notifyDataSetChanged();
}
@VisibleForTesting
void showCustomEndSessionDialog(MediaDevice device) {
MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
mContext, () -> transferOutput(device), mController.getColorButtonBackground(),
mController.getColorItemContent());
mediaSessionReleaseDialog.show();
}
private void cancelMuteAwaitConnection() {
mController.cancelMuteAwaitConnection();
notifyDataSetChanged();
}
private void setUpContentDescriptionForView(View view, boolean clickable,
MediaDevice device) {
view.setClickable(clickable);
view.setContentDescription(
mContext.getString(device.getDeviceType()
== MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
? R.string.accessibility_bluetooth_name
: R.string.accessibility_cast_name, device.getName()));
}
}
class MediaGroupDividerViewHolder extends RecyclerView.ViewHolder {
final TextView mTitleText;
MediaGroupDividerViewHolder(@NonNull View itemView) {
super(itemView);
mTitleText = itemView.requireViewById(R.id.title);
}
void onBind(String groupDividerTitle) {
mTitleText.setTextColor(mController.getColorItemContent());
mTitleText.setText(groupDividerTitle);
}
}
@RequiresApi(34)
private static class Api34Impl {
@DoNotInline
static View.OnClickListener getClickListenerBasedOnSelectionBehavior(MediaDevice device,
MediaOutputController controller, View.OnClickListener defaultTransferListener) {
switch (device.getSelectionBehavior()) {
case SELECTION_BEHAVIOR_NONE:
return null;
case SELECTION_BEHAVIOR_TRANSFER:
return defaultTransferListener;
case SELECTION_BEHAVIOR_GO_TO_APP:
return v -> controller.tryToLaunchInAppRoutingIntent(device.getId(), v);
}
return defaultTransferListener;
}
@DoNotInline
static Drawable getDeviceStatusIconBasedOnSelectionBehavior(MediaDevice device,
Context context) {
switch (device.getSelectionBehavior()) {
case SELECTION_BEHAVIOR_NONE:
return context.getDrawable(R.drawable.media_output_status_failed);
case SELECTION_BEHAVIOR_TRANSFER:
return null;
case SELECTION_BEHAVIOR_GO_TO_APP:
return context.getDrawable(R.drawable.media_output_status_help);
}
return null;
}
}
}