Merge changes from topic "media_output_dialog" into rvc-qpr-dev

* changes:
  Apply animation for Media Ouput Switcher dialog
  Add dynamic item in output switcher if it is available
  Fix NPE when notification icon is empty
  Fix output switcher didn't show album art
  Add dynamic group operation in output switcher
  Enhancement for group operation in output switcher
  Add dismiss timing for Media Output Dialog
  Update OutputSwitcher from SettingsPanel to MediaOutputDialog in SystemUI
  Add constant for dismiss Settings panel
  Add animation when switching output device
  Add dismiss intent and method for MediaOutputDialog
  Add "Stop" button for remote device
  Add constant for dialog dismiss
  Rename MediaOutDialogReceiver to MediaOutputDialogReceiver
  Add getFeatures() for get device supporting features
  Add a receiver to launch Output Switcher dialog
  Add MediaOutputDialogFactory to create MediaOutputDialog
  Add Media Output Dialog for Output Switcher
  Add controller for Media operation
  Add MediaOutputAdapter for Media device list
  Define strings for output switcher dialog in SystemUI
  Add image utility methods
This commit is contained in:
tim peng
2020-12-08 05:46:13 +00:00
committed by Android (Google) Code Review
36 changed files with 3713 additions and 24 deletions

View File

@@ -13,6 +13,7 @@ import android.content.res.ColorStateList;
import android.content.res.Resources; import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
import android.graphics.ColorMatrix; import android.graphics.ColorMatrix;
@@ -32,6 +33,10 @@ import android.telephony.AccessNetworkConstants;
import android.telephony.NetworkRegistrationInfo; import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState; import android.telephony.ServiceState;
import androidx.annotation.NonNull;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.UserIcons; import com.android.internal.util.UserIcons;
import com.android.launcher3.icons.IconFactory; import com.android.launcher3.icons.IconFactory;
@@ -537,4 +542,25 @@ public class Utils {
== NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING); == NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING);
return !isInIwlan; return !isInIwlan;
} }
/**
* Returns a bitmap with rounded corner.
*
* @param context application context.
* @param source bitmap to apply round corner.
* @param cornerRadius corner radius value.
*/
public static Bitmap convertCornerRadiusBitmap(@NonNull Context context,
@NonNull Bitmap source, @NonNull float cornerRadius) {
final Bitmap roundedBitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(),
Bitmap.Config.ARGB_8888);
final RoundedBitmapDrawable drawable =
RoundedBitmapDrawableFactory.create(context.getResources(), source);
drawable.setAntiAlias(true);
drawable.setCornerRadius(cornerRadius);
final Canvas canvas = new Canvas(roundedBitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return roundedBitmap;
}
} }

View File

@@ -18,6 +18,7 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.core.graphics.drawable.IconCompat;
import com.android.settingslib.R; import com.android.settingslib.R;
import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveIcon;
@@ -215,6 +216,23 @@ public class BluetoothUtils {
return new Pair<>(pair.first, pair.second); return new Pair<>(pair.first, pair.second);
} }
/**
* Create an Icon pointing to a drawable.
*/
public static IconCompat createIconWithDrawable(Drawable drawable) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
final int width = drawable.getIntrinsicWidth();
final int height = drawable.getIntrinsicHeight();
bitmap = createBitmap(drawable,
width > 0 ? width : 1,
height > 0 ? height : 1);
}
return IconCompat.createWithBitmap(bitmap);
}
/** /**
* Build device icon with advanced outline * Build device icon with advanced outline
*/ */

View File

@@ -46,6 +46,7 @@ import com.android.settingslib.R;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.List;
/** /**
* MediaDevice represents a media device(such like Bluetooth device, cast device and phone device). * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device).
@@ -353,6 +354,13 @@ public abstract class MediaDevice implements Comparable<MediaDevice> {
} }
} }
/**
* Gets the supported features of the route.
*/
public List<String> getFeatures() {
return mRouteInfo.getFeatures();
}
/** /**
* Check if it is CarKit device * Check if it is CarKit device
* @return true if it is CarKit device * @return true if it is CarKit device

View File

@@ -60,8 +60,42 @@ public class MediaOutputSliceConstants {
"com.android.settings.panel.action.MEDIA_OUTPUT_GROUP"; "com.android.settings.panel.action.MEDIA_OUTPUT_GROUP";
/** /**
* An string extra specifying a media package name. * A string extra specifying a media package name.
*/ */
public static final String EXTRA_PACKAGE_NAME = public static final String EXTRA_PACKAGE_NAME =
"com.android.settings.panel.extra.PACKAGE_NAME"; "com.android.settings.panel.extra.PACKAGE_NAME";
/**
* An intent action to launch media output dialog.
*/
public static final String ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG =
"com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG";
/**
* An intent action to dismiss media output dialog.
*/
public static final String ACTION_DISMISS_MEDIA_OUTPUT_DIALOG =
"com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG";
/**
* Settings package name.
*/
public static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
/**
* An intent action to launch Bluetooth paring page.
*/
public static final String ACTION_LAUNCH_BLUETOOTH_PAIRING =
"com.android.settings.action.LAUNCH_BLUETOOTH_PAIRING";
/**
* SystemUi package name.
*/
public static final String SYSTEMUI_PACKAGE_NAME = "com.android.systemui";
/**
* An intent action to close settings panel.
*/
public static final String ACTION_CLOSE_PANEL =
"com.android.settings.panel.action.CLOSE_PANEL";
} }

View File

@@ -781,5 +781,13 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".media.dialog.MediaOutputDialogReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
<action android:name="com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,26 @@
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/checked"
android:state_checked="true"
android:drawable="@drawable/ic_check_box_blue_24dp" />
<item
android:id="@+id/unchecked"
android:state_checked="false"
android:drawable="@drawable/ic_check_box_outline_24dp" />
</selector>

View File

@@ -0,0 +1,26 @@
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"
android:fillColor="#4285F4"/>
</vector>

View File

@@ -0,0 +1,26 @@
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"
android:fillColor="#757575"/>
</vector>

View File

