Add dynamic group operation in output switcher

-Add MediaOutputGroupAdapter for device view layout
-Add MediaOutputGroupDialog for dialog header and buttom
-Add test cases

Bug: 155822415
Test: atest MediaOutputAdapterTest MediaOutputControllerTest MediaOutputBaseDialogTest MediaOutputDialogTest MediaOutputGroupAdapterTest MediaOutputGroupDialogTest
Merged-In: I82b52485229b7aa8b80a6ef13fc9aac14af87446
Change-Id: I82b52485229b7aa8b80a6ef13fc9aac14af87446
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 0000000..ceb4495
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+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 android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.bluetooth.BluetoothUtils;
+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 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;
+                }
+            });
+        }
+
+        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 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);
+        }
+
+        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 0000000..4079304
--- /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/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 0000000..1f85112
--- /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 0000000..10f2bd5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.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 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);
+        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()));
+    }
+
+}