[Output Switcher] Update Layout and support

Update layout for disabled device and add support for Host session
device.

Test: atest MediaOutputAdapterTest MediaOutputControllerTest MediaOutputBaseDialogTest MediaOutputDialogTest
Bug: 262013662
Change-Id: I912c80baa99c4d8c628c7f2fb79bea0af6d07b9c
This commit is contained in:
shaoweishen
2023-02-02 15:54:32 +00:00
committed by Shaowei Shen
parent f761035d74
commit b4993b73cf
8 changed files with 219 additions and 60 deletions

View File

@@ -32,7 +32,7 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION;
import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED_ROUTE;
import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_NONE;
import static android.media.RouteListingPreference.Item.SELECTION_BEHAVIOR_TRANSFER;
import static android.media.RouteListingPreference.Item.SUBTEXT_AD_ROUTING_DISALLOWED;
import static android.media.RouteListingPreference.Item.SUBTEXT_CUSTOM;
import static android.media.RouteListingPreference.Item.SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED;
@@ -209,7 +209,7 @@ public abstract class MediaDevice implements Comparable<MediaDevice> {
@RouteListingPreference.Item.SubText
public int getSelectionBehavior() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_NONE;
? mItem.getSelectionBehavior() : SELECTION_BEHAVIOR_TRANSFER;
}
/**

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M1,20V17.2Q1,16.35 1.438,15.637Q1.875,14.925 2.6,14.55Q4.15,13.775 5.75,13.387Q7.35,13 9,13Q10.65,13 12.25,13.387Q13.85,13.775 15.4,14.55Q16.125,14.925 16.562,15.637Q17,16.35 17,17.2V20ZM19,20V17Q19,15.9 18.388,14.887Q17.775,13.875 16.65,13.15Q17.925,13.3 19.05,13.662Q20.175,14.025 21.15,14.55Q22.05,15.05 22.525,15.662Q23,16.275 23,17V20ZM9,12Q7.35,12 6.175,10.825Q5,9.65 5,8Q5,6.35 6.175,5.175Q7.35,4 9,4Q10.65,4 11.825,5.175Q13,6.35 13,8Q13,9.65 11.825,10.825Q10.65,12 9,12ZM19,8Q19,9.65 17.825,10.825Q16.65,12 15,12Q14.725,12 14.3,11.938Q13.875,11.875 13.6,11.8Q14.275,11 14.637,10.025Q15,9.05 15,8Q15,6.95 14.637,5.975Q14.275,5 13.6,4.2Q13.95,4.075 14.3,4.037Q14.65,4 15,4Q16.65,4 17.825,5.175Q19,6.35 19,8Z"/>
</vector>

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,20V12H8V20ZM10,20V4H14V20ZM16,20V9H20V20Z"/>
</vector>

View File

@@ -152,5 +152,16 @@
android:button="@drawable/media_output_item_check_box"
android:visibility="gone"
/>
<ImageView
android:id="@+id/media_output_item_end_click_icon"
android:src="@drawable/media_output_status_edit_session"
android:layout_width="24dp"
android:layout_height="24dp"
android:focusable="false"
android:importantForAccessibility="no"
android:layout_gravity="center"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"/>
</FrameLayout>
</FrameLayout>

View File

@@ -54,6 +54,8 @@ 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;
public MediaOutputAdapter(MediaOutputController controller) {
super(controller);
@@ -201,26 +203,48 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
&& mController.isSubStatusSupported()
&& mController.isAdvancedLayoutSupported() && device.hasSubtext()) {
boolean isActiveWithOngoingSession =
device.hasOngoingSession() && currentlyConnected;
if (isActiveWithOngoingSession) {
//Selected device which has ongoing session, disable seekbar since we
//only allow volume control on Host
mSeekBar.setVolume(0);
disableSeekBar();
(device.hasOngoingSession() && currentlyConnected);
boolean isHost = mController.isVolumeControlEnabled(device)
&& 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
initSeekbar(device, isCurrentSeekbarInvisible);
mCurrentActivePosition = position;
}
setUpDeviceIcon(device);
mSubTitleText.setText(device.getSubtextString());
Drawable deviceStatusIcon =
isActiveWithOngoingSession ? mContext.getDrawable(
R.drawable.media_output_status_session)
: Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(
device,
mContext);
if (deviceStatusIcon != null) {
updateDeviceStatusIcon(deviceStatusIcon);
}
updateTwoLineLayoutContentAlpha(
updateClickActionBasedOnSelectionBehavior(device)
? DEVICE_CONNECTED_ALPHA : DEVICE_DISCONNECTED_ALPHA);
setTwoLineLayout(device, isActiveWithOngoingSession /* bFocused */,
isActiveWithOngoingSession /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
deviceStatusIcon != null /* showStatus */,
isActiveWithOngoingSession /* isFakeActive */);
}
setUpDeviceIcon(device);
mSubTitleText.setText(device.getSubtextString());
Drawable deviceStatusIcon =
Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext);
if (deviceStatusIcon != null) {
updateDeviceStatusIcon(deviceStatusIcon);
}
updateClickActionBasedOnSelectionBehavior(device);
setTwoLineLayout(device, isActiveWithOngoingSession /* bFocused */,
isActiveWithOngoingSession /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
deviceStatusIcon != null /* showStatus */);
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
updateConnectionFailedStatusIcon();
@@ -228,7 +252,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
updateFullItemClickListener(v -> onItemClick(v, device));
setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
false /* showProgressBar */, true /* showSubtitle */,
true /* showStatus */);
true /* showStatus */, false /*isFakeActive*/);
} else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
setUpDeviceIcon(device);
updateProgressBarColor();
@@ -317,10 +341,35 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
ColorStateList(states, colors));
}
private void updateClickActionBasedOnSelectionBehavior(MediaDevice device) {
private void updateTwoLineLayoutContentAlpha(float alphaValue) {
mSubTitleText.setAlpha(alphaValue);
mTitleIcon.setAlpha(alphaValue);
mTwoLineTitleText.setAlpha(alphaValue);
mStatusIcon.setAlpha(alphaValue);
}
private void updateEndClickAreaAsSessionEditing(MediaDevice device) {
mEndClickIcon.setOnClickListener(null);
mEndTouchArea.setOnClickListener(null);
updateEndClickAreaColor(mController.getColorSeekbarProgress());
mEndClickIcon.setColorFilter(mController.getColorItemContent());
mEndClickIcon.setOnClickListener(
v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v));
mEndTouchArea.setOnClickListener(v -> mCheckBox.performClick());
}
public void updateEndClickAreaColor(int color) {
if (mController.isAdvancedLayoutSupported()) {
mEndTouchArea.getBackground().setColorFilter(
new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
}
}
private boolean updateClickActionBasedOnSelectionBehavior(MediaDevice device) {
View.OnClickListener clickListener = Api34Impl.getClickListenerBasedOnSelectionBehavior(
device, mController, v -> onItemClick(v, device));
updateFullItemClickListener(clickListener);
return clickListener != null;
}
private void updateConnectionFailedStatusIcon() {
@@ -457,7 +506,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
case SELECTION_BEHAVIOR_GO_TO_APP:
return v -> controller.tryToLaunchInAppRoutingIntent(device.getId(), v);
}
return null;
return defaultTransferListener;
}
@DoNotInline