@@ -0,0 +1,31 @@
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
android:fillColor="#000000"/>
<path
android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
android:fillColor="#000000"/>
<path
android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
android:fillColor="#000000"/>
</vector>

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,114 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_output_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="94dp"
android:gravity="start|center_vertical"
android:paddingStart="16dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/header_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="@dimen/media_output_dialog_header_icon_padding"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:textSize="20sp"/>
<TextView
android:id="@+id/header_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:fontFamily="roboto-regular"
android:textSize="14sp"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>
<LinearLayout
android:id="@+id/device_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_result"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/stop"
style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:text="@string/keyboard_key_media_stop"
android:visibility="gone"/>
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"/>
<Button
android:id="@+id/done"
style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_marginEnd="0dp"
android:text="@string/inline_done_button"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/device_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="64dp">
<FrameLayout
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp">
<ImageView
android:id="@+id/title_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="68dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"/>
<RelativeLayout
android:id="@+id/two_line_layout"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="52dp"
android:layout_marginEnd="69dp"
android:layout_marginTop="10dp">
<TextView
android:id="@+id/two_line_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="15dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="15dp"
android:layout_marginBottom="7dp"
android:layout_alignParentBottom="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
android:fontFamily="roboto-regular"
android:visibility="gone"/>
<SeekBar
android:id="@+id/volume_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
<ProgressBar
android:id="@+id/volume_indeterminate_progress"
style="@*android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="258dp"
android:layout_height="18dp"
android:layout_marginStart="68dp"
android:layout_marginTop="40dp"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"/>
<View
android:id="@+id/end_divider"
android:layout_width="1dp"
android:layout_height="36dp"
android:layout_marginEnd="68dp"
android:layout_gravity="right|center_vertical"
android:background="?android:attr/listDivider"
android:visibility="gone"/>
<ImageView
android:id="@+id/add_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="right|center_vertical"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_add"
android:tint="?android:attr/colorAccent"
android:visibility="gone"/>
<CheckBox
android:id="@+id/check_box"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="right|center_vertical"
android:layout_marginEnd="24dp"
android:button="@drawable/ic_check_box"
android:visibility="gone"/>
</FrameLayout>
<View
android:id="@+id/bottom_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:layout_gravity="bottom"
android:background="?android:attr/listDivider"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -1402,4 +1402,13 @@
<dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen> <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_top">@*android:dimen/rounded_corner_radius_top</dimen>
<dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen> <dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
<!-- Output switcher panel related dimensions -->
<dimen name="media_output_dialog_list_margin">12dp</dimen>
<dimen name="media_output_dialog_list_max_height">364dp</dimen>
<dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
<dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
<dimen name="media_output_dialog_header_icon_padding">16dp</dimen>
<dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
<dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
</resources> </resources>

View File

