Merge changes Icb5a1f24,Idbfc558b,I3086a404,Ida509ed4,I9eb6e3b0

* changes:
  Add a receiver to launch Output Switcher dialog
  Add MediaOutputDialogFactory to create MediaOutputDialog
  Add Media Output Dialog for Output Switcher
  Add MediaOutputAdapter for Media device list
  Add controller for Media operation
This commit is contained in:
tim peng
2020-09-15 01:22:39 +00:00
committed by Android (Google) Code Review
18 changed files with 2225 additions and 1 deletions

View File

@@ -772,5 +772,12 @@
</intent-filter>
</receiver>
<receiver android:name=".media.dialog.MediaOutDialogReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1366,4 +1366,11 @@
<dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen>
<dimen name="config_rounded_mask_size_top">@*android:dimen/rounded_corner_radius_top</dimen>
<dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
<!-- Output switcher panel related dimensions -->
<dimen name="media_output_dialog_padding_top">11dp</dimen>
<dimen name="media_output_dialog_list_max_height">364dp</dimen>
<dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
<dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
<dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
</resources>

View File

@@ -2799,4 +2799,19 @@
<string name="udfps_hbm_enable_command" translatable="false"></string>
<!-- Device-specific payload for disabling the high-brightness mode -->
<string name="udfps_hbm_disable_command" translatable="false"></string>
<!-- Title for the media output group dialog with media related devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_add_output">Add outputs</string>
<!-- Title for the media output slice with group devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_group">Group</string>
<!-- Summary for media output group with only one device which is active [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_single_device">1 device selected</string>
<!-- Summary for media output group with the active device count [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_multiple_devices"><xliff:g id="count" example="2">%1$d</xliff:g> devices selected</string>
<!-- Summary for disconnected status [CHAR LIMIT=50] -->
<string name="media_output_dialog_disconnected"><xliff:g id="device_name" example="My device">%1$s</xliff:g> (disconnected)</string>
<!-- Summary for connecting error message [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_connect_failed">Couldn\'t connect. Try again.</string>
<!-- Title for pairing item [CHAR LIMIT=60] -->
<string name="media_output_dialog_pairing_new">Pair new device</string>
</resources>

View File

@@ -388,6 +388,10 @@
<item name="android:windowIsFloating">true</item>
</style>
<style name="Theme.SystemUI.Dialog.MediaOutput">
<item name="android:windowBackground">@drawable/media_output_dialog_background</item>
</style>
<style name="QSBorderlessButton">
<item name="android:padding">12dp</item>
<item name="android:background">@drawable/qs_btn_borderless_rect</item>
@@ -735,5 +739,4 @@
* Title: headline, medium 20sp
* Message: body, 16 sp -->
<style name="Theme.ControlsRequestDialog" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert"/>
</resources>

View File

@@ -18,6 +18,7 @@ package com.android.systemui.dagger;
import android.content.BroadcastReceiver;
import com.android.systemui.media.dialog.MediaOutDialogReceiver;
import com.android.systemui.screenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.DeleteScreenshotReceiver;
import com.android.systemui.screenshot.SmartActionsReceiver;
@@ -59,4 +60,13 @@ public abstract class DefaultBroadcastReceiverBinder {
public abstract BroadcastReceiver bindSmartActionsReceiver(
SmartActionsReceiver broadcastReceiver);
/**
*
*/
@Binds
@IntoMap
@ClassKey(MediaOutDialogReceiver.class)
public abstract BroadcastReceiver bindMediaOutDialogReceiver(
MediaOutDialogReceiver broadcastReceiver);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.text.TextUtils
import com.android.settingslib.media.MediaOutputSliceConstants
import javax.inject.Inject
/**
* BroadcastReceiver for handling media output intent
*/
class MediaOutDialogReceiver @Inject constructor(
private var mediaOutputDialogFactory: MediaOutputDialogFactory
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (TextUtils.equals(MediaOutputSliceConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG,
intent.action)) {
mediaOutputDialogFactory.create(
intent.getStringExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME), false)
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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;
}
});
}
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,445 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.RoutingSessionInfo;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.ShadeController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Inject;
/**
* Controller for media output dialog
*/
public class MediaOutputController implements LocalMediaManager.DeviceCallback{
private static final String TAG = "MediaOutputController";
private static final boolean DEBUG = false;
private final String mPackageName;
private final Context mContext;
private final MediaSessionManager mMediaSessionManager;
private final ShadeController mShadeController;
private final ActivityStarter mActivityStarter;
@VisibleForTesting
final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
private MediaController mMediaController;
@VisibleForTesting
Callback mCallback;
@VisibleForTesting
LocalMediaManager mLocalMediaManager;
@Inject
public MediaOutputController(@NonNull Context context, String packageName,
MediaSessionManager mediaSessionManager, LocalBluetoothManager
lbm, ShadeController shadeController, ActivityStarter starter) {
mContext = context;
mPackageName = packageName;
mMediaSessionManager = mediaSessionManager;
mShadeController = shadeController;
mActivityStarter = starter;
InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
}
void start(@NonNull Callback cb) {
mMediaDevices.clear();
if (!TextUtils.isEmpty(mPackageName)) {
for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
mMediaController = controller;
mMediaController.unregisterCallback(mCb);
mMediaController.registerCallback(mCb);
break;
}
}
}
if (mMediaController == null) {
if (DEBUG) {
Log.d(TAG, "No media controller for " + mPackageName);
}
}
if (mLocalMediaManager == null) {
if (DEBUG) {
Log.d(TAG, "No local media manager " + mPackageName);
}
return;
}
mCallback = cb;
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
mLocalMediaManager.registerCallback(this);
mLocalMediaManager.startScan();
}
void stop() {
if (mMediaController != null) {
mMediaController.unregisterCallback(mCb);
}
if (mLocalMediaManager != null) {
mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan();
}
mMediaDevices.clear();
}
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
buildMediaDevices(devices);
mCallback.onRouteChanged();
}
@Override
public void onSelectedDeviceStateChanged(MediaDevice device,
@LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
}
@Override
public void onDeviceAttributesChanged() {
mCallback.onRouteChanged();
}
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
}
CharSequence getHeaderTitle() {
if (mMediaController != null) {
final MediaMetadata metadata = mMediaController.getMetadata();
if (metadata != null) {
return metadata.getDescription().getTitle();
}
}
return mContext.getText(R.string.controls_media_title);
}
CharSequence getHeaderSubTitle() {
if (mMediaController == null) {
return null;
}
final MediaMetadata metadata = mMediaController.getMetadata();
if (metadata == null) {
return null;
}
return metadata.getDescription().getSubtitle();
}
IconCompat getHeaderIcon() {
if (mMediaController == null) {
return null;
}
final MediaMetadata metadata = mMediaController.getMetadata();
if (metadata != null) {
final Bitmap bitmap = metadata.getDescription().getIconBitmap();
if (bitmap != null) {
final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap,
(float) mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_icon_corner_radius));
return IconCompat.createWithBitmap(roundBitmap);
}
}
if (DEBUG) {
Log.d(TAG, "Media meta data does not contain icon information");
}
return getPackageIcon();
}
IconCompat getDeviceIconCompat(MediaDevice device) {
Drawable drawable = device.getIcon();
if (drawable == null) {
if (DEBUG) {
Log.d(TAG, "getDeviceIconCompat() device : " + device.getName()
+ ", drawable is null");
}
// Use default Bluetooth device icon to handle getIcon() is null case.
drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
}
return BluetoothUtils.createIconWithDrawable(drawable);
}
private IconCompat getPackageIcon() {
if (TextUtils.isEmpty(mPackageName)) {
return null;
}
try {
final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName);
if (drawable instanceof BitmapDrawable) {
return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap());
}
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return IconCompat.createWithBitmap(bitmap);
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) {
Log.e(TAG, "Package is not found. Unable to get package icon.");
}
}
return null;
}
private void buildMediaDevices(List<MediaDevice> devices) {
// For the first time building list, to make sure the top device is the connected device.
if (mMediaDevices.isEmpty()) {
final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
if (connectedMediaDevice == null) {
if (DEBUG) {
Log.d(TAG, "No connected media device.");
}
mMediaDevices.addAll(devices);
return;
}
for (MediaDevice device : devices) {
if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
mMediaDevices.add(0, device);
} else {
mMediaDevices.add(device);
}
}
return;
}
// To keep the same list order
final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
for (MediaDevice originalDevice : mMediaDevices) {
for (MediaDevice newDevice : devices) {
if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
targetMediaDevices.add(newDevice);
break;
}
}
}
if (targetMediaDevices.size() != devices.size()) {
devices.removeAll(targetMediaDevices);
targetMediaDevices.addAll(devices);
}
mMediaDevices.clear();
mMediaDevices.addAll(targetMediaDevices);
}
void connectDevice(MediaDevice device) {
ThreadUtils.postOnBackgroundThread(() -> {
mLocalMediaManager.connectDevice(device);
});
}
Collection<MediaDevice> getMediaDevices() {
return mMediaDevices;
}
MediaDevice getCurrentConnectedMediaDevice() {
return mLocalMediaManager.getCurrentConnectedDevice();
}
private MediaDevice getMediaDeviceById(String id) {
return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
}
boolean addDeviceToPlayMedia(MediaDevice device) {
return mLocalMediaManager.addDeviceToPlayMedia(device);
}
boolean removeDeviceFromPlayMedia(MediaDevice device) {
return mLocalMediaManager.removeDeviceFromPlayMedia(device);
}
List<MediaDevice> getSelectableMediaDevice() {
return mLocalMediaManager.getSelectableMediaDevice();
}
List<MediaDevice> getSelectedMediaDevice() {
return mLocalMediaManager.getSelectedMediaDevice();
}
List<MediaDevice> getDeselectableMediaDevice() {
return mLocalMediaManager.getDeselectableMediaDevice();
}
boolean isDeviceIncluded(Collection<MediaDevice> deviceCollection, MediaDevice targetDevice) {
for (MediaDevice device : deviceCollection) {
if (TextUtils.equals(device.getId(), targetDevice.getId())) {
return true;
}
}
return false;
}
void adjustSessionVolume(String sessionId, int volume) {
mLocalMediaManager.adjustSessionVolume(sessionId, volume);
}
void adjustSessionVolume(int volume) {
mLocalMediaManager.adjustSessionVolume(volume);
}
int getSessionVolumeMax() {
return mLocalMediaManager.getSessionVolumeMax();
}
int getSessionVolume() {
return mLocalMediaManager.getSessionVolume();
}
CharSequence getSessionName() {
return mLocalMediaManager.getSessionName();
}
void releaseSession() {
mLocalMediaManager.releaseSession();
}
List<RoutingSessionInfo> getActiveRemoteMediaDevices() {
final List<RoutingSessionInfo> sessionInfos = new ArrayList<>();
for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) {
if (!info.isSystemSession()) {
sessionInfos.add(info);
}
}
return sessionInfos;
}
void adjustVolume(MediaDevice device, int volume) {
ThreadUtils.postOnBackgroundThread(() -> {
device.requestSetVolume(volume);
});
}
String getPackageName() {
return mPackageName;
}
boolean hasAdjustVolumeUserRestriction() {
if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
return true;
}
final UserManager um = mContext.getSystemService(UserManager.class);
return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
UserHandle.of(UserHandle.myUserId()));
}
boolean isTransferring() {
for (MediaDevice device : mMediaDevices) {
if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
return true;
}
}
return false;
}
boolean isZeroMode() {
if (mMediaDevices.size() == 1) {
final MediaDevice device = mMediaDevices.iterator().next();
// Add "pair new" only when local output device exists
final int type = device.getDeviceType();
if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
|| type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
|| type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
return true;
}
}
return false;
}
void launchBluetoothPairing() {
mCallback.dismissDialog();
final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
mContext.sendBroadcast(new Intent()
.setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING)
.setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME));
mShadeController.animateCollapsePanels();
return true;
};
mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
}
private final MediaController.Callback mCb = new MediaController.Callback() {
@Override
public void onMetadataChanged(MediaMetadata metadata) {
mCallback.onMediaChanged();
}
@Override
public void onPlaybackStateChanged(PlaybackState playbackState) {
final int state = playbackState.getState();
if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) {
mCallback.onMediaStoppedOrPaused();
}
}
};
interface Callback {
/**
* Override to handle the media content updating.
*/
void onMediaChanged();
/**
* Override to handle the media state updating.
*/
void onMediaStoppedOrPaused();
/**
* Override to handle the device updating.
*/
void onRouteChanged();
/**
* Override to dismiss dialog.
*/
void dismissDialog();
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import androidx.core.graphics.drawable.IconCompat;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
/**
* Dialog for media output transferring.
*/
@SysUISingleton
public class MediaOutputDialog extends MediaOutputBaseDialog {
MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController
mediaOutputController) {
super(context, mediaOutputController);
mAdapter = new MediaOutputAdapter(mMediaOutputController);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
show();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGroupItemController.setVisibility(View.GONE);
mGroupDivider.setVisibility(View.GONE);
}
@Override
int getHeaderIconRes() {
return 0;
}
@Override
IconCompat getHeaderIcon() {
return mMediaOutputController.getHeaderIcon();
}
@Override
int getHeaderIconSize() {
return mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_header_album_icon_size);
}
@Override
CharSequence getHeaderText() {
return mMediaOutputController.getHeaderTitle();
}
@Override
CharSequence getHeaderSubtitle() {
return mMediaOutputController.getHeaderSubTitle();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog
import android.content.Context
import android.media.session.MediaSessionManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.ShadeController
import javax.inject.Inject
/**
* Factory to create [MediaOutputDialog] objects.
*/
class MediaOutputDialogFactory @Inject constructor(
private val context: Context,
private val mediaSessionManager: MediaSessionManager,
private val lbm: LocalBluetoothManager?,
private val shadeController: ShadeController,
private val starter: ActivityStarter
) {
/** Creates a [MediaOutputDialog] for the given package. */
fun create(packageName: String, aboveStatusBar: Boolean) {
MediaOutputController(context, packageName, mediaSessionManager, lbm, shadeController,
starter).run {
MediaOutputDialog(context, aboveStatusBar, this) }
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.RoutingSessionInfo;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.ShadeController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class MediaOutputControllerTest extends SysuiTestCase {
private static final String TEST_PACKAGE_NAME = "com.test.package.name";
private static final String TEST_DEVICE_1_ID = "test_device_1_id";
private static final String TEST_DEVICE_2_ID = "test_device_2_id";
private static final String TEST_ARTIST = "test_artist";
private static final String TEST_SONG = "test_song";
private static final String TEST_SESSION_ID = "test_session_id";
private static final String TEST_SESSION_NAME = "test_session_name";
// Mock
private MediaController mMediaController = mock(MediaController.class);
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager =
mock(CachedBluetoothDeviceManager.class);
private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
private ShadeController mShadeController = mock(ShadeController.class);
private ActivityStarter mStarter = mock(ActivityStarter.class);
private Context mSpyContext;
private MediaOutputController mMediaOutputController;
private LocalMediaManager mLocalMediaManager;
private List<MediaController> mMediaControllers = new ArrayList<>();
private List<MediaDevice> mMediaDevices = new ArrayList<>();
private MediaDescription mMediaDescription;
private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
@Before
public void setUp() {
mSpyContext = spy(mContext);
when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
mMediaControllers.add(mMediaController);
when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
doReturn(mMediaSessionManager).when(mSpyContext).getSystemService(
MediaSessionManager.class);
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
mCachedBluetoothDeviceManager);
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
MediaDescription.Builder builder = new MediaDescription.Builder();
builder.setTitle(TEST_SONG);
builder.setSubtitle(TEST_ARTIST);
mMediaDescription = builder.build();
when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
mMediaDevices.add(mMediaDevice1);
mMediaDevices.add(mMediaDevice2);
}
@Test
public void start_verifyLocalMediaManagerInit() {
mMediaOutputController.start(mCb);
verify(mLocalMediaManager).registerCallback(mMediaOutputController);
verify(mLocalMediaManager).startScan();
}
@Test
public void stop_verifyLocalMediaManagerDeinit() {
mMediaOutputController.start(mCb);
reset(mLocalMediaManager);
mMediaOutputController.stop();
verify(mLocalMediaManager).unregisterCallback(mMediaOutputController);
verify(mLocalMediaManager).stopScan();
}
@Test
public void start_withPackageName_verifyMediaControllerInit() {
mMediaOutputController.start(mCb);
verify(mMediaController).registerCallback(any());
}
@Test
public void start_withoutPackageName_verifyMediaControllerInit() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
mLocalBluetoothManager, mShadeController, mStarter);
mMediaOutputController.start(mCb);
verify(mMediaController, never()).registerCallback(any());
}
@Test
public void stop_withPackageName_verifyMediaControllerDeinit() {
mMediaOutputController.start(mCb);
reset(mMediaController);
mMediaOutputController.stop();
verify(mMediaController).unregisterCallback(any());
}
@Test
public void stop_withoutPackageName_verifyMediaControllerDeinit() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
mLocalBluetoothManager, mShadeController, mStarter);
mMediaOutputController.start(mCb);
mMediaOutputController.stop();
verify(mMediaController, never()).unregisterCallback(any());
}
@Test
public void onDeviceListUpdate_verifyDeviceListCallback() {
mMediaOutputController.start(mCb);
reset(mCb);
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
final List<MediaDevice> devices = new ArrayList<>(mMediaOutputController.getMediaDevices());
assertThat(devices.containsAll(mMediaDevices)).isTrue();
assertThat(devices.size()).isEqualTo(mMediaDevices.size());
verify(mCb).onRouteChanged();
}
@Test
public void onSelectedDeviceStateChanged_verifyCallback() {
mMediaOutputController.start(mCb);
reset(mCb);
mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1,
LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
verify(mCb).onRouteChanged();
}
@Test
public void onDeviceAttributesChanged_verifyCallback() {
mMediaOutputController.start(mCb);
reset(mCb);
mMediaOutputController.onDeviceAttributesChanged();
verify(mCb).onRouteChanged();
}
@Test
public void onRequestFailed_verifyCallback() {
mMediaOutputController.start(mCb);
reset(mCb);
mMediaOutputController.onRequestFailed(0 /* reason */);
verify(mCb).onRouteChanged();
}
@Test
public void getHeaderTitle_withoutMetadata_returnDefaultString() {
when(mMediaController.getMetadata()).thenReturn(null);
mMediaOutputController.start(mCb);
assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(
mContext.getText(R.string.controls_media_title));
}
@Test
public void getHeaderTitle_withMetadata_returnSongName() {
when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
mMediaOutputController.start(mCb);
assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG);
}
@Test
public void getHeaderSubTitle_withoutMetadata_returnNull() {
when(mMediaController.getMetadata()).thenReturn(null);
mMediaOutputController.start(mCb);
assertThat(mMediaOutputController.getHeaderSubTitle()).isNull();
}
@Test
public void getHeaderSubTitle_withMetadata_returnArtistName() {
when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
mMediaOutputController.start(mCb);
assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST);
}
@Test
public void connectDevice_verifyConnect() {
mMediaOutputController.connectDevice(mMediaDevice1);
// Wait for background thread execution
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
verify(mLocalMediaManager).connectDevice(mMediaDevice1);
}
@Test
public void getActiveRemoteMediaDevice_isSystemSession_returnSession() {
when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
when(mRemoteSessionInfo.getVolume()).thenReturn(10);
when(mRemoteSessionInfo.isSystemSession()).thenReturn(false);
mRoutingSessionInfos.add(mRemoteSessionInfo);
when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly(
mRemoteSessionInfo);
}
@Test
public void getActiveRemoteMediaDevice_notSystemSession_returnEmpty() {
when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
when(mRemoteSessionInfo.getVolume()).thenReturn(10);
when(mRemoteSessionInfo.isSystemSession()).thenReturn(true);
mRoutingSessionInfos.add(mRemoteSessionInfo);
when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).isEmpty();
}
@Test
public void isZeroMode_onlyFromPhoneOutput_returnTrue() {
// Multiple available devices
assertThat(mMediaOutputController.isZeroMode()).isFalse();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
mMediaDevices.clear();
mMediaDevices.add(mMediaDevice1);
mMediaOutputController.start(mCb);
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
assertThat(mMediaOutputController.isZeroMode()).isTrue();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE);
assertThat(mMediaOutputController.isZeroMode()).isTrue();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE);
assertThat(mMediaOutputController.isZeroMode()).isTrue();
}
@Test
public void isZeroMode_notFromPhoneOutput_returnFalse() {
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_UNKNOWN);
mMediaDevices.clear();
mMediaDevices.add(mMediaDevice1);
mMediaOutputController.start(mCb);
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
assertThat(mMediaOutputController.isZeroMode()).isFalse();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE);
assertThat(mMediaOutputController.isZeroMode()).isFalse();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
assertThat(mMediaOutputController.isZeroMode()).isFalse();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
assertThat(mMediaOutputController.isZeroMode()).isFalse();
when(mMediaDevice1.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE);
assertThat(mMediaOutputController.isZeroMode()).isFalse();
}
}