View File

@@ -147,6 +147,7 @@ public abstract class MediaOutputBaseAdapter extends
final ImageView mStatusIcon;
final CheckBox mCheckBox;
final ViewGroup mEndTouchArea;
final ImageView mEndClickIcon;
@VisibleForTesting
MediaOutputSeekbar mSeekBar;
private String mDeviceId;
@@ -168,11 +169,13 @@ public abstract class MediaOutputBaseAdapter extends
mCheckBox = view.requireViewById(R.id.check_box);
mEndTouchArea = view.requireViewById(R.id.end_action_area);
if (mController.isAdvancedLayoutSupported()) {
mEndClickIcon = view.requireViewById(R.id.media_output_item_end_click_icon);
mVolumeValueText = view.requireViewById(R.id.volume_value);
mIconAreaLayout = view.requireViewById(R.id.icon_area);
} else {
mVolumeValueText = null;
mIconAreaLayout = null;
mEndClickIcon = null;
}
initAnimator();
}
@@ -218,20 +221,7 @@ public abstract class MediaOutputBaseAdapter extends
.mutate();
mItemLayout.setBackground(backgroundDrawable);
if (showSeekBar) {
final ClipDrawable clipDrawable =
(ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
.findDrawableByLayerId(android.R.id.progress);
final GradientDrawable progressDrawable =
(GradientDrawable) clipDrawable.getDrawable();
if (mController.isAdvancedLayoutSupported()) {
progressDrawable.setCornerRadii(
new float[]{0, 0, mController.getActiveRadius(),
mController.getActiveRadius(),
mController.getActiveRadius(),
mController.getActiveRadius(), 0, 0});
} else {
progressDrawable.setCornerRadius(mController.getActiveRadius());
}
updateSeekbarProgressBackground();
}
}
mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
@@ -265,14 +255,15 @@ public abstract class MediaOutputBaseAdapter extends
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
boolean showProgressBar, boolean showSubtitle, boolean showStatus) {
boolean showProgressBar, boolean showSubtitle, boolean showStatus,
boolean isFakeActive) {
setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle,
showStatus);
showStatus, false, isFakeActive);
}
private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
boolean showSeekBar, boolean showProgressBar, boolean showSubtitle,
boolean showStatus) {
boolean showStatus , boolean showEndTouchArea, boolean isFakeActive) {
mTitleText.setVisibility(View.GONE);
mTwoLineLayout.setVisibility(View.VISIBLE);
mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE);
@@ -287,12 +278,21 @@ public abstract class MediaOutputBaseAdapter extends
showSeekBar ? mController.getColorConnectedItemBackground()
: mController.getColorItemBackground(), PorterDuff.Mode.SRC_IN));
mIconAreaLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
showSeekBar ? mController.getColorConnectedItemBackground()
showProgressBar || isFakeActive
? mController.getColorConnectedItemBackground()
: showSeekBar ? mController.getColorSeekbarProgress()
: mController.getColorItemBackground(),
PorterDuff.Mode.SRC_IN));
if (showSeekBar) {
updateSeekbarProgressBackground();
}
//update end click area by isActive
mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
mEndClickIcon.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
params.rightMargin = mController.getItemMarginEndDefault();
params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
: mController.getItemMarginEndDefault();
} else {
backgroundDrawable = mContext.getDrawable(
R.drawable.media_output_item_background)
@@ -312,6 +312,23 @@ public abstract class MediaOutputBaseAdapter extends
Typeface.NORMAL));
}
void updateSeekbarProgressBackground() {
final ClipDrawable clipDrawable =
(ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
.findDrawableByLayerId(android.R.id.progress);
final GradientDrawable progressDrawable =
(GradientDrawable) clipDrawable.getDrawable();
if (mController.isAdvancedLayoutSupported()) {
progressDrawable.setCornerRadii(
new float[]{0, 0, mController.getActiveRadius(),
mController.getActiveRadius(),
mController.getActiveRadius(),
mController.getActiveRadius(), 0, 0});
} else {
progressDrawable.setCornerRadius(mController.getActiveRadius());
}
}
void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
if (!mController.isVolumeControlEnabled(device)) {
disableSeekBar();

View File

@@ -704,19 +704,21 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback,
devices.removeAll(targetMediaDevices);
targetMediaDevices.addAll(devices);
}
mMediaItemList.clear();
mMediaItemList.addAll(
targetMediaDevices.stream().map(MediaItem::new).collect(Collectors.toList()));
List<MediaItem> finalMediaItems = targetMediaDevices.stream().map(
MediaItem::new).collect(Collectors.toList());
dividerItems.forEach((key, item) -> {
mMediaItemList.add(key, item);
finalMediaItems.add(key, item);
});
mMediaItemList.add(new MediaItem());
finalMediaItems.add(new MediaItem());
mMediaItemList.clear();
mMediaItemList.addAll(finalMediaItems);
}
}
private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices,
boolean needToHandleMutingExpectedDevice) {
synchronized (mMediaDevicesLock) {
List<MediaItem> finalMediaItems = new ArrayList<>();
Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
MediaDevice::getId).collect(Collectors.toSet());
if (connectedMediaDevice != null) {
@@ -726,32 +728,32 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback,
boolean displayGroupAdded = false;
for (MediaDevice device : devices) {
if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) {
mMediaItemList.add(0, new MediaItem(device));
finalMediaItems.add(0, new MediaItem(device));
} else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains(
device.getId())) {
mMediaItemList.add(0, new MediaItem(device));
finalMediaItems.add(0, new MediaItem(device));
} else {
if (device.isSuggestedDevice() && !suggestedDeviceAdded) {
attachGroupDivider(mContext.getString(
attachGroupDivider(finalMediaItems, mContext.getString(
R.string.media_output_group_title_suggested_device));
suggestedDeviceAdded = true;
} else if (!device.isSuggestedDevice() && !displayGroupAdded) {
attachGroupDivider(mContext.getString(
attachGroupDivider(finalMediaItems, mContext.getString(
R.string.media_output_group_title_speakers_and_displays));
displayGroupAdded = true;
}
mMediaItemList.add(new MediaItem(device));
finalMediaItems.add(new MediaItem(device));
}
}
mMediaItemList.add(new MediaItem());
finalMediaItems.add(new MediaItem());
mMediaItemList.clear();
mMediaItemList.addAll(finalMediaItems);
}
}
private void attachGroupDivider(String title) {
synchronized (mMediaDevicesLock) {
mMediaItemList.add(
new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
}
private void attachGroupDivider(List<MediaItem> mediaItems, String title) {
mediaItems.add(
new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
}
private void attachRangeInfo(List<MediaDevice> devices) {

View File

@@ -435,6 +435,34 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
}
@Test
public void subStatusSupported_onBindViewHolder_bindHostDeviceWithOngoingSession_verifyView() {
when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true);
when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
when(mMediaDevice1.hasSubtext()).thenReturn(true);
when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM);
when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT);
when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mEndClickIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mSubTitleText.getText().toString()).isEqualTo(TEST_CUSTOM_SUBTEXT);
assertThat(mViewHolder.mTwoLineTitleText.getText().toString()).isEqualTo(
TEST_DEVICE_NAME_1);
assertThat(mViewHolder.mContainerLayout.hasOnClickListeners()).isFalse();
}
@Test
public void subStatusSupported_onBindViewHolder_bindDeviceRequirePremium_verifyView() {
String deviceStatus = (String) mContext.getText(
@@ -505,7 +533,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mStatusIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mSubTitleText.getText().toString()).isEqualTo(TEST_CUSTOM_SUBTEXT);