@@ -2855,4 +2855,19 @@
<string name="controls_menu_add">Add controls</string> <string name="controls_menu_add">Add controls</string>
<!-- Controls menu, edit [CHAR_LIMIT=30] --> <!-- Controls menu, edit [CHAR_LIMIT=30] -->
<string name="controls_menu_edit">Edit controls</string> <string name="controls_menu_edit">Edit controls</string>
<!-- Title for the media output group dialog with media related devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_add_output">Add outputs</string>
<!-- Title for the media output slice with group devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_group">Group</string>
<!-- Summary for media output group with only one device which is active [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_single_device">1 device selected</string>
<!-- Summary for media output group with the active device count [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_multiple_devices"><xliff:g id="count" example="2">%1$d</xliff:g> devices selected</string>
<!-- Summary for disconnected status [CHAR LIMIT=50] -->
<string name="media_output_dialog_disconnected"><xliff:g id="device_name" example="My device">%1$s</xliff:g> (disconnected)</string>
<!-- Summary for connecting error message [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_connect_failed">Couldn\'t connect. Try again.</string>
<!-- Title for pairing item [CHAR LIMIT=60] -->
<string name="media_output_dialog_pairing_new">Pair new device</string>
</resources> </resources>

View File

@@ -320,6 +320,9 @@
<style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast"> <style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
</style> </style>
<style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod">
</style>
<!-- Standard animations for hiding and showing the status bar. --> <!-- Standard animations for hiding and showing the status bar. -->
<style name="Animation.StatusBar"> <style name="Animation.StatusBar">
</style> </style>
@@ -400,6 +403,10 @@
<item name="android:windowIsFloating">true</item> <item name="android:windowIsFloating">true</item>
</style> </style>
<style name="Theme.SystemUI.Dialog.MediaOutput">
<item name="android:windowBackground">@drawable/media_output_dialog_background</item>
</style>
<style name="QSBorderlessButton"> <style name="QSBorderlessButton">
<item name="android:padding">12dp</item> <item name="android:padding">12dp</item>
<item name="android:background">@drawable/qs_btn_borderless_rect</item> <item name="android:background">@drawable/qs_btn_borderless_rect</item>
@@ -789,5 +796,4 @@
* Title: headline, medium 20sp * Title: headline, medium 20sp
* Message: body, 16 sp --> * Message: body, 16 sp -->
<style name="Theme.ControlsRequestDialog" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert"/> <style name="Theme.ControlsRequestDialog" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert"/>
</resources> </resources>

View File

@@ -46,6 +46,7 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.fragments.FragmentService; import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher;
@@ -324,6 +325,7 @@ public class Dependency {
@Inject Lazy<RecordingController> mRecordingController; @Inject Lazy<RecordingController> mRecordingController;
@Inject Lazy<ProtoTracer> mProtoTracer; @Inject Lazy<ProtoTracer> mProtoTracer;
@Inject Lazy<Divider> mDivider; @Inject Lazy<Divider> mDivider;
@Inject Lazy<MediaOutputDialogFactory> mMediaOutputDialogFactory;
@Inject @Inject
public Dependency() { public Dependency() {
@@ -522,6 +524,8 @@ public class Dependency {
mProviders.put(RecordingController.class, mRecordingController::get); mProviders.put(RecordingController.class, mRecordingController::get);
mProviders.put(Divider.class, mDivider::get); mProviders.put(Divider.class, mDivider::get);
mProviders.put(MediaOutputDialogFactory.class, mMediaOutputDialogFactory::get);
sDependency = this; sDependency = this;
} }

View File

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

View File

@@ -42,10 +42,10 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.animation.TransitionLayout;
@@ -93,7 +93,7 @@ public class MediaControlPanel {
private int mAlbumArtRadius; private int mAlbumArtRadius;
// This will provide the corners for the album art. // This will provide the corners for the album art.
private final ViewOutlineProvider mViewOutlineProvider; private final ViewOutlineProvider mViewOutlineProvider;
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
/** /**
* Initialize a new control panel * Initialize a new control panel
* @param context * @param context
@@ -104,7 +104,8 @@ public class MediaControlPanel {
public MediaControlPanel(Context context, @Background Executor backgroundExecutor, public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
ActivityStarter activityStarter, MediaViewController mediaViewController, ActivityStarter activityStarter, MediaViewController mediaViewController,
SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
KeyguardDismissUtil keyguardDismissUtil) { KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
mediaOutputDialogFactory) {
mContext = context; mContext = context;
mBackgroundExecutor = backgroundExecutor; mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
@@ -112,6 +113,7 @@ public class MediaControlPanel {
mMediaViewController = mediaViewController; mMediaViewController = mediaViewController;
mMediaDataManagerLazy = lazyMediaDataManager; mMediaDataManagerLazy = lazyMediaDataManager;
mKeyguardDismissUtil = keyguardDismissUtil; mKeyguardDismissUtil = keyguardDismissUtil;
mMediaOutputDialogFactory = mediaOutputDialogFactory;
loadDimens(); loadDimens();
mViewOutlineProvider = new ViewOutlineProvider() { mViewOutlineProvider = new ViewOutlineProvider() {
@@ -274,13 +276,7 @@ public class MediaControlPanel {
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
mViewHolder.getSeamless().setOnClickListener(v -> { mViewHolder.getSeamless().setOnClickListener(v -> {
final Intent intent = new Intent() mMediaOutputDialogFactory.create(data.getPackageName(), true);
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
data.getPackageName())
.putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
}); });
ImageView iconView = mViewHolder.getSeamlessIcon(); ImageView iconView = mViewHolder.getSeamlessIcon();

View File

@@ -0,0 +1,227 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.android.settingslib.Utils;
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import java.util.List;
/**
* Adapter for media output dialog.
*/
public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private static final String TAG = "MediaOutputAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private ViewGroup mConnectedItem;
private boolean mInclueDynamicGroup;
public MediaOutputAdapter(MediaOutputController controller) {
super(controller);
}
@Override
public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
super.onCreateViewHolder(viewGroup, viewType);
return new MediaDeviceViewHolder(mHolderView);
}
@Override
public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) {
final int size = mController.getMediaDevices().size();
if (position == size && mController.isZeroMode()) {
viewHolder.onBind(CUSTOMIZED_ITEM_PAIR_NEW, false /* topMargin */,
true /* bottomMargin */);
} else if (mInclueDynamicGroup) {
if (position == 0) {
viewHolder.onBind(CUSTOMIZED_ITEM_DYNAMIC_GROUP, true /* topMargin */,
false /* bottomMargin */);
} else {
// When group item is added at the first(position == 0), devices will be added from
// the second item(position == 1). It means that the index of device list starts
// from "position - 1".
viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices()))
.get(position - 1),
false /* topMargin */, position == size /* bottomMargin */);
}
} else if (position < size) {
viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position),
position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */);
} else if (DEBUG) {
Log.d(TAG, "Incorrect position: " + position);
}
}
@Override
public int getItemCount() {
mInclueDynamicGroup = mController.getSelectedMediaDevice().size() > 1;
if (mController.isZeroMode() || mInclueDynamicGroup) {
// Add extra one for "pair new" or dynamic group
return mController.getMediaDevices().size() + 1;
}
return mController.getMediaDevices().size();
}
@Override
CharSequence getItemTitle(MediaDevice device) {
if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
&& !device.isConnected()) {
final CharSequence deviceName = device.getName();
// Append status to title only for the disconnected Bluetooth device.
final SpannableString spannableTitle = new SpannableString(
mContext.getString(R.string.media_output_dialog_disconnected, deviceName));
spannableTitle.setSpan(new ForegroundColorSpan(
Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)),
deviceName.length(),
spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableTitle;
}
return super.getItemTitle(device);
}
class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
MediaDeviceViewHolder(View view) {
super(view);
}
@Override
void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) {
super.onBind(device, topMargin, bottomMargin);
final boolean currentlyConnected = !mInclueDynamicGroup && isCurrentlyConnected(device);
if (currentlyConnected) {
mConnectedItem = mContainerLayout;
}
mBottomDivider.setVisibility(View.GONE);
mCheckBox.setVisibility(View.GONE);
if (currentlyConnected && mController.isActiveRemoteDevice(device)) {
// Init active device layout
mDivider.setVisibility(View.VISIBLE);
mDivider.setTransitionAlpha(1);
mAddIcon.setVisibility(View.VISIBLE);
mAddIcon.setTransitionAlpha(1);
mAddIcon.setOnClickListener(v -> onEndItemClick());
} else {
// Init non-active device layout
mDivider.setVisibility(View.GONE);
mAddIcon.setVisibility(View.GONE);
}
if (mController.isTransferring()) {
if (device.getState() == MediaDeviceState.STATE_CONNECTING
&& !mController.hasAdjustVolumeUserRestriction()) {
setTwoLineLayout(device, true /* bFocused */, false /* showSeekBar*/,
true /* showProgressBar */, false /* showSubtitle */);
} else {
setSingleLineLayout(getItemTitle(device), false /* bFocused */);
}
} else {
// Set different layout for each device
if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setTwoLineLayout(device, false /* bFocused */,
false /* showSeekBar */, false /* showProgressBar */,
true /* showSubtitle */);
mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
} else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) {
setTwoLineLayout(device, true /* bFocused */, true /* showSeekBar */,
false /* showProgressBar */, false /* showSubtitle */);
initSeekbar(device);
} else {
setSingleLineLayout(getItemTitle(device), false /* bFocused */);
mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
}
}
}
@Override
void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
super.onBind(customizedItem, topMargin, bottomMargin);
if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
mCheckBox.setVisibility(View.GONE);
mDivider.setVisibility(View.GONE);
mAddIcon.setVisibility(View.GONE);
mBottomDivider.setVisibility(View.GONE);
setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
false /* bFocused */);
final Drawable d = mContext.getDrawable(R.drawable.ic_add);
d.setColorFilter(new PorterDuffColorFilter(
Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
mTitleIcon.setImageDrawable(d);
mContainerLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW));
} else if (customizedItem == CUSTOMIZED_ITEM_DYNAMIC_GROUP) {
mConnectedItem = mContainerLayout;
mBottomDivider.setVisibility(View.GONE);
mCheckBox.setVisibility(View.GONE);
mDivider.setVisibility(View.VISIBLE);
mDivider.setTransitionAlpha(1);
mAddIcon.setVisibility(View.VISIBLE);
mAddIcon.setTransitionAlpha(1);
mAddIcon.setOnClickListener(v -> onEndItemClick());
mTitleIcon.setImageDrawable(getSpeakerDrawable());
final CharSequence sessionName = mController.getSessionName();
final CharSequence title = TextUtils.isEmpty(sessionName)
? mContext.getString(R.string.media_output_dialog_group) : sessionName;
setTwoLineLayout(title, true /* bFocused */, true /* showSeekBar */,
false /* showProgressBar */, false /* showSubtitle */);
initSessionSeekbar();
}
}
private void onItemClick(View view, MediaDevice device) {
if (mController.isTransferring()) {
return;
}
playSwitchingAnim(mConnectedItem, view);
mController.connectDevice(device);
device.setState(MediaDeviceState.STATE_CONNECTING);
if (!isAnimating()) {
notifyDataSetChanged();
}
}
private void onItemClick(int customizedItem) {
if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
mController.launchBluetoothPairing();
}
}
private void onEndItemClick() {
mController.launchMediaOutputGroupDialog();
}
}
}

