Add Media Output Dialog for Output Switcher

-Add MediaOutputBaseDialog to provide common method for different media operations UI
-Add MediaOutputDialog for showing Bluetooth device
-Add resources for background image, style and layout
-Add MediaOutputBaseDialogTest for unit test

Bug: 155822415
Test: atest MediaOutputBaseDialogTest
Merged-In: I3086a4049f240870ca1ad870946d6848e500b561
Change-Id: I3086a4049f240870ca1ad870946d6848e500b561
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 0000000..3ceb0f6
--- /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 0000000..0229e6e
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/media_output_dialog"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="94dp"
+        android:gravity="start|center_vertical"
+        android:paddingStart="16dp"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+id/header_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingEnd="16dp"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="16dp"
+            android:orientation="vertical">
+            <TextView
+                android:id="@+id/header_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:maxLines="1"
+                android:textColor="?android:attr/textColorPrimary"
+                android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+                android:textSize="20sp"/>
+
+            <TextView
+                android:id="@+id/header_subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:maxLines="1"
+                android:fontFamily="roboto-regular"
+                android:textSize="14sp"/>
+
+        </LinearLayout>
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?android:attr/listDivider"/>
+
+    <LinearLayout
+        android:id="@+id/device_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:orientation="vertical">
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="12dp"/>
+
+        <include
+            layout="@layout/media_output_list_item"
+            android:id="@+id/group_item_controller"
+            android:visibility="gone"/>
+
+        <View
+            android:id="@+id/group_item_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?android:attr/listDivider"
+            android:visibility="gone"/>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/list_result"
+            android:scrollbars="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:overScrollMode="never"/>
+
+        <View
+            android:id="@+id/list_bottom_padding"
+            android:layout_width="match_parent"
+            android:layout_height="12dp"/>
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?android:attr/listDivider"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/stop"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_width="wrap_content"
+            android:layout_height="64dp"
+            android:text="@string/keyboard_key_media_stop"
+            android:visibility="gone"/>
+
+        <Space
+            android:layout_weight="1"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"/>
+
+        <Button
+            android:id="@+id/done"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_width="wrap_content"
+            android:layout_height="64dp"
+            android:layout_marginEnd="0dp"
+            android:text="@string/inline_done_button"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 68c2a38..44f0cce 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -400,6 +400,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 +793,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/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
new file mode 100644
index 0000000..781bf8d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settingslib.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+/**
+ * Base dialog for media output UI
+ */
+public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
+        MediaOutputController.Callback {
+
+    private static final String TAG = "MediaOutputDialog";
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+    private final RecyclerView.LayoutManager mLayoutManager;
+
+    final Context mContext;
+    final MediaOutputController mMediaOutputController;
+
+    @VisibleForTesting
+    View mDialogView;
+    private TextView mHeaderTitle;
+    private TextView mHeaderSubtitle;
+    private ImageView mHeaderIcon;
+    private RecyclerView mDevicesRecyclerView;
+    private LinearLayout mDeviceListLayout;
+    private Button mDoneButton;
+    private Button mStopButton;
+    private View mListBottomPadding;
+    private int mListMaxHeight;
+
+    MediaOutputBaseAdapter mAdapter;
+    FrameLayout mGroupItemController;
+    View mGroupDivider;
+
+    private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
+        // Set max height for list
+        if (mDeviceListLayout.getHeight() > mListMaxHeight) {
+            ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
+            params.height = mListMaxHeight;
+            mDeviceListLayout.setLayoutParams(params);
+        }
+    };
+
+    public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
+        super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
+        mContext = context;
+        mMediaOutputController = mediaOutputController;
+        mLayoutManager = new LinearLayoutManager(mContext);
+        mListMaxHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.media_output_dialog_list_max_height);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
+        final Window window = getWindow();
+        final WindowManager.LayoutParams lp = window.getAttributes();
+        lp.gravity = Gravity.BOTTOM;
+        // Config insets to make sure the layout is above the navigation bar
+        lp.setFitInsetsTypes(statusBars() | navigationBars());
+        lp.setFitInsetsSides(WindowInsets.Side.all());
+        lp.setFitInsetsIgnoringVisibility(true);
+        window.setAttributes(lp);
+        window.setContentView(mDialogView);
+        window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+        mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
+        mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
+        mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
+        mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+        mGroupItemController = mDialogView.requireViewById(R.id.group_item_controller);
+        mGroupDivider = mDialogView.requireViewById(R.id.group_item_divider);
+        mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
+        mDoneButton = mDialogView.requireViewById(R.id.done);
+        mStopButton = mDialogView.requireViewById(R.id.stop);
+        mListBottomPadding = mDialogView.requireViewById(R.id.list_bottom_padding);
+
+        mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
+                mDeviceListLayoutListener);
+        // Init device list
+        mDevicesRecyclerView.setLayoutManager(mLayoutManager);
+        mDevicesRecyclerView.setAdapter(mAdapter);
+        // Init bottom buttons
+        mDoneButton.setOnClickListener(v -> dismiss());
+        mStopButton.setOnClickListener(v -> {
+            mMediaOutputController.releaseSession();
+            dismiss();
+        });
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mMediaOutputController.start(this);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mMediaOutputController.stop();
+    }
+
+    @VisibleForTesting
+    void refresh() {
+        // Update header icon
+        final int iconRes = getHeaderIconRes();
+        final IconCompat iconCompat = getHeaderIcon();
+        if (iconRes != 0) {
+            mHeaderIcon.setVisibility(View.VISIBLE);
+            mHeaderIcon.setImageResource(iconRes);
+        } else if (iconCompat != null) {
+            mHeaderIcon.setVisibility(View.VISIBLE);
+            mHeaderIcon.setImageIcon(iconCompat.toIcon(mContext));
+        } else {
+            mHeaderIcon.setVisibility(View.GONE);
+        }
+        if (mHeaderIcon.getVisibility() == View.VISIBLE) {
+            final int size = getHeaderIconSize();
+            mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size, size));
+        }
+        // Update title and subtitle
+        mHeaderTitle.setText(getHeaderText());
+        final CharSequence subTitle = getHeaderSubtitle();
+        if (TextUtils.isEmpty(subTitle)) {
+            mHeaderSubtitle.setVisibility(View.GONE);
+            mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+        } else {
+            mHeaderSubtitle.setVisibility(View.VISIBLE);
+            mHeaderSubtitle.setText(subTitle);
+            mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+        }
+        if (!mAdapter.isDragging()) {
+            mAdapter.notifyDataSetChanged();
+        }
+        // Add extra padding when device amount is less than 6
+        if (mMediaOutputController.getMediaDevices().size() < 6) {
+            mListBottomPadding.setVisibility(View.VISIBLE);
+        } else {
+            mListBottomPadding.setVisibility(View.GONE);
+        }
+    }
+
+    abstract int getHeaderIconRes();
+
+    abstract IconCompat getHeaderIcon();
+
+    abstract int getHeaderIconSize();
+
+    abstract CharSequence getHeaderText();
+
+    abstract CharSequence getHeaderSubtitle();
+
+    @Override
+    public void onMediaChanged() {
+        mMainThreadHandler.post(() -> refresh());
+    }
+
+    @Override
+    public void onMediaStoppedOrPaused() {
+        if (isShowing()) {
+            dismiss();
+        }
+    }
+
+    @Override
+    public void onRouteChanged() {
+        mMainThreadHandler.post(() -> refresh());
+    }
+
+    @Override
+    public void dismissDialog() {
+        dismiss();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
new file mode 100644
index 0000000..d59971d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -0,0 +1,78 @@
+/*
+ * 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);
+        mGroupItemController.setVisibility(View.GONE);
+        mGroupDivider.setVisibility(View.GONE);
+    }
+
+    @Override
+    int getHeaderIconRes() {
+        return 0;
+    }
+
+    @Override
+    IconCompat getHeaderIcon() {
+        return mMediaOutputController.getHeaderIcon();
+    }
+
+    @Override
+    int getHeaderIconSize() {
+        return mContext.getResources().getDimensionPixelSize(
+                R.dimen.media_output_dialog_header_album_icon_size);
+    }
+
+    @Override
+    CharSequence getHeaderText() {
+        return mMediaOutputController.getHeaderTitle();
+    }
+
+    @Override
+    CharSequence getHeaderSubtitle() {
+        return mMediaOutputController.getHeaderSubTitle();
+    }
+}
diff --git a/packages/SystemUI/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 0000000..42b21c6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class MediaOutputBaseDialogTest extends SysuiTestCase {
+
+    private static final String TEST_PACKAGE = "test_package";
+
+    // Mock
+    private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class);
+
+    private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
+    private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
+    private ShadeController mShadeController = mock(ShadeController.class);
+    private ActivityStarter mStarter = mock(ActivityStarter.class);
+
+    private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
+    private MediaOutputController mMediaOutputController;
+    private int mHeaderIconRes;
+    private IconCompat mIconCompat;
+    private CharSequence mHeaderTitle;
+    private CharSequence mHeaderSubtitle;
+
+    @Before
+    public void setUp() {
+        mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
+                mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
+        mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
+                mMediaOutputController);
+        mMediaOutputBaseDialogImpl.onCreate(new Bundle());
+    }
+
+    @Test
+    public void refresh_withIconRes_iconIsVisible() {
+        mHeaderIconRes = 1;
+        mMediaOutputBaseDialogImpl.refresh();
+        final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_icon);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void refresh_withIconCompat_iconIsVisible() {
+        mIconCompat = mock(IconCompat.class);
+        mMediaOutputBaseDialogImpl.refresh();
+        final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_icon);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void refresh_noIcon_iconLayoutNotVisible() {
+        mHeaderIconRes = 0;
+        mIconCompat = null;
+        mMediaOutputBaseDialogImpl.refresh();
+        final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_icon);
+
+        assertThat(view.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void refresh_checkTitle() {
+        mHeaderTitle = "test_string";
+
+        mMediaOutputBaseDialogImpl.refresh();
+        final TextView titleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_title);
+
+        assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(titleView.getText()).isEqualTo(mHeaderTitle);
+    }
+
+    @Test
+    public void refresh_withSubtitle_checkSubtitle() {
+        mHeaderSubtitle = "test_string";
+
+        mMediaOutputBaseDialogImpl.refresh();
+        final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_subtitle);
+
+        assertThat(subtitleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(subtitleView.getText()).isEqualTo(mHeaderSubtitle);
+    }
+
+    @Test
+    public void refresh_noSubtitle_checkSubtitle() {
+        mMediaOutputBaseDialogImpl.refresh();
+        final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_subtitle);
+
+        assertThat(subtitleView.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void refresh_inDragging_notUpdateAdapter() {
+        when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true);
+        mMediaOutputBaseDialogImpl.refresh();
+
+        verify(mMediaOutputBaseAdapter, never()).notifyDataSetChanged();
+    }
+
+    @Test
+    public void refresh_notInDragging_verifyUpdateAdapter() {
+        when(mMediaOutputBaseAdapter.isDragging()).thenReturn(false);
+        mMediaOutputBaseDialogImpl.refresh();
+
+        verify(mMediaOutputBaseAdapter).notifyDataSetChanged();
+    }
+
+    @Test
+    public void refresh_with6Devices_checkBottomPaddingVisibility() {
+        for (int i = 0; i < 6; i++) {
+            mMediaOutputController.mMediaDevices.add(mock(MediaDevice.class));
+        }
+        mMediaOutputBaseDialogImpl.refresh();
+        final View view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.list_bottom_padding);
+
+        assertThat(view.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void refresh_with5Devices_checkBottomPaddingVisibility() {
+        for (int i = 0; i < 5; i++) {
+            mMediaOutputController.mMediaDevices.add(mock(MediaDevice.class));
+        }
+        mMediaOutputBaseDialogImpl.refresh();
+        final View view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.list_bottom_padding);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
+
+        MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) {
+            super(context, mediaOutputController);
+
+            mAdapter = mMediaOutputBaseAdapter;
+        }
+
+        int getHeaderIconRes() {
+            return mHeaderIconRes;
+        }
+
+        IconCompat getHeaderIcon() {
+            return mIconCompat;
+        }
+
+        int getHeaderIconSize() {
+            return 10;
+        }
+
+        CharSequence getHeaderText() {
+            return mHeaderTitle;
+        }
+
+        CharSequence getHeaderSubtitle() {
+            return mHeaderSubtitle;
+        }
+    }
+}