summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author tim peng <timhypeng@google.com> 2020-09-15 01:22:39 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2020-09-15 01:22:39 +0000
commite9897deb31553c96fcce8e7d20500afc65a41766 (patch)
tree771d79c961b8bc58bb4b905de10f74878c0467c2
parent2618b23c8903504b5727ba5b6ece9e690dabf7f8 (diff)
parent6d31848fb2441947f9708e131014cfecb21d9f65 (diff)
Merge changes Icb5a1f24,Idbfc558b,I3086a404,Ida509ed4,I9eb6e3b0
* changes: Add a receiver to launch Output Switcher dialog Add MediaOutputDialogFactory to create MediaOutputDialog Add Media Output Dialog for Output Switcher Add MediaOutputAdapter for Media device list Add controller for Media operation
-rw-r--r--packages/SystemUI/AndroidManifest.xml7
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_background.xml23
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml136
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item.xml112
-rw-r--r--packages/SystemUI/res/values/dimens.xml7
-rw-r--r--packages/SystemUI/res/values/strings.xml15
-rw-r--r--packages/SystemUI/res/values/styles.xml5
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java163
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java165
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java220
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java445
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java77
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt42
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java200
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java212
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java348
18 files changed, 2225 insertions, 1 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 06a97b17a73c..57c15e3211e1 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -772,5 +772,12 @@
</intent-filter>
</receiver>
+ <receiver android:name=".media.dialog.MediaOutDialogReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
+ </intent-filter>
+ </receiver>
+
</application>
</manifest>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_background.xml
new file mode 100644
index 000000000000..3ceb0f6ac06a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android">
+ <shape android:shape="rectangle">
+ <corners android:radius="8dp" />
+ <solid android:color="?android:attr/colorBackground" />
+ </shape>
+</inset>
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
new file mode 100644
index 000000000000..0229e6e9d4dd
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_output_dialog"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="94dp"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/header_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="16dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/header_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textSize="20sp"/>
+
+ <TextView
+ android:id="@+id/header_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:fontFamily="roboto-regular"
+ android:textSize="14sp"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"/>
+
+ <LinearLayout
+ android:id="@+id/device_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="12dp"/>
+
+ <include
+ layout="@layout/media_output_list_item"
+ android:id="@+id/group_item_controller"
+ android:visibility="gone"/>
+
+ <View
+ android:id="@+id/group_item_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"
+ android:visibility="gone"/>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_result"
+ android:scrollbars="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:overScrollMode="never"/>
+
+ <View
+ android:id="@+id/list_bottom_padding"
+ android:layout_width="match_parent"
+ android:layout_height="12dp"/>
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/listDivider"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/stop"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="64dp"
+ android:text="@string/keyboard_key_media_stop"
+ android:visibility="gone"/>
+
+ <Space
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"/>
+
+ <Button
+ android:id="@+id/done"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="64dp"
+ android:layout_marginEnd="0dp"
+ android:text="@string/inline_done_button"/>
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
new file mode 100644
index 000000000000..92d0858a1a31
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<FrameLayout
+ android:id="@+id/device_container"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="64dp">
+
+ <FrameLayout
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="16dp">
+ <ImageView
+ android:id="@+id/title_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"/>
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="68dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="14sp"/>
+
+ <RelativeLayout
+ android:id="@+id/two_line_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginStart="52dp"
+ android:layout_marginEnd="69dp"
+ android:layout_marginTop="10dp">
+ <TextView
+ android:id="@+id/two_line_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="15dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="14sp"/>
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="15dp"
+ android:layout_marginBottom="7dp"
+ android:layout_alignParentBottom="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="12sp"
+ android:fontFamily="roboto-regular"
+ android:visibility="gone"/>
+ <ProgressBar
+ android:id="@+id/volume_indeterminate_progress"
+ style="@*android:style/Widget.Material.ProgressBar.Horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="15dp"
+ android:layout_marginBottom="1dp"
+ android:layout_alignParentBottom="true"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:visibility="gone"/>
+ <SeekBar
+ android:id="@+id/volume_seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"/>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="1dp"
+ android:layout_height="36dp"
+ android:layout_marginEnd="68dp"
+ android:layout_gravity="right|center_vertical"
+ android:background="?android:attr/listDivider"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/end_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="right|center_vertical"
+ android:layout_marginEnd="24dp"
+ android:visibility="gone"/>
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 875fe1471b1c..98e8cde40275 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1366,4 +1366,11 @@
<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_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>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e577b967a85e..a6a39034c5eb 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2799,4 +2799,19 @@
<string name="udfps_hbm_enable_command" translatable="false"></string>
<!-- Device-specific payload for disabling the high-brightness mode -->
<string name="udfps_hbm_disable_command" translatable="false"></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>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 58563f49dce4..2b0a963bff18 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -388,6 +388,10 @@
<item name="android:windowIsFloating">true</item>
</style>
+ <style name="Theme.SystemUI.Dialog.MediaOutput">
+ <item name="android:windowBackground">@drawable/media_output_dialog_background</item>
+ </style>
+
<style name="QSBorderlessButton">
<item name="android:padding">12dp</item>
<item name="android:background">@drawable/qs_btn_borderless_rect</item>
@@ -735,5 +739,4 @@
* Title: headline, medium 20sp
* Message: body, 16 sp -->
<style name="Theme.ControlsRequestDialog" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert"/>
-
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
index 6e8d63b2c516..307362fe790e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
@@ -18,6 +18,7 @@ package com.android.systemui.dagger;
import android.content.BroadcastReceiver;
+import com.android.systemui.media.dialog.MediaOutDialogReceiver;
import com.android.systemui.screenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.DeleteScreenshotReceiver;
import com.android.systemui.screenshot.SmartActionsReceiver;
@@ -59,4 +60,13 @@ public abstract class DefaultBroadcastReceiverBinder {
public abstract BroadcastReceiver bindSmartActionsReceiver(
SmartActionsReceiver broadcastReceiver);
+ /**
+ *
+ */
+ @Binds
+ @IntoMap
+ @ClassKey(MediaOutDialogReceiver.class)
+ public abstract BroadcastReceiver bindMediaOutDialogReceiver(
+ MediaOutDialogReceiver broadcastReceiver);
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt
new file mode 100644
index 000000000000..d60771394ded
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.text.TextUtils
+import com.android.settingslib.media.MediaOutputSliceConstants
+import javax.inject.Inject
+
+/**
+ * BroadcastReceiver for handling media output intent
+ */
+class MediaOutDialogReceiver @Inject constructor(
+ private var mediaOutputDialogFactory: MediaOutputDialogFactory
+) : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (TextUtils.equals(MediaOutputSliceConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG,
+ intent.action)) {
+ mediaOutputDialogFactory.create(
+ intent.getStringExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME), false)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
new file mode 100644
index 000000000000..9fc64d51cdf7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -0,0 +1,163 @@
+/*
+ * 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 android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
+
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.Utils;
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Adapter for media output dialog.
+ */
+public class MediaOutputAdapter extends MediaOutputBaseAdapter {
+
+ private static final String TAG = "MediaOutputAdapter";
+ private static final int PAIR_NEW = 1;
+
+ public MediaOutputAdapter(MediaOutputController controller) {
+ super(controller);
+ }
+
+ @Override
+ public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+ int viewType) {
+ super.onCreateViewHolder(viewGroup, viewType);
+
+ return new MediaDeviceViewHolder(mHolderView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) {
+ if (mController.isZeroMode() && position == (mController.getMediaDevices().size())) {
+ viewHolder.onBind(PAIR_NEW);
+ } else if (position < (mController.getMediaDevices().size())) {
+ viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position));
+ } else {
+ Log.d(TAG, "Incorrect position: " + position);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mController.isZeroMode()) {
+ // Add extra one for "pair new"
+ return mController.getMediaDevices().size() + 1;
+ }
+ return mController.getMediaDevices().size();
+ }
+
+ void onItemClick(MediaDevice device) {
+ mController.connectDevice(device);
+ device.setState(MediaDeviceState.STATE_CONNECTING);
+ notifyDataSetChanged();
+ }
+
+ void onItemClick(int customizedItem) {
+ if (customizedItem == PAIR_NEW) {
+ mController.launchBluetoothPairing();
+ }
+ }
+
+ @Override
+ CharSequence getItemTitle(MediaDevice device) {
+ if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
+ && !device.isConnected()) {
+ final CharSequence deviceName = device.getName();
+ // Append status to title only for the disconnected Bluetooth device.
+ final SpannableString spannableTitle = new SpannableString(
+ mContext.getString(R.string.media_output_dialog_disconnected, deviceName));
+ spannableTitle.setSpan(new ForegroundColorSpan(
+ Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)),
+ deviceName.length(),
+ spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
+ return spannableTitle;
+ }
+ return super.getItemTitle(device);
+ }
+
+ class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
+
+ MediaDeviceViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ void onBind(MediaDevice device) {
+ super.onBind(device);
+ if (mController.isTransferring()) {
+ if (device.getState() == MediaDeviceState.STATE_CONNECTING
+ && !mController.hasAdjustVolumeUserRestriction()) {
+ setTwoLineLayout(device, true);
+ mProgressBar.setVisibility(View.VISIBLE);
+ mSeekBar.setVisibility(View.GONE);
+ mSubTitleText.setVisibility(View.GONE);
+ } else {
+ setSingleLineLayout(getItemTitle(device), false);
+ }
+ } else {
+ // Set different layout for each device
+ if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
+ setTwoLineLayout(device, false);
+ mSubTitleText.setVisibility(View.VISIBLE);
+ mSeekBar.setVisibility(View.GONE);
+ mProgressBar.setVisibility(View.GONE);
+ mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
+ mFrameLayout.setOnClickListener(v -> onItemClick(device));
+ } else if (!mController.hasAdjustVolumeUserRestriction()
+ && isCurrentConnected(device)) {
+ setTwoLineLayout(device, true);
+ mSeekBar.setVisibility(View.VISIBLE);
+ mProgressBar.setVisibility(View.GONE);
+ mSubTitleText.setVisibility(View.GONE);
+ initSeekbar(device);
+ } else {
+ setSingleLineLayout(getItemTitle(device), false);
+ mFrameLayout.setOnClickListener(v -> onItemClick(device));
+ }
+ }
+ }
+
+ @Override
+ void onBind(int customizedItem) {
+ if (customizedItem == PAIR_NEW) {
+ setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
+ false);
+ final Drawable d = mContext.getDrawable(R.drawable.ic_add);
+ d.setColorFilter(new PorterDuffColorFilter(
+ Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
+ mTitleIcon.setImageDrawable(d);
+ mFrameLayout.setOnClickListener(v -> onItemClick(PAIR_NEW));
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
new file mode 100644
index 000000000000..7579c25b030a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -0,0 +1,165 @@
+/*
+ * 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.graphics.Typeface;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+/**
+ * Base adapter for media output dialog.
+ */
+public abstract class MediaOutputBaseAdapter extends
+ RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> {
+
+ private static final String FONT_SELECTED_TITLE = "sans-serif-medium";
+ private static final String FONT_TITLE = "sans-serif";
+
+ final MediaOutputController mController;
+
+ private boolean mIsDragging;
+
+ Context mContext;
+ View mHolderView;
+
+ public MediaOutputBaseAdapter(MediaOutputController controller) {
+ mController = controller;
+ mIsDragging = false;
+ }
+
+ @Override
+ public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+ int viewType) {
+ mContext = viewGroup.getContext();
+ mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
+ viewGroup, false);
+
+ return null;
+ }
+
+ CharSequence getItemTitle(MediaDevice device) {
+ return device.getName();
+ }
+
+ boolean isCurrentConnected(MediaDevice device) {
+ return TextUtils.equals(device.getId(),
+ mController.getCurrentConnectedMediaDevice().getId());
+ }
+
+ boolean isDragging() {
+ return mIsDragging;
+ }
+
+ /**
+ * ViewHolder for binding device view.
+ */
+ abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+ final FrameLayout mFrameLayout;
+ final TextView mTitleText;
+ final TextView mTwoLineTitleText;
+ final TextView mSubTitleText;
+ final ImageView mTitleIcon;
+ final ImageView mEndIcon;
+ final ProgressBar mProgressBar;
+ final SeekBar mSeekBar;
+ final RelativeLayout mTwoLineLayout;
+
+ MediaDeviceBaseViewHolder(View view) {
+ super(view);
+ mFrameLayout = view.requireViewById(R.id.device_container);
+ mTitleText = view.requireViewById(R.id.title);
+ mSubTitleText = view.requireViewById(R.id.subtitle);
+ mTwoLineLayout = view.requireViewById(R.id.two_line_layout);
+ mTwoLineTitleText = view.requireViewById(R.id.two_line_title);
+ mTitleIcon = view.requireViewById(R.id.title_icon);
+ mEndIcon = view.requireViewById(R.id.end_icon);
+ mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
+ mSeekBar = view.requireViewById(R.id.volume_seekbar);
+ }
+
+ void onBind(MediaDevice device) {
+ mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext));
+ }
+
+ void onBind(int customizedItem) { }
+
+ void setSingleLineLayout(CharSequence title, boolean bFocused) {
+ mTitleText.setVisibility(View.VISIBLE);
+ mTwoLineLayout.setVisibility(View.GONE);
+ mTitleText.setText(title);
+ if (bFocused) {
+ mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL));
+ } else {
+ mTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+ }
+ }
+
+ void setTwoLineLayout(MediaDevice device, boolean bFocused) {
+ mTitleText.setVisibility(View.GONE);
+ mTwoLineLayout.setVisibility(View.VISIBLE);
+ mTwoLineTitleText.setText(getItemTitle(device));
+ if (bFocused) {
+ mTwoLineTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE,
+ Typeface.NORMAL));
+ } else {
+ mTwoLineTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+ }
+ }
+
+ void initSeekbar(MediaDevice device) {
+ mSeekBar.setMax(device.getMaxVolume());
+ mSeekBar.setMin(0);
+ if (mSeekBar.getProgress() != device.getCurrentVolume()) {
+ mSeekBar.setProgress(device.getCurrentVolume());
+ }
+ mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (device == null || !fromUser) {
+ return;
+ }
+ mController.adjustVolume(device, progress);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mIsDragging = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mIsDragging = false;
+ }
+ });
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
new file mode 100644
index 000000000000..781bf8d74d88
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -0,0 +1,220 @@
+/*
+ * 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 android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settingslib.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+/**
+ * Base dialog for media output UI
+ */
+public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
+ MediaOutputController.Callback {
+
+ private static final String TAG = "MediaOutputDialog";
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final RecyclerView.LayoutManager mLayoutManager;
+
+ final Context mContext;
+ final MediaOutputController mMediaOutputController;
+
+ @VisibleForTesting
+ View mDialogView;
+ private TextView mHeaderTitle;
+ private TextView mHeaderSubtitle;
+ private ImageView mHeaderIcon;
+ private RecyclerView mDevicesRecyclerView;
+ private LinearLayout mDeviceListLayout;
+ private Button mDoneButton;
+ private Button mStopButton;
+ private View mListBottomPadding;
+ private int mListMaxHeight;
+
+ MediaOutputBaseAdapter mAdapter;
+ FrameLayout mGroupItemController;
+ View mGroupDivider;
+
+ private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
+ // Set max height for list
+ if (mDeviceListLayout.getHeight() > mListMaxHeight) {
+ ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
+ params.height = mListMaxHeight;
+ mDeviceListLayout.setLayoutParams(params);
+ }
+ };
+
+ public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
+ super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
+ mContext = context;
+ mMediaOutputController = mediaOutputController;
+ mLayoutManager = new LinearLayoutManager(mContext);
+ mListMaxHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_list_max_height);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
+ final Window window = getWindow();
+ final WindowManager.LayoutParams lp = window.getAttributes();
+ lp.gravity = Gravity.BOTTOM;
+ // Config insets to make sure the layout is above the navigation bar
+ lp.setFitInsetsTypes(statusBars() | navigationBars());
+ lp.setFitInsetsSides(WindowInsets.Side.all());
+ lp.setFitInsetsIgnoringVisibility(true);
+ window.setAttributes(lp);
+ window.setContentView(mDialogView);
+ window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
+ mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
+ mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
+ mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+ mGroupItemController = mDialogView.requireViewById(R.id.group_item_controller);
+ mGroupDivider = mDialogView.requireViewById(R.id.group_item_divider);
+ mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
+ mDoneButton = mDialogView.requireViewById(R.id.done);
+ mStopButton = mDialogView.requireViewById(R.id.stop);
+ mListBottomPadding = mDialogView.requireViewById(R.id.list_bottom_padding);
+
+ mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
+ mDeviceListLayoutListener);
+ // Init device list
+ mDevicesRecyclerView.setLayoutManager(mLayoutManager);
+ mDevicesRecyclerView.setAdapter(mAdapter);
+ // Init bottom buttons
+ mDoneButton.setOnClickListener(v -> dismiss());
+ mStopButton.setOnClickListener(v -> {
+ mMediaOutputController.releaseSession();
+ dismiss();
+ });
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mMediaOutputController.start(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mMediaOutputController.stop();
+ }
+
+ @VisibleForTesting
+ void refresh() {
+ // Update header icon
+ final int iconRes = getHeaderIconRes();
+ final IconCompat iconCompat = getHeaderIcon();
+ if (iconRes != 0) {
+ mHeaderIcon.setVisibility(View.VISIBLE);
+ mHeaderIcon.setImageResource(iconRes);
+ } else if (iconCompat != null) {
+ mHeaderIcon.setVisibility(View.VISIBLE);
+ mHeaderIcon.setImageIcon(iconCompat.toIcon(mContext));
+ } else {
+ mHeaderIcon.setVisibility(View.GONE);
+ }
+ if (mHeaderIcon.getVisibility() == View.VISIBLE) {
+ final int size = getHeaderIconSize();
+ mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size, size));
+ }
+ // Update title and subtitle
+ mHeaderTitle.setText(getHeaderText());
+ final CharSequence subTitle = getHeaderSubtitle();
+ if (TextUtils.isEmpty(subTitle)) {
+ mHeaderSubtitle.setVisibility(View.GONE);
+ mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ } else {
+ mHeaderSubtitle.setVisibility(View.VISIBLE);
+ mHeaderSubtitle.setText(subTitle);
+ mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+ }
+ if (!mAdapter.isDragging()) {
+ mAdapter.notifyDataSetChanged();
+ }
+ // Add extra padding when device amount is less than 6
+ if (mMediaOutputController.getMediaDevices().size() < 6) {
+ mListBottomPadding.setVisibility(View.VISIBLE);
+ } else {
+ mListBottomPadding.setVisibility(View.GONE);
+ }
+ }
+
+ abstract int getHeaderIconRes();
+
+ abstract IconCompat getHeaderIcon();
+
+ abstract int getHeaderIconSize();
+
+ abstract CharSequence getHeaderText();
+
+ abstract CharSequence getHeaderSubtitle();
+
+ @Override
+ public void onMediaChanged() {
+ mMainThreadHandler.post(() -> refresh());
+ }
+
+ @Override
+ public void onMediaStoppedOrPaused() {
+ if (isShowing()) {
+ dismiss();
+ }
+ }
+
+ @Override
+ public void onRouteChanged() {
+ mMainThreadHandler.post(() -> refresh());
+ }
+
+ @Override
+ public void dismissDialog() {
+ dismiss();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
new file mode 100644
index 000000000000..64d20a273931
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -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();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
new file mode 100644
index 000000000000..ac9d8ce52d88
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -0,0 +1,77 @@
+/*
+ * 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.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
+
+/**
+ * Dialog for media output transferring.
+ */
+@SysUISingleton
+public class MediaOutputDialog extends MediaOutputBaseDialog {
+
+ MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController
+ mediaOutputController) {
+ super(context, mediaOutputController);
+ mAdapter = new MediaOutputAdapter(mMediaOutputController);
+ if (!aboveStatusbar) {
+ getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+ }
+ show();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mGroupItemController.setVisibility(View.GONE);
+ mGroupDivider.setVisibility(View.GONE);
+ }
+
+ @Override
+ int getHeaderIconRes() {
+ return 0;
+ }
+
+ @Override
+ IconCompat getHeaderIcon() {
+ return mMediaOutputController.getHeaderIcon();
+ }
+
+ @Override
+ int getHeaderIconSize() {
+ return mContext.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_header_album_icon_size);
+ }
+
+ @Override
+ CharSequence getHeaderText() {
+ return mMediaOutputController.getHeaderTitle();
+ }
+
+ @Override
+ CharSequence getHeaderSubtitle() {
+ return mMediaOutputController.getHeaderSubTitle();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
new file mode 100644
index 000000000000..bc1dca58990d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.media.session.MediaSessionManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.ShadeController
+import javax.inject.Inject
+
+/**
+ * Factory to create [MediaOutputDialog] objects.
+ */
+class MediaOutputDialogFactory @Inject constructor(
+ private val context: Context,
+ private val mediaSessionManager: MediaSessionManager,
+ private val lbm: LocalBluetoothManager?,
+ private val shadeController: ShadeController,
+ private val starter: ActivityStarter
+) {
+ /** Creates a [MediaOutputDialog] for the given package. */
+ fun create(packageName: String, aboveStatusBar: Boolean) {
+ MediaOutputController(context, packageName, mediaSessionManager, lbm, shadeController,
+ starter).run {
+ MediaOutputDialog(context, aboveStatusBar, this) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
new file mode 100644
index 000000000000..0e376bd356a2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.drawable.Icon;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+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 MediaOutputAdapterTest extends SysuiTestCase {
+
+ private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
+ private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
+ private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
+ private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
+
+ // Mock
+ private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
+ private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
+ private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+ private Icon mIcon = mock(Icon.class);
+ private IconCompat mIconCompat = mock(IconCompat.class);
+
+ private MediaOutputAdapter mMediaOutputAdapter;
+ private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
+ private List<MediaDevice> mMediaDevices = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+ mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+ .onCreateViewHolder(new FrameLayout(mContext), 0);
+
+ when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
+ when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
+ when(mMediaOutputController.isZeroMode()).thenReturn(false);
+ when(mMediaOutputController.isTransferring()).thenReturn(false);
+ when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
+ when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
+ when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
+ when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
+ when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
+ when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
+ when(mMediaDevice2.getName()).thenReturn(TEST_DEVICE_NAME_2);
+ when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_ID_2);
+ when(mMediaDevice1.getState()).thenReturn(
+ LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+ when(mMediaDevice2.getState()).thenReturn(
+ LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+ mMediaDevices.add(mMediaDevice1);
+ mMediaDevices.add(mMediaDevice2);
+ }
+
+ @Test
+ public void getItemCount_nonZeroMode_isDeviceSize() {
+ assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size());
+ }
+
+ @Test
+ public void getItemCount_zeroMode_containExtraOneForPairNew() {
+ when(mMediaOutputController.isZeroMode()).thenReturn(true);
+
+ assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1);
+ }
+
+ @Test
+ public void onBindViewHolder_zeroMode_bindPairNew_verifyView() {
+ when(mMediaOutputController.isZeroMode()).thenReturn(true);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText(
+ R.string.media_output_dialog_pairing_new));
+ }
+
+ @Test
+ public void onBindViewHolder_bindConnectedDevice_verifyView() {
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+ }
+
+ @Test
+ public void onBindViewHolder_bindDisconnectedBluetoothDevice_verifyView() {
+ when(mMediaDevice2.getDeviceType()).thenReturn(
+ MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
+ when(mMediaDevice2.isConnected()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(
+ mContext.getString(R.string.media_output_dialog_disconnected, TEST_DEVICE_NAME_2));
+ }
+
+ @Test
+ public void onBindViewHolder_bindFailedStateDevice_verifyView() {
+ when(mMediaDevice2.getState()).thenReturn(
+ LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(mContext.getText(
+ R.string.media_output_dialog_connect_failed));
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+ }
+
+ @Test
+ public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
+ when(mMediaOutputController.isTransferring()).thenReturn(true);
+ when(mMediaDevice1.getState()).thenReturn(
+ LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+ }
+
+ @Test
+ public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
+ when(mMediaOutputController.isTransferring()).thenReturn(true);
+ when(mMediaDevice2.getState()).thenReturn(
+ LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+ }
+
+ @Test
+ public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
+ when(mMediaOutputController.isZeroMode()).thenReturn(true);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
+ mViewHolder.mFrameLayout.performClick();
+
+ verify(mMediaOutputController).launchBluetoothPairing();
+ }
+
+ @Test
+ public void onItemClick_clickDevice_verifyConnectDevice() {
+ assertThat(mMediaDevice2.getState()).isEqualTo(
+ LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+ mViewHolder.mFrameLayout.performClick();
+
+ verify(mMediaOutputController).connectDevice(mMediaDevice2);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
new file mode 100644
index 000000000000..42b21c61510a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class MediaOutputBaseDialogTest extends SysuiTestCase {
+
+ private static final String TEST_PACKAGE = "test_package";
+
+ // Mock
+ private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class);
+
+ private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
+ private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
+ private ShadeController mShadeController = mock(ShadeController.class);
+ private ActivityStarter mStarter = mock(ActivityStarter.class);
+
+ private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
+ private MediaOutputController mMediaOutputController;
+ private int mHeaderIconRes;
+ private IconCompat mIconCompat;
+ private CharSequence mHeaderTitle;
+ private CharSequence mHeaderSubtitle;
+
+ @Before
+ public void setUp() {
+ mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
+ mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
+ mMediaOutputController);
+ mMediaOutputBaseDialogImpl.onCreate(new Bundle());
+ }
+
+ @Test
+ public void refresh_withIconRes_iconIsVisible() {
+ mHeaderIconRes = 1;
+ mMediaOutputBaseDialogImpl.refresh();
+ final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.header_icon);
+
+ assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void refresh_withIconCompat_iconIsVisible() {
+ mIconCompat = mock(IconCompat.class);
+ mMediaOutputBaseDialogImpl.refresh();
+ final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.header_icon);
+
+ assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void refresh_noIcon_iconLayoutNotVisible() {
+ mHeaderIconRes = 0;
+ mIconCompat = null;
+ mMediaOutputBaseDialogImpl.refresh();
+ final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.header_icon);
+
+ assertThat(view.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void refresh_checkTitle() {
+ mHeaderTitle = "test_string";
+
+ mMediaOutputBaseDialogImpl.refresh();
+ final TextView titleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.header_title);
+
+ assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(titleView.getText()).isEqualTo(mHeaderTitle);
+ }
+
+ @Test
+ public void refresh_withSubtitle_checkSubtitle() {
+ mHeaderSubtitle = "test_string";
+
+ mMediaOutputBaseDialogImpl.refresh();
+ final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.header_subtitle);
+
+ assertThat(subtitleView.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(subtitleView.getText()).isEqualTo(mHeaderSubtitle);
+ }
+
+ @Test
+ public void refresh_noSubtitle_checkSubtitle() {
+ mMediaOutputBaseDialogImpl.refresh();
+ final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.header_subtitle);
+
+ assertThat(subtitleView.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void refresh_inDragging_notUpdateAdapter() {
+ when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true);
+ mMediaOutputBaseDialogImpl.refresh();
+
+ verify(mMediaOutputBaseAdapter, never()).notifyDataSetChanged();
+ }
+
+ @Test
+ public void refresh_notInDragging_verifyUpdateAdapter() {
+ when(mMediaOutputBaseAdapter.isDragging()).thenReturn(false);
+ mMediaOutputBaseDialogImpl.refresh();
+
+ verify(mMediaOutputBaseAdapter).notifyDataSetChanged();
+ }
+
+ @Test
+ public void refresh_with6Devices_checkBottomPaddingVisibility() {
+ for (int i = 0; i < 6; i++) {
+ mMediaOutputController.mMediaDevices.add(mock(MediaDevice.class));
+ }
+ mMediaOutputBaseDialogImpl.refresh();
+ final View view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.list_bottom_padding);
+
+ assertThat(view.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void refresh_with5Devices_checkBottomPaddingVisibility() {
+ for (int i = 0; i < 5; i++) {
+ mMediaOutputController.mMediaDevices.add(mock(MediaDevice.class));
+ }
+ mMediaOutputBaseDialogImpl.refresh();
+ final View view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+ R.id.list_bottom_padding);
+
+ assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
+
+ MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) {
+ super(context, mediaOutputController);
+
+ mAdapter = mMediaOutputBaseAdapter;
+ }
+
+ int getHeaderIconRes() {
+ return mHeaderIconRes;
+ }
+
+ IconCompat getHeaderIcon() {
+ return mIconCompat;
+ }
+
+ int getHeaderIconSize() {
+ return 10;
+ }
+
+ CharSequence getHeaderText() {
+ return mHeaderTitle;
+ }
+
+ CharSequence getHeaderSubtitle() {
+ return mHeaderSubtitle;
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
new file mode 100644
index 000000000000..0dcdecfdaadb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -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();
+ }
+}