View File

@@ -0,0 +1,322 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
/**
* Base adapter for media output dialog.
*/
public abstract class MediaOutputBaseAdapter extends
RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> {
static final int CUSTOMIZED_ITEM_PAIR_NEW = 1;
static final int CUSTOMIZED_ITEM_GROUP = 2;
static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3;
final MediaOutputController mController;
private int mMargin;
private boolean mIsAnimating;
Context mContext;
View mHolderView;
boolean mIsDragging;
public MediaOutputBaseAdapter(MediaOutputController controller) {
mController = controller;
mIsDragging = false;
}
@Override
public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
mContext = viewGroup.getContext();
mMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_list_margin);
mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
viewGroup, false);
return null;
}
CharSequence getItemTitle(MediaDevice device) {
return device.getName();
}
boolean isCurrentlyConnected(MediaDevice device) {
return TextUtils.equals(device.getId(),
mController.getCurrentConnectedMediaDevice().getId());
}
boolean isDragging() {
return mIsDragging;
}
boolean isAnimating() {
return mIsAnimating;
}
/**
* ViewHolder for binding device view.
*/
abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
private static final int ANIM_DURATION = 200;
final LinearLayout mContainerLayout;
final TextView mTitleText;
final TextView mTwoLineTitleText;
final TextView mSubTitleText;
final ImageView mTitleIcon;
final ImageView mAddIcon;
final ProgressBar mProgressBar;
final SeekBar mSeekBar;
final RelativeLayout mTwoLineLayout;
final View mDivider;
final View mBottomDivider;
final CheckBox mCheckBox;
MediaDeviceBaseViewHolder(View view) {
super(view);
mContainerLayout = view.requireViewById(R.id.device_container);
mTitleText = view.requireViewById(R.id.title);
mSubTitleText = view.requireViewById(R.id.subtitle);
mTwoLineLayout = view.requireViewById(R.id.two_line_layout);
mTwoLineTitleText = view.requireViewById(R.id.two_line_title);
mTitleIcon = view.requireViewById(R.id.title_icon);
mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
mSeekBar = view.requireViewById(R.id.volume_seekbar);
mDivider = view.requireViewById(R.id.end_divider);
mBottomDivider = view.requireViewById(R.id.bottom_divider);
mAddIcon = view.requireViewById(R.id.add_icon);
mCheckBox = view.requireViewById(R.id.check_box);
}
void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) {
mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext));
setMargin(topMargin, bottomMargin);
}
void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
setMargin(topMargin, bottomMargin);
}
private void setMargin(boolean topMargin, boolean bottomMargin) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mContainerLayout
.getLayoutParams();
params.topMargin = topMargin ? mMargin : 0;
params.bottomMargin = bottomMargin ? mMargin : 0;
mContainerLayout.setLayoutParams(params);
}
void setSingleLineLayout(CharSequence title, boolean bFocused) {
mTwoLineLayout.setVisibility(View.GONE);
mProgressBar.setVisibility(View.GONE);
mTitleText.setVisibility(View.VISIBLE);
mTitleText.setTranslationY(0);
mTitleText.setText(title);
if (bFocused) {
mTitleText.setTypeface(Typeface.create(mContext.getString(
com.android.internal.R.string.config_headlineFontFamilyMedium),
Typeface.NORMAL));
} else {
mTitleText.setTypeface(Typeface.create(mContext.getString(
com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL));
}
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
boolean showProgressBar, boolean showSubtitle) {
setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle);
}
void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar,
boolean showProgressBar, boolean showSubtitle) {
setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle);
}
private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
boolean showSeekBar, boolean showProgressBar, boolean showSubtitle) {
mTitleText.setVisibility(View.GONE);
mTwoLineLayout.setVisibility(View.VISIBLE);
mSeekBar.setAlpha(1);
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
mTwoLineTitleText.setTranslationY(0);
if (device == null) {
mTwoLineTitleText.setText(title);
} else {
mTwoLineTitleText.setText(getItemTitle(device));
}
if (bFocused) {
mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
com.android.internal.R.string.config_headlineFontFamilyMedium),
Typeface.NORMAL));
} else {
mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL));
}
}
void initSeekbar(MediaDevice device) {
mSeekBar.setMax(device.getMaxVolume());
mSeekBar.setMin(0);
final int currentVolume = device.getCurrentVolume();
if (mSeekBar.getProgress() != currentVolume) {
mSeekBar.setProgress(currentVolume);
}
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (device == null || !fromUser) {
return;
}
mController.adjustVolume(device, progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mIsDragging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mIsDragging = false;
}
});
}
void initSessionSeekbar() {
mSeekBar.setMax(mController.getSessionVolumeMax());
mSeekBar.setMin(0);
final int currentVolume = mController.getSessionVolume();
if (mSeekBar.getProgress() != currentVolume) {
mSeekBar.setProgress(currentVolume);
}
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser) {
return;
}
mController.adjustSessionVolume(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mIsDragging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mIsDragging = false;
}
});
}
void playSwitchingAnim(@NonNull View from, @NonNull View to) {
final float delta = (float) (mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_title_anim_y_delta));
final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar);
final TextView toTitleText = to.requireViewById(R.id.title);
if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility()
!= View.VISIBLE) {
return;
}
mIsAnimating = true;
// Animation for title text
toTitleText.setTypeface(Typeface.create(mContext.getString(
com.android.internal.R.string.config_headlineFontFamilyMedium),
Typeface.NORMAL));
toTitleText.animate()
.setDuration(ANIM_DURATION)
.translationY(-delta)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
to.requireViewById(R.id.volume_indeterminate_progress).setVisibility(
View.VISIBLE);
}
});
// Animation for seek bar
fromSeekBar.animate()
.alpha(0)
.setDuration(ANIM_DURATION)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
final TextView fromTitleText = from.requireViewById(
R.id.two_line_title);
fromTitleText.setTypeface(Typeface.create(mContext.getString(
com.android.internal.R.string.config_headlineFontFamily),
Typeface.NORMAL));
fromTitleText.animate()
.setDuration(ANIM_DURATION)
.translationY(delta)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mIsAnimating = false;
notifyDataSetChanged();
}
});
}
});
}
Drawable getSpeakerDrawable() {
final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp)
.mutate();
final ColorStateList list = mContext.getResources().getColorStateList(
R.color.advanced_icon_color, mContext.getTheme());
drawable.setColorFilter(new PorterDuffColorFilter(list.getDefaultColor(),
PorterDuff.Mode.SRC_IN));
return BluetoothUtils.buildAdvancedDrawable(mContext, drawable);
}
}
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settingslib.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
/**
* Base dialog for media output UI
*/
public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
MediaOutputController.Callback, Window.Callback {
private static final String TAG = "MediaOutputDialog";
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final RecyclerView.LayoutManager mLayoutManager;
final Context mContext;
final MediaOutputController mMediaOutputController;
@VisibleForTesting
View mDialogView;
private TextView mHeaderTitle;
private TextView mHeaderSubtitle;
private ImageView mHeaderIcon;
private RecyclerView mDevicesRecyclerView;
private LinearLayout mDeviceListLayout;
private Button mDoneButton;
private Button mStopButton;
private int mListMaxHeight;
MediaOutputBaseAdapter mAdapter;
private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
// Set max height for list
if (mDeviceListLayout.getHeight() > mListMaxHeight) {
ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
params.height = mListMaxHeight;
mDeviceListLayout.setLayoutParams(params);
}
};
public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
mContext = context;
mMediaOutputController = mediaOutputController;
mLayoutManager = new LinearLayoutManager(mContext);
mListMaxHeight = context.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_list_max_height);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
final Window window = getWindow();
final WindowManager.LayoutParams lp = window.getAttributes();
lp.gravity = Gravity.BOTTOM;
// Config insets to make sure the layout is above the navigation bar
lp.setFitInsetsTypes(statusBars() | navigationBars());
lp.setFitInsetsSides(WindowInsets.Side.all());
lp.setFitInsetsIgnoringVisibility(true);
window.setAttributes(lp);
window.setContentView(mDialogView);
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
window.setWindowAnimations(R.style.Animation_MediaOutputDialog);
mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
mDoneButton = mDialogView.requireViewById(R.id.done);
mStopButton = mDialogView.requireViewById(R.id.stop);
mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
mDeviceListLayoutListener);
// Init device list
mDevicesRecyclerView.setLayoutManager(mLayoutManager);
mDevicesRecyclerView.setAdapter(mAdapter);
// Init header icon
mHeaderIcon.setOnClickListener(v -> onHeaderIconClick());
// Init bottom buttons
mDoneButton.setOnClickListener(v -> dismiss());
mStopButton.setOnClickListener(v -> {
mMediaOutputController.releaseSession();
dismiss();
});
}
@Override
public void onStart() {
super.onStart();
mMediaOutputController.start(this);
}
@Override
public void onStop() {
super.onStop();
mMediaOutputController.stop();
}
@VisibleForTesting
void refresh() {
// Update header icon
final int iconRes = getHeaderIconRes();
final IconCompat iconCompat = getHeaderIcon();
if (iconRes != 0) {
mHeaderIcon.setVisibility(View.VISIBLE);
mHeaderIcon.setImageResource(iconRes);
} else if (iconCompat != null) {
mHeaderIcon.setVisibility(View.VISIBLE);
mHeaderIcon.setImageIcon(iconCompat.toIcon(mContext));
} else {
mHeaderIcon.setVisibility(View.GONE);
}
if (mHeaderIcon.getVisibility() == View.VISIBLE) {
final int size = getHeaderIconSize();
final int padding = mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_header_icon_padding);
mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
}
// Update title and subtitle
mHeaderTitle.setText(getHeaderText());
final CharSequence subTitle = getHeaderSubtitle();
if (TextUtils.isEmpty(subTitle)) {
mHeaderSubtitle.setVisibility(View.GONE);
mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
} else {
mHeaderSubtitle.setVisibility(View.VISIBLE);
mHeaderSubtitle.setText(subTitle);
mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
}
if (!mAdapter.isDragging() && !mAdapter.isAnimating()) {
mAdapter.notifyDataSetChanged();
}
// Show when remote media session is available
mStopButton.setVisibility(getStopButtonVisibility());
}
abstract int getHeaderIconRes();
abstract IconCompat getHeaderIcon();
abstract int getHeaderIconSize();
abstract CharSequence getHeaderText();
abstract CharSequence getHeaderSubtitle();
abstract int getStopButtonVisibility();
@Override
public void onMediaChanged() {
mMainThreadHandler.post(() -> refresh());
}
@Override
public void onMediaStoppedOrPaused() {
if (isShowing()) {
dismiss();
}
}
@Override
public void onRouteChanged() {
mMainThreadHandler.post(() -> refresh());
}
@Override
public void dismissDialog() {
dismiss();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!hasFocus && isShowing()) {
dismiss();
}
}
void onHeaderIconClick() {
}
}

