Add MediaOutputAdapter for Media device list

-Build device item layout by different cases
-Provide UI component for different operations
-Add MediaOutputBaseAdapter for common method
-Add MediaOutputAdapterTest for unit test

Bug: 155822415
Test: atest MediaOutputAdapterTest
Merged-In: Ida509ed401320b3665d7a5f29f4df5db096ff29c
Change-Id: Ida509ed401320b3665d7a5f29f4df5db096ff29c
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 0000000..92d0858
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<FrameLayout
+    android:id="@+id/device_container"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="64dp">
+
+    <FrameLayout
+        android:layout_width="36dp"
+        android:layout_height="36dp"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="16dp">
+        <ImageView
+            android:id="@+id/title_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"/>
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="68dp"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textColor="?android:attr/textColorPrimary"
+        android:textSize="14sp"/>
+
+    <RelativeLayout
+        android:id="@+id/two_line_layout"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:layout_marginStart="52dp"
+        android:layout_marginEnd="69dp"
+        android:layout_marginTop="10dp">
+        <TextView
+            android:id="@+id/two_line_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="15dp"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="14sp"/>
+        <TextView
+            android:id="@+id/subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="15dp"
+            android:layout_marginBottom="7dp"
+            android:layout_alignParentBottom="true"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:textColor="?android:attr/textColorSecondary"
+            android:textSize="12sp"
+            android:fontFamily="roboto-regular"
+            android:visibility="gone"/>
+        <ProgressBar
+            android:id="@+id/volume_indeterminate_progress"
+            style="@*android:style/Widget.Material.ProgressBar.Horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="15dp"
+            android:layout_marginBottom="1dp"
+            android:layout_alignParentBottom="true"
+            android:indeterminate="true"
+            android:indeterminateOnly="true"
+            android:visibility="gone"/>
+        <SeekBar
+            android:id="@+id/volume_seekbar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"/>
+    </RelativeLayout>
+
+    <View
+        android:layout_width="1dp"
+        android:layout_height="36dp"
+        android:layout_marginEnd="68dp"
+        android:layout_gravity="right|center_vertical"
+        android:background="?android:attr/listDivider"
+        android:visibility="gone"/>
+
+    <ImageView
+        android:id="@+id/end_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="right|center_vertical"
+        android:layout_marginEnd="24dp"
+        android:visibility="gone"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
new file mode 100644
index 0000000..9fc64d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
+
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.Utils;
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Adapter for media output dialog.
+ */
+public class MediaOutputAdapter extends MediaOutputBaseAdapter {
+
+    private static final String TAG = "MediaOutputAdapter";
+    private static final int PAIR_NEW = 1;
+
+    public MediaOutputAdapter(MediaOutputController controller) {
+        super(controller);
+    }
+
+    @Override
+    public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+            int viewType) {
+        super.onCreateViewHolder(viewGroup, viewType);
+
+        return new MediaDeviceViewHolder(mHolderView);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) {
+        if (mController.isZeroMode() && position == (mController.getMediaDevices().size())) {
+            viewHolder.onBind(PAIR_NEW);
+        } else if (position < (mController.getMediaDevices().size())) {
+            viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position));
+        } else {
+            Log.d(TAG, "Incorrect position: " + position);
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        if (mController.isZeroMode()) {
+            // Add extra one for "pair new"
+            return mController.getMediaDevices().size() + 1;
+        }
+        return mController.getMediaDevices().size();
+    }
+
+    void onItemClick(MediaDevice device) {
+        mController.connectDevice(device);
+        device.setState(MediaDeviceState.STATE_CONNECTING);
+        notifyDataSetChanged();
+    }
+
+    void onItemClick(int customizedItem) {
+        if (customizedItem == PAIR_NEW) {
+            mController.launchBluetoothPairing();
+        }
+    }
+
+    @Override
+    CharSequence getItemTitle(MediaDevice device) {
+        if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
+                && !device.isConnected()) {
+            final CharSequence deviceName = device.getName();
+            // Append status to title only for the disconnected Bluetooth device.
+            final SpannableString spannableTitle = new SpannableString(
+                    mContext.getString(R.string.media_output_dialog_disconnected, deviceName));
+            spannableTitle.setSpan(new ForegroundColorSpan(
+                    Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)),
+                    deviceName.length(),
+                    spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
+            return spannableTitle;
+        }
+        return super.getItemTitle(device);
+    }
+
+    class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
+
+        MediaDeviceViewHolder(View view) {
+            super(view);
+        }
+
+        @Override
+        void onBind(MediaDevice device) {
+            super.onBind(device);
+            if (mController.isTransferring()) {
+                if (device.getState() == MediaDeviceState.STATE_CONNECTING
+                        && !mController.hasAdjustVolumeUserRestriction()) {
+                    setTwoLineLayout(device, true);
+                    mProgressBar.setVisibility(View.VISIBLE);
+                    mSeekBar.setVisibility(View.GONE);
+                    mSubTitleText.setVisibility(View.GONE);
+                } else {
+                    setSingleLineLayout(getItemTitle(device), false);
+                }
+            } else {
+                // Set different layout for each device
+                if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
+                    setTwoLineLayout(device, false);
+                    mSubTitleText.setVisibility(View.VISIBLE);
+                    mSeekBar.setVisibility(View.GONE);
+                    mProgressBar.setVisibility(View.GONE);
+                    mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
+                    mFrameLayout.setOnClickListener(v -> onItemClick(device));
+                } else if (!mController.hasAdjustVolumeUserRestriction()
+                        && isCurrentConnected(device)) {
+                    setTwoLineLayout(device, true);
+                    mSeekBar.setVisibility(View.VISIBLE);
+                    mProgressBar.setVisibility(View.GONE);
+                    mSubTitleText.setVisibility(View.GONE);
+                    initSeekbar(device);
+                } else {
+                    setSingleLineLayout(getItemTitle(device), false);
+                    mFrameLayout.setOnClickListener(v -> onItemClick(device));
+                }
+            }
+        }
+
+        @Override
+        void onBind(int customizedItem) {
+            if (customizedItem == PAIR_NEW) {
+                setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
+                        false);
+                final Drawable d = mContext.getDrawable(R.drawable.ic_add);
+                d.setColorFilter(new PorterDuffColorFilter(
+                        Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
+                mTitleIcon.setImageDrawable(d);
+                mFrameLayout.setOnClickListener(v -> onItemClick(PAIR_NEW));
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
new file mode 100644
index 0000000..7579c25
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+/**
+ * Base adapter for media output dialog.
+ */
+public abstract class MediaOutputBaseAdapter extends
+        RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> {
+
+    private static final String FONT_SELECTED_TITLE = "sans-serif-medium";
+    private static final String FONT_TITLE = "sans-serif";
+
+    final MediaOutputController mController;
+
+    private boolean mIsDragging;
+
+    Context mContext;
+    View mHolderView;
+
+    public MediaOutputBaseAdapter(MediaOutputController controller) {
+        mController = controller;
+        mIsDragging = false;
+    }
+
+    @Override
+    public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+            int viewType) {
+        mContext = viewGroup.getContext();
+        mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
+                viewGroup, false);
+
+        return null;
+    }
+
+    CharSequence getItemTitle(MediaDevice device) {
+        return device.getName();
+    }
+
+    boolean isCurrentConnected(MediaDevice device) {
+        return TextUtils.equals(device.getId(),
+                mController.getCurrentConnectedMediaDevice().getId());
+    }
+
+    boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /**
+     * ViewHolder for binding device view.
+     */
+    abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+        final FrameLayout mFrameLayout;
+        final TextView mTitleText;
+        final TextView mTwoLineTitleText;
+        final TextView mSubTitleText;
+        final ImageView mTitleIcon;
+        final ImageView mEndIcon;
+        final ProgressBar mProgressBar;
+        final SeekBar mSeekBar;
+        final RelativeLayout mTwoLineLayout;
+
+        MediaDeviceBaseViewHolder(View view) {
+            super(view);
+            mFrameLayout = view.requireViewById(R.id.device_container);
+            mTitleText = view.requireViewById(R.id.title);
+            mSubTitleText = view.requireViewById(R.id.subtitle);
+            mTwoLineLayout = view.requireViewById(R.id.two_line_layout);
+            mTwoLineTitleText = view.requireViewById(R.id.two_line_title);
+            mTitleIcon = view.requireViewById(R.id.title_icon);
+            mEndIcon = view.requireViewById(R.id.end_icon);
+            mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
+            mSeekBar = view.requireViewById(R.id.volume_seekbar);
+        }
+
+        void onBind(MediaDevice device) {
+            mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext));
+        }
+
+        void onBind(int customizedItem) { }
+
+        void setSingleLineLayout(CharSequence title, boolean bFocused) {
+            mTitleText.setVisibility(View.VISIBLE);
+            mTwoLineLayout.setVisibility(View.GONE);
+            mTitleText.setText(title);
+            if (bFocused) {
+                mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL));
+            } else {
+                mTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+            }
+        }
+
+        void setTwoLineLayout(MediaDevice device, boolean bFocused) {
+            mTitleText.setVisibility(View.GONE);
+            mTwoLineLayout.setVisibility(View.VISIBLE);
+            mTwoLineTitleText.setText(getItemTitle(device));
+            if (bFocused) {
+                mTwoLineTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE,
+                        Typeface.NORMAL));
+            } else {
+                mTwoLineTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+            }
+        }
+
+        void initSeekbar(MediaDevice device) {
+            mSeekBar.setMax(device.getMaxVolume());
+            mSeekBar.setMin(0);
+            if (mSeekBar.getProgress() != device.getCurrentVolume()) {
+                mSeekBar.setProgress(device.getCurrentVolume());
+            }
+            mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+                @Override
+                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                    if (device == null || !fromUser) {
+                        return;
+                    }
+                    mController.adjustVolume(device, progress);
+                }
+
+                @Override
+                public void onStartTrackingTouch(SeekBar seekBar) {
+                    mIsDragging = true;
+                }
+
+                @Override
+                public void onStopTrackingTouch(SeekBar seekBar) {
+                    mIsDragging = false;
+                }
+            });
+        }
+    }
+}
diff --git a/packages/SystemUI/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 0000000..0e376bd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.drawable.Icon;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MediaOutputAdapterTest extends SysuiTestCase {
+
+    private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
+    private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
+    private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
+    private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
+
+    // Mock
+    private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
+    private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
+    private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+    private Icon mIcon = mock(Icon.class);
+    private IconCompat mIconCompat = mock(IconCompat.class);
+
+    private MediaOutputAdapter mMediaOutputAdapter;
+    private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
+    private List<MediaDevice> mMediaDevices = new ArrayList<>();
+
+    @Before
+    public void setUp() {
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+                .onCreateViewHolder(new FrameLayout(mContext), 0);
+
+        when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
+        when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
+        when(mMediaOutputController.isZeroMode()).thenReturn(false);
+        when(mMediaOutputController.isTransferring()).thenReturn(false);
+        when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
+        when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
+        when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
+        when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
+        when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
+        when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
+        when(mMediaDevice2.getName()).thenReturn(TEST_DEVICE_NAME_2);
+        when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_ID_2);
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+        when(mMediaDevice2.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+        mMediaDevices.add(mMediaDevice1);
+        mMediaDevices.add(mMediaDevice2);
+    }
+
+    @Test
+    public void getItemCount_nonZeroMode_isDeviceSize() {
+        assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size());
+    }
+
+    @Test
+    public void getItemCount_zeroMode_containExtraOneForPairNew() {
+        when(mMediaOutputController.isZeroMode()).thenReturn(true);
+
+        assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1);
+    }
+
+    @Test
+    public void onBindViewHolder_zeroMode_bindPairNew_verifyView() {
+        when(mMediaOutputController.isZeroMode()).thenReturn(true);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText(
+                R.string.media_output_dialog_pairing_new));
+    }
+
+    @Test
+    public void onBindViewHolder_bindConnectedDevice_verifyView() {
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onBindViewHolder_bindDisconnectedBluetoothDevice_verifyView() {
+        when(mMediaDevice2.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
+        when(mMediaDevice2.isConnected()).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(
+                mContext.getString(R.string.media_output_dialog_disconnected, TEST_DEVICE_NAME_2));
+    }
+
+    @Test
+    public void onBindViewHolder_bindFailedStateDevice_verifyView() {
+        when(mMediaDevice2.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(mContext.getText(
+                R.string.media_output_dialog_connect_failed));
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+    }
+
+    @Test
+    public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
+        when(mMediaOutputController.isTransferring()).thenReturn(true);
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
+        when(mMediaOutputController.isTransferring()).thenReturn(true);
+        when(mMediaDevice2.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
+        when(mMediaOutputController.isZeroMode()).thenReturn(true);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
+        mViewHolder.mFrameLayout.performClick();
+
+        verify(mMediaOutputController).launchBluetoothPairing();
+    }
+
+    @Test
+    public void onItemClick_clickDevice_verifyConnectDevice() {
+        assertThat(mMediaDevice2.getState()).isEqualTo(
+                LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+        mViewHolder.mFrameLayout.performClick();
+
+        verify(mMediaOutputController).connectDevice(mMediaDevice2);
+    }
+}