summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortim peng <timhypeng@google.com>2020-12-08 05:46:13 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2020-12-08 05:46:13 +0000
commit7de579f1ce46f67018033287cc941c4d88d9d87e (patch)
tree4e87a3843b5f0b72878b1d6c2a6bdd1527829814
parent73ba6273638a5d0860fc11407ddb59060ee45827 (diff)
parentb71aeae0cb8a032be550a94b5a4cd7e7ae16be27 (diff)
Merge changes from topic "media_output_dialog" into rvc-qpr-dev
* changes: Apply animation for Media Ouput Switcher dialog Add dynamic item in output switcher if it is available Fix NPE when notification icon is empty Fix output switcher didn't show album art Add dynamic group operation in output switcher Enhancement for group operation in output switcher Add dismiss timing for Media Output Dialog Update OutputSwitcher from SettingsPanel to MediaOutputDialog in SystemUI Add constant for dismiss Settings panel Add animation when switching output device Add dismiss intent and method for MediaOutputDialog Add "Stop" button for remote device Add constant for dialog dismiss Rename MediaOutDialogReceiver to MediaOutputDialogReceiver Add getFeatures() for get device supporting features Add a receiver to launch Output Switcher dialog Add MediaOutputDialogFactory to create MediaOutputDialog Add Media Output Dialog for Output Switcher Add controller for Media operation Add MediaOutputAdapter for Media device list Define strings for output switcher dialog in SystemUI Add image utility methods
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/Utils.java26
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java18
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java36
-rw-r--r--packages/SystemUI/AndroidManifest.xml8
-rw-r--r--packages/SystemUI/res/drawable/ic_check_box.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml26
-rw-r--r--packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml31
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_background.xml23
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml114
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item.xml138
-rw-r--r--packages/SystemUI/res/values/dimens.xml9
-rw-r--r--packages/SystemUI/res/values/strings.xml15
-rw-r--r--packages/SystemUI/res/values/styles.xml8
-rw-r--r--packages/SystemUI/src/com/android/systemui/Dependency.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java227
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java322
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java227
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java494
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java82
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java177
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java88
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java290
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java200
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java507
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java109
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java248
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java108
36 files changed, 3713 insertions, 24 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 9e59ce3ea166..3cbf2685af26 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -13,6 +13,7 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
@@ -32,6 +33,10 @@ import android.telephony.AccessNetworkConstants;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
+import androidx.annotation.NonNull;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.UserIcons;
import com.android.launcher3.icons.IconFactory;
@@ -537,4 +542,25 @@ public class Utils {
== NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING);
return !isInIwlan;
}
+
+ /**
+ * Returns a bitmap with rounded corner.
+ *
+ * @param context application context.
+ * @param source bitmap to apply round corner.
+ * @param cornerRadius corner radius value.
+ */
+ public static Bitmap convertCornerRadiusBitmap(@NonNull Context context,
+ @NonNull Bitmap source, @NonNull float cornerRadius) {
+ final Bitmap roundedBitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ final RoundedBitmapDrawable drawable =
+ RoundedBitmapDrawableFactory.create(context.getResources(), source);
+ drawable.setAntiAlias(true);
+ drawable.setCornerRadius(cornerRadius);
+ final Canvas canvas = new Canvas(roundedBitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return roundedBitmap;
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 95e916b9871a..df2f973e85c5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -18,6 +18,7 @@ import android.util.Log;
import android.util.Pair;
import androidx.annotation.DrawableRes;
+import androidx.core.graphics.drawable.IconCompat;
import com.android.settingslib.R;
import com.android.settingslib.widget.AdaptiveIcon;
@@ -216,6 +217,23 @@ public class BluetoothUtils {
}
/**
+ * Create an Icon pointing to a drawable.
+ */
+ public static IconCompat createIconWithDrawable(Drawable drawable) {
+ Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ final int width = drawable.getIntrinsicWidth();
+ final int height = drawable.getIntrinsicHeight();
+ bitmap = createBitmap(drawable,
+ width > 0 ? width : 1,
+ height > 0 ? height : 1);
+ }
+ return IconCompat.createWithBitmap(bitmap);
+ }
+
+ /**
* Build device icon with advanced outline
*/
public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 126f9b91b0d2..41d6afc9c234 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -46,6 +46,7 @@ import com.android.settingslib.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.List;
/**
* MediaDevice represents a media device(such like Bluetooth device, cast device and phone device).
@@ -354,6 +355,13 @@ public abstract class MediaDevice implements Comparable<MediaDevice> {
}
/**
+ * Gets the supported features of the route.
+ */
+ public List<String> getFeatures() {
+ return mRouteInfo.getFeatures();
+ }
+
+ /**
* Check if it is CarKit device
* @return true if it is CarKit device
*/
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java
index 2821af97ed98..fc16eb6b4277 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java
@@ -60,8 +60,42 @@ public class MediaOutputSliceConstants {
"com.android.settings.panel.action.MEDIA_OUTPUT_GROUP";
/**
- * An string extra specifying a media package name.
+ * A string extra specifying a media package name.
*/
public static final String EXTRA_PACKAGE_NAME =
"com.android.settings.panel.extra.PACKAGE_NAME";
+
+ /**
+ * An intent action to launch media output dialog.
+ */
+ public static final String ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG =
+ "com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG";
+
+ /**
+ * An intent action to dismiss media output dialog.
+ */
+ public static final String ACTION_DISMISS_MEDIA_OUTPUT_DIALOG =
+ "com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG";
+
+ /**
+ * Settings package name.
+ */
+ public static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
+
+ /**
+ * An intent action to launch Bluetooth paring page.
+ */
+ public static final String ACTION_LAUNCH_BLUETOOTH_PAIRING =
+ "com.android.settings.action.LAUNCH_BLUETOOTH_PAIRING";
+
+ /**
+ * SystemUi package name.
+ */
+ public static final String SYSTEMUI_PACKAGE_NAME = "com.android.systemui";
+
+ /**
+ * An intent action to close settings panel.
+ */
+ public static final String ACTION_CLOSE_PANEL =
+ "com.android.settings.panel.action.CLOSE_PANEL";
}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 6e74184cef02..ce1eea0a715c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -781,5 +781,13 @@
</intent-filter>
</receiver>
+ <receiver android:name=".media.dialog.MediaOutputDialogReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
+ <action android:name="com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG" />
+ </intent-filter>
+ </receiver>
+
</application>
</manifest>
diff --git a/packages/SystemUI/res/drawable/ic_check_box.xml b/packages/SystemUI/res/drawable/ic_check_box.xml
new file mode 100644
index 000000000000..a8d1a652b35b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_check_box.xml
@@ -0,0 +1,26 @@
+<!--
+ 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
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/checked"
+ android:state_checked="true"
+ android:drawable="@drawable/ic_check_box_blue_24dp" />
+ <item
+ android:id="@+id/unchecked"
+ android:state_checked="false"
+ android:drawable="@drawable/ic_check_box_outline_24dp" />
+</selector>
diff --git a/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml b/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml
new file mode 100644
index 000000000000..43cae6983981
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml
@@ -0,0 +1,26 @@
+<!--
+ 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
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"
+ android:fillColor="#4285F4"/>
+</vector>
+
diff --git a/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml b/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml
new file mode 100644
index 000000000000..f6f453af2a26
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml
@@ -0,0 +1,26 @@
+<!--
+ 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
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"
+ android:fillColor="#757575"/>
+</vector>
+
diff --git a/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml b/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml
new file mode 100644
index 000000000000..ae0d56217fd9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml
@@ -0,0 +1,31 @@
+<!--
+ 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
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
+ android:fillColor="#000000"/>
+</vector>
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..73beefc9da83
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -0,0 +1,114 @@
+<?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="@dimen/media_output_dialog_header_icon_padding"/>
+
+ <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:orientation="vertical">
+
+ <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"/>
+ </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..c98c3a0beb65
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -0,0 +1,138 @@
+<?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/device_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <FrameLayout
+ 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"/>
+ <SeekBar
+ android:id="@+id/volume_seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"/>
+ </RelativeLayout>
+
+ <ProgressBar
+ android:id="@+id/volume_indeterminate_progress"
+ style="@*android:style/Widget.Material.ProgressBar.Horizontal"
+ android:layout_width="258dp"
+ android:layout_height="18dp"
+ android:layout_marginStart="68dp"
+ android:layout_marginTop="40dp"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:visibility="gone"/>
+
+ <View
+ android:id="@+id/end_divider"
+ 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/add_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="right|center_vertical"
+ android:layout_marginEnd="24dp"
+ android:src="@drawable/ic_add"
+ android:tint="?android:attr/colorAccent"
+ android:visibility="gone"/>
+
+ <CheckBox
+ android:id="@+id/check_box"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="right|center_vertical"
+ android:layout_marginEnd="24dp"
+ android:button="@drawable/ic_check_box"
+ android:visibility="gone"/>
+ </FrameLayout>
+
+ <View
+ android:id="@+id/bottom_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="12dp"
+ android:layout_gravity="bottom"
+ android:background="?android:attr/listDivider"
+ android:visibility="gone"/>
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f07627a1a346..f002a271a43b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1402,4 +1402,13 @@
<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_list_margin">12dp</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_header_icon_padding">16dp</dimen>
+ <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
+ <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 95de4860ddfa..824521ecd1e7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2855,4 +2855,19 @@
<string name="controls_menu_add">Add controls</string>
<!-- Controls menu, edit [CHAR_LIMIT=30] -->
<string name="controls_menu_edit">Edit controls</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 68c2a38f53c3..990e092ec4cc 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -320,6 +320,9 @@
<style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
</style>
+ <style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod">
+ </style>
+
<!-- Standard animations for hiding and showing the status bar. -->
<style name="Animation.StatusBar">
</style>
@@ -400,6 +403,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>
@@ -789,5 +796,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/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 59580bbf1ae2..ded129defe07 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -46,6 +46,7 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.DarkIconDispatcher;
@@ -324,6 +325,7 @@ public class Dependency {
@Inject Lazy<RecordingController> mRecordingController;
@Inject Lazy<ProtoTracer> mProtoTracer;
@Inject Lazy<Divider> mDivider;
+ @Inject Lazy<MediaOutputDialogFactory> mMediaOutputDialogFactory;
@Inject
public Dependency() {
@@ -522,6 +524,8 @@ public class Dependency {
mProviders.put(RecordingController.class, mRecordingController::get);
mProviders.put(Divider.class, mDivider::get);
+ mProviders.put(MediaOutputDialogFactory.class, mMediaOutputDialogFactory::get);
+
sDependency = this;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
index 6e8d63b2c516..25d02a66d56d 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.MediaOutputDialogReceiver;
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(MediaOutputDialogReceiver.class)
+ public abstract BroadcastReceiver bindMediaOutputDialogReceiver(
+ MediaOutputDialogReceiver broadcastReceiver);
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index d853e3d4a57c..bffe05085887 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -42,10 +42,10 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintSet;
import com.android.settingslib.Utils;
-import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.util.animation.TransitionLayout;
@@ -93,7 +93,7 @@ public class MediaControlPanel {
private int mAlbumArtRadius;
// This will provide the corners for the album art.
private final ViewOutlineProvider mViewOutlineProvider;
-
+ private final MediaOutputDialogFactory mMediaOutputDialogFactory;
/**
* Initialize a new control panel
* @param context
@@ -104,7 +104,8 @@ public class MediaControlPanel {
public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
ActivityStarter activityStarter, MediaViewController mediaViewController,
SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
- KeyguardDismissUtil keyguardDismissUtil) {
+ KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
+ mediaOutputDialogFactory) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
@@ -112,6 +113,7 @@ public class MediaControlPanel {
mMediaViewController = mediaViewController;
mMediaDataManagerLazy = lazyMediaDataManager;
mKeyguardDismissUtil = keyguardDismissUtil;
+ mMediaOutputDialogFactory = mediaOutputDialogFactory;
loadDimens();
mViewOutlineProvider = new ViewOutlineProvider() {
@@ -274,13 +276,7 @@ public class MediaControlPanel {
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
mViewHolder.getSeamless().setOnClickListener(v -> {
- final Intent intent = new Intent()
- .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
- .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
- data.getPackageName())
- .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
- mActivityStarter.startActivity(intent, false, true /* dismissShade */,
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mMediaOutputDialogFactory.create(data.getPackageName(), true);
});
ImageView iconView = mViewHolder.getSeamlessIcon();
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..0d5faff65aab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -0,0 +1,227 @@
+/*
+ * 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.TextUtils;
+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 boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private ViewGroup mConnectedItem;
+ private boolean mInclueDynamicGroup;
+
+ 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) {
+ final int size = mController.getMediaDevices().size();
+ if (position == size && mController.isZeroMode()) {
+ viewHolder.onBind(CUSTOMIZED_ITEM_PAIR_NEW, false /* topMargin */,
+ true /* bottomMargin */);
+ } else if (mInclueDynamicGroup) {
+ if (position == 0) {
+ viewHolder.onBind(CUSTOMIZED_ITEM_DYNAMIC_GROUP, true /* topMargin */,
+ false /* bottomMargin */);
+ } else {
+ // When group item is added at the first(position == 0), devices will be added from
+ // the second item(position == 1). It means that the index of device list starts
+ // from "position - 1".
+ viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices()))
+ .get(position - 1),
+ false /* topMargin */, position == size /* bottomMargin */);
+ }
+ } else if (position < size) {
+ viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position),
+ position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */);
+ } else if (DEBUG) {
+ Log.d(TAG, "Incorrect position: " + position);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ mInclueDynamicGroup = mController.getSelectedMediaDevice().size() > 1;
+ if (mController.isZeroMode() || mInclueDynamicGroup) {
+ // Add extra one for "pair new" or dynamic group
+ return mController.getMediaDevices().size() + 1;
+ }
+ return mController.getMediaDevices().size();
+ }
+
+ @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, boolean topMargin, boolean bottomMargin) {
+ super.onBind(device, topMargin, bottomMargin);
+ final boolean currentlyConnected = !mInclueDynamicGroup && isCurrentlyConnected(device);
+ if (currentlyConnected) {
+ mConnectedItem = mContainerLayout;
+ }
+ mBottomDivider.setVisibility(View.GONE);
+ mCheckBox.setVisibility(View.GONE);
+ if (currentlyConnected && mController.isActiveRemoteDevice(device)) {
+ // Init active device layout
+ mDivider.setVisibility(View.VISIBLE);
+ mDivider.setTransitionAlpha(1);
+ mAddIcon.setVisibility(View.VISIBLE);
+ mAddIcon.setTransitionAlpha(1);
+ mAddIcon.setOnClickListener(v -> onEndItemClick());
+ } else {
+ // Init non-active device layout
+ mDivider.setVisibility(View.GONE);
+ mAddIcon.setVisibility(View.GONE);
+ }
+ if (mController.isTransferring()) {
+ if (device.getState() == MediaDeviceState.STATE_CONNECTING
+ && !mController.hasAdjustVolumeUserRestriction()) {
+ setTwoLineLayout(device, true /* bFocused */, false /* showSeekBar*/,
+ true /* showProgressBar */, false /* showSubtitle */);
+ } else {
+ setSingleLineLayout(getItemTitle(device), false /* bFocused */);
+ }
+ } else {
+ // Set different layout for each device
+ if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
+ setTwoLineLayout(device, false /* bFocused */,
+ false /* showSeekBar */, false /* showProgressBar */,
+ true /* showSubtitle */);
+ mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
+ mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+ } else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) {
+ setTwoLineLayout(device, true /* bFocused */, true /* showSeekBar */,
+ false /* showProgressBar */, false /* showSubtitle */);
+ initSeekbar(device);
+ } else {
+ setSingleLineLayout(getItemTitle(device), false /* bFocused */);
+ mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+ }
+ }
+ }
+
+ @Override
+ void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
+ super.onBind(customizedItem, topMargin, bottomMargin);
+ if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
+ mCheckBox.setVisibility(View.GONE);
+ mDivider.setVisibility(View.GONE);
+ mAddIcon.setVisibility(View.GONE);
+ mBottomDivider.setVisibility(View.GONE);
+ setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
+ false /* bFocused */);
+ final Drawable d = mContext.getDrawable(R.drawable.ic_add);
+ d.setColorFilter(new PorterDuffColorFilter(
+ Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
+ mTitleIcon.setImageDrawable(d);
+ mContainerLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW));
+ } else if (customizedItem == CUSTOMIZED_ITEM_DYNAMIC_GROUP) {
+ mConnectedItem = mContainerLayout;
+ mBottomDivider.setVisibility(View.GONE);
+ mCheckBox.setVisibility(View.GONE);
+ mDivider.setVisibility(View.VISIBLE);
+ mDivider.setTransitionAlpha(1);
+ mAddIcon.setVisibility(View.VISIBLE);
+ mAddIcon.setTransitionAlpha(1);
+ mAddIcon.setOnClickListener(v -> onEndItemClick());
+ mTitleIcon.setImageDrawable(getSpeakerDrawable());
+ final CharSequence sessionName = mController.getSessionName();
+ final CharSequence title = TextUtils.isEmpty(sessionName)
+ ? mContext.getString(R.string.media_output_dialog_group) : sessionName;
+ setTwoLineLayout(title, true /* bFocused */, true /* showSeekBar */,
+ false /* showProgressBar */, false /* showSubtitle */);
+ initSessionSeekbar();
+ }
+ }
+
+ private void onItemClick(View view, MediaDevice device) {
+ if (mController.isTransferring()) {
+ return;
+ }
+
+ playSwitchingAnim(mConnectedItem, view);
+ mController.connectDevice(device);
+ device.setState(MediaDeviceState.STATE_CONNECTING);
+ if (!isAnimating()) {
+ notifyDataSetChanged();
+ }
+ }
+
+ private void onItemClick(int customizedItem) {
+ if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
+ mController.launchBluetoothPairing();
+ }
+ }
+
+ private void onEndItemClick() {
+ mController.launchMediaOutputGroupDialog();
+ }
+ }
+}
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..f1d4804aa622
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -0,0 +1,322 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+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.bluetooth.BluetoothUtils;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+
+/**
+ * Base adapter for media output dialog.
+ */
+public abstract class MediaOutputBaseAdapter extends
+ RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> {
+
+ static final int CUSTOMIZED_ITEM_PAIR_NEW = 1;
+ static final int CUSTOMIZED_ITEM_GROUP = 2;
+ static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3;
+
+ final MediaOutputController mController;
+
+ private int mMargin;
+ private boolean mIsAnimating;
+
+ Context mContext;
+ View mHolderView;
+ boolean mIsDragging;
+
+ public MediaOutputBaseAdapter(MediaOutputController controller) {
+ mController = controller;
+ mIsDragging = false;
+ }
+
+ @Override
+ public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+ int viewType) {
+ mContext = viewGroup.getContext();
+ mMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_list_margin);
+ mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
+ viewGroup, false);
+
+ return null;
+ }
+
+ CharSequence getItemTitle(MediaDevice device) {
+ return device.getName();
+ }
+
+ boolean isCurrentlyConnected(MediaDevice device) {
+ return TextUtils.equals(device.getId(),
+ mController.getCurrentConnectedMediaDevice().getId());
+ }
+
+ boolean isDragging() {
+ return mIsDragging;
+ }
+
+ boolean isAnimating() {
+ return mIsAnimating;
+ }
+
+ /**
+ * ViewHolder for binding device view.
+ */
+ abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+
+ private static final int ANIM_DURATION = 200;
+
+ final LinearLayout mContainerLayout;
+ final TextView mTitleText;
+ final TextView mTwoLineTitleText;
+ final TextView mSubTitleText;
+ final ImageView mTitleIcon;
+ final ImageView mAddIcon;
+ final ProgressBar mProgressBar;
+ final SeekBar mSeekBar;
+ final RelativeLayout mTwoLineLayout;
+ final View mDivider;
+ final View mBottomDivider;
+ final CheckBox mCheckBox;
+
+ MediaDeviceBaseViewHolder(View view) {
+ super(view);
+ mContainerLayout = 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);
+ mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
+ mSeekBar = view.requireViewById(R.id.volume_seekbar);
+ mDivider = view.requireViewById(R.id.end_divider);
+ mBottomDivider = view.requireViewById(R.id.bottom_divider);
+ mAddIcon = view.requireViewById(R.id.add_icon);
+ mCheckBox = view.requireViewById(R.id.check_box);
+ }
+
+ void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) {
+ mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext));
+ setMargin(topMargin, bottomMargin);
+ }
+
+ void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
+ setMargin(topMargin, bottomMargin);
+ }
+
+ private void setMargin(boolean topMargin, boolean bottomMargin) {
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mContainerLayout
+ .getLayoutParams();
+ params.topMargin = topMargin ? mMargin : 0;
+ params.bottomMargin = bottomMargin ? mMargin : 0;
+ mContainerLayout.setLayoutParams(params);
+ }
+
+ void setSingleLineLayout(CharSequence title, boolean bFocused) {
+ mTwoLineLayout.setVisibility(View.GONE);
+ mProgressBar.setVisibility(View.GONE);
+ mTitleText.setVisibility(View.VISIBLE);
+ mTitleText.setTranslationY(0);
+ mTitleText.setText(title);
+ if (bFocused) {
+ mTitleText.setTypeface(Typeface.create(mContext.getString(
+ com.android.internal.R.string.config_headlineFontFamilyMedium),
+ Typeface.NORMAL));
+ } else {
+ mTitleText.setTypeface(Typeface.create(mContext.getString(
+ com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL));
+ }
+ }
+
+ void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
+ boolean showProgressBar, boolean showSubtitle) {
+ setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle);
+ }
+
+ void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar,
+ boolean showProgressBar, boolean showSubtitle) {
+ setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle);
+ }
+
+ private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
+ boolean showSeekBar, boolean showProgressBar, boolean showSubtitle) {
+ mTitleText.setVisibility(View.GONE);
+ mTwoLineLayout.setVisibility(View.VISIBLE);
+ mSeekBar.setAlpha(1);
+ mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
+ mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
+ mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
+ mTwoLineTitleText.setTranslationY(0);
+ if (device == null) {
+ mTwoLineTitleText.setText(title);
+ } else {
+ mTwoLineTitleText.setText(getItemTitle(device));
+ }
+
+ if (bFocused) {
+ mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
+ com.android.internal.R.string.config_headlineFontFamilyMedium),
+ Typeface.NORMAL));
+ } else {
+ mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
+ com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL));
+ }
+ }
+
+ void initSeekbar(MediaDevice device) {
+ mSeekBar.setMax(device.getMaxVolume());
+ mSeekBar.setMin(0);
+ final int currentVolume = device.getCurrentVolume();
+ if (mSeekBar.getProgress() != currentVolume) {
+ mSeekBar.setProgress(currentVolume);
+ }
+ 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;
+ }
+ });
+ }
+
+ void initSessionSeekbar() {
+ mSeekBar.setMax(mController.getSessionVolumeMax());
+ mSeekBar.setMin(0);
+ final int currentVolume = mController.getSessionVolume();
+ if (mSeekBar.getProgress() != currentVolume) {
+ mSeekBar.setProgress(currentVolume);
+ }
+ mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (!fromUser) {
+ return;
+ }
+ mController.adjustSessionVolume(progress);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mIsDragging = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mIsDragging = false;
+ }
+ });
+ }
+
+ void playSwitchingAnim(@NonNull View from, @NonNull View to) {
+ final float delta = (float) (mContext.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_title_anim_y_delta));
+ final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar);
+ final TextView toTitleText = to.requireViewById(R.id.title);
+ if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility()
+ != View.VISIBLE) {
+ return;
+ }
+ mIsAnimating = true;
+ // Animation for title text
+ toTitleText.setTypeface(Typeface.create(mContext.getString(
+ com.android.internal.R.string.config_headlineFontFamilyMedium),
+ Typeface.NORMAL));
+ toTitleText.animate()
+ .setDuration(ANIM_DURATION)
+ .translationY(-delta)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ to.requireViewById(R.id.volume_indeterminate_progress).setVisibility(
+ View.VISIBLE);
+ }
+ });
+ // Animation for seek bar
+ fromSeekBar.animate()
+ .alpha(0)
+ .setDuration(ANIM_DURATION)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final TextView fromTitleText = from.requireViewById(
+ R.id.two_line_title);
+ fromTitleText.setTypeface(Typeface.create(mContext.getString(
+ com.android.internal.R.string.config_headlineFontFamily),
+ Typeface.NORMAL));
+ fromTitleText.animate()
+ .setDuration(ANIM_DURATION)
+ .translationY(delta)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsAnimating = false;
+ notifyDataSetChanged();
+ }
+ });
+ }
+ });
+ }
+
+ Drawable getSpeakerDrawable() {
+ final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp)
+ .mutate();
+ final ColorStateList list = mContext.getResources().getColorStateList(
+ R.color.advanced_icon_color, mContext.getTheme());
+ drawable.setColorFilter(new PorterDuffColorFilter(list.getDefaultColor(),
+ PorterDuff.Mode.SRC_IN));
+ return BluetoothUtils.buildAdvancedDrawable(mContext, drawable);
+ }
+ }
+}
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..745f36bfc7e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -0,0 +1,227 @@
+/*
+ * 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.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, Window.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 int mListMaxHeight;
+
+ MediaOutputBaseAdapter mAdapter;
+
+ 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);
+ window.setWindowAnimations(R.style.Animation_MediaOutputDialog);
+
+ 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);
+ mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
+ mDoneButton = mDialogView.requireViewById(R.id.done);
+ mStopButton = mDialogView.requireViewById(R.id.stop);
+
+ mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
+ mDeviceListLayoutListener);
+ // Init device list
+ mDevicesRecyclerView.setLayoutManager(mLayoutManager);
+ mDevicesRecyclerView.setAdapter(mAdapter);
+ // Init header icon
+ mHeaderIcon.setOnClickListener(v -> onHeaderIconClick());
+ // 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();
+ final int padding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_header_icon_padding);
+ mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, 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.isAnimating()) {
+ mAdapter.notifyDataSetChanged();
+ }
+ // Show when remote media session is available
+ mStopButton.setVisibility(getStopButtonVisibility());
+ }
+
+ abstract int getHeaderIconRes();
+
+ abstract IconCompat getHeaderIcon();
+
+ abstract int getHeaderIconSize();
+
+ abstract CharSequence getHeaderText();
+
+ abstract CharSequence getHeaderSubtitle();
+
+ abstract int getStopButtonVisibility();
+
+ @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();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (!hasFocus && isShowing()) {
+ dismiss();
+ }
+ }
+
+ void onHeaderIconClick() {
+ }
+}
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..451bd42bd053
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -0,0 +1,494 @@
+/*
+ * 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.app.Notification;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.media.MediaMetadata;
+import android.media.MediaRoute2Info;
+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.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+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 = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final String mPackageName;
+ private final Context mContext;
+ private final MediaSessionManager mMediaSessionManager;
+ private final ShadeController mShadeController;
+ private final ActivityStarter mActivityStarter;
+ private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
+ private final boolean mAboveStatusbar;
+ private final NotificationEntryManager mNotificationEntryManager;
+ @VisibleForTesting
+ final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+
+ private MediaController mMediaController;
+ @VisibleForTesting
+ Callback mCallback;
+ @VisibleForTesting
+ LocalMediaManager mLocalMediaManager;
+
+ @Inject
+ public MediaOutputController(@NonNull Context context, String packageName,
+ boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
+ lbm, ShadeController shadeController, ActivityStarter starter,
+ NotificationEntryManager notificationEntryManager) {
+ mContext = context;
+ mPackageName = packageName;
+ mMediaSessionManager = mediaSessionManager;
+ mShadeController = shadeController;
+ mActivityStarter = starter;
+ mAboveStatusbar = aboveStatusbar;
+ mNotificationEntryManager = notificationEntryManager;
+ 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 getNotificationIcon();
+ }
+
+ 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);
+ }
+
+ IconCompat getNotificationIcon() {
+ if (TextUtils.isEmpty(mPackageName)) {
+ return null;
+ }
+ for (NotificationEntry entry
+ : mNotificationEntryManager.getActiveNotificationsForCurrentUser()) {
+ final Notification notification = entry.getSbn().getNotification();
+ if (notification.hasMediaSession()
+ && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) {
+ final Icon icon = notification.getLargeIcon();
+ if (icon == null) {
+ break;
+ }
+ return IconCompat.createFromIcon(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);
+ }
+
+ List<MediaDevice> getGroupMediaDevices() {
+ final List<MediaDevice> selectedDevices = getSelectedMediaDevice();
+ final List<MediaDevice> selectableDevices = getSelectableMediaDevice();
+ if (mGroupMediaDevices.isEmpty()) {
+ mGroupMediaDevices.addAll(selectedDevices);
+ mGroupMediaDevices.addAll(selectableDevices);
+ return mGroupMediaDevices;
+ }
+ // To keep the same list order
+ final Collection<MediaDevice> sourceDevices = new ArrayList<>();
+ final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
+ sourceDevices.addAll(selectedDevices);
+ sourceDevices.addAll(selectableDevices);
+ for (MediaDevice originalDevice : mGroupMediaDevices) {
+ for (MediaDevice newDevice : sourceDevices) {
+ if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
+ targetMediaDevices.add(newDevice);
+ sourceDevices.remove(newDevice);
+ break;
+ }
+ }
+ }
+ // Add new devices at the end of list if necessary
+ if (!sourceDevices.isEmpty()) {
+ targetMediaDevices.addAll(sourceDevices);
+ }
+ mGroupMediaDevices.clear();
+ mGroupMediaDevices.addAll(targetMediaDevices);
+
+ return mGroupMediaDevices;
+ }
+
+ void resetGroupMediaDevices() {
+ mGroupMediaDevices.clear();
+ }
+
+ 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();
+ }
+
+ 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);
+ }
+
+ void launchMediaOutputDialog() {
+ mCallback.dismissDialog();
+ new MediaOutputDialog(mContext, mAboveStatusbar, this);
+ }
+
+ void launchMediaOutputGroupDialog() {
+ mCallback.dismissDialog();
+ new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
+ }
+
+ boolean isActiveRemoteDevice(@NonNull MediaDevice device) {
+ final List<String> features = device.getFeatures();
+ return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)
+ || features.contains(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK)
+ || features.contains(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK)
+ || features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK));
+ }
+
+ 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..c0138f0e200a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -0,0 +1,82 @@
+/*
+ * 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 javax.inject.Singleton;
+
+/**
+ * Dialog for media output transferring.
+ */
+@Singleton
+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);
+ }
+
+ @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();
+ }
+
+ @Override
+ int getStopButtonVisibility() {
+ return mMediaOutputController.isActiveRemoteDevice(
+ mMediaOutputController.getCurrentConnectedMediaDevice()) ? View.VISIBLE : View.GONE;
+ }
+}
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..7d1a7ced7472
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.notification.NotificationEntryManager
+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,
+ private val notificationEntryManager: NotificationEntryManager
+) {
+ companion object {
+ var mediaOutputDialog: MediaOutputDialog? = null
+ }
+
+ /** Creates a [MediaOutputDialog] for the given package. */
+ fun create(packageName: String, aboveStatusBar: Boolean) {
+ mediaOutputDialog?.dismiss()
+ mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar,
+ mediaSessionManager, lbm, shadeController, starter, notificationEntryManager).run {
+ MediaOutputDialog(context, aboveStatusBar, this) }
+ }
+
+ /** dismiss [MediaOutputDialog] if exist. */
+ fun dismiss() {
+ mediaOutputDialog?.dismiss()
+ mediaOutputDialog = null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
new file mode 100644
index 000000000000..0ce0c020ccde
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.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 MediaOutputDialogReceiver @Inject constructor(
+ private val 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/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
new file mode 100644
index 000000000000..24e076bb22f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
@@ -0,0 +1,177 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Adapter for media output dynamic group dialog.
+ */
+public class MediaOutputGroupAdapter extends MediaOutputBaseAdapter {
+
+ private static final String TAG = "MediaOutputGroupAdapter";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final List<MediaDevice> mGroupMediaDevices;
+
+ public MediaOutputGroupAdapter(MediaOutputController controller) {
+ super(controller);
+ mGroupMediaDevices = controller.getGroupMediaDevices();
+ }
+
+ @Override
+ public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+ int viewType) {
+ super.onCreateViewHolder(viewGroup, viewType);
+
+ return new GroupViewHolder(mHolderView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) {
+ // Add "Group"
+ if (position == 0) {
+ viewHolder.onBind(CUSTOMIZED_ITEM_GROUP, true /* topMargin */,
+ false /* bottomMargin */);
+ return;
+ }
+ // Add available devices
+ final int newPosition = position - 1;
+ final int size = mGroupMediaDevices.size();
+ if (newPosition < size) {
+ viewHolder.onBind(mGroupMediaDevices.get(newPosition), false /* topMargin */,
+ newPosition == (size - 1) /* bottomMargin */);
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Incorrect position: " + position);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ // Require extra item for group volume operation
+ return mGroupMediaDevices.size() + 1;
+ }
+
+ @Override
+ CharSequence getItemTitle(MediaDevice device) {
+ return super.getItemTitle(device);
+ }
+
+ class GroupViewHolder extends MediaDeviceBaseViewHolder {
+
+ GroupViewHolder(View view) {
+ super(view);
+ }
+
+ @Override
+ void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) {
+ super.onBind(device, topMargin, bottomMargin);
+ mDivider.setVisibility(View.GONE);
+ mAddIcon.setVisibility(View.GONE);
+ mBottomDivider.setVisibility(View.GONE);
+ mCheckBox.setVisibility(View.VISIBLE);
+ mCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ onCheckBoxClicked(isChecked, device);
+ });
+ setTwoLineLayout(device, false /* bFocused */, true /* showSeekBar */,
+ false /* showProgressBar */, false /* showSubtitle*/);
+ initSeekbar(device);
+ final List<MediaDevice> selectedDevices = mController.getSelectedMediaDevice();
+ if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
+ mCheckBox.setButtonDrawable(R.drawable.ic_check_box);
+ mCheckBox.setChecked(false);
+ mCheckBox.setEnabled(true);
+ } else if (isDeviceIncluded(selectedDevices, device)) {
+ if (selectedDevices.size() == 1 || !isDeviceIncluded(
+ mController.getDeselectableMediaDevice(), device)) {
+ mCheckBox.setButtonDrawable(getDisabledCheckboxDrawable());
+ mCheckBox.setChecked(true);
+ mCheckBox.setEnabled(false);
+ } else {
+ mCheckBox.setButtonDrawable(R.drawable.ic_check_box);
+ mCheckBox.setChecked(true);
+ mCheckBox.setEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
+ super.onBind(customizedItem, topMargin, bottomMargin);
+ if (customizedItem == CUSTOMIZED_ITEM_GROUP) {
+ setTwoLineLayout(mContext.getText(R.string.media_output_dialog_group),
+ true /* bFocused */, true /* showSeekBar */, false /* showProgressBar */,
+ false /* showSubtitle*/);
+ mTitleIcon.setImageDrawable(getSpeakerDrawable());
+ mBottomDivider.setVisibility(View.VISIBLE);
+ mCheckBox.setVisibility(View.GONE);
+ mDivider.setVisibility(View.GONE);
+ mAddIcon.setVisibility(View.GONE);
+ initSessionSeekbar();
+ }
+ }
+
+ private void onCheckBoxClicked(boolean isChecked, MediaDevice device) {
+ if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
+ mController.addDeviceToPlayMedia(device);
+ } else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
+ device)) {
+ mController.removeDeviceFromPlayMedia(device);
+ }
+ }
+
+ private Drawable getDisabledCheckboxDrawable() {
+ final Drawable drawable = mContext.getDrawable(R.drawable.ic_check_box_blue_24dp)
+ .mutate();
+ final Bitmap checkbox = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(checkbox);
+ TypedValue value = new TypedValue();
+ mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
+ drawable.setAlpha((int) (value.getFloat() * 255));
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return drawable;
+ }
+
+ private boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
+ for (MediaDevice device : deviceList) {
+ if (TextUtils.equals(device.getId(), targetDevice.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
new file mode 100644
index 000000000000..407930492fbe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+/**
+ * Dialog for media output group.
+ */
+public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
+
+ MediaOutputGroupDialog(Context context, boolean aboveStatusbar, MediaOutputController
+ mediaOutputController) {
+ super(context, mediaOutputController);
+ mMediaOutputController.resetGroupMediaDevices();
+ mAdapter = new MediaOutputGroupAdapter(mMediaOutputController);
+ if (!aboveStatusbar) {
+ getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+ }
+ show();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ int getHeaderIconRes() {
+ return R.drawable.ic_arrow_back;
+ }
+
+ @Override
+ IconCompat getHeaderIcon() {
+ return null;
+ }
+
+ @Override
+ int getHeaderIconSize() {
+ return mContext.getResources().getDimensionPixelSize(
+ R.dimen.media_output_dialog_header_back_icon_size);
+ }
+
+ @Override
+ CharSequence getHeaderText() {
+ return mContext.getString(R.string.media_output_dialog_add_output);
+ }
+
+ @Override
+ CharSequence getHeaderSubtitle() {
+ final int size = mMediaOutputController.getSelectedMediaDevice().size();
+ if (size == 1) {
+ return mContext.getText(R.string.media_output_dialog_single_device);
+ }
+ return mContext.getString(R.string.media_output_dialog_multiple_devices, size);
+ }
+
+ @Override
+ int getStopButtonVisibility() {
+ return View.VISIBLE;
+ }
+
+ @Override
+ void onHeaderIconClick() {
+ mMediaOutputController.launchMediaOutputDialog();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java
index ac3523b2fffd..1b1a51b8a57b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar;
import android.content.Context;
-import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
@@ -36,10 +35,9 @@ 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.widget.AdaptiveIcon;
import com.android.systemui.Dependency;
-import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -51,7 +49,7 @@ import java.util.List;
*/
public class MediaTransferManager {
private final Context mContext;
- private final ActivityStarter mActivityStarter;
+ private final MediaOutputDialogFactory mMediaOutputDialogFactory;
private MediaDevice mDevice;
private List<View> mViews = new ArrayList<>();
private LocalMediaManager mLocalMediaManager;
@@ -74,12 +72,7 @@ public class MediaTransferManager {
ViewParent parent = view.getParent();
StatusBarNotification statusBarNotification =
getRowForParent(parent).getEntry().getSbn();
- final Intent intent = new Intent()
- .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
- .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
- statusBarNotification.getPackageName());
- mActivityStarter.startActivity(intent, false, true /* dismissShade */,
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mMediaOutputDialogFactory.create(statusBarNotification.getPackageName(), true);
return true;
}
};
@@ -107,7 +100,7 @@ public class MediaTransferManager {
public MediaTransferManager(Context context) {
mContext = context;
- mActivityStarter = Dependency.get(ActivityStarter.class);
+ mMediaOutputDialogFactory = Dependency.get(MediaOutputDialogFactory.class);
LocalBluetoothManager lbm = Dependency.get(LocalBluetoothManager.class);
InfoMediaManager imm = new InfoMediaManager(mContext, null, null, lbm);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, null);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 6fe11ed1792b..06a92272692d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -88,6 +88,7 @@ import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.Prefs;
import com.android.systemui.R;
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.VolumeDialog;
import com.android.systemui.plugins.VolumeDialogController;
@@ -470,6 +471,7 @@ public class VolumeDialogImpl implements VolumeDialog,
Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
dismissH(DISMISS_REASON_SETTINGS_CLICKED);
+ Dependency.get(MediaOutputDialogFactory.class).dismiss();
Dependency.get(ActivityStarter.class).startActivity(intent,
true /* dismissShade */);
});
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index ad703614fdb4..af677c9d9f64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -38,6 +38,7 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.util.animation.TransitionLayout
@@ -92,6 +93,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var expandedSet: ConstraintSet
@Mock private lateinit var collapsedSet: ConstraintSet
+ @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
private lateinit var appIcon: ImageView
private lateinit var appName: TextView
private lateinit var albumView: ImageView
@@ -128,7 +130,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
- seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil)
+ seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
+ mediaOutputDialogFactory)
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Mock out a view holder for the player to attach to.
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..6e216428992f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.LinearLayout;
+
+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";
+ private static final String TEST_SESSION_NAME = "test_session_name";
+
+ // 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 LinearLayout(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(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
+ 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 getItemCount_withDynamicGroup_containExtraOneForGroup() {
+ when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
+ when(mMediaOutputController.isZeroMode()).thenReturn(false);
+
+ 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.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText(
+ R.string.media_output_dialog_pairing_new));
+ }
+
+ @Test
+ public void onBindViewHolder_bindGroup_withSessionName_verifyView() {
+ when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
+ when(mMediaOutputController.isZeroMode()).thenReturn(false);
+ when(mMediaOutputController.getSessionName()).thenReturn(TEST_SESSION_NAME);
+ mMediaOutputAdapter.getItemCount();
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_SESSION_NAME);
+ }
+
+ @Test
+ public void onBindViewHolder_bindGroup_noSessionName_verifyView() {
+ when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
+ when(mMediaOutputController.isZeroMode()).thenReturn(false);
+ when(mMediaOutputController.getSessionName()).thenReturn(null);
+ mMediaOutputAdapter.getItemCount();
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(mContext.getString(
+ R.string.media_output_dialog_group));
+ }
+
+ @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.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+ }
+
+ @Test
+ public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ }
+
+ @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.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.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.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.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.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mAddIcon.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.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
+ 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.mContainerLayout.performClick();
+
+ verify(mMediaOutputController).launchBluetoothPairing();
+ }
+
+ @Test
+ public void onItemClick_clickDevice_verifyConnectDevice() {
+ assertThat(mMediaDevice2.getState()).isEqualTo(
+ LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+ mViewHolder.mContainerLayout.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..c897d8a53e20
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.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.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.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
+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 NotificationEntryManager mNotificationEntryManager =
+ mock(NotificationEntryManager.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, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+ 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();
+ }
+
+ class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
+
+ MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) {
+ super(context, mediaOutputController);
+
+ mAdapter = mMediaOutputBaseAdapter;
+ }
+
+ @Override
+ int getHeaderIconRes() {
+ return mHeaderIconRes;
+ }
+
+ @Override
+ IconCompat getHeaderIcon() {
+ return mIconCompat;
+ }
+
+ @Override
+ int getHeaderIconSize() {
+ return 10;
+ }
+
+ @Override
+ CharSequence getHeaderText() {
+ return mHeaderTitle;
+ }
+
+ @Override
+ CharSequence getHeaderSubtitle() {
+ return mHeaderSubtitle;
+ }
+
+ @Override
+ int getStopButtonVisibility() {
+ return 0;
+ }
+ }
+}
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..0d352c1b42d9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -0,0 +1,507 @@
+/*
+ * 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.app.Notification;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.RoutingSessionInfo;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.text.TextUtils;
+
+import androidx.core.graphics.drawable.IconCompat;
+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.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+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_DEVICE_3_ID = "test_device_3_id";
+ private static final String TEST_DEVICE_4_ID = "test_device_4_id";
+ private static final String TEST_DEVICE_5_ID = "test_device_5_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 NotificationEntryManager mNotificationEntryManager =
+ mock(NotificationEntryManager.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, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+ 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, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+
+ 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, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+
+ 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();
+ }
+
+ @Test
+ public void getGroupMediaDevices_differentDeviceOrder_showingSameOrder() {
+ final MediaDevice selectedMediaDevice1 = mock(MediaDevice.class);
+ final MediaDevice selectedMediaDevice2 = mock(MediaDevice.class);
+ final MediaDevice selectableMediaDevice1 = mock(MediaDevice.class);
+ final MediaDevice selectableMediaDevice2 = mock(MediaDevice.class);
+ final List<MediaDevice> selectedMediaDevices = new ArrayList<>();
+ final List<MediaDevice> selectableMediaDevices = new ArrayList<>();
+ when(selectedMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
+ when(selectedMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
+ when(selectableMediaDevice1.getId()).thenReturn(TEST_DEVICE_3_ID);
+ when(selectableMediaDevice2.getId()).thenReturn(TEST_DEVICE_4_ID);
+ selectedMediaDevices.add(selectedMediaDevice1);
+ selectedMediaDevices.add(selectedMediaDevice2);
+ selectableMediaDevices.add(selectableMediaDevice1);
+ selectableMediaDevices.add(selectableMediaDevice2);
+ doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
+ doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
+ final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices();
+ // Reset order
+ selectedMediaDevices.clear();
+ selectedMediaDevices.add(selectedMediaDevice2);
+ selectedMediaDevices.add(selectedMediaDevice1);
+ selectableMediaDevices.clear();
+ selectableMediaDevices.add(selectableMediaDevice2);
+ selectableMediaDevices.add(selectableMediaDevice1);
+ final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices();
+
+ assertThat(newDevices.size()).isEqualTo(groupMediaDevices.size());
+ for (int i = 0; i < groupMediaDevices.size(); i++) {
+ assertThat(TextUtils.equals(groupMediaDevices.get(i).getId(),
+ newDevices.get(i).getId())).isTrue();
+ }
+ }
+
+ @Test
+ public void getGroupMediaDevices_newDevice_verifyDeviceOrder() {
+ final MediaDevice selectedMediaDevice1 = mock(MediaDevice.class);
+ final MediaDevice selectedMediaDevice2 = mock(MediaDevice.class);
+ final MediaDevice selectableMediaDevice1 = mock(MediaDevice.class);
+ final MediaDevice selectableMediaDevice2 = mock(MediaDevice.class);
+ final MediaDevice selectableMediaDevice3 = mock(MediaDevice.class);
+ final List<MediaDevice> selectedMediaDevices = new ArrayList<>();
+ final List<MediaDevice> selectableMediaDevices = new ArrayList<>();
+ when(selectedMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
+ when(selectedMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
+ when(selectableMediaDevice1.getId()).thenReturn(TEST_DEVICE_3_ID);
+ when(selectableMediaDevice2.getId()).thenReturn(TEST_DEVICE_4_ID);
+ when(selectableMediaDevice3.getId()).thenReturn(TEST_DEVICE_5_ID);
+ selectedMediaDevices.add(selectedMediaDevice1);
+ selectedMediaDevices.add(selectedMediaDevice2);
+ selectableMediaDevices.add(selectableMediaDevice1);
+ selectableMediaDevices.add(selectableMediaDevice2);
+ doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
+ doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
+ final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices();
+ // Reset order
+ selectedMediaDevices.clear();
+ selectedMediaDevices.add(selectedMediaDevice2);
+ selectedMediaDevices.add(selectedMediaDevice1);
+ selectableMediaDevices.clear();
+ selectableMediaDevices.add(selectableMediaDevice3);
+ selectableMediaDevices.add(selectableMediaDevice2);
+ selectableMediaDevices.add(selectableMediaDevice1);
+ final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices();
+
+ assertThat(newDevices.size()).isEqualTo(5);
+ for (int i = 0; i < groupMediaDevices.size(); i++) {
+ assertThat(TextUtils.equals(groupMediaDevices.get(i).getId(),
+ newDevices.get(i).getId())).isTrue();
+ }
+ assertThat(newDevices.get(4).getId()).isEqualTo(TEST_DEVICE_5_ID);
+ }
+
+ @Test
+ public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
+ mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+
+ assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+ }
+
+ @Test
+ public void getNotificationLargeIcon_withoutLargeIcon_returnsNull() {
+ final List<NotificationEntry> entryList = new ArrayList<>();
+ final NotificationEntry entry = mock(NotificationEntry.class);
+ final StatusBarNotification sbn = mock(StatusBarNotification.class);
+ final Notification notification = mock(Notification.class);
+ entryList.add(entry);
+
+ when(mNotificationEntryManager.getActiveNotificationsForCurrentUser())
+ .thenReturn(entryList);
+ when(entry.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(notification);
+ when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+ when(notification.hasMediaSession()).thenReturn(true);
+ when(notification.getLargeIcon()).thenReturn(null);
+
+ assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+ }
+
+ @Test
+ public void getNotificationLargeIcon_withPackageNameAndMediaSession_returnsIconCompat() {
+ final List<NotificationEntry> entryList = new ArrayList<>();
+ final NotificationEntry entry = mock(NotificationEntry.class);
+ final StatusBarNotification sbn = mock(StatusBarNotification.class);
+ final Notification notification = mock(Notification.class);
+ final Icon icon = mock(Icon.class);
+ entryList.add(entry);
+
+ when(mNotificationEntryManager.getActiveNotificationsForCurrentUser())
+ .thenReturn(entryList);
+ when(entry.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(notification);
+ when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+ when(notification.hasMediaSession()).thenReturn(true);
+ when(notification.getLargeIcon()).thenReturn(icon);
+
+ assertThat(mMediaOutputController.getNotificationIcon() instanceof IconCompat).isTrue();
+ }
+
+ @Test
+ public void getNotificationLargeIcon_withPackageNameAndNoMediaSession_returnsNull() {
+ final List<NotificationEntry> entryList = new ArrayList<>();
+ final NotificationEntry entry = mock(NotificationEntry.class);
+ final StatusBarNotification sbn = mock(StatusBarNotification.class);
+ final Notification notification = mock(Notification.class);
+ final Icon icon = mock(Icon.class);
+ entryList.add(entry);
+
+ when(mNotificationEntryManager.getActiveNotificationsForCurrentUser())
+ .thenReturn(entryList);
+ when(entry.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(notification);
+ when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+ when(notification.hasMediaSession()).thenReturn(false);
+ when(notification.getLargeIcon()).thenReturn(icon);
+
+ assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
new file mode 100644
index 000000000000..de8ce55ddcbb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.when;
+
+import android.media.MediaRoute2Info;
+import android.media.session.MediaSessionManager;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
+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)
+@TestableLooper.RunWithLooper
+public class MediaOutputDialogTest extends SysuiTestCase {
+
+ private static final String TEST_PACKAGE = "test_package";
+
+ // Mock
+ 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 LocalMediaManager mLocalMediaManager = mock(LocalMediaManager.class);
+ private MediaDevice mMediaDevice = mock(MediaDevice.class);
+ private NotificationEntryManager mNotificationEntryManager =
+ mock(NotificationEntryManager.class);
+
+ private MediaOutputDialog mMediaOutputDialog;
+ private MediaOutputController mMediaOutputController;
+ private List<String> mFeatures = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+ mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
+ mMediaOutputDialog = new MediaOutputDialog(mContext, false, mMediaOutputController);
+
+ when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
+ when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
+ }
+
+ @Test
+ public void getStopButtonVisibility_remoteDevice_returnVisible() {
+ mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK);
+
+ assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
+
+ mFeatures.clear();
+ mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK);
+
+ assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
+
+ mFeatures.clear();
+ mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK);
+
+ assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
+
+ mFeatures.clear();
+ mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK);
+
+ assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void getStopButtonVisibility_localDevice_returnGone() {
+ mFeatures.add(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK);
+
+ assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.GONE);
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
new file mode 100644
index 000000000000..1f85112dfb74
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.LinearLayout;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+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 MediaOutputGroupAdapterTest 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";
+ private static final int TEST_VOLUME = 10;
+ private static final int TEST_MAX_VOLUME = 50;
+
+ // 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 MediaOutputGroupAdapter mGroupAdapter;
+ private MediaOutputGroupAdapter.GroupViewHolder mGroupViewHolder;
+ private List<MediaDevice> mGroupMediaDevices = new ArrayList<>();
+ private List<MediaDevice> mSelectableMediaDevices = new ArrayList<>();
+ private List<MediaDevice> mSelectedMediaDevices = new ArrayList<>();
+ private List<MediaDevice> mDeselectableMediaDevices = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ when(mMediaOutputController.getGroupMediaDevices()).thenReturn(mGroupMediaDevices);
+ when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
+ when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
+ when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(mSelectableMediaDevices);
+ when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mSelectedMediaDevices);
+ when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn(
+ mDeselectableMediaDevices);
+ 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);
+ mGroupMediaDevices.add(mMediaDevice1);
+ mGroupMediaDevices.add(mMediaDevice2);
+ mSelectedMediaDevices.add(mMediaDevice1);
+ mSelectableMediaDevices.add(mMediaDevice2);
+ mDeselectableMediaDevices.add(mMediaDevice1);
+
+ mGroupAdapter = new MediaOutputGroupAdapter(mMediaOutputController);
+ mGroupViewHolder = (MediaOutputGroupAdapter.GroupViewHolder) mGroupAdapter
+ .onCreateViewHolder(new LinearLayout(mContext), 0);
+ }
+
+ @Test
+ public void onBindViewHolder_verifyGroupItem() {
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 0);
+
+ assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(mContext.getText(
+ R.string.media_output_dialog_group));
+ }
+
+ @Test
+ public void onBindViewHolder_singleSelectedDevice_verifyView() {
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
+
+ assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+ assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mCheckBox.isChecked()).isTrue();
+ // Disabled checkBox
+ assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void onBindViewHolder_multipleSelectedDevice_verifyView() {
+ mSelectedMediaDevices.clear();
+ mSelectedMediaDevices.add(mMediaDevice1);
+ mSelectedMediaDevices.add(mMediaDevice2);
+ mDeselectableMediaDevices.clear();
+ mDeselectableMediaDevices.add(mMediaDevice1);
+ mDeselectableMediaDevices.add(mMediaDevice2);
+ mSelectableMediaDevices.clear();
+
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
+
+ assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+ assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mCheckBox.isChecked()).isTrue();
+ // Enabled checkBox
+ assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void onBindViewHolder_notDeselectedDevice_verifyView() {
+ mSelectedMediaDevices.clear();
+ mSelectedMediaDevices.add(mMediaDevice1);
+ mSelectedMediaDevices.add(mMediaDevice2);
+ mDeselectableMediaDevices.clear();
+ mDeselectableMediaDevices.add(mMediaDevice1);
+ mSelectableMediaDevices.clear();
+
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 2);
+
+ assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mCheckBox.isChecked()).isTrue();
+ // Disabled checkBox
+ assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void onBindViewHolder_selectableDevice_verifyCheckBox() {
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 2);
+
+ assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mGroupViewHolder.mCheckBox.isChecked()).isFalse();
+ // Enabled checkBox
+ assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void onBindViewHolder_verifySessionVolume() {
+ when(mMediaOutputController.getSessionVolume()).thenReturn(TEST_VOLUME);
+ when(mMediaOutputController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
+
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 0);
+
+ assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
+ assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
+ }
+
+ @Test
+ public void onBindViewHolder_verifyDeviceVolume() {
+ when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_VOLUME);
+ when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
+
+ assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
+ assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
+ }
+
+ @Test
+ public void clickSelectedDevice_verifyRemoveDeviceFromPlayMedia() {
+ mSelectedMediaDevices.clear();
+ mSelectedMediaDevices.add(mMediaDevice1);
+ mSelectedMediaDevices.add(mMediaDevice2);
+ mDeselectableMediaDevices.clear();
+ mDeselectableMediaDevices.add(mMediaDevice1);
+ mDeselectableMediaDevices.add(mMediaDevice2);
+ mSelectableMediaDevices.clear();
+
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
+ mGroupViewHolder.mCheckBox.performClick();
+
+ verify(mMediaOutputController).removeDeviceFromPlayMedia(mMediaDevice1);
+ }
+
+ @Test
+ public void clickSelectabelDevice_verifyAddDeviceToPlayMedia() {
+ mGroupAdapter.onBindViewHolder(mGroupViewHolder, 2);
+
+ mGroupViewHolder.mCheckBox.performClick();
+
+ verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
new file mode 100644
index 000000000000..581335027671
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.when;
+
+import android.media.session.MediaSessionManager;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+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.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import org.junit.After;
+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)
+@TestableLooper.RunWithLooper
+public class MediaOutputGroupDialogTest extends SysuiTestCase {
+
+ private static final String TEST_PACKAGE = "test_package";
+
+ // Mock
+ 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 LocalMediaManager mLocalMediaManager = mock(LocalMediaManager.class);
+ private MediaDevice mMediaDevice = mock(MediaDevice.class);
+ private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
+ private NotificationEntryManager mNotificationEntryManager =
+ mock(NotificationEntryManager.class);
+
+ private MediaOutputGroupDialog mMediaOutputGroupDialog;
+ private MediaOutputController mMediaOutputController;
+ private List<MediaDevice> mMediaDevices = new ArrayList<>();
+
+ @Before
+ public void setUp() {
+ mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
+ mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
+ mNotificationEntryManager);
+ mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
+ mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
+ mMediaOutputController);
+ when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
+ }
+
+ @After
+ public void tearDown() {
+ mMediaOutputGroupDialog.dismissDialog();
+ }
+
+ @Test
+ public void getStopButtonVisibility_returnVisible() {
+ assertThat(mMediaOutputGroupDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void getHeaderSubtitle_singleDevice_verifyTitle() {
+ mMediaDevices.add(mMediaDevice);
+
+ assertThat(mMediaOutputGroupDialog.getHeaderSubtitle()).isEqualTo(
+ mContext.getText(R.string.media_output_dialog_single_device));
+ }
+
+ @Test
+ public void getHeaderSubtitle_multipleDevices_verifyTitle() {
+ mMediaDevices.add(mMediaDevice);
+ mMediaDevices.add(mMediaDevice1);
+
+ assertThat(mMediaOutputGroupDialog.getHeaderSubtitle()).isEqualTo(mContext.getString(
+ R.string.media_output_dialog_multiple_devices, mMediaDevices.size()));
+ }
+
+}