View File

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

View File

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

View File

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

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 MediaOutputDialogReceiver @Inject constructor(
private val mediaOutputDialogFactory: MediaOutputDialogFactory
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (TextUtils.equals(MediaOutputSliceConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG,
intent.action)) {
mediaOutputDialogFactory.create(
intent.getStringExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME), false)
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import java.util.List;
/**
* Adapter for media output dynamic group dialog.
*/
public class MediaOutputGroupAdapter extends MediaOutputBaseAdapter {
private static final String TAG = "MediaOutputGroupAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final List<MediaDevice> mGroupMediaDevices;
public MediaOutputGroupAdapter(MediaOutputController controller) {
super(controller);
mGroupMediaDevices = controller.getGroupMediaDevices();
}
@Override
public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
super.onCreateViewHolder(viewGroup, viewType);
return new GroupViewHolder(mHolderView);
}
@Override
public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) {
// Add "Group"
if (position == 0) {
viewHolder.onBind(CUSTOMIZED_ITEM_GROUP, true /* topMargin */,
false /* bottomMargin */);
return;
}
// Add available devices
final int newPosition = position - 1;
final int size = mGroupMediaDevices.size();
if (newPosition < size) {
viewHolder.onBind(mGroupMediaDevices.get(newPosition), false /* topMargin */,
newPosition == (size - 1) /* bottomMargin */);
return;
}
if (DEBUG) {
Log.d(TAG, "Incorrect position: " + position);
}
}
@Override
public int getItemCount() {
// Require extra item for group volume operation
return mGroupMediaDevices.size() + 1;
}
@Override
CharSequence getItemTitle(MediaDevice device) {
return super.getItemTitle(device);
}
class GroupViewHolder extends MediaDeviceBaseViewHolder {
GroupViewHolder(View view) {
super(view);
}
@Override
void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) {
super.onBind(device, topMargin, bottomMargin);
mDivider.setVisibility(View.GONE);
mAddIcon.setVisibility(View.GONE);
mBottomDivider.setVisibility(View.GONE);
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
onCheckBoxClicked(isChecked, device);
});
setTwoLineLayout(device, false /* bFocused */, true /* showSeekBar */,
false /* showProgressBar */, false /* showSubtitle*/);
initSeekbar(device);
final List<MediaDevice> selectedDevices = mController.getSelectedMediaDevice();
if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
mCheckBox.setButtonDrawable(R.drawable.ic_check_box);
mCheckBox.setChecked(false);
mCheckBox.setEnabled(true);
} else if (isDeviceIncluded(selectedDevices, device)) {
if (selectedDevices.size() == 1 || !isDeviceIncluded(
mController.getDeselectableMediaDevice(), device)) {
mCheckBox.setButtonDrawable(getDisabledCheckboxDrawable());
mCheckBox.setChecked(true);
mCheckBox.setEnabled(false);
} else {
mCheckBox.setButtonDrawable(R.drawable.ic_check_box);
mCheckBox.setChecked(true);
mCheckBox.setEnabled(true);
}
}
}
@Override
void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
super.onBind(customizedItem, topMargin, bottomMargin);
if (customizedItem == CUSTOMIZED_ITEM_GROUP) {
setTwoLineLayout(mContext.getText(R.string.media_output_dialog_group),
true /* bFocused */, true /* showSeekBar */, false /* showProgressBar */,
false /* showSubtitle*/);
mTitleIcon.setImageDrawable(getSpeakerDrawable());
mBottomDivider.setVisibility(View.VISIBLE);
mCheckBox.setVisibility(View.GONE);
mDivider.setVisibility(View.GONE);
mAddIcon.setVisibility(View.GONE);
initSessionSeekbar();
}
}
private void onCheckBoxClicked(boolean isChecked, MediaDevice device) {
if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
mController.addDeviceToPlayMedia(device);
} else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
device)) {
mController.removeDeviceFromPlayMedia(device);
}
}
private Drawable getDisabledCheckboxDrawable() {
final Drawable drawable = mContext.getDrawable(R.drawable.ic_check_box_blue_24dp)
.mutate();
final Bitmap checkbox = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(checkbox);
TypedValue value = new TypedValue();
mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
drawable.setAlpha((int) (value.getFloat() * 255));
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return drawable;
}
private boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) {
for (MediaDevice device : deviceList) {
if (TextUtils.equals(device.getId(), targetDevice.getId())) {
return true;
}
}
return false;
}
}
}

View File

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

View File

@@ -17,7 +17,6 @@
package com.android.systemui.statusbar; package com.android.systemui.statusbar;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable;
@@ -36,10 +35,9 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InfoMediaManager; import com.android.settingslib.media.InfoMediaManager;
import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -51,7 +49,7 @@ import java.util.List;
*/ */
public class MediaTransferManager { public class MediaTransferManager {
private final Context mContext; private final Context mContext;
private final ActivityStarter mActivityStarter; private final MediaOutputDialogFactory mMediaOutputDialogFactory;
private MediaDevice mDevice; private MediaDevice mDevice;
private List<View> mViews = new ArrayList<>(); private List<View> mViews = new ArrayList<>();
private LocalMediaManager mLocalMediaManager; private LocalMediaManager mLocalMediaManager;
@@ -74,12 +72,7 @@ public class MediaTransferManager {
ViewParent parent = view.getParent(); ViewParent parent = view.getParent();
StatusBarNotification statusBarNotification = StatusBarNotification statusBarNotification =
getRowForParent(parent).getEntry().getSbn(); getRowForParent(parent).getEntry().getSbn();
final Intent intent = new Intent() mMediaOutputDialogFactory.create(statusBarNotification.getPackageName(), true);
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
statusBarNotification.getPackageName());
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
return true; return true;
} }
}; };
@@ -107,7 +100,7 @@ public class MediaTransferManager {
public MediaTransferManager(Context context) { public MediaTransferManager(Context context) {
mContext = context; mContext = context;
mActivityStarter = Dependency.get(ActivityStarter.class); mMediaOutputDialogFactory = Dependency.get(MediaOutputDialogFactory.class);
LocalBluetoothManager lbm = Dependency.get(LocalBluetoothManager.class); LocalBluetoothManager lbm = Dependency.get(LocalBluetoothManager.class);
InfoMediaManager imm = new InfoMediaManager(mContext, null, null, lbm); InfoMediaManager imm = new InfoMediaManager(mContext, null, null, lbm);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, null); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, null);

View File

@@ -88,6 +88,7 @@ import com.android.settingslib.Utils;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.Prefs; import com.android.systemui.Prefs;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialog;
import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController;
@@ -470,6 +471,7 @@ public class VolumeDialogImpl implements VolumeDialog,
Events.writeEvent(Events.EVENT_SETTINGS_CLICK); Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
Intent intent = new Intent(Settings.Panel.ACTION_VOLUME); Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
dismissH(DISMISS_REASON_SETTINGS_CLICKED); dismissH(DISMISS_REASON_SETTINGS_CLICKED);
Dependency.get(MediaOutputDialogFactory.class).dismiss();
Dependency.get(ActivityStarter.class).startActivity(intent, Dependency.get(ActivityStarter.class).startActivity(intent,
true /* dismissShade */); true /* dismissShade */);
}); });

View File

@@ -38,6 +38,7 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayout
@@ -92,6 +93,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var expandedSet: ConstraintSet @Mock private lateinit var expandedSet: ConstraintSet
@Mock private lateinit var collapsedSet: ConstraintSet @Mock private lateinit var collapsedSet: ConstraintSet
@Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
private lateinit var appIcon: ImageView private lateinit var appIcon: ImageView
private lateinit var appName: TextView private lateinit var appName: TextView
private lateinit var albumView: ImageView private lateinit var albumView: ImageView
@@ -128,7 +130,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController, player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil) seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
mediaOutputDialogFactory)
whenever(seekBarViewModel.progress).thenReturn(seekBarData) whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Mock out a view holder for the player to attach to. // Mock out a view holder for the player to attach to.

View File

@@ -0,0 +1,290 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.drawable.Icon;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.LinearLayout;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.filters.SmallTest;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class MediaOutputAdapterTest extends SysuiTestCase {
private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
private static final String TEST_SESSION_NAME = "test_session_name";
// Mock
private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
private Icon mIcon = mock(Icon.class);
private IconCompat mIconCompat = mock(IconCompat.class);
private MediaOutputAdapter mMediaOutputAdapter;
private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
private List<MediaDevice> mMediaDevices = new ArrayList<>();
@Before
public void setUp() {
mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
when(mMediaOutputController.isZeroMode()).thenReturn(false);
when(mMediaOutputController.isTransferring()).thenReturn(false);
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
when(mMediaDevice2.getName()).thenReturn(TEST_DEVICE_NAME_2);
when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_ID_2);
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
mMediaDevices.add(mMediaDevice1);
mMediaDevices.add(mMediaDevice2);
}
@Test
public void getItemCount_nonZeroMode_isDeviceSize() {
assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size());
}
@Test
public void getItemCount_zeroMode_containExtraOneForPairNew() {
when(mMediaOutputController.isZeroMode()).thenReturn(true);
assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1);
}
@Test
public void getItemCount_withDynamicGroup_containExtraOneForGroup() {
when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
when(mMediaOutputController.isZeroMode()).thenReturn(false);
assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1);
}
@Test
public void onBindViewHolder_zeroMode_bindPairNew_verifyView() {
when(mMediaOutputController.isZeroMode()).thenReturn(true);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText(
R.string.media_output_dialog_pairing_new));
}
@Test
public void onBindViewHolder_bindGroup_withSessionName_verifyView() {
when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
when(mMediaOutputController.isZeroMode()).thenReturn(false);
when(mMediaOutputController.getSessionName()).thenReturn(TEST_SESSION_NAME);
mMediaOutputAdapter.getItemCount();
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_SESSION_NAME);
}
@Test
public void onBindViewHolder_bindGroup_noSessionName_verifyView() {
when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
when(mMediaOutputController.isZeroMode()).thenReturn(false);
when(mMediaOutputController.getSessionName()).thenReturn(null);
mMediaOutputAdapter.getItemCount();
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(mContext.getString(
R.string.media_output_dialog_group));
}
@Test
public void onBindViewHolder_bindConnectedDevice_verifyView() {
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
}
@Test
public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
}
@Test
public void onBindViewHolder_bindDisconnectedBluetoothDevice_verifyView() {
when(mMediaDevice2.getDeviceType()).thenReturn(
MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
when(mMediaDevice2.isConnected()).thenReturn(false);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(
mContext.getString(R.string.media_output_dialog_disconnected, TEST_DEVICE_NAME_2));
}
@Test
public void onBindViewHolder_bindFailedStateDevice_verifyView() {
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(mContext.getText(
R.string.media_output_dialog_connect_failed));
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
}
@Test
public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
when(mMediaOutputController.isTransferring()).thenReturn(true);
when(mMediaDevice1.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
}
@Test
public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
when(mMediaOutputController.isTransferring()).thenReturn(true);
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
}
@Test
public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
when(mMediaOutputController.isZeroMode()).thenReturn(true);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
mViewHolder.mContainerLayout.performClick();
verify(mMediaOutputController).launchBluetoothPairing();
}
@Test
public void onItemClick_clickDevice_verifyConnectDevice() {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
mViewHolder.mContainerLayout.performClick();
verify(mMediaOutputController).connectDevice(mMediaDevice2);
}
}

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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.media.session.MediaSessionManager;
import android.os.Bundle;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.filters.SmallTest;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MediaOutputBaseDialogTest extends SysuiTestCase {
private static final String TEST_PACKAGE = "test_package";
// Mock
private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class);
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
private ShadeController mShadeController = mock(ShadeController.class);
private ActivityStarter mStarter = mock(ActivityStarter.class);
private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
private MediaOutputController mMediaOutputController;
private int mHeaderIconRes;
private IconCompat mIconCompat;
private CharSequence mHeaderTitle;
private CharSequence mHeaderSubtitle;
@Before
public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager);
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
mMediaOutputController);
mMediaOutputBaseDialogImpl.onCreate(new Bundle());
}
@Test
public void refresh_withIconRes_iconIsVisible() {
mHeaderIconRes = 1;
mMediaOutputBaseDialogImpl.refresh();
final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
R.id.header_icon);
assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void refresh_withIconCompat_iconIsVisible() {
mIconCompat = mock(IconCompat.class);
mMediaOutputBaseDialogImpl.refresh();
final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
R.id.header_icon);
assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void refresh_noIcon_iconLayoutNotVisible() {
mHeaderIconRes = 0;
mIconCompat = null;
mMediaOutputBaseDialogImpl.refresh();
final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
R.id.header_icon);
assertThat(view.getVisibility()).isEqualTo(View.GONE);
}
@Test
public void refresh_checkTitle() {
mHeaderTitle = "test_string";
mMediaOutputBaseDialogImpl.refresh();
final TextView titleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
R.id.header_title);
assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(titleView.getText()).isEqualTo(mHeaderTitle);
}
@Test
public void refresh_withSubtitle_checkSubtitle() {
mHeaderSubtitle = "test_string";
mMediaOutputBaseDialogImpl.refresh();
final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
R.id.header_subtitle);
assertThat(subtitleView.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(subtitleView.getText()).isEqualTo(mHeaderSubtitle);
}
@Test
public void refresh_noSubtitle_checkSubtitle() {
mMediaOutputBaseDialogImpl.refresh();
final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
R.id.header_subtitle);
assertThat(subtitleView.getVisibility()).isEqualTo(View.GONE);
}
@Test
public void refresh_inDragging_notUpdateAdapter() {
when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true);
mMediaOutputBaseDialogImpl.refresh();
verify(mMediaOutputBaseAdapter, never()).notifyDataSetChanged();
}
@Test
public void refresh_notInDragging_verifyUpdateAdapter() {
when(mMediaOutputBaseAdapter.isDragging()).thenReturn(false);
mMediaOutputBaseDialogImpl.refresh();
verify(mMediaOutputBaseAdapter).notifyDataSetChanged();
}
class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) {
super(context, mediaOutputController);
mAdapter = mMediaOutputBaseAdapter;
}
@Override
int getHeaderIconRes() {
return mHeaderIconRes;
}
@Override
IconCompat getHeaderIcon() {
return mIconCompat;
}
@Override
int getHeaderIconSize() {
return 10;
}
@Override
CharSequence getHeaderText() {
return mHeaderTitle;
}
@Override
CharSequence getHeaderSubtitle() {
return mHeaderSubtitle;
}
@Override
int getStopButtonVisibility() {
return 0;
}
}
}

View File

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

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.media.MediaRoute2Info;
import android.media.session.MediaSessionManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MediaOutputDialogTest extends SysuiTestCase {
private static final String TEST_PACKAGE = "test_package";
// Mock
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
private ShadeController mShadeController = mock(ShadeController.class);
private ActivityStarter mStarter = mock(ActivityStarter.class);
private LocalMediaManager mLocalMediaManager = mock(LocalMediaManager.class);
private MediaDevice mMediaDevice = mock(MediaDevice.class);
private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private MediaOutputDialog mMediaOutputDialog;
private MediaOutputController mMediaOutputController;
private List<String> mFeatures = new ArrayList<>();
@Before
public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputDialog = new MediaOutputDialog(mContext, false, mMediaOutputController);
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
}
@Test
public void getStopButtonVisibility_remoteDevice_returnVisible() {
mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK);
assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
mFeatures.clear();
mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK);
assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
mFeatures.clear();
mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK);
assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
mFeatures.clear();
mFeatures.add(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK);
assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void getStopButtonVisibility_localDevice_returnGone() {
mFeatures.add(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK);
assertThat(mMediaOutputDialog.getStopButtonVisibility()).isEqualTo(View.GONE);
}
}

View File

@@ -0,0 +1,248 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.drawable.Icon;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.LinearLayout;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.filters.SmallTest;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class MediaOutputGroupAdapterTest extends SysuiTestCase {
private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
private static final int TEST_VOLUME = 10;
private static final int TEST_MAX_VOLUME = 50;
// Mock
private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
private Icon mIcon = mock(Icon.class);
private IconCompat mIconCompat = mock(IconCompat.class);
private MediaOutputGroupAdapter mGroupAdapter;
private MediaOutputGroupAdapter.GroupViewHolder mGroupViewHolder;
private List<MediaDevice> mGroupMediaDevices = new ArrayList<>();
private List<MediaDevice> mSelectableMediaDevices = new ArrayList<>();
private List<MediaDevice> mSelectedMediaDevices = new ArrayList<>();
private List<MediaDevice> mDeselectableMediaDevices = new ArrayList<>();
@Before
public void setUp() {
when(mMediaOutputController.getGroupMediaDevices()).thenReturn(mGroupMediaDevices);
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(mSelectableMediaDevices);
when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mSelectedMediaDevices);
when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn(
mDeselectableMediaDevices);
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
when(mMediaDevice2.getName()).thenReturn(TEST_DEVICE_NAME_2);
when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_ID_2);
mGroupMediaDevices.add(mMediaDevice1);
mGroupMediaDevices.add(mMediaDevice2);
mSelectedMediaDevices.add(mMediaDevice1);
mSelectableMediaDevices.add(mMediaDevice2);
mDeselectableMediaDevices.add(mMediaDevice1);
mGroupAdapter = new MediaOutputGroupAdapter(mMediaOutputController);
mGroupViewHolder = (MediaOutputGroupAdapter.GroupViewHolder) mGroupAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
}
@Test
public void onBindViewHolder_verifyGroupItem() {
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 0);
assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(mContext.getText(
R.string.media_output_dialog_group));
}
@Test
public void onBindViewHolder_singleSelectedDevice_verifyView() {
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mCheckBox.isChecked()).isTrue();
// Disabled checkBox
assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isFalse();
}
@Test
public void onBindViewHolder_multipleSelectedDevice_verifyView() {
mSelectedMediaDevices.clear();
mSelectedMediaDevices.add(mMediaDevice1);
mSelectedMediaDevices.add(mMediaDevice2);
mDeselectableMediaDevices.clear();
mDeselectableMediaDevices.add(mMediaDevice1);
mDeselectableMediaDevices.add(mMediaDevice2);
mSelectableMediaDevices.clear();
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mCheckBox.isChecked()).isTrue();
// Enabled checkBox
assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isTrue();
}
@Test
public void onBindViewHolder_notDeselectedDevice_verifyView() {
mSelectedMediaDevices.clear();
mSelectedMediaDevices.add(mMediaDevice1);
mSelectedMediaDevices.add(mMediaDevice2);
mDeselectableMediaDevices.clear();
mDeselectableMediaDevices.add(mMediaDevice1);
mSelectableMediaDevices.clear();
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 2);
assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mCheckBox.isChecked()).isTrue();
// Disabled checkBox
assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isFalse();
}
@Test
public void onBindViewHolder_selectableDevice_verifyCheckBox() {
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 2);
assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mGroupViewHolder.mCheckBox.isChecked()).isFalse();
// Enabled checkBox
assertThat(mGroupViewHolder.mCheckBox.isEnabled()).isTrue();
}
@Test
public void onBindViewHolder_verifySessionVolume() {
when(mMediaOutputController.getSessionVolume()).thenReturn(TEST_VOLUME);
when(mMediaOutputController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 0);
assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
}
@Test
public void onBindViewHolder_verifyDeviceVolume() {
when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_VOLUME);
when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
}
@Test
public void clickSelectedDevice_verifyRemoveDeviceFromPlayMedia() {
mSelectedMediaDevices.clear();
mSelectedMediaDevices.add(mMediaDevice1);
mSelectedMediaDevices.add(mMediaDevice2);
mDeselectableMediaDevices.clear();
mDeselectableMediaDevices.add(mMediaDevice1);
mDeselectableMediaDevices.add(mMediaDevice2);
mSelectableMediaDevices.clear();
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
mGroupViewHolder.mCheckBox.performClick();
verify(mMediaOutputController).removeDeviceFromPlayMedia(mMediaDevice1);
}
@Test
public void clickSelectabelDevice_verifyAddDeviceToPlayMedia() {
mGroupAdapter.onBindViewHolder(mGroupViewHolder, 2);
mGroupViewHolder.mCheckBox.performClick();
verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dialog;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.media.session.MediaSessionManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MediaOutputGroupDialogTest extends SysuiTestCase {
private static final String TEST_PACKAGE = "test_package";
// Mock
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
private ShadeController mShadeController = mock(ShadeController.class);
private ActivityStarter mStarter = mock(ActivityStarter.class);
private LocalMediaManager mLocalMediaManager = mock(LocalMediaManager.class);
private MediaDevice mMediaDevice = mock(MediaDevice.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class);
private MediaOutputGroupDialog mMediaOutputGroupDialog;
private MediaOutputController mMediaOutputController;
private List<MediaDevice> mMediaDevices = new ArrayList<>();
@Before
public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
mMediaOutputController);
when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
}
@After
public void tearDown() {
mMediaOutputGroupDialog.dismissDialog();
}
@Test
public void getStopButtonVisibility_returnVisible() {
assertThat(mMediaOutputGroupDialog.getStopButtonVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void getHeaderSubtitle_singleDevice_verifyTitle() {
mMediaDevices.add(mMediaDevice);
assertThat(mMediaOutputGroupDialog.getHeaderSubtitle()).isEqualTo(
mContext.getText(R.string.media_output_dialog_single_device));
}
@Test
public void getHeaderSubtitle_multipleDevices_verifyTitle() {
mMediaDevices.add(mMediaDevice);
mMediaDevices.add(mMediaDevice1);
assertThat(mMediaOutputGroupDialog.getHeaderSubtitle()).isEqualTo(mContext.getString(
R.string.media_output_dialog_multiple_devices, mMediaDevices.size()));
}
}