diff options
56 files changed, 3091 insertions, 2084 deletions
diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index e79c9a40918c..80db3bec86c1 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -19,4 +19,4 @@ systemui:rippleMinSize="30dp" systemui:rippleMaxSize="135dp" systemui:highlight="15" - systemui:cornerRadius="@dimen/qs_media_corner_radius" />
\ No newline at end of file + systemui:cornerRadius="?android:attr/dialogCornerRadius" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/keyguard_media_header.xml b/packages/SystemUI/res/layout/keyguard_media_header.xml index 20ec10ca1e1b..a520719566ab 100644 --- a/packages/SystemUI/res/layout/keyguard_media_header.xml +++ b/packages/SystemUI/res/layout/keyguard_media_header.xml @@ -45,109 +45,4 @@ android:layout_height="match_parent" /> - <!-- Layout for media controls. --> - <LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/keyguard_media_view" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:gravity="center" - android:padding="16dp" - > - <ImageView - android:id="@+id/album_art" - android:layout_width="@dimen/qs_media_album_size" - android:layout_height="@dimen/qs_media_album_size" - android:layout_marginRight="16dp" - android:layout_weight="0" - /> - - <!-- Media information --> - <LinearLayout - android:orientation="vertical" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - > - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="5dp" - /> - <TextView - android:id="@+id/app_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Song name --> - <TextView - android:id="@+id/header_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:singleLine="true" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:textSize="18sp" - android:paddingBottom="6dp" - android:gravity="center"/> - - <!-- Artist name --> - <TextView - android:id="@+id/header_artist" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Controls --> - <LinearLayout - android:id="@+id/media_actions" - android:orientation="horizontal" - android:layoutDirection="ltr" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center" - android:layout_gravity="center" - > - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action0" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action1" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action2" - /> - </LinearLayout> - </LinearLayout> - </com.android.systemui.statusbar.notification.stack.MediaHeaderView> diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 149446c55fc5..03e74676f03e 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -16,20 +16,22 @@ --> <!-- Carousel for media controls --> -<HorizontalScrollView +<com.android.systemui.media.UnboundHorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="@dimen/qs_media_padding" android:scrollbars="none" - android:visibility="gone" + android:clipChildren="false" + android:clipToPadding="false" > <LinearLayout android:id="@+id/media_carousel" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" + android:clipChildren="false" + android:clipToPadding="false" > <!-- QSMediaPlayers will be added here dynamically --> </LinearLayout> -</HorizontalScrollView> +</com.android.systemui.media.UnboundHorizontalScrollView> diff --git a/packages/SystemUI/res/layout/qqs_media_panel.xml b/packages/SystemUI/res/layout/qqs_media_panel.xml deleted file mode 100644 index 2e86732f3cad..000000000000 --- a/packages/SystemUI/res/layout/qqs_media_panel.xml +++ /dev/null @@ -1,90 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2019 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 - --> - -<!-- Layout for QQS media controls --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/qqs_media_controls" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:gravity="center" - android:paddingTop="16dp" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:paddingBottom="12dp" - android:background="@drawable/qs_media_background" - > - <!-- Top line: icon + song name --> - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:clipChildren="false" - android:gravity="center" - android:layout_marginBottom="12dp" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="14dp" - android:layout_height="14dp" - android:layout_marginEnd="5dp" - /> - <TextView - android:id="@+id/header_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Bottom section: controls --> - <LinearLayout - android:id="@+id/media_actions" - android:orientation="horizontal" - android:layoutDirection="ltr" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - > - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action0" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action1" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action2" - /> - </LinearLayout> -</LinearLayout> diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index 0c9ce3938420..ebfd0a0fd537 100644 --- a/packages/SystemUI/res/layout/qs_footer_impl.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -23,7 +23,6 @@ android:layout_height="@dimen/qs_footer_height" android:layout_marginStart="@dimen/qs_footer_margin" android:layout_marginEnd="@dimen/qs_footer_margin" - android:elevation="4dp" android:background="@android:color/transparent" android:baselineAligned="false" android:clickable="false" @@ -128,13 +127,4 @@ </com.android.systemui.statusbar.AlphaOptimizedFrameLayout> </com.android.keyguard.AlphaOptimizedLinearLayout> </LinearLayout> - <View - android:id="@+id/qs_drag_handle_view" - android:layout_width="48dp" - android:layout_height="4dp" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" - android:layout_gravity="center_horizontal|bottom" - android:background="@drawable/qs_footer_drag_handle" /> - </com.android.systemui.qs.QSFooterImpl> diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml index d633ff40df9e..9ad380d260c0 100644 --- a/packages/SystemUI/res/layout/qs_media_panel.xml +++ b/packages/SystemUI/res/layout/qs_media_panel.xml @@ -16,236 +16,173 @@ --> <!-- Layout for media controls inside QSPanel carousel --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/qs_media_controls" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" android:gravity="center_horizontal|fill_vertical" - android:paddingTop="@dimen/qs_media_panel_outer_padding" - android:paddingBottom="@dimen/qs_media_panel_outer_padding" - android:background="@drawable/qs_media_background" - > + app:layoutDescription="@xml/media_scene"> - <!-- Buttons to remove this view when no longer needed --> - <include - layout="@layout/qs_media_panel_options" - android:visibility="gone"/> + <View + android:id="@+id/media_background" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/qs_media_background" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + /> - <LinearLayout - android:id="@+id/media_guts" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <!-- Header section --> - <LinearLayout - android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" - android:paddingStart="@dimen/qs_media_panel_outer_padding" - android:paddingEnd="16dp" - > - - <ImageView - android:id="@+id/album_art" - android:layout_width="@dimen/qs_media_album_size" - android:layout_height="@dimen/qs_media_album_size" - android:layout_marginRight="16dp" - android:layout_weight="0" - /> - - <LinearLayout - android:orientation="vertical" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - > - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="5dp" - /> - <TextView - android:id="@+id/app_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Song name --> - <TextView - android:id="@+id/header_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:singleLine="true" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:textSize="18sp" - android:paddingBottom="6dp" - android:gravity="center"/> - - <!-- Artist name --> - <TextView - android:id="@+id/header_artist" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:textSize="14sp" - android:singleLine="true" - /> - </LinearLayout> - - <!-- Output chip --> - <LinearLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:visibility="gone" - android:paddingTop="6dp" - android:paddingBottom="6dp" - android:paddingLeft="12dp" - android:paddingRight="12dp" - android:gravity="center" - android:id="@+id/media_seamless" - android:background="@*android:drawable/media_seamless_background" - android:layout_weight="1" - android:forceHasOverlappingRendering="false" - > - <ImageView - android:layout_width="@dimen/qs_seamless_icon_size" - android:layout_height="@dimen/qs_seamless_icon_size" - android:src="@*android:drawable/ic_media_seamless" - android:layout_marginRight="8dp" - android:id="@+id/media_seamless_image" - /> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:text="@*android:string/ext_media_seamless_action" - android:textSize="14sp" - android:id="@+id/media_seamless_text" - android:singleLine="true" - /> - </LinearLayout> - </LinearLayout> - - <!-- Seek Bar --> - <SeekBar - android:id="@+id/media_progress_bar" - style="@android:style/Widget.ProgressBar.Horizontal" - android:clickable="true" + <FrameLayout + android:id="@+id/notification_media_progress_time" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:forceHasOverlappingRendering="false"> + <!-- width is set to "match_parent" to avoid extra layout calls --> + <TextView + android:id="@+id/media_elapsed_time" android:layout_width="match_parent" android:layout_height="wrap_content" - android:maxHeight="3dp" - android:paddingTop="24dp" - android:paddingBottom="24dp" - android:layout_marginBottom="-24dp" - android:layout_marginTop="-24dp" - android:splitTrack="false" - /> + android:layout_alignParentLeft="true" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:gravity="left" + android:textSize="14sp" /> - <FrameLayout - android:id="@+id/notification_media_progress_time" + <TextView + android:id="@+id/media_total_time" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="@dimen/qs_media_panel_outer_padding" - android:paddingEnd="@dimen/qs_media_panel_outer_padding" - android:layout_marginBottom="10dp" - android:layout_gravity="center" - > - <!-- width is set to "match_parent" to avoid extra layout calls --> - <TextView - android:id="@+id/media_elapsed_time" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:textSize="14sp" - android:gravity="left" - /> - <TextView - android:id="@+id/media_total_time" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fontFamily="@*android:string/config_bodyFontFamily" - android:layout_alignParentRight="true" - android:textSize="14sp" - android:gravity="right" - /> - </FrameLayout> - - <!-- Controls --> - <LinearLayout - android:id="@+id/media_actions" - android:orientation="horizontal" - android:layoutDirection="ltr" - android:layout_width="match_parent" + android:layout_alignParentRight="true" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:gravity="right" + android:textSize="14sp" /> + </FrameLayout> + + <ImageButton + android:id="@+id/action0" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <ImageButton + android:id="@+id/action1" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <ImageButton + android:id="@+id/action2" + style="@style/MediaPlayer.Button" + android:layout_width="52dp" + android:layout_height="52dp" /> + + <ImageButton + android:id="@+id/action3" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <ImageButton + android:id="@+id/action4" + style="@style/MediaPlayer.Button" + android:layout_width="48dp" + android:layout_height="48dp" /> + + <!-- Album Art --> + <ImageView + android:id="@+id/album_art" + android:layout_width="@dimen/qs_media_album_size" + android:layout_height="@dimen/qs_media_album_size" /> + + <!-- Seamless Output Switcher --> + <LinearLayout + android:id="@+id/media_seamless" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="@*android:drawable/media_seamless_background" + android:orientation="horizontal" + android:forceHasOverlappingRendering="false" + android:paddingLeft="12dp" + android:paddingTop="6dp" + android:paddingRight="12dp" + android:paddingBottom="6dp"> + + <ImageView + android:id="@+id/media_seamless_image" + android:layout_width="@dimen/qs_seamless_icon_size" + android:layout_height="@dimen/qs_seamless_icon_size" + android:layout_marginRight="8dp" + android:src="@*android:drawable/ic_media_seamless" /> + + <TextView + android:id="@+id/media_seamless_text" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingStart="@dimen/qs_media_panel_outer_padding" - android:paddingEnd="@dimen/qs_media_panel_outer_padding" - android:gravity="center" - > - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action0" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action1" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="52dp" - android:layout_height="52dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action2" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action3" - /> - <ImageButton - style="@style/MediaPlayer.Button" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" - android:gravity="center" - android:visibility="gone" - android:id="@+id/action4" - /> - </LinearLayout> + android:fontFamily="@*android:string/config_bodyFontFamily" + android:singleLine="true" + android:text="@*android:string/ext_media_seamless_action" + android:textSize="14sp" /> </LinearLayout> -</LinearLayout> + + <!-- Seek Bar --> + <SeekBar + android:id="@+id/media_progress_bar" + style="@android:style/Widget.ProgressBar.Horizontal" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:clickable="true" + android:maxHeight="3dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:splitTrack="false" /> + + <!-- App name --> + <TextView + android:id="@+id/app_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:singleLine="true" + android:textSize="14sp" /> + + <!-- Song name --> + <TextView + android:id="@+id/header_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" + android:singleLine="true" + android:textSize="18sp" /> + + <!-- Artist name --> + <TextView + android:id="@+id/header_artist" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:singleLine="true" + android:textSize="14sp" /> + + <com.android.internal.widget.CachingIconView + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" /> + + <!-- Buttons to remove this view when no longer needed --> + <include + layout="@layout/qs_media_panel_options" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/view_width" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="300dp" /> +</androidx.constraintlayout.motion.widget.MotionLayout> diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml index 01dfeb281e1b..cdf84260e399 100644 --- a/packages/SystemUI/res/layout/qs_panel.xml +++ b/packages/SystemUI/res/layout/qs_panel.xml @@ -54,20 +54,32 @@ android:layout_marginTop="@*android:dimen/quick_qs_offset_height" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/qs_footer_height" android:elevation="4dp" android:background="@android:color/transparent" android:focusable="true" - android:accessibilityTraversalBefore="@android:id/edit" - /> + android:accessibilityTraversalBefore="@android:id/edit"> + <include layout="@layout/qs_footer_impl" /> + </com.android.systemui.qs.QSPanel> <include layout="@layout/quick_status_bar_expanded_header" /> - <include layout="@layout/qs_footer_impl" /> - <include android:id="@+id/qs_detail" layout="@layout/qs_detail" /> <include android:id="@+id/qs_customize" layout="@layout/qs_customize_panel" android:visibility="gone" /> + <FrameLayout + android:id="@+id/qs_drag_handle_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:elevation="4dp" + android:paddingBottom="5dp"> + <View + android:layout_width="46dp" + android:layout_height="3dp" + android:background="@drawable/qs_footer_drag_handle" /> + </FrameLayout> + + </com.android.systemui.qs.QSContainerImpl> diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index e99b91787072..9a7c344baf20 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -20,7 +20,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" - android:layout_height="@*android:dimen/quick_qs_total_height" + android:layout_height="wrap_content" android:layout_gravity="@integer/notification_panel_layout_gravity" android:background="@android:color/transparent" android:baselineAligned="false" @@ -29,6 +29,7 @@ android:clipToPadding="false" android:paddingTop="0dp" android:paddingEnd="0dp" + android:paddingBottom="10dp" android:paddingStart="0dp" android:elevation="4dp" > @@ -45,8 +46,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/quick_qs_status_icons" - android:layout_marginStart="@dimen/qs_header_tile_margin_horizontal" - android:layout_marginEnd="@dimen/qs_header_tile_margin_horizontal" android:accessibilityTraversalAfter="@+id/date_time_group" android:accessibilityTraversalBefore="@id/expand_indicator" android:clipChildren="false" @@ -54,15 +53,6 @@ android:focusable="true" android:importantForAccessibility="yes" /> - <com.android.systemui.statusbar.AlphaOptimizedImageView - android:id="@+id/qs_detail_header_progress" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:alpha="0" - android:background="@color/qs_detail_progress_track" - android:src="@drawable/indeterminate_anim"/> - <TextView android:id="@+id/header_debug_info" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 99e347eb1a69..b4a05c6da780 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -498,6 +498,7 @@ <dimen name="qs_quick_tile_padding">12dp</dimen> <dimen name="qs_header_gear_translation">16dp</dimen> <dimen name="qs_header_tile_margin_horizontal">4dp</dimen> + <dimen name="qs_header_tile_margin_bottom">18dp</dimen> <dimen name="qs_page_indicator_width">16dp</dimen> <dimen name="qs_page_indicator_height">8dp</dimen> <dimen name="qs_tile_icon_size">24dp</dimen> @@ -1043,6 +1044,10 @@ <dimen name="bottom_padding">48dp</dimen> <dimen name="edge_margin">8dp</dimen> + <!-- The absolute side margins of quick settings --> + <dimen name="quick_settings_side_margins">16dp</dimen> + <dimen name="quick_settings_expanded_bottom_margin">16dp</dimen> + <dimen name="quick_settings_media_extra_bottom_margin">4dp</dimen> <dimen name="rounded_corner_content_padding">0dp</dimen> <dimen name="nav_content_padding">0dp</dimen> <dimen name="nav_quick_scrub_track_edge_padding">24dp</dimen> @@ -1230,12 +1235,12 @@ <!-- Size of media cards in the QSPanel carousel --> <dimen name="qs_media_width">350dp</dimen> - <dimen name="qs_media_padding">8dp</dimen> + <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_panel_outer_padding">16dp</dimen> - <dimen name="qs_media_corner_radius">10dp</dimen> - <dimen name="qs_media_album_size">72dp</dimen> + <dimen name="qs_media_album_size">52dp</dimen> <dimen name="qs_seamless_icon_size">20dp</dimen> <dimen name="qqs_media_spacing">8dp</dimen> + <dimen name="qqs_horizonal_tile_padding_bottom">8dp</dimen> <dimen name="magnification_border_size">5dp</dimen> <dimen name="magnification_frame_move_short">5dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 8156e8dc9bf1..76ca385bd9d9 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -70,6 +70,26 @@ <item type="id" name="panel_alpha_animator_end_tag"/> <item type="id" name="cross_fade_layer_type_changed_tag"/> + <item type="id" name="absolute_x_animator_tag"/> + <item type="id" name="absolute_x_animator_start_tag"/> + <item type="id" name="absolute_x_animator_end_tag"/> + <item type="id" name="absolute_x_current_value"/> + + <item type="id" name="absolute_y_animator_tag"/> + <item type="id" name="absolute_y_animator_start_tag"/> + <item type="id" name="absolute_y_animator_end_tag"/> + <item type="id" name="absolute_y_current_value"/> + + <item type="id" name="view_height_animator_tag"/> + <item type="id" name="view_height_animator_start_tag"/> + <item type="id" name="view_height_animator_end_tag"/> + <item type="id" name="view_height_current_value"/> + + <item type="id" name="view_width_animator_tag"/> + <item type="id" name="view_width_animator_start_tag"/> + <item type="id" name="view_width_animator_end_tag"/> + <item type="id" name="view_width_current_value"/> + <!-- Whether the icon is from a notification for which targetSdk < L --> <item type="id" name="icon_is_pre_L"/> diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml new file mode 100644 index 000000000000..f61b2b096d3c --- /dev/null +++ b/packages/SystemUI/res/xml/media_scene.xml @@ -0,0 +1,447 @@ +<?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 + --> +<MotionScene + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <Transition + app:constraintSetStart="@id/collapsed" + app:constraintSetEnd="@id/expanded" + app:duration="1000" > + <KeyFrameSet > + <KeyPosition + app:motionTarget="@+id/action0" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action1" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action2" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action3" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/action4" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyPosition + app:motionTarget="@+id/media_progress_bar" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyAttribute + app:motionTarget="@id/media_progress_bar" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/media_progress_bar" + app:framePosition="70" + android:alpha="0.0"/> + <KeyPosition + app:motionTarget="@+id/notification_media_progress_time" + app:keyPositionType="pathRelative" + app:framePosition="70" + app:sizePercent="0.9" /> + <KeyAttribute + app:motionTarget="@id/notification_media_progress_time" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/notification_media_progress_time" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action0" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action0" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action1" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action1" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action2" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action2" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action3" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action3" + app:framePosition="70" + android:alpha="0.0"/> + <KeyAttribute + app:motionTarget="@id/action4" + app:framePosition="0" + android:alpha="0.0" /> + <KeyAttribute + app:motionTarget="@+id/action4" + app:framePosition="70" + android:alpha="0.0"/> + </KeyFrameSet> + </Transition> + + <ConstraintSet android:id="@+id/expanded"> + <Constraint + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginStart="18dp" + android:layout_marginTop="22dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + /> + + <Constraint + android:id="@+id/app_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="10dp" + android:layout_marginStart="10dp" + android:layout_marginTop="20dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintEnd_toStartOf="@id/media_seamless" + app:layout_constraintHorizontal_bias="0" + /> + + <Constraint + android:id="@+id/media_seamless" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_min="60dp" + android:layout_marginTop="@dimen/qs_media_panel_outer_padding" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + /> + + <Constraint + android:id="@+id/album_art" + android:layout_width="@dimen/qs_media_album_size" + android:layout_height="@dimen/qs_media_album_size" + android:layout_marginTop="14dp" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + app:layout_constraintTop_toBottomOf="@+id/app_name" + app:layout_constraintStart_toStartOf="parent" + /> + + <!-- Song name --> + <Constraint + android:id="@+id/header_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginTop="17dp" + android:layout_marginStart="16dp" + app:layout_constraintTop_toBottomOf="@+id/app_name" + app:layout_constraintStart_toEndOf="@id/album_art" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Artist name --> + <Constraint + android:id="@+id/header_artist" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginTop="3dp" + app:layout_constraintTop_toBottomOf="@id/header_title" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Seek Bar --> + <Constraint + android:id="@+id/media_progress_bar" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="3dp" + app:layout_constraintTop_toBottomOf="@id/header_artist" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + /> + + <Constraint + android:id="@+id/notification_media_progress_time" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="38dp" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + app:layout_constraintTop_toBottomOf="@id/header_artist" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + /> + + <Constraint + android:id="@+id/action0" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginTop="5dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@id/action1" + app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action1" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintLeft_toRightOf="@id/action0" + app:layout_constraintRight_toLeftOf="@id/action2" + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action2" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintLeft_toRightOf="@id/action1" + app:layout_constraintRight_toLeftOf="@id/action3" + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action3" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + app:layout_constraintLeft_toRightOf="@id/action2" + app:layout_constraintRight_toLeftOf="@id/action4" + app:layout_constraintTop_toTopOf="@id/action0" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + + <Constraint + android:id="@+id/action4" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginBottom="@dimen/qs_media_panel_outer_padding" + app:layout_constraintLeft_toRightOf="@id/action3" + app:layout_constraintRight_toRightOf="@id/view_width" + app:layout_constraintTop_toTopOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent"> + </Constraint> + </ConstraintSet> + + <ConstraintSet android:id="@+id/collapsed"> + <Constraint + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginStart="18dp" + android:layout_marginTop="22dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + /> + + <Constraint + android:id="@+id/app_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="10dp" + android:layout_marginStart="10dp" + android:layout_marginTop="20dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintEnd_toStartOf="@id/media_seamless" + app:layout_constraintHorizontal_bias="0" + /> + + <Constraint + android:id="@+id/media_seamless" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="@id/view_width" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_min="60dp" + android:layout_marginTop="@dimen/qs_media_panel_outer_padding" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + /> + + <Constraint + android:id="@+id/album_art" + android:layout_width="@dimen/qs_media_album_size" + android:layout_height="@dimen/qs_media_album_size" + android:layout_marginTop="16dp" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + android:layout_marginBottom="24dp" + app:layout_constraintTop_toBottomOf="@id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + /> + + <!-- Song name --> + <Constraint + android:id="@+id/header_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="17dp" + android:layout_marginStart="16dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintBottom_toTopOf="@id/header_artist" + app:layout_constraintStart_toEndOf="@id/album_art" + app:layout_constraintEnd_toStartOf="@id/action0" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Artist name --> + <Constraint + android:id="@+id/header_artist" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="3dp" + android:layout_marginBottom="24dp" + app:layout_constraintTop_toBottomOf="@id/header_title" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/action0" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="0"/> + + <!-- Seek Bar --> + <Constraint + android:id="@+id/media_progress_bar" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:alpha="0.0" + app:layout_constraintTop_toBottomOf="@id/album_art" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + android:visibility="gone" + /> + + <Constraint + android:id="@+id/notification_media_progress_time" + android:alpha="0.0" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="35dp" + android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" + android:layout_marginStart="@dimen/qs_media_panel_outer_padding" + app:layout_constraintTop_toBottomOf="@id/album_art" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="@id/view_width" + android:visibility="gone" + /> + + <Constraint + android:id="@+id/action0" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginTop="16dp" + android:visibility="gone" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/header_title" + app:layout_constraintRight_toLeftOf="@id/action1" + > + </Constraint> + + <Constraint + android:id="@+id/action1" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action0" + app:layout_constraintRight_toLeftOf="@id/action2" + > + </Constraint> + + <Constraint + android:id="@+id/action2" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action1" + app:layout_constraintRight_toLeftOf="@id/action3" + > + </Constraint> + + <Constraint + android:id="@+id/action3" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action2" + app:layout_constraintRight_toLeftOf="@id/action4" + > + </Constraint> + + <Constraint + android:id="@+id/action4" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:visibility="gone" + android:layout_marginTop="18dp" + app:layout_constraintTop_toBottomOf="@id/app_name" + app:layout_constraintLeft_toRightOf="@id/action3" + app:layout_constraintRight_toRightOf="@id/view_width" + > + </Constraint> + </ConstraintSet> +</MotionScene> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java deleted file mode 100644 index af5196f92bcb..000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMediaPlayer.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 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.keyguard; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.util.Log; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; -import androidx.palette.graphics.Palette; - -import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.media.MediaControllerFactory; -import com.android.systemui.statusbar.notification.MediaNotificationProcessor; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.stack.MediaHeaderView; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Media controls to display on the lockscreen - * - * TODO: Should extend MediaControlPanel to avoid code duplication. - * Unfortunately, it isn't currently possible because the ActivatableNotificationView background is - * different. - */ -@Singleton -public class KeyguardMediaPlayer { - - private static final String TAG = "KeyguardMediaPlayer"; - // Buttons that can be displayed on lock screen media controls. - private static final int[] ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; - - private final Context mContext; - private final Executor mBackgroundExecutor; - private final KeyguardMediaViewModel mViewModel; - private KeyguardMediaObserver mObserver; - - @Inject - public KeyguardMediaPlayer(Context context, MediaControllerFactory factory, - @Background Executor backgroundExecutor) { - mContext = context; - mBackgroundExecutor = backgroundExecutor; - mViewModel = new KeyguardMediaViewModel(context, factory); - } - - /** Binds media controls to a view hierarchy. */ - public void bindView(View v) { - if (mObserver != null) { - throw new IllegalStateException("cannot bind views, already bound"); - } - mViewModel.loadDimens(); - mObserver = new KeyguardMediaObserver(v); - // Control buttons - for (int i = 0; i < ACTION_IDS.length; i++) { - ImageButton button = v.findViewById(ACTION_IDS[i]); - if (button == null) { - continue; - } - final int index = i; - button.setOnClickListener(unused -> mViewModel.onActionClick(index)); - } - mViewModel.getKeyguardMedia().observeForever(mObserver); - } - - /** Unbinds media controls. */ - public void unbindView() { - if (mObserver == null) { - throw new IllegalStateException("cannot unbind views, nothing bound"); - } - mViewModel.getKeyguardMedia().removeObserver(mObserver); - mObserver = null; - } - - /** Clear the media controls because there isn't an active session. */ - public void clearControls() { - mBackgroundExecutor.execute(mViewModel::clearControls); - } - - /** - * Update the media player - * - * TODO: consider registering a MediaLister instead of exposing this update method. - * - * @param entry Media notification that will be used to update the player - * @param appIcon Icon for the app playing the media - * @param mediaMetadata Media metadata that will be used to update the player - */ - public void updateControls(NotificationEntry entry, Icon appIcon, - MediaMetadata mediaMetadata) { - if (mObserver == null) { - throw new IllegalStateException("cannot update controls, views not bound"); - } - if (mediaMetadata == null) { - Log.d(TAG, "media metadata was null, closing media controls"); - // Note that clearControls() executes on the same background executor, so there - // shouldn't be an issue with an outdated update running after clear. However, if stale - // controls are observed then consider removing any enqueued updates. - clearControls(); - return; - } - mBackgroundExecutor.execute(() -> mViewModel.updateControls(entry, appIcon, mediaMetadata)); - } - - /** ViewModel for KeyguardMediaControls. */ - private static final class KeyguardMediaViewModel { - - private final Context mContext; - private final MediaControllerFactory mMediaControllerFactory; - private final MutableLiveData<KeyguardMedia> mMedia = new MutableLiveData<>(); - private final Object mActionsLock = new Object(); - private List<PendingIntent> mActions; - private float mAlbumArtRadius; - private int mAlbumArtSize; - - KeyguardMediaViewModel(Context context, MediaControllerFactory factory) { - mContext = context; - mMediaControllerFactory = factory; - loadDimens(); - } - - /** Close the media player because there isn't an active session. */ - public void clearControls() { - synchronized (mActionsLock) { - mActions = null; - } - mMedia.postValue(null); - } - - /** Update the media player with information about the active session. */ - public void updateControls(NotificationEntry entry, Icon appIcon, - MediaMetadata mediaMetadata) { - - // Check the playback state of the media controller. If it is null, then the session was - // probably destroyed. Don't update in this case. - final MediaSession.Token token = entry.getSbn().getNotification().extras - .getParcelable(Notification.EXTRA_MEDIA_SESSION); - final MediaController controller = token != null - ? mMediaControllerFactory.create(token) : null; - if (controller != null && controller.getPlaybackState() == null) { - clearControls(); - return; - } - - // Foreground and Background colors computed from album art - Notification notif = entry.getSbn().getNotification(); - int fgColor = notif.color; - int bgColor = entry.getRow() == null ? -1 : entry.getRow().getCurrentBackgroundTint(); - Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); - if (artworkBitmap == null) { - artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - } - if (artworkBitmap != null) { - // If we have art, get colors from that - Palette p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) - .generate(); - Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(p); - bgColor = swatch.getRgb(); - fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p); - } - // Make sure colors will be legible - boolean isDark = !ContrastColorUtil.isColorLight(bgColor); - fgColor = ContrastColorUtil.resolveContrastColor(mContext, fgColor, bgColor, - isDark); - fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark); - - // Album art - RoundedBitmapDrawable artwork = null; - if (artworkBitmap != null) { - Bitmap original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true); - Bitmap scaled = Bitmap.createScaledBitmap(original, mAlbumArtSize, mAlbumArtSize, - false); - artwork = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); - artwork.setCornerRadius(mAlbumArtRadius); - } - - // App name - Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif); - String app = builder.loadHeaderAppName(); - - // App Icon - Drawable appIconDrawable = appIcon.loadDrawable(mContext); - - // Song name - String song = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - - // Artist name - String artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); - - // Control buttons - List<Drawable> actionIcons = new ArrayList<>(); - final List<PendingIntent> intents = new ArrayList<>(); - Notification.Action[] actions = notif.actions; - final int[] actionsToShow = notif.extras.getIntArray( - Notification.EXTRA_COMPACT_ACTIONS); - - Context packageContext = entry.getSbn().getPackageContext(mContext); - for (int i = 0; i < ACTION_IDS.length; i++) { - if (actionsToShow != null && actions != null && i < actionsToShow.length - && actionsToShow[i] < actions.length) { - final int idx = actionsToShow[i]; - actionIcons.add(actions[idx].getIcon().loadDrawable(packageContext)); - intents.add(actions[idx].actionIntent); - } else { - actionIcons.add(null); - intents.add(null); - } - } - synchronized (mActionsLock) { - mActions = intents; - } - - KeyguardMedia data = new KeyguardMedia(fgColor, bgColor, app, appIconDrawable, artist, - song, artwork, actionIcons); - mMedia.postValue(data); - } - - /** Gets state for the lock screen media controls. */ - public LiveData<KeyguardMedia> getKeyguardMedia() { - return mMedia; - } - - /** - * Handle user clicks on media control buttons (actions). - * - * @param index position of the button that was clicked. - */ - public void onActionClick(int index) { - PendingIntent intent = null; - // This might block the ui thread to wait for the lock. Currently, however, the - // lock is held by the bg thread to assign a member, which should be fast. An - // alternative could be to add the intents to the state and let the observer set - // the onClick listeners. - synchronized (mActionsLock) { - if (mActions != null && index < mActions.size()) { - intent = mActions.get(index); - } - } - if (intent != null) { - try { - intent.send(); - } catch (PendingIntent.CanceledException e) { - Log.d(TAG, "failed to send action intent", e); - } - } - } - - void loadDimens() { - mAlbumArtRadius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); - mAlbumArtSize = (int) mContext.getResources().getDimension( - R.dimen.qs_media_album_size); - } - } - - /** Observer for state changes of lock screen media controls. */ - private static final class KeyguardMediaObserver implements Observer<KeyguardMedia> { - - private final View mRootView; - private final MediaHeaderView mMediaHeaderView; - private final ImageView mAlbumView; - private final ImageView mAppIconView; - private final TextView mAppNameView; - private final TextView mTitleView; - private final TextView mArtistView; - private final List<ImageButton> mButtonViews = new ArrayList<>(); - - KeyguardMediaObserver(View v) { - mRootView = v; - mMediaHeaderView = v instanceof MediaHeaderView ? (MediaHeaderView) v : null; - mAlbumView = v.findViewById(R.id.album_art); - mAppIconView = v.findViewById(R.id.icon); - mAppNameView = v.findViewById(R.id.app_name); - mTitleView = v.findViewById(R.id.header_title); - mArtistView = v.findViewById(R.id.header_artist); - for (int i = 0; i < ACTION_IDS.length; i++) { - mButtonViews.add(v.findViewById(ACTION_IDS[i])); - } - } - - /** Updates lock screen media player views when state changes. */ - @Override - public void onChanged(KeyguardMedia data) { - if (data == null) { - mRootView.setVisibility(View.GONE); - return; - } - mRootView.setVisibility(View.VISIBLE); - - // Background color - if (mMediaHeaderView != null) { - mMediaHeaderView.setBackgroundColor(data.getBackgroundColor()); - } - - // Album art - if (mAlbumView != null) { - mAlbumView.setImageDrawable(data.getArtwork()); - mAlbumView.setVisibility(data.getArtwork() == null ? View.GONE : View.VISIBLE); - } - - // App icon - if (mAppIconView != null) { - Drawable iconDrawable = data.getAppIcon(); - iconDrawable.setTint(data.getForegroundColor()); - mAppIconView.setImageDrawable(iconDrawable); - } - - // App name - if (mAppNameView != null) { - String appNameString = data.getApp(); - mAppNameView.setText(appNameString); - mAppNameView.setTextColor(data.getForegroundColor()); - } - - // Song name - if (mTitleView != null) { - mTitleView.setText(data.getSong()); - mTitleView.setTextColor(data.getForegroundColor()); - } - - // Artist name - if (mArtistView != null) { - mArtistView.setText(data.getArtist()); - mArtistView.setTextColor(data.getForegroundColor()); - } - - // Control buttons - for (int i = 0; i < ACTION_IDS.length; i++) { - ImageButton button = mButtonViews.get(i); - if (button == null) { - continue; - } - Drawable icon = data.getActionIcons().get(i); - if (icon == null) { - button.setVisibility(View.GONE); - button.setImageDrawable(null); - } else { - button.setVisibility(View.VISIBLE); - button.setImageDrawable(icon); - button.setImageTintList(ColorStateList.valueOf(data.getForegroundColor())); - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 96494cfe640f..3a37c0fd4634 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -451,7 +451,8 @@ public class KeyguardSliceProvider extends SliceProvider implements * @param metadata New metadata. */ @Override - public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) { + public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, + @PlaybackState.State int state) { synchronized (this) { boolean nextVisible = NotificationMediaManager.isPlayingState(state); mMediaHandler.removeCallbacksAndMessages(null); diff --git a/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt b/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt new file mode 100644 index 000000000000..2fe0d9f4711f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt @@ -0,0 +1,52 @@ +/* + * 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 + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup + +private val EMPTY_RECT = Rect(0,0,0,0) + +private val LAYOUT_CHANGE_LISTENER = object : View.OnLayoutChangeListener { + + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, + oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + v?.let { + if (v.visibility == View.GONE) { + v.clipBounds = EMPTY_RECT + } else { + v.clipBounds = null + } + } + } +} +/** + * A helper class that clips all GONE children. Useful for transitions in motionlayout which + * don't clip its children. + */ +class GoneChildrenHideHelper private constructor() { + companion object { + @JvmStatic + fun clipGoneChildrenOnLayout(layout: ViewGroup) { + val childCount = layout.childCount + for (i in 0 until childCount) { + val child = layout.getChildAt(i) + child.addOnLayoutChangeListener(LAYOUT_CHANGE_LISTENER) + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt index 937472735bb0..743216556434 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt @@ -6,6 +6,7 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.res.ColorStateList import android.content.res.Resources +import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter @@ -49,6 +50,7 @@ private data class RippleData( @Keep class IlluminationDrawable : Drawable() { + private var themeAttrs: IntArray? = null private var cornerRadius = 0f private var highlightColor = Color.TRANSPARENT private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f) @@ -139,13 +141,41 @@ class IlluminationDrawable : Drawable() { theme: Resources.Theme? ) { val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable) - cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) - rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) - rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) - rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f + themeAttrs = a.extractThemeAttrs() + updateStateFromTypedArray(a) a.recycle() } + private fun updateStateFromTypedArray(a: TypedArray) { + if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) { + cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, + cornerRadius) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) { + rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) { + rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) + } + if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { + rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / + 100f + } + } + + override fun canApplyTheme(): Boolean { + return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme() + } + + override fun applyTheme(t: Resources.Theme) { + super.applyTheme(t) + themeAttrs?.let { + val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable) + updateStateFromTypedArray(a) + a.recycle() + } + } + override fun setColorFilter(p0: ColorFilter?) { throw UnsupportedOperationException("Color filters are not supported") } diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt new file mode 100644 index 000000000000..524c6955ba4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -0,0 +1,71 @@ +/* + * 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 + +import android.view.View +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.MediaHeaderView +import com.android.systemui.statusbar.phone.KeyguardBypassController +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class that controls the media notifications on the lock screen, handles its visibility and + * is responsible for the embedding of he media experience. + */ +@Singleton +class KeyguardMediaController @Inject constructor( + private val mediaHost: MediaHost, + private val bypassController: KeyguardBypassController, + private val statusBarStateController: SysuiStatusBarStateController +) { + + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + updateVisibility() + } + }) + } + private var view: MediaHeaderView? = null + + /** + * Attach this controller to a media view, initializing its state + */ + fun attach(mediaView: MediaHeaderView) { + view = mediaView + // First let's set the desired state that we want for this host + mediaHost.visibleChangedListener = { updateVisibility() } + mediaHost.expansion = 0.0f + mediaHost.showsOnlyActiveMedia = true + + // Let's now initialize this view, which also creates the host view for us. + mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) + mediaView.setContentView(mediaHost.hostView) + } + + private fun updateVisibility() { + val shouldBeVisible = mediaHost.visible + && !bypassController.bypassEnabled + && (statusBarStateController.state == StatusBarState.KEYGUARD || + statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER) + view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt new file mode 100644 index 000000000000..a366725a4398 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt @@ -0,0 +1,115 @@ +/* + * 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 + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import com.android.systemui.statusbar.notification.AnimatableProperty +import com.android.systemui.statusbar.notification.PropertyAnimator +import com.android.systemui.statusbar.notification.stack.AnimationProperties + +/** + * A utility class that helps with animations of bound changes designed for motionlayout which + * doesn't work together with regular changeBounds. + */ +class LayoutAnimationHelper { + + private val layout: ViewGroup + private var sizeAnimationPending = false + private val desiredBounds = mutableMapOf<View, Rect>() + private val animationProperties = AnimationProperties() + private val layoutListener = object : View.OnLayoutChangeListener { + override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, + oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { + v?.let { + if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 || + oldTop - oldBottom == 0) { + return + } + if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) { + val rect = desiredBounds.getOrPut(v, { Rect() }) + rect.set(left, top, right, bottom) + onDesiredLocationChanged(v, rect) + } + } + } + } + + constructor(layout: ViewGroup) { + this.layout = layout + val childCount = this.layout.childCount + for (i in 0 until childCount) { + val child = this.layout.getChildAt(i) + child.addOnLayoutChangeListener(layoutListener) + } + } + + private fun onDesiredLocationChanged(v: View, rect: Rect) { + if (!sizeAnimationPending) { + applyBounds(v, rect, animate = false) + } + // We need to reapply the current bounds in every frame since the layout may override + // the layout bounds making this view jump and not all calls to apply bounds actually + // reapply them, for example if there's already an animator to the same target + reapplyProperty(v, AnimatableProperty.ABSOLUTE_X); + reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y); + reapplyProperty(v, AnimatableProperty.WIDTH); + reapplyProperty(v, AnimatableProperty.HEIGHT); + } + + private fun reapplyProperty(v: View, property: AnimatableProperty) { + property.property.set(v, property.property.get(v)) + } + + private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) { + PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(), + animationProperties, animate) + PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(), + animationProperties, animate) + } + + private fun startBoundAnimation(v: View) { + val target = desiredBounds[v] ?: return + applyBounds(v, target, animate = true) + } + + fun animatePendingSizeChange(duration: Long, delay: Long) { + animationProperties.duration = duration + animationProperties.delay = delay + if (!sizeAnimationPending) { + sizeAnimationPending = true + layout.viewTreeObserver.addOnPreDrawListener ( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + layout.viewTreeObserver.removeOnPreDrawListener(this) + sizeAnimationPending = false + val childCount = layout.childCount + for (i in 0 until childCount) { + val child = layout.getChildAt(i) + startBoundAnimation(child) + } + return true + } + }) + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 557132bdf08e..60c2ed2fa2be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,10 +16,8 @@ package com.android.systemui.media; -import android.annotation.LayoutRes; import android.app.PendingIntent; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -27,35 +25,38 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.graphics.Bitmap; -import android.graphics.ImageDecoder; +import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; -import android.media.MediaDescription; -import android.media.MediaMetadata; -import android.media.ThumbnailUtils; import android.media.session.MediaController; import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession; import android.media.session.PlaybackState; -import android.net.Uri; import android.service.media.MediaBrowserService; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.constraintlayout.motion.widget.Key; +import androidx.constraintlayout.motion.widget.KeyAttributes; +import androidx.constraintlayout.motion.widget.KeyFrames; +import androidx.constraintlayout.motion.widget.MotionLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import com.android.settingslib.Utils; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaOutputSliceConstants; @@ -64,23 +65,40 @@ import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.Assert; +import com.android.systemui.util.concurrency.DelayableExecutor; -import java.io.IOException; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; /** - * Base media control panel for System UI + * A view controller used for Media Playback. */ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; @Nullable private final LocalMediaManager mLocalMediaManager; + + // Button IDs for QS controls + static final int[] ACTION_IDS = { + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4 + }; + + private final SeekBarViewModel mSeekBarViewModel; + private final SeekBarObserver mSeekBarObserver; private final Executor mForegroundExecutor; protected final Executor mBackgroundExecutor; private final ActivityStarter mActivityStarter; + private final LayoutAnimationHelper mLayoutAnimationHelper; private Context mContext; - protected LinearLayout mMediaNotifView; + private MotionLayout mMediaNotifView; + private final View mBackground; private View mSeamless; private MediaSession.Token mToken; private MediaController mController; @@ -89,9 +107,11 @@ public class MediaControlPanel { private MediaDevice mDevice; protected ComponentName mServiceComponent; private boolean mIsRegistered = false; + private final List<KeyFrames> mKeyFrames; private String mKey; - - private final int[] mActionIds; + private int mAlbumArtSize; + private int mAlbumArtRadius; + private int mViewWidth; public static final String MEDIA_PREFERENCES = "media_control_prefs"; public static final String MEDIA_PREFERENCE_KEY = "browser_components"; @@ -100,22 +120,6 @@ public class MediaControlPanel { private boolean mIsRemotePlayback; private QSMediaBrowser mQSMediaBrowser; - // Button IDs used in notifications - protected static final int[] NOTIF_ACTION_IDS = { - com.android.internal.R.id.action0, - com.android.internal.R.id.action1, - com.android.internal.R.id.action2, - com.android.internal.R.id.action3, - com.android.internal.R.id.action4 - }; - - // URI fields to try loading album art from - private static final String[] ART_URIS = { - MediaMetadata.METADATA_KEY_ALBUM_ART_URI, - MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI - }; - private final MediaController.Callback mSessionCallback = new MediaController.Callback() { @Override public void onSessionDestroyed() { @@ -135,17 +139,6 @@ public class MediaControlPanel { } }; - private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View unused) { - makeActive(); - } - @Override - public void onViewDetachedFromWindow(View unused) { - makeInactive(); - } - }; - private final LocalMediaManager.DeviceCallback mDeviceCallback = new LocalMediaManager.DeviceCallback() { @Override @@ -175,41 +168,65 @@ public class MediaControlPanel { * @param context * @param parent * @param routeManager Manager used to listen for device change events. - * @param layoutId layout resource to use for this control panel - * @param actionIds resource IDs for action buttons in the layout * @param foregroundExecutor foreground executor * @param backgroundExecutor background executor, used for processing artwork * @param activityStarter activity starter */ public MediaControlPanel(Context context, ViewGroup parent, - @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds, - Executor foregroundExecutor, Executor backgroundExecutor, - ActivityStarter activityStarter) { + @Nullable LocalMediaManager routeManager, Executor foregroundExecutor, + DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) { mContext = context; LayoutInflater inflater = LayoutInflater.from(mContext); - mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false); - // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views - // mStateListener shouldn't need to be unregistered since this object shares the same - // lifecycle with the inflated view. It would be better, however, if this controller used an - // attach/detach of views instead of inflating them in the constructor, which would allow - // mStateListener to be unregistered in detach. - mMediaNotifView.addOnAttachStateChangeListener(mStateListener); + mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false); + mBackground = mMediaNotifView.findViewById(R.id.media_background); + mLayoutAnimationHelper = new LayoutAnimationHelper(mMediaNotifView); + GoneChildrenHideHelper.clipGoneChildrenOnLayout(mMediaNotifView); + mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList(); mLocalMediaManager = routeManager; - mActionIds = actionIds; mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; + mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); + mSeekBarObserver = new SeekBarObserver(getView()); + mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); + SeekBar bar = getView().findViewById(R.id.media_progress_bar); + bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); + bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); + loadDimens(); + } + + public void onDestroy() { + mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); + makeInactive(); + } + + private void loadDimens() { + mAlbumArtRadius = mContext.getResources().getDimensionPixelSize( + Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius)); + mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size); } /** * Get the view used to display media controls * @return the view */ - public View getView() { + public MotionLayout getView() { return mMediaNotifView; } /** + * Sets the listening state of the player. + * + * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid + * unnecessary work when the QS panel is closed. + * + * @param listening True when player should be active. Otherwise, false. + */ + public void setListening(boolean listening) { + mSeekBarViewModel.setListening(listening); + } + + /** * Get the context * @return context */ @@ -218,20 +235,12 @@ public class MediaControlPanel { } /** - * Update the media panel view for the given media session - * @param token - * @param iconDrawable - * @param largeIcon - * @param iconColor - * @param bgColor - * @param contentIntent - * @param appNameString - * @param key + * Bind this view based on the data given */ - public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon, - int iconColor, int bgColor, PendingIntent contentIntent, String appNameString, - String key) { - // Ensure that component names are updated if token has changed + public void bind(@NotNull MediaData data) { + MediaSession.Token token = data.getToken(); + mForegroundColor = data.getForegroundColor(); + mBackgroundColor = data.getBackgroundColor(); if (mToken == null || !mToken.equals(token)) { if (mQSMediaBrowser != null) { Log.d(TAG, "Disconnecting old media browser"); @@ -243,20 +252,21 @@ public class MediaControlPanel { mCheckedForResumption = false; } - mForegroundColor = iconColor; - mBackgroundColor = bgColor; mController = new MediaController(mContext, mToken); - mKey = key; + + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); // Try to find a browser service component for this app // TODO also check for a media button receiver intended for restarting (b/154127084) // Only check if we haven't tried yet or the session token changed - final String pkgName = mController.getPackageName(); + final String pkgName = data.getPackageName(); if (mServiceComponent == null && !mCheckedForResumption) { Log.d(TAG, "Checking for service component"); PackageManager pm = mContext.getPackageManager(); Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE); List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0); + // TODO: look into this resumption if (resumeInfo != null) { for (ResolveInfo inf : resumeInfo) { if (inf.serviceInfo.packageName.equals(mController.getPackageName())) { @@ -271,25 +281,51 @@ public class MediaControlPanel { mController.registerCallback(mSessionCallback); - mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); + mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList( + ColorStateList.valueOf(mBackgroundColor)); // Click action - if (contentIntent != null) { + PendingIntent clickIntent = data.getClickIntent(); + if (clickIntent != null) { mMediaNotifView.setOnClickListener(v -> { - mActivityStarter.postStartActivityDismissingKeyguard(contentIntent); + mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); }); } + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + // TODO: migrate this to a view with rounded corners instead of baking the rounding + // into the bitmap + Drawable artwork = createRoundedBitmap(data.getArtwork()); + albumView.setImageDrawable(artwork); + // App icon - ImageView appIcon = mMediaNotifView.findViewById(R.id.icon); + ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon); + Drawable iconDrawable = data.getAppIcon().mutate(); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); + // Song name + TextView titleText = mMediaNotifView.requireViewById(R.id.header_title); + titleText.setText(data.getSong()); + titleText.setTextColor(data.getForegroundColor()); + + // App title + TextView appName = mMediaNotifView.requireViewById(R.id.app_name); + appName.setText(data.getApp()); + appName.setTextColor(mForegroundColor); + + // Artist name + TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist); + artistText.setText(data.getArtist()); + artistText.setTextColor(mForegroundColor); + // Transfer chip mSeamless = mMediaNotifView.findViewById(R.id.media_seamless); if (mSeamless != null) { if (mLocalMediaManager != null) { mSeamless.setVisibility(View.VISIBLE); + setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); + setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); updateDevice(mLocalMediaManager.getCurrentConnectedDevice()); mSeamless.setOnClickListener(v -> { final Intent intent = new Intent() @@ -311,43 +347,110 @@ public class MediaControlPanel { Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback."); mIsRemotePlayback = false; } + List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact(); + // Media controls + int i = 0; + List<MediaAction> actionIcons = data.getActions(); + for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) { + int actionId = ACTION_IDS[i]; + final ImageButton button = mMediaNotifView.findViewById(actionId); + MediaAction mediaAction = actionIcons.get(i); + button.setImageDrawable(mediaAction.getDrawable()); + button.setContentDescription(mediaAction.getContentDescription()); + button.setImageTintList(ColorStateList.valueOf(mForegroundColor)); + PendingIntent actionIntent = mediaAction.getIntent(); + + if (mBackground.getBackground() instanceof IlluminationDrawable) { + ((IlluminationDrawable) mBackground.getBackground()) + .setupTouch(button, mMediaNotifView); + } - makeActive(); + button.setOnClickListener(v -> { + if (actionIntent != null) { + try { + actionIntent.send(); + } catch (PendingIntent.CanceledException e) { + e.printStackTrace(); + } + } + }); + boolean visibleInCompat = actionsWhenCollapsed.contains(i); + updateKeyFrameVisibility(actionId, visibleInCompat); + setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat); + setVisibleAndAlpha(expandedSet, actionId, true /*visible */); + } - // App title (not in mini player) - TextView appName = mMediaNotifView.findViewById(R.id.app_name); - if (appName != null) { - appName.setText(appNameString); - appName.setTextColor(mForegroundColor); + // Hide any unused buttons + for (; i < ACTION_IDS.length; i++) { + setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); } - // Can be null! - MediaMetadata mediaMetadata = mController.getMetadata(); + // Seek Bar + final MediaController controller = getController(); + mBackgroundExecutor.execute( + () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor())); - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView)); - } + // Set up long press menu + // TODO: b/156036025 bring back media guts - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - String songName = ""; - if (mediaMetadata != null) { - songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + makeActive(); + + // Update both constraint sets to regenerate the animation. + mMediaNotifView.updateState(R.id.collapsed, collapsedSet); + mMediaNotifView.updateState(R.id.expanded, expandedSet); + } + + @UiThread + private Drawable createRoundedBitmap(Icon icon) { + if (icon == null) { + return null; } - titleText.setText(songName); - titleText.setTextColor(mForegroundColor); - - // Artist name (not in mini player) - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - String artistName = ""; - if (mediaMetadata != null) { - artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); + // Let's scale down the View, such that the content always nicely fills the view. + // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect + // ratios + Drawable drawable = icon.loadDrawable(mContext); + float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); + Rect bounds; + if (aspectRatio > 1.0f) { + bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio)); + } else { + bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize); + } + if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) { + float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f; + float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f; + bounds.offset((int) -offsetX,(int) -offsetY); + } + drawable.setBounds(bounds); + Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(scaled); + drawable.draw(canvas); + RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create( + mContext.getResources(), scaled); + artwork.setCornerRadius(mAlbumArtRadius); + return artwork; + } + + /** + * Updates the keyframe visibility such that only views that are not visible actually go + * through a transition and fade in. + * + * @param actionId the id to change + * @param visible is the view visible + */ + private void updateKeyFrameVisibility(int actionId, boolean visible) { + for (int i = 0; i < mKeyFrames.size(); i++) { + KeyFrames keyframe = mKeyFrames.get(i); + ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId); + for (int j = 0; j < viewKeyFrames.size(); j++) { + Key key = viewKeyFrames.get(j); + if (key instanceof KeyAttributes) { + KeyAttributes attributes = (KeyAttributes) key; + attributes.setValue("alpha", visible ? 1.0f : 0.0f); + } } - artistText.setText(artistName); - artistText.setTextColor(mForegroundColor); } } @@ -421,120 +524,6 @@ public class MediaControlPanel { } /** - * Process album art for layout - * @param description media description - * @param albumView view to hold the album art - */ - protected void processAlbumArt(MediaDescription description, ImageView albumView) { - Bitmap albumArt = null; - - // First try loading from URI - albumArt = loadBitmapFromUri(description.getIconUri()); - - // Then check bitmap - if (albumArt == null) { - albumArt = description.getIconBitmap(); - } - - processAlbumArtInternal(albumArt, albumView); - } - - /** - * Process album art for layout - * @param metadata media metadata - * @param largeIcon from notification, checked as a fallback if metadata does not have art - * @param albumView view to hold the album art - */ - private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) { - Bitmap albumArt = null; - - if (metadata != null) { - // First look in URI fields - for (String field : ART_URIS) { - String uriString = metadata.getString(field); - if (!TextUtils.isEmpty(uriString)) { - albumArt = loadBitmapFromUri(Uri.parse(uriString)); - if (albumArt != null) { - Log.d(TAG, "loaded art from " + field); - break; - } - } - } - - // Then check bitmap field - if (albumArt == null) { - albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - } - } - - // Finally try the notification's largeIcon - if (albumArt == null && largeIcon != null) { - albumArt = largeIcon.getBitmap(); - } - - processAlbumArtInternal(albumArt, albumView); - } - - /** - * Load a bitmap from a URI - * @param uri - * @return bitmap, or null if couldn't be loaded - */ - private Bitmap loadBitmapFromUri(Uri uri) { - // ImageDecoder requires a scheme of the following types - if (uri.getScheme() == null) { - return null; - } - - if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT) - && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE) - && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { - return null; - } - - ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri); - try { - return ImageDecoder.decodeBitmap(source); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } - - /** - * Resize and crop the image if provided and update the control view - * @param albumArt Bitmap of art to display, or null to hide view - * @param albumView View that will hold the art - */ - private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) { - // Resize - RoundedBitmapDrawable roundedDrawable = null; - if (albumArt != null) { - float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); - Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true); - int albumSize = (int) mContext.getResources().getDimension( - R.dimen.qs_media_album_size); - Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize); - roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); - roundedDrawable.setCornerRadius(radius); - } else { - Log.e(TAG, "No album art available"); - } - - // Now that it's resized, update the UI - final RoundedBitmapDrawable result = roundedDrawable; - mForegroundExecutor.execute(() -> { - if (result != null) { - albumView.setImageDrawable(result); - albumView.setVisibility(View.VISIBLE); - } else { - albumView.setImageDrawable(null); - albumView.setVisibility(View.GONE); - } - }); - } - - /** * Update the current device information * @param device device information to display */ @@ -613,15 +602,16 @@ public class MediaControlPanel { */ protected void resetButtons() { // Hide all the old buttons - for (int i = 0; i < mActionIds.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); - if (thisBtn != null) { - thisBtn.setVisibility(View.GONE); - } + + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + for (int i = 1; i < ACTION_IDS.length; i++) { + setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); } // Add a restart button - ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]); + ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]); btn.setOnClickListener(v -> { Log.d(TAG, "Attempting to restart session"); if (mQSMediaBrowser != null) { @@ -643,7 +633,25 @@ public class MediaControlPanel { }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); - btn.setVisibility(View.VISIBLE); + setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */); + setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */); + + mSeekBarViewModel.clearController(); + // TODO: fix guts + // View guts = mMediaNotifView.findViewById(R.id.media_guts); + View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); + + mMediaNotifView.setOnLongClickListener(v -> { + // Replace player view with close/cancel view +// guts.setVisibility(View.GONE); + options.setVisibility(View.VISIBLE); + return true; // consumed click + }); + } + + private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { + set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE); + set.setAlpha(actionId, visible ? 1.0f : 0.0f); } private void makeActive() { @@ -667,7 +675,6 @@ public class MediaControlPanel { mIsRegistered = false; } } - /** * Verify that we can connect to the given component with a MediaBrowser, and if so, add that * component to the list of resumption components @@ -739,4 +746,25 @@ public class MediaControlPanel { * Called when a player can't be resumed to give it an opportunity to hide or remove itself */ protected void removePlayer() { } + + public void measure(@Nullable MediaMeasurementInput input) { + if (input != null) { + int width = input.getWidth(); + setPlayerWidth(width); + mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec()); + } + } + + public void setPlayerWidth(int width) { + ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded); + ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed); + collapsedSet.setGuidelineBegin(R.id.view_width, width); + expandedSet.setGuidelineBegin(R.id.view_width, width); + mMediaNotifView.updateState(R.id.collapsed, collapsedSet); + mMediaNotifView.updateState(R.id.expanded, expandedSet); + } + + public void animatePendingSizeChange(long duration, long startDelay) { + mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 487c29573a14..6a2646170e85 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMedia.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -11,23 +11,36 @@ * 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. + * limitations under the License */ -package com.android.keyguard +package com.android.systemui.media +import android.app.PendingIntent import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.media.session.MediaSession -import java.util.List - -/** State for lock screen media controls. */ -data class KeyguardMedia( +/** State of a media view. */ +data class MediaData( + val initialized: Boolean = false, val foregroundColor: Int, val backgroundColor: Int, val app: String?, val appIcon: Drawable?, - val artist: String?, - val song: String?, - val artwork: Drawable?, - val actionIcons: List<Drawable> + val artist: CharSequence?, + val song: CharSequence?, + val artwork: Icon?, + val actions: List<MediaAction>, + val actionsToShowInCompact: List<Int>, + val packageName: String?, + val token: MediaSession.Token?, + val clickIntent: PendingIntent? +) + +/** State of a media action. */ +data class MediaAction( + val drawable: Drawable?, + val intent: PendingIntent?, + val contentDescription: CharSequence? ) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt new file mode 100644 index 000000000000..e7d0f7ec1a37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -0,0 +1,306 @@ +/* + * 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 + +import android.app.Notification +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ImageDecoder +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.media.MediaMetadata +import android.media.session.MediaSession +import android.net.Uri +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.text.TextUtils +import android.util.Log +import com.android.internal.util.ContrastColorUtil +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.notification.MediaNotificationProcessor +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import java.io.IOException +import java.util.* +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.collections.LinkedHashMap + +// URI fields to try loading album art from +private val ART_URIS = arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI +) + +private const val TAG = "MediaDataManager" + +private val LOADING = MediaData(false, 0, 0, null, null, null, null, null, + emptyList(), emptyList(), null, null, null) + +/** + * A class that facilitates management and loading of Media Data, ready for binding. + */ +@Singleton +class MediaDataManager @Inject constructor( + private val context: Context, + private val mediaControllerFactory: MediaControllerFactory, + @Background private val backgroundExecutor: Executor, + @Main private val foregroundExcecutor: Executor +) { + + private val listeners: MutableSet<Listener> = mutableSetOf() + private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + + fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (isMediaNotification(sbn)) { + if (!mediaEntries.containsKey(key)) { + mediaEntries.put(key, LOADING) + } + loadMediaData(key, sbn) + } else { + onNotificationRemoved(key) + } + } + + private fun loadMediaData(key: String, sbn: StatusBarNotification) { + backgroundExecutor.execute { + loadMediaDataInBg(key, sbn) + } + } + + /** + * Add a listener for changes in this class + */ + fun addListener(listener: Listener) = listeners.add(listener) + + /** + * Remove a listener for changes in this class + */ + fun removeListener(listener: Listener) = listeners.remove(listener) + + private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) { + val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) + as MediaSession.Token? + val metadata = mediaControllerFactory.create(token).metadata + + if (metadata == null) { + // TODO: handle this better, removing media notification + return + } + + // Foreground and Background colors computed from album art + val notif: Notification = sbn.notification + var fgColor = notif.color + var bgColor = -1 + var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART) + if (artworkBitmap == null) { + artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + if (artworkBitmap == null) { + artworkBitmap = loadBitmapFromUri(metadata) + } + val artWorkIcon = if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + if (artWorkIcon != null) { + // If we have art, get colors from that + if (artworkBitmap == null) { + if (artWorkIcon.type == Icon.TYPE_BITMAP + || artWorkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP) { + artworkBitmap = artWorkIcon.bitmap + } else { + val drawable: Drawable = artWorkIcon.loadDrawable(context) + artworkBitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888) + val canvas = Canvas(artworkBitmap) + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable.draw(canvas) + } + } + val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap) + .generate() + val swatch = MediaNotificationProcessor.findBackgroundSwatch(p) + bgColor = swatch.rgb + fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p) + } + // Make sure colors will be legible + val isDark = !ContrastColorUtil.isColorLight(bgColor) + fgColor = ContrastColorUtil.resolveContrastColor(context, fgColor, bgColor, + isDark) + fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark) + + // App name + val builder = Notification.Builder.recoverBuilder(context, notif) + val app = builder.loadHeaderAppName() + + // App Icon + val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context) + + // Song name + var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song == null) { + song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song == null) { + song = HybridGroupManager.resolveTitle(notif) + } + + // Artist name + var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist == null) { + artist = HybridGroupManager.resolveText(notif) + } + + // Control buttons + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + val actionsToShowCollapsed = notif.extras.getIntArray( + Notification.EXTRA_COMPACT_ACTIONS)?.toList() ?: emptyList() + // TODO: b/153736623 look into creating actions when this isn't a media style notification + + val packageContext: Context = sbn.getPackageContext(context) + for (action in actions) { + val mediaAction = MediaAction( + action.getIcon().loadDrawable(packageContext), + action.actionIntent, + action.title) + actionIcons.add(mediaAction) + } + + foregroundExcecutor.execute { + onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist, + song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, + notif.contentIntent)) + } + + } + + /** + * Load a bitmap from the various Art metadata URIs + */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + Log.d(TAG, "loaded art from $uri") + break + } + } + } + return null + } + + /** + * Load a bitmap from a URI + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if (uri.getScheme() == null) { + return null; + } + + if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT) + && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE) + && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { + return null; + } + + val source = ImageDecoder.createSource(context.getContentResolver(), uri) + return try { + ImageDecoder.decodeBitmap(source) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + fun onMediaDataLoaded(key: String, data: MediaData) { + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + listeners.forEach { + it.onMediaDataLoaded(key, data) + } + } + } + + fun onNotificationRemoved(key: String) { + val removed = mediaEntries.remove(key) + if (removed != null) { + listeners.forEach { + it.onMediaDataRemoved(key) + } + } + } + + private fun isMediaNotification(sbn: StatusBarNotification) : Boolean { + if (!useUniversalMediaPlayer()) { + return false + } + if (!sbn.notification.hasMediaSession()) { + return false + } + val notificationStyle = sbn.notification.notificationStyle + if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) + || Notification.MediaStyle::class.java.equals(notificationStyle)) { + return true + } + return false + } + + /** + * are we using the universal media player + */ + private fun useUniversalMediaPlayer() + = Settings.System.getInt(context.contentResolver, "qs_media_player", 1) > 0 + + /** + * Are there any media notifications active? + */ + fun hasActiveMedia() = mediaEntries.size > 0 + + fun hasAnyMedia(): Boolean { + // TODO: implement this when we implemented resumption + return hasActiveMedia() + } + + interface Listener { + + /** + * Called whenever there's new MediaData Loaded for the consumption in views + */ + fun onMediaDataLoaded(key: String, data: MediaData) {} + + /** + * Called whenever a previously existing Media notification was removed + */ + fun onMediaDataRemoved(key: String) {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt new file mode 100644 index 000000000000..6b1c520db7b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -0,0 +1,425 @@ +/* + * 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 + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.annotation.IntDef +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroupOverlay +import com.android.systemui.Interpolators +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.StackStateAnimator +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.animation.UniqueObjectHostView +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This manager is responsible for placement of the unique media view between the different hosts + * and animate the positions of the views to achieve seamless transitions. + */ +@Singleton +class MediaHierarchyManager @Inject constructor( + private val context: Context, + private val statusBarStateController: SysuiStatusBarStateController, + private val keyguardStateController: KeyguardStateController, + private val bypassController: KeyguardBypassController, + private val mediaViewManager: MediaViewManager, + private val mediaMeasurementProvider: MediaMeasurementManager +) { + /** + * The root overlay of the hierarchy. This is where the media notification is attached to + * whenever the view is transitioning from one host to another. It also make sure that the + * view is always in its final state when it is attached to a view host. + */ + private var rootOverlay: ViewGroupOverlay? = null + private lateinit var currentState: MediaState + private val mediaCarousel + get() = mediaViewManager.mediaCarousel + private var animationStartState: MediaState? = null + private var statusbarState: Int = statusBarStateController.state + private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { + interpolator = Interpolators.FAST_OUT_SLOW_IN + addUpdateListener { + updateTargetState() + applyState(animationStartState!!.interpolate(targetState!!, animatedFraction)) + } + addListener(object : AnimatorListenerAdapter() { + private var cancelled: Boolean = false + + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + } + override fun onAnimationEnd(animation: Animator?) { + if (!cancelled) { + applyTargetStateIfNotAnimating() + } + } + + override fun onAnimationStart(animation: Animator?) { + cancelled = false + } + }) + } + private var targetState: MediaState? = null + private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1) + + /** + * The last location where this view was at before going to the desired location. This is + * useful for guided transitions. + */ + @MediaLocation private var previousLocation = -1 + + /** + * The desired location where the view will be at the end of the transition. + */ + @MediaLocation private var desiredLocation = -1 + + /** + * The current attachment location where the view is currently attached. + * Usually this matches the desired location except for animations whenever a view moves + * to the new desired location, during which it is in [IN_OVERLAY]. + */ + @MediaLocation private var currentAttachmentLocation = -1 + + var qsExpansion: Float = 0.0f + set(value) { + if (field != value) { + field = value + updateDesiredLocation() + if (getQSTransformationProgress() >= 0) { + updateTargetState() + applyTargetStateIfNotAnimating() + } + } + } + + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onStatePreChange(oldState: Int, newState: Int) { + // We're updating the location before the state change happens, since we want the + // location of the previous state to still be up to date when the animation starts + statusbarState = newState + updateDesiredLocation() + } + + override fun onStateChanged(newState: Int) { + updateTargetState() + } + }) + } + + /** + * Register a media host and create a view can be attached to a view hierarchy + * and where the players will be placed in when the host is the currently desired state. + * + * @return the hostView associated with this location + */ + fun register(mediaObject: MediaHost) : ViewGroup { + val viewHost = createUniqueObjectHost(mediaObject) + mediaObject.hostView = viewHost; + mediaHosts[mediaObject.location] = mediaObject + if (mediaObject.location == desiredLocation) { + // In case we are overriding a view that is already visible, make sure we attach it + // to this new host view in the below call + desiredLocation = -1 + } + if (mediaObject.location == currentAttachmentLocation) { + currentAttachmentLocation = -1 + } + updateDesiredLocation() + return viewHost + } + + private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView { + val viewHost = UniqueObjectHostView(context) + viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host) + viewHost.onMeasureListener = { input -> + if (host.location == desiredLocation) { + // Measurement of the currently active player is happening, Let's make + // sure the player width is up to date + val measuringInput = host.getMeasuringInput(input) + mediaViewManager.setPlayerWidth(measuringInput.width) + } + } + + viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(p0: View?) { + if (rootOverlay == null) { + rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay) + } + viewHost.removeOnAttachStateChangeListener(this) + } + + override fun onViewDetachedFromWindow(p0: View?) { + } + }) + return viewHost + } + + /** + * Updates the location that the view should be in. If it changes, an animation may be triggered + * going from the old desired location to the new one. + */ + private fun updateDesiredLocation() { + val desiredLocation = calculateLocation() + if (desiredLocation != this.desiredLocation) { + if (this.desiredLocation >= 0) { + previousLocation = this.desiredLocation + } + val isNewView = this.desiredLocation == -1 + this.desiredLocation = desiredLocation + // Let's perform a transition + val animate = shouldAnimateTransition(desiredLocation, previousLocation) + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState, + animate, animDuration, delay) + performTransitionToNewLocation(isNewView, animate) + } + } + + private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) { + if (previousLocation < 0 || isNewView) { + cancelAnimationAndApplyDesiredState() + return + } + val currentHost = getHost(desiredLocation) + val previousHost = getHost(previousLocation) + if (currentHost == null || previousHost == null) { + cancelAnimationAndApplyDesiredState() + return + } + updateTargetState() + if (isCurrentlyInGuidedTransformation()) { + applyTargetStateIfNotAnimating() + } else if (animate) { + animator.cancel() + if (currentAttachmentLocation == IN_OVERLAY + || !previousHost.hostView.isAttachedToWindow) { + // Let's animate to the new position, starting from the current position + // We also go in here in case the view was detached, since the bounds wouldn't + // be correct anymore + animationStartState = currentState.copy() + } else { + // otherwise, let's take the freshest state, since the current one could + // be outdated + animationStartState = previousHost.currentState.copy() + } + adjustAnimatorForTransition(desiredLocation, previousLocation) + animator.start() + } else { + cancelAnimationAndApplyDesiredState() + } + } + + private fun shouldAnimateTransition( + @MediaLocation currentLocation: Int, + @MediaLocation previousLocation: Int + ): Boolean { + if (currentLocation == LOCATION_QQS + && previousLocation == LOCATION_LOCKSCREEN + && (statusBarStateController.leaveOpenOnKeyguardHide() + || statusbarState == StatusBarState.SHADE_LOCKED)) { + // Usually listening to the isShown is enough to determine this, but there is some + // non-trivial reattaching logic happening that will make the view not-shown earlier + return true + } + return mediaCarousel.isShown || animator.isRunning + } + + private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) { + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + animator.apply { + duration = animDuration + startDelay = delay + } + + } + + private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> { + var animDuration = 200L + var delay = 0L + if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { + // Going to the full shade, let's adjust the animation duration + if (statusbarState == StatusBarState.SHADE + && keyguardStateController.isKeyguardFadingAway) { + delay = keyguardStateController.keyguardFadingAwayDelay + } + animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong() + } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) { + animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong() + } + return animDuration to delay + } + + private fun applyTargetStateIfNotAnimating() { + if (!animator.isRunning) { + // Let's immediately apply the target state (which is interpolated) if there is + // no animation running. Otherwise the animation update will already update + // the location + applyState(targetState!!) + } + } + + /** + * Updates the state that the view wants to be in at the end of the animation. + */ + private fun updateTargetState() { + if (isCurrentlyInGuidedTransformation()) { + val progress = getTransformationProgress() + val currentHost = getHost(desiredLocation)!! + val previousHost = getHost(previousLocation)!! + val newState = currentHost.currentState + val previousState = previousHost.currentState + targetState = previousState.interpolate(newState, progress) + } else { + targetState = getHost(desiredLocation)?.currentState + } + } + + /** + * @return true if this transformation is guided by an external progress like a finger + */ + private fun isCurrentlyInGuidedTransformation() : Boolean { + return getTransformationProgress() >= 0 + } + + /** + * @return the current transformation progress if we're in a guided transformation and -1 + * otherwise + */ + private fun getTransformationProgress(): Float { + val progress = getQSTransformationProgress() + if (progress >= 0) { + return progress + } + return -1.0f + } + + private fun getQSTransformationProgress(): Float { + val currentHost = getHost(desiredLocation) + val previousHost = getHost(previousLocation) + if (currentHost?.location == LOCATION_QS) { + if (previousHost?.location == LOCATION_QQS) { + return qsExpansion + } + } + return -1.0f + } + + private fun getHost(@MediaLocation location: Int): MediaHost? { + if (location < 0) { + return null + } + return mediaHosts[location] + } + + private fun cancelAnimationAndApplyDesiredState() { + animator.cancel() + getHost(desiredLocation)?.let { + applyState(it.currentState) + } + } + + private fun applyState(state: MediaState) { + currentState = state.copy() + mediaViewManager.setCurrentState(currentState) + updateHostAttachment() + if (currentAttachmentLocation == IN_OVERLAY) { + val boundsOnScreen = state.boundsOnScreen + mediaCarousel.setLeftTopRightBottom( + boundsOnScreen.left, + boundsOnScreen.top, + boundsOnScreen.right, + boundsOnScreen.bottom) + } + } + + private fun updateHostAttachment() { + val inOverlay = isTransitionRunning() && rootOverlay != null + val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation + if (currentAttachmentLocation != newLocation) { + currentAttachmentLocation = newLocation + + // Remove the carousel from the old host + (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel) + + // Add it to the new one + val targetHost = getHost(desiredLocation)!!.hostView + if (inOverlay) { + rootOverlay!!.add(mediaCarousel) + } else { + targetHost.addView(mediaCarousel) + mediaViewManager.onViewReattached() + } + } + } + + private fun isTransitionRunning(): Boolean { + return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f + || animator.isRunning + } + + @MediaLocation + private fun calculateLocation() : Int { + val onLockscreen = (!bypassController.bypassEnabled + && (statusbarState == StatusBarState.KEYGUARD + || statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) + return when { + qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS + qsExpansion > 0.4f && onLockscreen -> LOCATION_QS + onLockscreen -> LOCATION_LOCKSCREEN + else -> LOCATION_QQS + } + } + + /** + * The expansion of quick settings + */ + @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN]) + @Retention(AnnotationRetention.SOURCE) + annotation class MediaLocation + + companion object { + /** + * Attached in expanded quick settings + */ + const val LOCATION_QS = 0 + + /** + * Attached in the collapsed QS + */ + const val LOCATION_QQS = 1 + + /** + * Attached on the lock screen + */ + const val LOCATION_LOCKSCREEN = 2 + + /** + * Attached at the root of the hierarchy in an overlay + */ + const val IN_OVERLAY = -1000 + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt new file mode 100644 index 000000000000..6e7b6bcb7502 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -0,0 +1,159 @@ +package com.android.systemui.media + +import android.graphics.Rect +import android.util.MathUtils +import android.view.View +import android.view.View.OnAttachStateChangeListener +import android.view.ViewGroup +import com.android.systemui.media.MediaHierarchyManager.MediaLocation +import com.android.systemui.util.animation.MeasurementInput +import javax.inject.Inject + +class MediaHost @Inject constructor( + private val state: MediaHostState, + private val mediaHierarchyManager: MediaHierarchyManager, + private val mediaDataManager: MediaDataManager +) : MediaState by state { + lateinit var hostView: ViewGroup + var location: Int = -1 + private set + var visibleChangedListener: ((Boolean) -> Unit)? = null + var visible: Boolean = false + private set + + private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0) + + /** + * Get the current Media state. This also updates the location on screen + */ + val currentState : MediaState + get () { + hostView.getLocationOnScreen(tmpLocationOnScreen) + var left = tmpLocationOnScreen[0] + hostView.paddingLeft + var top = tmpLocationOnScreen[1] + hostView.paddingTop + var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight + var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom + // Handle cases when the width or height is 0 but it has padding. In those cases + // the above could return negative widths, which is wrong + if (right < left) { + left = 0 + right = 0; + } + if (bottom < top) { + bottom = 0 + top = 0; + } + state.boundsOnScreen.set(left, top, right, bottom) + return state + } + + private val listener = object : MediaDataManager.Listener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateViewVisibility() + } + + override fun onMediaDataRemoved(key: String) { + updateViewVisibility() + } + } + + /** + * Initialize this MediaObject and create a host view. + * + * @param location the location this host name has. Used to identify the host during + * transitions. + */ + fun init(@MediaLocation location: Int) { + this.location = location; + hostView = mediaHierarchyManager.register(this) + hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) { + mediaDataManager.addListener(listener) + updateViewVisibility() + } + + override fun onViewDetachedFromWindow(v: View?) { + mediaDataManager.removeListener(listener) + } + }) + updateViewVisibility() + } + + private fun updateViewVisibility() { + if (showsOnlyActiveMedia) { + visible = mediaDataManager.hasActiveMedia() + } else { + visible = mediaDataManager.hasAnyMedia() + } + hostView.visibility = if (visible) View.VISIBLE else View.GONE + visibleChangedListener?.invoke(visible) + } + + class MediaHostState @Inject constructor() : MediaState { + var measurementInput: MediaMeasurementInput? = null + override var expansion: Float = 0.0f + override var showsOnlyActiveMedia: Boolean = false + override val boundsOnScreen: Rect = Rect() + + override fun copy() : MediaState { + val mediaHostState = MediaHostState() + mediaHostState.expansion = expansion + mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia + mediaHostState.boundsOnScreen.set(boundsOnScreen) + mediaHostState.measurementInput = measurementInput + return mediaHostState + } + + override fun interpolate(other: MediaState, amount: Float) : MediaState { + val result = MediaHostState() + result.expansion = MathUtils.lerp(expansion, other.expansion, amount) + val left = MathUtils.lerp(boundsOnScreen.left.toFloat(), + other.boundsOnScreen.left.toFloat(), amount).toInt() + val top = MathUtils.lerp(boundsOnScreen.top.toFloat(), + other.boundsOnScreen.top.toFloat(), amount).toInt() + val right = MathUtils.lerp(boundsOnScreen.right.toFloat(), + other.boundsOnScreen.right.toFloat(), amount).toInt() + val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(), + other.boundsOnScreen.bottom.toFloat(), amount).toInt() + result.boundsOnScreen.set(left, top, right, bottom) + result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia + if (amount > 0.0f) { + if (other is MediaHostState) { + result.measurementInput = other.measurementInput + } + } else { + result.measurementInput + } + return result + } + + override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput { + measurementInput = MediaMeasurementInput(input, expansion) + return measurementInput as MediaMeasurementInput + } + } +} + +interface MediaState { + var expansion: Float + var showsOnlyActiveMedia: Boolean + val boundsOnScreen: Rect + fun copy() : MediaState + fun interpolate(other: MediaState, amount: Float) : MediaState + fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput +} +/** + * The measurement input for a Media View + */ +data class MediaMeasurementInput( + private val viewInput: MeasurementInput, + val expansion: Float) : MeasurementInput by viewInput { + + override fun sameAs(input: MeasurementInput?): Boolean { + if (!(input is MediaMeasurementInput)) { + return false + } + return width == input.width && expansion == input.expansion + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt new file mode 100644 index 000000000000..4bbf5eb9f0dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt @@ -0,0 +1,59 @@ +/* + * 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 + +import com.android.systemui.util.animation.BaseMeasurementCache +import com.android.systemui.util.animation.GuaranteedMeasurementCache +import com.android.systemui.util.animation.MeasurementCache +import com.android.systemui.util.animation.MeasurementInput +import com.android.systemui.util.animation.MeasurementOutput +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class responsible creating measurement caches for media hosts which also coordinates with + * the view manager to obtain sizes for unknown measurement inputs. + */ +@Singleton +class MediaMeasurementManager @Inject constructor( + private val mediaViewManager: MediaViewManager +) { + private val baseCache: MeasurementCache + + init { + baseCache = BaseMeasurementCache() + } + + private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? { + return mediaViewManager.obtainMeasurement(input) + } + + /** + * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that + * requesting any size from the cache will always return the correct value. + */ + fun obtainCache(host: MediaState): GuaranteedMeasurementCache { + val remapper = { input: MeasurementInput -> + host.getMeasuringInput(input) + } + val provider = { input: MeasurementInput -> + provideMeasurement(input as MediaMeasurementInput) + } + return GuaranteedMeasurementCache(baseCache, remapper, provider) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt new file mode 100644 index 000000000000..49d2d8860a2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -0,0 +1,302 @@ +package com.android.systemui.media + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.HorizontalScrollView +import android.widget.LinearLayout +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.media.InfoMediaManager +import com.android.settingslib.media.LocalMediaManager +import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.notification.VisualStabilityManager +import com.android.systemui.util.animation.MeasurementOutput +import com.android.systemui.util.animation.UniqueObjectHostView +import com.android.systemui.util.concurrency.DelayableExecutor +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Class that is responsible for keeping the view carousel up to date. + * This also handles changes in state and applies them to the media carousel like the expansion. + */ +@Singleton +class MediaViewManager @Inject constructor( + private val context: Context, + @Main private val foregroundExecutor: Executor, + @Background private val backgroundExecutor: DelayableExecutor, + private val localBluetoothManager: LocalBluetoothManager?, + private val visualStabilityManager: VisualStabilityManager, + private val activityStarter: ActivityStarter, + mediaManager: MediaDataManager +) { + private var playerWidth: Int = 0 + private var playerWidthPlusPadding: Int = 0 + private var desiredState: MediaHost.MediaHostState? = null + private var currentState: MediaState? = null + val mediaCarousel: HorizontalScrollView + private val mediaContent: ViewGroup + private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() + private val visualStabilityCallback = ::reorderAllPlayers + private var activeMediaIndex: Int = 0 + private var scrollIntoCurrentMedia: Int = 0 + + private var currentlyExpanded = true + set(value) { + if (field != value) { + field = value + for (player in mediaPlayers.values) { + player.setListening(field) + } + } + } + private val scrollChangedListener = object : View.OnScrollChangeListener { + override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int, + oldScrollY: Int) { + if (playerWidthPlusPadding == 0) { + return + } + onMediaScrollingChanged(scrollX / playerWidthPlusPadding, + scrollX % playerWidthPlusPadding) + } + } + + init { + mediaCarousel = inflateMediaCarousel() + mediaCarousel.setOnScrollChangeListener(scrollChangedListener) + mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) + mediaManager.addListener(object : MediaDataManager.Listener { + override fun onMediaDataLoaded(key: String, data: MediaData) { + updateView(key, data) + updatePlayerVisibilities() + } + + override fun onMediaDataRemoved(key: String) { + val removed = mediaPlayers.remove(key) + removed?.apply { + val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex + mediaContent.removeView(removed.view) + removed.onDestroy() + updateMediaPaddings() + if (beforeActive) { + // also update the index here since the scroll below might not always lead + // to a scrolling changed + activeMediaIndex = Math.max(0, activeMediaIndex - 1) + mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX + - playerWidthPlusPadding, 0) + } + updatePlayerVisibilities() + } + } + }) + } + + private fun inflateMediaCarousel(): HorizontalScrollView { + return LayoutInflater.from(context).inflate(R.layout.media_carousel, + UniqueObjectHostView(context), false) as HorizontalScrollView + } + + private fun reorderAllPlayers() { + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { + mediaContent.removeView(view) + mediaContent.addView(view, 0) + } + } + updateMediaPaddings() + updatePlayerVisibilities() + } + + private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) { + val wasScrolledIn = scrollIntoCurrentMedia != 0 + scrollIntoCurrentMedia = scrollInAmount + val nowScrolledIn = scrollIntoCurrentMedia != 0 + if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { + activeMediaIndex = newIndex + updatePlayerVisibilities() + } + } + + private fun updatePlayerVisibilities() { + val scrolledIn = scrollIntoCurrentMedia != 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn) + view.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + } + + private fun updateView(key: String, data: MediaData) { + var existingPlayer = mediaPlayers[key] + if (existingPlayer == null) { + // Set up listener for device changes + // TODO: integrate with MediaTransferManager? + val imm = InfoMediaManager(context, data.packageName, + null /* notification */, localBluetoothManager) + val routeManager = LocalMediaManager(context, localBluetoothManager, + imm, data.packageName) + + existingPlayer = MediaControlPanel(context, mediaContent, routeManager, + foregroundExecutor, backgroundExecutor, activityStarter) + mediaPlayers[key] = existingPlayer + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + existingPlayer.view.setLayoutParams(lp) + existingPlayer.setListening(currentlyExpanded) + if (existingPlayer.isPlaying) { + mediaContent.addView(existingPlayer.view, 0) + } else { + mediaContent.addView(existingPlayer.view) + } + updatePlayerToCurrentState(existingPlayer) + } else if (existingPlayer.isPlaying && + mediaContent.indexOfChild(existingPlayer.view) != 0) { + if (visualStabilityManager.isReorderingAllowed) { + mediaContent.removeView(existingPlayer.view) + mediaContent.addView(existingPlayer.view, 0) + } else { + visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback) + } + } + existingPlayer.bind(data) + // Resetting the progress to make sure it's taken into account for the latest + // motion model + existingPlayer.view.progress = currentState?.expansion ?: 0.0f + updateMediaPaddings() + } + + private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) { + if (desiredState != null && desiredState!!.measurementInput != null) { + // make sure the player width is set to the current state + existingPlayer.setPlayerWidth(playerWidth) + } + } + + private fun updateMediaPaddings() { + val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + val childCount = mediaContent.childCount + for (i in 0 until childCount) { + val mediaView = mediaContent.getChildAt(i) + val desiredPaddingEnd = if (i == childCount - 1) 0 else padding + val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams + if (layoutParams.marginEnd != desiredPaddingEnd) { + layoutParams.marginEnd = desiredPaddingEnd + mediaView.layoutParams = layoutParams + } + } + + } + + /** + * Set the current state of a view. This is updated often during animations and we shouldn't + * do anything expensive. + */ + fun setCurrentState(state: MediaState) { + currentState = state + currentlyExpanded = state.expansion > 0 + for (mediaPlayer in mediaPlayers.values) { + val view = mediaPlayer.view + view.progress = state.expansion + } + } + + /** + * The desired location of this view has changed. We should remeasure the view to match + * the new bounds and kick off bounds animations if necessary. + * If an animation is happening, an animation is kicked of externally, which sets a new + * current state until we reach the targetState. + * + * @param desiredState the target state we're transitioning to + * @param animate should this be animated + */ + fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long, + startDelay: Long) { + if (desiredState is MediaHost.MediaHostState) { + // This is a hosting view, let's remeasure our players + this.desiredState = desiredState + val width = desiredState.boundsOnScreen.width() + if (playerWidth != width) { + setPlayerWidth(width) + for (mediaPlayer in mediaPlayers.values) { + if (animate && mediaPlayer.view.visibility == View.VISIBLE) { + mediaPlayer.animatePendingSizeChange(duration, startDelay) + } + } + val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0 + var left = 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + view.measure(widthSpec, heightSpec) + view.layout(left, 0, left + width, view.measuredHeight) + left = left + playerWidthPlusPadding + } + } + } + } + + fun setPlayerWidth(width: Int) { + if (width != playerWidth) { + playerWidth = width + playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize( + R.dimen.qs_media_padding) + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.setPlayerWidth(width) + } + // The player width has changed, let's update the scroll position to make sure + // it's still at the same place + var newScroll = activeMediaIndex * playerWidthPlusPadding + if (scrollIntoCurrentMedia > playerWidthPlusPadding) { + newScroll += playerWidthPlusPadding + - (scrollIntoCurrentMedia - playerWidthPlusPadding) + } else { + newScroll += scrollIntoCurrentMedia + } + mediaCarousel.scrollX = newScroll + } + } + + /** + * Get a measurement for the given input state. This measures the first player and returns + * its bounds as if it were measured with the given measurement dimensions + */ + fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? { + val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null + // Let's measure the size of the first player and return its height + val previousProgress = firstPlayer.view.progress + val previousRight = firstPlayer.view.right + val previousBottom = firstPlayer.view.bottom + firstPlayer.view.progress = input.expansion + firstPlayer.measure(input) + // Relayouting is necessary in motionlayout to obtain its size properly .... + firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth, + firstPlayer.view.measuredHeight) + val result = MeasurementOutput(firstPlayer.view.measuredWidth, + firstPlayer.view.measuredHeight) + firstPlayer.view.progress = previousProgress + if (desiredState != null) { + // remeasure it to the old size again! + firstPlayer.measure(desiredState!!.measurementInput) + firstPlayer.view.layout(0, 0, previousRight, previousBottom) + } + return result + } + + fun onViewReattached() { + if (desiredState is MediaHost.MediaHostState) { + // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of + // a measure to force it. + val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0 + for (mediaPlayer in mediaPlayers.values) { + mediaPlayer.view.measure(widthSpec, heightSpec) + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt new file mode 100644 index 000000000000..8efc9549068a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt @@ -0,0 +1,31 @@ +package com.android.systemui.media + +import android.content.Context +import android.util.AttributeSet +import android.widget.HorizontalScrollView + +/** + * A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful + * when only measuring children but not the parent, when trying to apply a new scroll position + */ +class UnboundHorizontalScrollView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) + : HorizontalScrollView(context, attrs, defStyleAttr) { + + /** + * Allow all scrolls to go through, use base implementation + */ + override fun scrollTo(x: Int, y: Int) { + if (mScrollX != x || mScrollY != y) { + val oldX: Int = mScrollX + val oldY: Int = mScrollY + mScrollX = x + mScrollY = y + invalidateParentCaches() + onScrollChanged(mScrollX, mScrollY, oldX, oldY) + if (!awakenScrollBars()) { + postInvalidateOnAnimation() + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt index c5ae3ab2c9fb..40d317c7bb22 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt @@ -117,7 +117,7 @@ class DoubleLineTileLayout( it.tileView.measure(exactly(smallTileSize), exactly(smallTileSize)) } - val height = twoLineHeight + val height = twoLineHeight + paddingBottom + paddingTop setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index a0ea7fae493d..ce002297e1a1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -269,14 +269,6 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha count++; } - - if (Utils.useQsMediaPlayer(mQsPanel.getContext())) { - View qsMediaView = mQsPanel.getMediaPanel(); - View qqsMediaView = mQuickQsPanel.getMediaPlayer().getView(); - translationXBuilder.addFloat(qsMediaView, "alpha", 0, 1); - translationXBuilder.addFloat(qqsMediaView, "alpha", 1, 0); - } - if (mAllowFancy) { // Make brightness appear static position and alpha in through second half. View brightness = mQsPanel.getBrightnessView(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index be8a8fd26150..6b0775f6c2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -42,7 +42,7 @@ public class QSContainerImpl extends FrameLayout { private QuickStatusBarHeader mHeader; private float mQsExpansion; private QSCustomizer mQSCustomizer; - private View mQSFooter; + private View mDragHandle; private View mBackground; private View mBackgroundGradient; @@ -62,7 +62,7 @@ public class QSContainerImpl extends FrameLayout { mQSDetail = findViewById(R.id.qs_detail); mHeader = findViewById(R.id.header); mQSCustomizer = findViewById(R.id.qs_customize); - mQSFooter = findViewById(R.id.qs_footer); + mDragHandle = findViewById(R.id.qs_drag_handle_view); mBackground = findViewById(R.id.quick_settings_background); mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background); mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view); @@ -167,8 +167,8 @@ public class QSContainerImpl extends FrameLayout { int height = calculateContainerHeight(); setBottom(getTop() + height); mQSDetail.setBottom(getTop() + height); - // Pin QS Footer to the bottom of the panel. - mQSFooter.setTranslationY(height - mQSFooter.getHeight()); + // Pin the drag handle to the bottom of the panel. + mDragHandle.setTranslationY(height - mDragHandle.getHeight()); mBackground.setTop(mQSPanel.getTop()); mBackground.setBottom(height); } @@ -192,13 +192,13 @@ public class QSContainerImpl extends FrameLayout { public void setExpansion(float expansion) { mQsExpansion = expansion; + mDragHandle.setAlpha(1.0f - expansion); updateExpansion(); } private void setMargins() { setMargins(mQSDetail); setMargins(mBackground); - setMargins(mQSFooter); mQSPanel.setMargins(mSideMargins); mHeader.setMargins(mSideMargins); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java index 5de6d1c42b4f..fc8e36ff22cf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -98,7 +98,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, private TouchAnimator mSettingsCogAnimator; private View mActionsContainer; - private View mDragHandle; private OnClickListener mExpandClickListener; @@ -146,7 +145,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mMultiUserSwitch = findViewById(R.id.multi_user_switch); mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); - mDragHandle = findViewById(R.id.qs_drag_handle_view); mActionsContainer = findViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); @@ -219,7 +217,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, return new TouchAnimator.Builder() .addFloat(mActionsContainer, "alpha", 0, 1) .addFloat(mEditContainer, "alpha", 0, 1) - .addFloat(mDragHandle, "alpha", 1, 0, 0) .addFloat(mPageIndicator, "alpha", 0, 1) .setStartDelay(0.15f) .build(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 5b09267a9e68..865fd079234e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -37,6 +37,7 @@ import androidx.annotation.VisibleForTesting; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.R.id; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizer; @@ -47,6 +48,7 @@ import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.InjectionInflationController; import com.android.systemui.util.LifecycleFragment; +import com.android.systemui.util.Utils; import javax.inject.Inject; @@ -91,6 +93,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca */ private int mState; private QSContainerImplController mQSContainerImplController; + private int[] mTmpLocation = new int[2]; @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, @@ -377,8 +380,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mLastKeyguardAndExpanded = onKeyguardAndExpanded; boolean fullyExpanded = expansion == 1; - int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom() - + mFooter.getHeight(); + int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom(); float panelTranslationY = translationScaleY * heightDiff; // Let the views animate their contents correctly by giving them the necessary context. @@ -404,6 +406,32 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mQSAnimator != null) { mQSAnimator.setPosition(expansion); } + updateMediaPositions(); + } + + private void updateMediaPositions() { + if (Utils.useQsMediaPlayer(getContext())) { + mContainer.getLocationOnScreen(mTmpLocation); + float absoluteBottomPosition = mTmpLocation[1] + mContainer.getHeight(); + pinToBottom(absoluteBottomPosition, mQSPanel.getMediaHost()); + pinToBottom(absoluteBottomPosition - mHeader.getPaddingBottom(), + mHeader.getHeaderQsPanel().getMediaHost()); + } + } + + private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost) { + View hostView = mediaHost.getHostView(); + if (mLastQSExpansion > 0) { + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) hostView.getLayoutParams(); + float targetPosition = absoluteBottomPosition - params.bottomMargin + - hostView.getHeight(); + float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top + - hostView.getTranslationY(); + hostView.setTranslationY(targetPosition - currentPosition); + } else { + hostView.setTranslationY(0); + } } private boolean headerWillBeAnimating() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java deleted file mode 100644 index 174441bdf065..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2019 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.qs; - -import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.MediaDescription; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import com.android.settingslib.media.LocalMediaManager; -import com.android.systemui.R; -import com.android.systemui.media.IlluminationDrawable; -import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.media.SeekBarObserver; -import com.android.systemui.media.SeekBarViewModel; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import java.util.concurrent.Executor; - -/** - * Single media player for carousel in QSPanel - */ -public class QSMediaPlayer extends MediaControlPanel { - - private static final String TAG = "QSMediaPlayer"; - - // Button IDs for QS controls - static final int[] QS_ACTION_IDS = { - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - }; - - private final QSPanel mParent; - private final Executor mForegroundExecutor; - private final DelayableExecutor mBackgroundExecutor; - private final SeekBarViewModel mSeekBarViewModel; - private final SeekBarObserver mSeekBarObserver; - private String mPackageName; - - /** - * Initialize quick shade version of player - * @param context - * @param parent - * @param routeManager Provides information about device - * @param foregroundExecutor - * @param backgroundExecutor - * @param activityStarter - */ - public QSMediaPlayer(Context context, ViewGroup parent, LocalMediaManager routeManager, - Executor foregroundExecutor, DelayableExecutor backgroundExecutor, - ActivityStarter activityStarter) { - super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, - foregroundExecutor, backgroundExecutor, activityStarter); - mParent = (QSPanel) parent; - mForegroundExecutor = foregroundExecutor; - mBackgroundExecutor = backgroundExecutor; - mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); - mSeekBarObserver = new SeekBarObserver(getView()); - // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust - // priority of players. As soon as it is removed, the lifecycle will end and the seek bar - // will stop updating. So, use the lifecycle of the parent instead. - mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver); - SeekBar bar = getView().findViewById(R.id.media_progress_bar); - bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); - bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); - } - - /** - * Add a media panel view based on a media description. Used for resumption - * @param description - * @param iconColor - * @param bgColor - * @param contentIntent - * @param pkgName - */ - public void setMediaSession(MediaSession.Token token, MediaDescription description, - int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) { - mPackageName = pkgName; - PackageManager pm = getContext().getPackageManager(); - Drawable icon = null; - CharSequence appName = pkgName.substring(pkgName.lastIndexOf(".")); - try { - icon = pm.getApplicationIcon(pkgName); - appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Error getting package information", e); - } - - // Set what we can normally - super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent, - appName.toString(), null); - - // Then add info from MediaDescription - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView)); - } - - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - CharSequence songName = description.getTitle(); - titleText.setText(songName); - titleText.setTextColor(iconColor); - - // Artist name (not in mini player) - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - CharSequence artistName = description.getSubtitle(); - artistText.setText(artistName); - artistText.setTextColor(iconColor); - } - - initLongPressMenu(iconColor); - - // Set buttons to resume state - resetButtons(); - } - - /** - * Update media panel view for the given media session - * @param token token for this media session - * @param icon app notification icon - * @param largeIcon notification's largeIcon, used as a fallback for album art - * @param iconColor foreground color (for text, icons) - * @param bgColor background color - * @param actionsContainer a LinearLayout containing the media action buttons - * @param contentIntent Intent to send when user taps on player - * @param appName Application title - * @param key original notification's key - */ - public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent, - String appName, String key) { - - super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName, - key); - - // Media controls - if (actionsContainer != null) { - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - final ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } - - if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { - ((IlluminationDrawable) mMediaNotifView.getBackground()) - .setupTouch(thisBtn, mMediaNotifView); - } - - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - Log.d(TAG, "clicking on other button"); - thatBtn.performClick(); - }); - } - - // Hide any unused buttons - for (; i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); - } - } - - // Seek Bar - final MediaController controller = new MediaController(getContext(), token); - mBackgroundExecutor.execute( - () -> mSeekBarViewModel.updateController(controller, iconColor)); - - initLongPressMenu(iconColor); - } - - private void initLongPressMenu(int iconColor) { - // Set up long press menu - View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); - options.setMinimumHeight(guts.getHeight()); - - View clearView = options.findViewById(R.id.remove); - clearView.setOnClickListener(b -> { - removePlayer(); - }); - ImageView removeIcon = options.findViewById(R.id.remove_icon); - removeIcon.setImageTintList(ColorStateList.valueOf(iconColor)); - TextView removeText = options.findViewById(R.id.remove_text); - removeText.setTextColor(iconColor); - - TextView cancelView = options.findViewById(R.id.cancel); - cancelView.setTextColor(iconColor); - cancelView.setOnClickListener(b -> { - options.setVisibility(View.GONE); - guts.setVisibility(View.VISIBLE); - }); - // ... but don't enable it yet, and make sure is reset when the session is updated - mMediaNotifView.setOnLongClickListener(null); - options.setVisibility(View.GONE); - guts.setVisibility(View.VISIBLE); - } - - @Override - protected void resetButtons() { - super.resetButtons(); - mSeekBarViewModel.clearController(); - View guts = mMediaNotifView.findViewById(R.id.media_guts); - View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); - - mMediaNotifView.setOnLongClickListener(v -> { - // Replace player view with close/cancel view - guts.setVisibility(View.GONE); - options.setVisibility(View.VISIBLE); - return true; // consumed click - }); - } - - /** - * Sets the listening state of the player. - * - * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid - * unnecessary work when the QS panel is closed. - * - * @param listening True when player should be active. Otherwise, false. - */ - public void setListening(boolean listening) { - mSeekBarViewModel.setListening(listening); - } - - @Override - public void removePlayer() { - Log.d(TAG, "removing player from parent: " + mParent); - // Ensure this happens on the main thread (could happen in QSMediaBrowser callback) - mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this)); - } - - @Override - public String getMediaPlayerPackage() { - if (getController() == null) { - return mPackageName; - } - return super.getMediaPlayerPackage(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index e8f6c9668e9b..80e5071c6b43 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -32,23 +32,19 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.media.MediaDescription; -import android.media.session.MediaSession; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.UserHandle; import android.os.UserManager; -import android.service.notification.StatusBarNotification; import android.service.quicksettings.Tile; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import com.android.internal.logging.MetricsLogger; @@ -56,17 +52,14 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.NotificationVisibility; import com.android.settingslib.Utils; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.media.InfoMediaManager; -import com.android.settingslib.media.LocalMediaManager; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaControlPanel; +import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; @@ -77,20 +70,16 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; -import com.android.systemui.statusbar.notification.NotificationEntryListener; -import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.inject.Inject; @@ -108,21 +97,13 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; + protected final MediaHost mMediaHost; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final QSTileRevealController mQsTileRevealController; - private final LinearLayout mMediaCarousel; - private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>(); - private final LocalBluetoothManager mLocalBluetoothManager; - private final Executor mForegroundExecutor; - private final DelayableExecutor mBackgroundExecutor; - private boolean mUpdateCarousel = false; - private ActivityStarter mActivityStarter; - private NotificationEntryManager mNotificationEntryManager; - protected boolean mExpanded; protected boolean mListening; @@ -158,15 +139,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } }; - private final NotificationEntryListener mNotificationEntryListener = - new NotificationEntryListener() { - @Override - public void onEntryRemoved(NotificationEntry entry, NotificationVisibility visibility, - boolean removedByUser, int reason) { - checkToRemoveMediaNotification(entry); - } - }; - @Inject public QSPanel( @Named(VIEW_CONTEXT) Context context, @@ -174,23 +146,15 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - @Main Executor foregroundExecutor, - @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager, - ActivityStarter activityStarter, - NotificationEntryManager entryManager, + MediaHost mediaHost, UiEventLogger uiEventLogger ) { super(context, attrs); + mMediaHost = mediaHost; mContext = context; mQSLogger = qsLogger; mDumpManager = dumpManager; - mForegroundExecutor = foregroundExecutor; - mBackgroundExecutor = backgroundExecutor; - mLocalBluetoothManager = localBluetoothManager; mBroadcastDispatcher = broadcastDispatcher; - mActivityStarter = activityStarter; - mNotificationEntryManager = entryManager; mUiEventLogger = uiEventLogger; setOrientation(VERTICAL); @@ -210,16 +174,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne addDivider(); - // Add media carousel - if (useQsMediaPlayer(context)) { - HorizontalScrollView mediaScrollView = (HorizontalScrollView) LayoutInflater.from( - mContext).inflate(R.layout.media_carousel, this, false); - mMediaCarousel = mediaScrollView.findViewById(R.id.media_carousel); - addView(mediaScrollView, 0); - } else { - mMediaCarousel = null; - } - mFooter = new QSSecurityFooter(this, context); addView(mFooter.getView()); @@ -230,145 +184,39 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - if (!isVisible && mUpdateCarousel) { - for (QSMediaPlayer player : mMediaPlayers) { - if (player.isPlaying()) { - LayoutParams lp = (LayoutParams) player.getView().getLayoutParams(); - mMediaCarousel.removeView(player.getView()); - mMediaCarousel.addView(player.getView(), 0, lp); - ((HorizontalScrollView) mMediaCarousel.getParent()).fullScroll(View.FOCUS_LEFT); - mUpdateCarousel = false; - break; - } - } - } - } - - /** - * Add or update a player for the associated media session - * @param token - * @param icon - * @param largeIcon - * @param iconColor - * @param bgColor - * @param actionsContainer - * @param notif - * @param key - */ - public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif, - String key) { - if (!useQsMediaPlayer(mContext)) { - // Shouldn't happen, but just in case - Log.e(TAG, "Tried to add media session without player!"); - return; - } - if (token == null) { - Log.e(TAG, "Media session token was null!"); - return; - } - - String packageName = notif.getPackageName(); - QSMediaPlayer player = findMediaPlayer(packageName, token, key); - - int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width); - int padding = (int) getResources().getDimension(R.dimen.qs_media_padding); - LayoutParams lp = new LayoutParams(playerWidth, ViewGroup.LayoutParams.MATCH_PARENT); - lp.setMarginStart(padding); - lp.setMarginEnd(padding); - - if (player == null) { - Log.d(TAG, "creating new player for " + packageName); - // Set up listener for device changes - // TODO: integrate with MediaTransferManager? - InfoMediaManager imm = new InfoMediaManager(mContext, notif.getPackageName(), - notif.getNotification(), mLocalBluetoothManager); - LocalMediaManager routeManager = new LocalMediaManager(mContext, mLocalBluetoothManager, - imm, notif.getPackageName()); - - player = new QSMediaPlayer(mContext, this, routeManager, mForegroundExecutor, - mBackgroundExecutor, mActivityStarter); - player.setListening(mListening); - if (player.isPlaying()) { - mMediaCarousel.addView(player.getView(), 0, lp); // add in front - } else { - mMediaCarousel.addView(player.getView(), lp); // add at end - } - mMediaPlayers.add(player); - } else if (player.isPlaying()) { - mUpdateCarousel = true; - } - - Log.d(TAG, "setting player session"); - String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification()) - .loadHeaderAppName(); - player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer, - notif.getNotification().contentIntent, appName, key); - - if (mMediaPlayers.size() > 0) { - ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); - } - } - - /** - * Check for an existing media player using the given information - * @param packageName - * @param token - * @param key - * @return a player, or null if no match found - */ - private QSMediaPlayer findMediaPlayer(String packageName, MediaSession.Token token, - String key) { - for (QSMediaPlayer player : mMediaPlayers) { - if (player.getKey() == null || key == null) { - // No notification key = loaded via mediabrowser, so just match on package - if (packageName.equals(player.getMediaPlayerPackage())) { - Log.d(TAG, "Found matching resume player by package: " + packageName); - return player; - } - } else if (player.getMediaSessionToken().equals(token)) { - Log.d(TAG, "Found matching player by token " + packageName); - return player; - } else if (packageName.equals(player.getMediaPlayerPackage()) - && key.equals(player.getKey())) { - // Also match if it's the same package and notification key - Log.d(TAG, "Found matching player by package " + packageName + ", " + key); - return player; - } - } - return null; - } - - protected View getMediaPanel() { - return mMediaCarousel; - } - - /** - * Remove the media player from the carousel - * @param player Player to remove - * @return true if removed, false if player was not found - */ - protected boolean removeMediaPlayer(QSMediaPlayer player) { - // Remove from list - if (!mMediaPlayers.remove(player)) { - return false; - } - - // Check if we need to collapse the carousel now - mMediaCarousel.removeView(player.getView()); - if (mMediaPlayers.size() == 0) { - ((View) mMediaCarousel.getParent()).setVisibility(View.GONE); - } - return true; + protected void onFinishInflate() { + super.onFinishInflate(); + // Add media carousel at the end + if (useQsMediaPlayer(getContext())) { + addMediaHostView(); + } + } + + protected void addMediaHostView() { + mMediaHost.init(MediaHierarchyManager.LOCATION_QS); + mMediaHost.setExpansion(1.0f); + mMediaHost.setShowsOnlyActiveMedia(false); + ViewGroup hostView = mMediaHost.getHostView(); + addView(hostView); + int sidePaddings = getResources().getDimensionPixelSize( + R.dimen.quick_settings_side_margins); + int bottomPadding = getResources().getDimensionPixelSize( + R.dimen.quick_settings_expanded_bottom_margin); + MarginLayoutParams layoutParams = (MarginLayoutParams) hostView.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.bottomMargin = bottomPadding; + hostView.setLayoutParams(layoutParams); + hostView.setPadding(sidePaddings, hostView.getPaddingTop(), sidePaddings, + hostView.getPaddingBottom()); } private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { @Override public void addTrack(MediaDescription desc, ComponentName component, QSMediaBrowser browser) { - if (component == null) { + // TODO: Fix Resumption b/156104922 +/* if (component == null) { Log.e(TAG, "Component cannot be null"); return; } @@ -404,7 +252,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int iconColor = Color.DKGRAY; int bgColor = Color.LTGRAY; player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(), - pkgName); + pkgName);*/ } }; @@ -441,27 +289,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mHasLoadedMediaControls = true; } - private void checkToRemoveMediaNotification(NotificationEntry entry) { - if (!useQsMediaPlayer(mContext)) { - return; - } - - if (!entry.isMediaNotification()) { - return; - } - - // If this entry corresponds to an existing set of controls, clear the controls - // This will handle apps that use an action to clear their notification - for (QSMediaPlayer p : mMediaPlayers) { - if (p.getKey() != null && p.getKey().equals(entry.getKey())) { - Log.d(TAG, "Clearing controls since notification removed " + entry.getKey()); - p.clearControls(); - return; - } - } - Log.d(TAG, "Media notification removed but no player found " + entry.getKey()); - } - protected void addDivider() { mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), @@ -482,7 +309,11 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne int numChildren = getChildCount(); for (int i = 0; i < numChildren; i++) { View child = getChildAt(i); - if (child.getVisibility() != View.GONE) height += child.getMeasuredHeight(); + if (child.getVisibility() != View.GONE) { + height += child.getMeasuredHeight(); + MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); + height += layoutParams.topMargin + layoutParams.bottomMargin; + } } setMeasuredDimension(getMeasuredWidth(), height); } @@ -528,7 +359,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne loadMediaResumptionControls(); } } - mNotificationEntryManager.addNotificationEntryListener(mNotificationEntryListener); } @Override @@ -545,7 +375,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } mDumpManager.unregisterDumpable(getDumpableTag()); mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); - mNotificationEntryManager.removeNotificationEntryListener(mNotificationEntryListener); super.onDetachedFromWindow(); } @@ -716,7 +545,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne public void setListening(boolean listening) { if (mListening == listening) return; - mListening = listening; if (mTileLayout != null) { mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); mTileLayout.setListening(listening); @@ -724,9 +552,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne if (mListening) { refreshAllTiles(); } - for (QSMediaPlayer player : mMediaPlayers) { - player.setListening(mListening); - } } private String getTilesSpecs() { @@ -1027,6 +852,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } } + public MediaHost getMediaHost() { + return mMediaHost; + } + private class H extends Handler { private static final int SHOW_DETAIL = 1; private static final int SET_TILE_VISIBILITY = 2; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java deleted file mode 100644 index 5cb75e60e22a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2019 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.qs; - -import android.app.PendingIntent; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import com.android.systemui.R; -import com.android.systemui.media.IlluminationDrawable; -import com.android.systemui.media.MediaControlPanel; -import com.android.systemui.plugins.ActivityStarter; - -import java.util.concurrent.Executor; - -/** - * QQS mini media player - */ -public class QuickQSMediaPlayer extends MediaControlPanel { - - private static final String TAG = "QQSMediaPlayer"; - - // Button IDs for QS controls - private static final int[] QQS_ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2}; - - /** - * Initialize mini media player for QQS - * @param context - * @param parent - * @param foregroundExecutor - * @param backgroundExecutor - * @param activityStarter - */ - public QuickQSMediaPlayer(Context context, ViewGroup parent, Executor foregroundExecutor, - Executor backgroundExecutor, ActivityStarter activityStarter) { - super(context, parent, null, R.layout.qqs_media_panel, QQS_ACTION_IDS, - foregroundExecutor, backgroundExecutor, activityStarter); - } - - /** - * Update media panel view for the given media session - * @param token token for this media session - * @param icon app notification icon - * @param largeIcon notification's largeIcon, used as a fallback for album art - * @param iconColor foreground color (for text, icons) - * @param bgColor background color - * @param actionsContainer a LinearLayout containing the media action buttons - * @param actionsToShow indices of which actions to display in the mini player - * (max 3: Notification.MediaStyle.MAX_MEDIA_BUTTONS_IN_COMPACT) - * @param contentIntent Intent to send when user taps on the view - * @param key original notification's key - */ - public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, - int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, - PendingIntent contentIntent, String key) { - // Only update if this is a different session and currently playing - String oldPackage = ""; - if (getController() != null) { - oldPackage = getController().getPackageName(); - } - MediaController controller = new MediaController(getContext(), token); - MediaSession.Token currentToken = getMediaSessionToken(); - boolean samePlayer = currentToken != null - && currentToken.equals(token) - && oldPackage.equals(controller.getPackageName()); - if (getController() != null && !samePlayer && !isPlaying(controller)) { - return; - } - - super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key); - - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - if (actionsToShow != null) { - int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount()); - maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length); - for (; i < maxButtons; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); - int thatId = NOTIF_ACTION_IDS[actionsToShow[i]]; - ImageButton thatBtn = parentActionsLayout.findViewById(thatId); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } - - if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) { - ((IlluminationDrawable) mMediaNotifView.getBackground()) - .setupTouch(thisBtn, mMediaNotifView); - } - - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - thatBtn.performClick(); - }); - } - } - - // Hide any unused buttons - for (; i < QQS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 6683a1ce4f4f..dfd385dda8e5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -18,38 +18,34 @@ package com.android.systemui.qs; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; -import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; import com.android.internal.logging.UiEventLogger; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.Utils; -import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; @@ -67,16 +63,17 @@ public class QuickQSPanel extends QSPanel { private boolean mDisabledByPolicy; private int mMaxTiles; protected QSPanel mFullPanel; - private QuickQSMediaPlayer mMediaPlayer; /** Whether or not the QS media player feature is enabled. */ private boolean mUsingMediaPlayer; /** Whether or not the QuickQSPanel currently contains a media player. */ - private boolean mHasMediaPlayer; + private boolean mShowHorizontalTileLayout; private LinearLayout mHorizontalLinearLayout; // Only used with media - private QSTileLayout mMediaTileLayout; + private QSTileLayout mHorizontalTileLayout; private QSTileLayout mRegularTileLayout; + private int mLastOrientation = -1; + private int mMediaBottomMargin; @Inject public QuickQSPanel( @@ -85,16 +82,11 @@ public class QuickQSPanel extends QSPanel { DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, - @Main Executor foregroundExecutor, - @Background DelayableExecutor backgroundExecutor, - @Nullable LocalBluetoothManager localBluetoothManager, - ActivityStarter activityStarter, - NotificationEntryManager entryManager, + MediaHost mediaHost, UiEventLogger uiEventLogger ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, - foregroundExecutor, backgroundExecutor, localBluetoothManager, activityStarter, - entryManager, uiEventLogger); + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, + uiEventLogger); if (mFooter != null) { removeView(mFooter.getView()); } @@ -104,6 +96,8 @@ public class QuickQSPanel extends QSPanel { } removeView((View) mTileLayout); } + mMediaBottomMargin = getResources().getDimensionPixelSize( + R.dimen.quick_settings_media_extra_bottom_margin); mUsingMediaPlayer = Utils.useQsMediaPlayer(context); if (mUsingMediaPlayer) { @@ -112,40 +106,95 @@ public class QuickQSPanel extends QSPanel { mHorizontalLinearLayout.setClipChildren(false); mHorizontalLinearLayout.setClipToPadding(false); - int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); - mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout, - foregroundExecutor, backgroundExecutor, activityStarter); - LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); - lp2.setMarginEnd(marginSize); - lp2.setMarginStart(0); - mHorizontalLinearLayout.addView(mMediaPlayer.getView(), lp2); - - mTileLayout = new DoubleLineTileLayout(context, mUiEventLogger); - mMediaTileLayout = mTileLayout; + DoubleLineTileLayout horizontalTileLayout = new DoubleLineTileLayout(context, + mUiEventLogger); + horizontalTileLayout.setPaddingRelative( + horizontalTileLayout.getPaddingStart(), + horizontalTileLayout.getPaddingTop(), + horizontalTileLayout.getPaddingEnd(), + mContext.getResources().getDimensionPixelSize( + R.dimen.qqs_horizonal_tile_padding_bottom)); + mHorizontalTileLayout = horizontalTileLayout; mRegularTileLayout = new HeaderTileLayout(context, mUiEventLogger); - LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1); - lp.setMarginEnd(0); - lp.setMarginStart(marginSize); - mHorizontalLinearLayout.addView((View) mTileLayout, lp); + LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); + int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); + lp.setMarginStart(0); + lp.setMarginEnd(marginSize); + lp.gravity = Gravity.CENTER_VERTICAL; + mHorizontalLinearLayout.addView((View) mHorizontalTileLayout, lp); sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); + boolean useHorizontal = shouldUseHorizontalTileLayout(); + mTileLayout = useHorizontal ? mHorizontalTileLayout : mRegularTileLayout; mTileLayout.setListening(mListening); addView(mHorizontalLinearLayout, 0 /* Between brightness and footer */); - ((View) mRegularTileLayout).setVisibility(View.GONE); + ((View) mRegularTileLayout).setVisibility(!useHorizontal ? View.VISIBLE : View.GONE); + mHorizontalLinearLayout.setVisibility(useHorizontal ? View.VISIBLE : View.GONE); addView((View) mRegularTileLayout, 0); super.setPadding(0, 0, 0, 0); + applySideMargins(mHorizontalLinearLayout); + applyBottomMargin((View) mRegularTileLayout); } else { sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); mTileLayout = new HeaderTileLayout(context, mUiEventLogger); mTileLayout.setListening(mListening); addView((View) mTileLayout, 0 /* Between brightness and footer */); super.setPadding(0, 0, 0, 0); + applyBottomMargin((View) mTileLayout); } } - public QuickQSMediaPlayer getMediaPlayer() { - return mMediaPlayer; + private void applyBottomMargin(View view) { + int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_bottom); + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.bottomMargin = margin; + view.setLayoutParams(layoutParams); + } + + private void applySideMargins(View view) { + int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_horizontal); + MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams(); + layoutParams.setMarginStart(margin); + layoutParams.setMarginEnd(margin); + view.setLayoutParams(layoutParams); + } + + private void reAttachMediaHost() { + if (mMediaHost == null) { + return; + } + boolean horizontal = shouldUseHorizontalTileLayout(); + ViewGroup host = mMediaHost.getHostView(); + ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; + ViewGroup currentParent = (ViewGroup) host.getParent(); + if (currentParent != newParent) { + if (currentParent != null) { + currentParent.removeView(host); + } + newParent.addView(host); + LinearLayout.LayoutParams layoutParams = (LayoutParams) host.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.weight = horizontal ? 1.5f : 0; + layoutParams.bottomMargin = mMediaBottomMargin; + int marginStart = horizontal + ? getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_horizontal) + : 0; + layoutParams.setMarginStart(marginStart); + } + } + + @Override + protected void addMediaHostView() { + mMediaHost.setVisibleChangedListener((visible) -> { + switchTileLayout(); + return null; + }); + mMediaHost.init(MediaHierarchyManager.LOCATION_QQS); + mMediaHost.setExpansion(0.0f); + mMediaHost.setShowsOnlyActiveMedia(true); + reAttachMediaHost(); } @Override @@ -191,10 +240,19 @@ public class QuickQSPanel extends QSPanel { super.drawTile(r, state); } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation != mLastOrientation) { + mLastOrientation = newConfig.orientation; + switchTileLayout(); + } + } + boolean switchTileLayout() { if (!mUsingMediaPlayer) return false; - mHasMediaPlayer = mMediaPlayer.hasMediaSession(); - if (mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.GONE) { + mShowHorizontalTileLayout = shouldUseHorizontalTileLayout(); + if (mShowHorizontalTileLayout && mHorizontalLinearLayout.getVisibility() == View.GONE) { mHorizontalLinearLayout.setVisibility(View.VISIBLE); ((View) mRegularTileLayout).setVisibility(View.GONE); mTileLayout.setListening(false); @@ -202,11 +260,13 @@ public class QuickQSPanel extends QSPanel { mTileLayout.removeTile(record); record.tile.removeCallback(record.callback); } - mTileLayout = mMediaTileLayout; + mTileLayout = mHorizontalTileLayout; if (mHost != null) setTiles(mHost.getTiles()); mTileLayout.setListening(mListening); + reAttachMediaHost(); return true; - } else if (!mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) { + } else if (!mShowHorizontalTileLayout + && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) { mHorizontalLinearLayout.setVisibility(View.GONE); ((View) mRegularTileLayout).setVisibility(View.VISIBLE); mTileLayout.setListening(false); @@ -217,14 +277,21 @@ public class QuickQSPanel extends QSPanel { mTileLayout = mRegularTileLayout; if (mHost != null) setTiles(mHost.getTiles()); mTileLayout.setListening(mListening); + reAttachMediaHost(); return true; } return false; } - /** Returns true if this panel currently contains a media player. */ - public boolean hasMediaPlayer() { - return mHasMediaPlayer; + private boolean shouldUseHorizontalTileLayout() { + return mMediaHost.getVisible() + && getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } + + /** Returns true if this panel currently uses a horizontal tile layout. */ + public boolean usesHorizontalLayout() { + return mShowHorizontalTileLayout; } @Override @@ -341,7 +408,7 @@ public class QuickQSPanel extends QSPanel { setClipChildren(false); setClipToPadding(false); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); + LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.CENTER_HORIZONTAL; setLayoutParams(lp); } @@ -354,6 +421,7 @@ public class QuickQSPanel extends QSPanel { @Override public void onFinishInflate(){ + super.onFinishInflate(); updateResources(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b15c6a3e3b59..3b2bea8f80f2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -220,6 +220,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements mNextAlarmTextView.setSelected(true); } + public QuickQSPanel getHeaderQsPanel() { + return mHeaderQsPanel; + } + private List<String> getIgnoredIconSlots() { ArrayList<String> ignored = new ArrayList<>(); ignored.add(mContext.getResources().getString( @@ -336,23 +340,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements com.android.internal.R.dimen.quick_qs_offset_height); mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); - - if (mQsDisabled) { - lp.height = resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_offset_height); - } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.hasMediaPlayer()) { - lp.height = Math.max(getMinimumHeight(), - resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_total_height_with_media)); - } else { - lp.height = Math.max(getMinimumHeight(), - resources.getDimensionPixelSize( - com.android.internal.R.dimen.quick_qs_total_height)); - } - - setLayoutParams(lp); - updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index e32d174d7c77..9f4932e74eaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -44,14 +44,17 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; +import androidx.annotation.NonNull; + import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.statusbar.NotificationVisibility; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaData; +import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; @@ -113,7 +116,6 @@ public class NotificationMediaManager implements Dumpable { private ScrimController mScrimController; @Nullable private LockscreenWallpaper mLockscreenWallpaper; - private final KeyguardMediaPlayer mMediaPlayer; private final Executor mMainExecutor; @@ -187,13 +189,12 @@ public class NotificationMediaManager implements Dumpable { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, - KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, - DeviceConfigProxy deviceConfig) { + DeviceConfigProxy deviceConfig, + MediaDataManager mediaDataManager) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; mKeyguardBypassController = keyguardBypassController; - mMediaPlayer = keyguardMediaPlayer; mMediaListeners = new ArrayList<>(); // TODO: use MediaSessionManager.SessionListener to hook us up to future updates // in session state @@ -204,14 +205,26 @@ public class NotificationMediaManager implements Dumpable { mNotificationShadeWindowController = notificationShadeWindowController; mEntryManager = notificationEntryManager; mMainExecutor = mainExecutor; + notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + @Override public void onPendingEntryAdded(NotificationEntry entry) { - findAndUpdateMediaNotifications(); + mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override public void onPreEntryUpdated(NotificationEntry entry) { + mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + } + + @Override + public void onEntryInflated(NotificationEntry entry) { + findAndUpdateMediaNotifications(); + } + + @Override + public void onEntryReinflated(NotificationEntry entry) { findAndUpdateMediaNotifications(); } @@ -222,6 +235,7 @@ public class NotificationMediaManager implements Dumpable { boolean removedByUser, int reason) { onNotificationRemoved(entry.getKey()); + mediaDataManager.onNotificationRemoved(entry.getKey()); } }); @@ -278,7 +292,7 @@ public class NotificationMediaManager implements Dumpable { public void addCallback(MediaListener callback) { mMediaListeners.add(callback); - callback.onMetadataOrStateChanged(mMediaMetadata, + callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, getMediaControllerPlaybackState(mMediaController)); } @@ -392,7 +406,7 @@ public class NotificationMediaManager implements Dumpable { @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController); ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners); for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).onMetadataOrStateChanged(mMediaMetadata, state); + callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state); } } @@ -473,7 +487,6 @@ public class NotificationMediaManager implements Dumpable { && mBiometricUnlockController.isWakeAndUnlock(); if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) { mBackdrop.setVisibility(View.INVISIBLE); - mMediaPlayer.clearControls(); Trace.endSection(); return; } @@ -496,14 +509,6 @@ public class NotificationMediaManager implements Dumpable { } } - NotificationEntry entry = mEntryManager - .getActiveNotificationUnfiltered(mMediaNotificationKey); - if (entry != null) { - mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata); - } else { - mMediaPlayer.clearControls(); - } - // Process artwork on a background thread and send the resulting bitmap to // finishUpdateMediaMetaData. if (metaDataChanged) { @@ -626,7 +631,6 @@ public class NotificationMediaManager implements Dumpable { // We are unlocking directly - no animation! mBackdrop.setVisibility(View.GONE); mBackdropBack.setImageDrawable(null); - mMediaPlayer.clearControls(); if (windowController != null) { windowController.setBackdropShowing(false); } @@ -643,7 +647,6 @@ public class NotificationMediaManager implements Dumpable { mBackdrop.setVisibility(View.GONE); mBackdropFront.animate().cancel(); mBackdropBack.setImageDrawable(null); - mMediaPlayer.clearControls(); mMainExecutor.execute(mHideBackdropFront); }); if (mKeyguardStateController.isKeyguardFadingAway()) { @@ -750,6 +753,7 @@ public class NotificationMediaManager implements Dumpable { * @param state Current playback state * @see PlaybackState.State */ - void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state); + default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, + @PlaybackState.State int state) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index de7e36d97b22..f0fed13114ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -21,9 +21,9 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.MediaArtworkProcessor; @@ -95,9 +95,9 @@ public interface StatusBarDependenciesModule { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, - KeyguardMediaPlayer keyguardMediaPlayer, @Main Executor mainExecutor, - DeviceConfigProxy deviceConfigProxy) { + DeviceConfigProxy deviceConfigProxy, + MediaDataManager mediaDataManager) { return new NotificationMediaManager( context, statusBarLazy, @@ -105,9 +105,9 @@ public interface StatusBarDependenciesModule { notificationEntryManager, mediaArtworkProcessor, keyguardBypassController, - keyguardMediaPlayer, mainExecutor, - deviceConfigProxy); + deviceConfigProxy, + mediaDataManager); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java index 75b41ca3e162..eee9cc683e2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.notification; +import android.graphics.drawable.Drawable; import android.util.FloatProperty; +import android.util.Log; import android.util.Property; import android.view.View; @@ -35,6 +37,100 @@ public abstract class AnimatableProperty { public static final AnimatableProperty Y = AnimatableProperty.from(View.Y, R.id.y_animator_tag, R.id.y_animator_tag_start_value, R.id.y_animator_tag_end_value); + /** + * Similar to X, however this doesn't allow for any other modifications other than from this + * property. When using X, it's possible that the view is laid out during the animation, + * which could break the continuity + */ + public static final AnimatableProperty ABSOLUTE_X = AnimatableProperty.from( + new FloatProperty<View>("ViewAbsoluteX") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.absolute_x_current_value, value); + View.X.set(view, value); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.absolute_x_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return View.X.get(view); + } + }, + R.id.absolute_x_animator_tag, + R.id.absolute_x_animator_start_tag, + R.id.absolute_x_animator_end_tag); + + /** + * Similar to Y, however this doesn't allow for any other modifications other than from this + * property. When using X, it's possible that the view is laid out during the animation, + * which could break the continuity + */ + public static final AnimatableProperty ABSOLUTE_Y = AnimatableProperty.from( + new FloatProperty<View>("ViewAbsoluteY") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.absolute_y_current_value, value); + View.Y.set(view, value); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.absolute_y_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return View.Y.get(view); + } + }, + R.id.absolute_y_animator_tag, + R.id.absolute_y_animator_start_tag, + R.id.absolute_y_animator_end_tag); + + public static final AnimatableProperty WIDTH = AnimatableProperty.from( + new FloatProperty<View>("ViewWidth") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.view_width_current_value, value); + view.setRight((int) (view.getLeft() + value)); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.view_width_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return (float) view.getWidth(); + } + }, + R.id.view_width_animator_tag, + R.id.view_width_animator_start_tag, + R.id.view_width_animator_end_tag); + + public static final AnimatableProperty HEIGHT = AnimatableProperty.from( + new FloatProperty<View>("ViewHeight") { + @Override + public void setValue(View view, float value) { + view.setTag(R.id.view_height_current_value, value); + view.setBottom((int) (view.getTop() + value)); + } + + @Override + public Float get(View view) { + Object tag = view.getTag(R.id.view_height_current_value); + if (tag instanceof Float) { + return (Float) tag; + } + return (float) view.getHeight(); + } + }, + R.id.view_height_animator_tag, + R.id.view_height_animator_start_tag, + R.id.view_height_animator_end_tag); + public abstract int getAnimationStartTag(); public abstract int getAnimationEndTag(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java index 1f9d3af70b4f..b1b6a1c12a0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java @@ -34,13 +34,20 @@ import com.android.systemui.statusbar.notification.stack.ViewState; */ public class PropertyAnimator { + /** + * Set a property on a view, updating its value, even if it's already animating. + * The @param animated can be used to request an animation. + * If the view isn't animated, this utility will update the current animation if existent, + * such that the end value will point to @param newEndValue or apply it directly if there's + * no animation. + */ public static <T extends View> void setProperty(final T view, AnimatableProperty animatableProperty, float newEndValue, AnimationProperties properties, boolean animated) { int animatorTag = animatableProperty.getAnimatorTag(); ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag); if (previousAnimator != null || animated) { - startAnimation(view, animatableProperty, newEndValue, properties); + startAnimation(view, animatableProperty, newEndValue, animated ? properties : null); } else { // no new animation needed, let's just apply the value animatableProperty.getProperty().set(view, newEndValue); @@ -60,8 +67,8 @@ public class PropertyAnimator { } int animatorTag = animatableProperty.getAnimatorTag(); ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag); - AnimationFilter filter = properties.getAnimationFilter(); - if (!filter.shouldAnimateProperty(property)) { + AnimationFilter filter = properties != null ? properties.getAnimationFilter() : null; + if (filter == null || !filter.shouldAnimateProperty(property)) { // just a local update was performed if (previousAnimator != null) { // we need to increase all animation keyframes of the previous animator by the @@ -82,6 +89,14 @@ public class PropertyAnimator { } Float currentValue = property.get(view); + AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); + if (currentValue.equals(newEndValue)) { + // Skip the animation! + if (listener != null) { + listener.onAnimationEnd(null); + } + return; + } ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue); animator.addUpdateListener( animation -> property.set(view, (Float) animation.getAnimatedValue())); @@ -96,7 +111,6 @@ public class PropertyAnimator { || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } - AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); if (listener != null) { animator.addListener(listener); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index cb0c2838c24d..634872d9d761 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -209,7 +209,7 @@ public final class NotificationEntry extends ListEntry { } /** The key for this notification. Guaranteed to be immutable and unique */ - public String getKey() { + @NonNull public String getKey() { return mKey; } @@ -217,7 +217,7 @@ public final class NotificationEntry extends ListEntry { * The StatusBarNotification that represents one half of a NotificationEntry (the other half * being the Ranking). This object is swapped out whenever a notification is updated. */ - public StatusBarNotification getSbn() { + @NonNull public StatusBarNotification getSbn() { return mSbn; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java index 59f119e987b4..3fab6f7c3857 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.notifcollection; +import android.annotation.NonNull; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; @@ -43,13 +44,13 @@ public interface NotifCollectionListener { * there is no guarantee of order and they may not have had a chance to initialize yet. Instead, * use {@link #onEntryAdded} which is called after all initialization. */ - default void onEntryInit(NotificationEntry entry) { + default void onEntryInit(@NonNull NotificationEntry entry) { } /** * Called whenever a notification with a new key is posted. */ - default void onEntryAdded(NotificationEntry entry) { + default void onEntryAdded(@NonNull NotificationEntry entry) { } /** @@ -64,7 +65,7 @@ public interface NotifCollectionListener { * immediately after a user dismisses a notification: we wait until we receive confirmation from * system server before considering the notification removed. */ - default void onEntryRemoved(NotificationEntry entry, @CancellationReason int reason) { + default void onEntryRemoved(@NonNull NotificationEntry entry, @CancellationReason int reason) { } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt index 88888d10e283..0fd865b603f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt @@ -75,7 +75,7 @@ class BypassHeadsUpNotifier @Inject constructor( mediaManager.addCallback(this) } - override fun onMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { + override fun onPrimaryMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) { val previous = currentMediaEntry var newEntry = entryManager .getActiveNotificationUnfiltered(mediaManager.mediaNotificationKey) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 5797944298d4..0831c0b66797 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -767,6 +767,10 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { return mContentTranslation; } + public boolean wantsAddAndRemoveAnimations() { + return true; + } + /** * A listener notifying when {@link #getActualHeight} changes. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index 0ccebc130b1d..56f8e087d64d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -109,7 +109,7 @@ public class HybridGroupManager { } @Nullable - private CharSequence resolveText(Notification notification) { + public static CharSequence resolveText(Notification notification) { CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT); if (contentText == null) { contentText = notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); @@ -118,7 +118,7 @@ public class HybridGroupManager { } @Nullable - private CharSequence resolveTitle(Notification notification) { + public static CharSequence resolveTitle(Notification notification) { CharSequence titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE); if (titleText == null) { titleText = notification.extras.getCharSequence(Notification.EXTRA_TITLE_BIG); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index b96cff830f31..93d3f3bdbe96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -178,38 +178,6 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi final MediaSession.Token token = mRow.getEntry().getSbn().getNotification().extras .getParcelable(Notification.EXTRA_MEDIA_SESSION); - if (Utils.useQsMediaPlayer(mContext) && token != null) { - final int[] compactActions = mRow.getEntry().getSbn().getNotification().extras - .getIntArray(Notification.EXTRA_COMPACT_ACTIONS); - int tintColor = getNotificationHeader().getOriginalIconColor(); - NotificationShadeWindowController ctrl = Dependency.get( - NotificationShadeWindowController.class); - QuickQSPanel panel = ctrl.getNotificationShadeView().findViewById( - com.android.systemui.R.id.quick_qs_panel); - StatusBarNotification sbn = mRow.getEntry().getSbn(); - Notification notif = sbn.getNotification(); - Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext); - panel.getMediaPlayer().setMediaSession(token, - iconDrawable, - notif.getLargeIcon(), - tintColor, - mBackgroundColor, - mActions, - compactActions, - notif.contentIntent, - sbn.getKey()); - QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById( - com.android.systemui.R.id.quick_settings_panel); - bigPanel.addMediaSession(token, - iconDrawable, - notif.getLargeIcon(), - tintColor, - mBackgroundColor, - mActions, - sbn, - sbn.getKey()); - } - boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar(); if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) { if (mSeekBarView != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java index ab055e1bdc36..3ac322fec071 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack; import android.content.Context; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -37,7 +38,6 @@ public class MediaHeaderView extends ActivatableNotificationView { @Override protected void onFinishInflate() { super.onFinishInflate(); - mContentView = findViewById(R.id.keyguard_media_view); } @Override @@ -52,4 +52,17 @@ public class MediaHeaderView extends ActivatableNotificationView { public void setBackgroundColor(int color) { setTintColor(color); } + + public void setContentView(ViewGroup contentView) { + mContentView = contentView; + addView(contentView); + ViewGroup.LayoutParams layoutParams = contentView.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + } + + @Override + public boolean wantsAddAndRemoveAnimations() { + return false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java index 6eec1ca33e14..2a257f6198d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java @@ -29,10 +29,12 @@ import android.content.Intent; import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import com.android.internal.annotations.VisibleForTesting; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.R; +import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -74,8 +76,8 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section private final StatusBarStateController mStatusBarStateController; private final ConfigurationController mConfigurationController; private final PeopleHubViewAdapter mPeopleHubViewAdapter; - private final KeyguardMediaPlayer mKeyguardMediaPlayer; private final NotificationSectionsFeatureManager mSectionsFeatureManager; + private final KeyguardMediaController mKeyguardMediaController; private final int mNumberOfSections; private final PeopleHubViewBoundary mPeopleHubViewBoundary = new PeopleHubViewBoundary() { @@ -123,15 +125,15 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section StatusBarStateController statusBarStateController, ConfigurationController configurationController, PeopleHubViewAdapter peopleHubViewAdapter, - KeyguardMediaPlayer keyguardMediaPlayer, + KeyguardMediaController keyguardMediaController, NotificationSectionsFeatureManager sectionsFeatureManager) { mActivityStarter = activityStarter; mStatusBarStateController = statusBarStateController; mConfigurationController = configurationController; mPeopleHubViewAdapter = peopleHubViewAdapter; - mKeyguardMediaPlayer = keyguardMediaPlayer; mSectionsFeatureManager = sectionsFeatureManager; mNumberOfSections = mSectionsFeatureManager.getNumberOfBuckets(); + mKeyguardMediaController = keyguardMediaController; } NotificationSection[] createSectionsForBuckets() { @@ -205,12 +207,9 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section mIncomingHeader.setHeaderText(R.string.notification_section_header_incoming); mIncomingHeader.setOnHeaderClickListener(this::onGentleHeaderClick); - if (mMediaControlsView != null) { - mKeyguardMediaPlayer.unbindView(); - } mMediaControlsView = reinflateView(mMediaControlsView, layoutInflater, R.layout.keyguard_media_header); - mKeyguardMediaPlayer.bindView(mMediaControlsView); + mKeyguardMediaController.attach(mMediaControlsView); } /** Listener for when the "clear all" button is clicked on the gentle notification header. */ @@ -267,7 +266,6 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section final boolean showHeaders = mStatusBarStateController.getState() != StatusBarState.KEYGUARD; final boolean usingPeopleFiltering = mSectionsFeatureManager.isFilteringEnabled(); - final boolean isKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; final boolean usingMediaControls = mSectionsFeatureManager.isMediaControlsEnabled(); boolean peopleNotifsPresent = false; @@ -275,7 +273,7 @@ public class NotificationSectionsManager implements StackScrollAlgorithm.Section int currentMediaControlsIdx = -1; // Currently, just putting media controls in the front and incrementing the position based // on the number of heads-up notifs. - int mediaControlsTarget = isKeyguard && usingMediaControls ? 0 : -1; + int mediaControlsTarget = usingMediaControls ? 0 : -1; int currentIncomingHeaderIdx = -1; int incomingHeaderTarget = -1; int currentPeopleHeaderIdx = -1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7f32c004808a..db9faf5c7ace 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -3074,6 +3074,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd */ @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private boolean generateRemoveAnimation(ExpandableView child) { + if (!child.wantsAddAndRemoveAnimations()) { + return false; + } if (removeRemovedChildFromHeadsUpChangeAnimations(child)) { mAddedHeadsUpChildren.remove(child); return false; @@ -3428,7 +3431,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @Override @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) { - if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) { + if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden() + && child.wantsAddAndRemoveAnimations()) { // Generate Animations mChildrenToAddAnimated.add(child); if (fromMoreCard) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index c9716d39590e..8161a83f403c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -69,6 +69,7 @@ import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.doze.DozeLog; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -242,6 +243,7 @@ public class NotificationPanelViewController extends PanelViewController { private final KeyguardBypassController mKeyguardBypassController; private final KeyguardUpdateMonitor mUpdateMonitor; private final ConversationNotificationManager mConversationNotificationManager; + private final MediaHierarchyManager mMediaHierarchyManager; private KeyguardAffordanceHelper mAffordanceHelper; private KeyguardUserSwitcher mKeyguardUserSwitcher; @@ -456,7 +458,8 @@ public class NotificationPanelViewController extends PanelViewController { ConfigurationController configurationController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, StatusBarTouchableRegionManager statusBarTouchableRegionManager, - ConversationNotificationManager conversationNotificationManager) { + ConversationNotificationManager conversationNotificationManager, + MediaHierarchyManager mediaHierarchyManager) { super(view, falsingManager, dozeLog, keyguardStateController, (SysuiStatusBarStateController) statusBarStateController, vibratorHelper, latencyTracker, flingAnimationUtilsBuilder, statusBarTouchableRegionManager); @@ -466,6 +469,7 @@ public class NotificationPanelViewController extends PanelViewController { mZenModeController = zenModeController; mConfigurationController = configurationController; mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder; + mMediaHierarchyManager = mediaHierarchyManager; mView.setWillNotDraw(!DEBUG); mInjectionInflationController = injectionInflationController; mFalsingManager = falsingManager; @@ -1609,7 +1613,7 @@ public class NotificationPanelViewController extends PanelViewController { if (mQs == null) return; float qsExpansionFraction = getQsExpansionFraction(); mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation()); - int heightDiff = mQs.getDesiredHeight() - mQs.getQsMinExpansionHeight(); + mMediaHierarchyManager.setQsExpansion(qsExpansionFraction); mNotificationStackScroller.setQsExpansionFraction(qsExpansionFraction); } @@ -3514,7 +3518,11 @@ public class NotificationPanelViewController extends PanelViewController { // Calculate quick setting heights. int oldMaxHeight = mQsMaxExpansionHeight; if (mQs != null) { + float previousMin = mQsMinExpansionHeight; mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight(); + if (mQsExpansionHeight == previousMin) { + mQsExpansionHeight = mQsMinExpansionHeight; + } mQsMaxExpansionHeight = mQs.getDesiredHeight(); mNotificationStackScroller.setMaxTopPadding( mQsMaxExpansionHeight + mQsNotificationTopPadding); diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt new file mode 100644 index 000000000000..2be698b4e796 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt @@ -0,0 +1,87 @@ +/* + * 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.util.animation + +/** + * A class responsible for caching view Measurements which guarantees that we always obtain a value + */ +class GuaranteedMeasurementCache constructor( + private val baseCache : MeasurementCache, + private val inputMapper: (MeasurementInput) -> MeasurementInput, + private val measurementProvider: (MeasurementInput) -> MeasurementOutput? +) : MeasurementCache { + + override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput { + val mappedInput = inputMapper.invoke(input) + if (!baseCache.contains(mappedInput)) { + var measurement = measurementProvider.invoke(mappedInput) + if (measurement != null) { + // Only cache measurings that actually have a size + baseCache.putMeasurement(mappedInput, measurement) + } else { + measurement = MeasurementOutput(0, 0) + } + return measurement + } else { + return baseCache.obtainMeasurement(mappedInput) + } + } + + override fun contains(input: MeasurementInput): Boolean { + return baseCache.contains(inputMapper.invoke(input)) + } + + override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) { + if (output.measuredWidth == 0 || output.measuredHeight == 0) { + // Only cache measurings that actually have a size + return; + } + val remappedInput = inputMapper.invoke(input) + baseCache.putMeasurement(remappedInput, output) + } +} + +/** + * A base implementation class responsible for caching view Measurements + */ +class BaseMeasurementCache : MeasurementCache { + private val dataCache: MutableMap<MeasurementInput, MeasurementOutput> = mutableMapOf() + + override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput { + val measurementOutput = dataCache[input] + if (measurementOutput == null) { + return MeasurementOutput(0, 0) + } else { + return measurementOutput + } + } + + override fun contains(input: MeasurementInput) : Boolean { + return dataCache[input] != null + } + + override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) { + dataCache[input] = output + } +} + +interface MeasurementCache { + fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput + fun contains(input: MeasurementInput) : Boolean + fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) +} + diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt new file mode 100644 index 000000000000..bf94c5d36ff7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt @@ -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.util.animation + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import android.widget.FrameLayout + +/** + * A special view that is designed to host a single "unique object". The unique object is + * dynamically added and removed from this view and may transition to other UniqueObjectHostViews + * available in the system. + * This is useful to share a singular instance of a view that can transition between completely + * independent parts of the view hierarchy. + * If the view currently hosts the unique object, it's measuring it normally, + * but if it's not attached, it will obtain the size by requesting a measure, as if it were + * always attached. + */ +class UniqueObjectHostView( + context: Context +) : FrameLayout(context) { + lateinit var measurementCache : GuaranteedMeasurementCache + var onMeasureListener: ((MeasurementInput) -> Unit)? = null + + @SuppressLint("DrawAllocation") + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val paddingHorizontal = paddingStart + paddingEnd + val paddingVertical = paddingTop + paddingBottom + val width = MeasureSpec.getSize(widthMeasureSpec) - paddingHorizontal + val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) + val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical + val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) + val measurementInput = MeasurementInputData(widthSpec, heightSpec) + onMeasureListener?.apply { + invoke(measurementInput) + } + if (!isCurrentHost()) { + // We're not currently the host, let's get the dimension from our cache (this might + // perform a measuring if the cache doesn't have it yet) + // The goal here is that the view will always have a consistent measuring, regardless + // if it's attached or not. + // The behavior is therefore very similar to the view being persistently attached to + // this host, which can prevent flickers. It also makes sure that we always know + // the size of the view during transitions even if it has never been attached here + // before. + val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput) + setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + // Let's update our cache + val child = getChildAt(0)!! + val output = MeasurementOutput(child.measuredWidth, child.measuredHeight) + measurementCache.putMeasurement(measurementInput, output) + } + } + + private fun isCurrentHost() = childCount != 0 +} + +/** + * A basic view measurement input + */ +interface MeasurementInput { + fun sameAs(input: MeasurementInput?): Boolean { + return equals(input) + } + val width : Int + get() { + return View.MeasureSpec.getSize(widthMeasureSpec) + } + val height : Int + get() { + return View.MeasureSpec.getSize(heightMeasureSpec) + } + var widthMeasureSpec: Int + var heightMeasureSpec: Int +} + +/** + * The output of a view measurement + */ +data class MeasurementOutput( + val measuredWidth: Int, + val measuredHeight: Int +) + +/** + * The data object holding a basic view measurement input + */ +data class MeasurementInputData( + override var widthMeasureSpec: Int, + override var heightMeasureSpec: Int +) : MeasurementInput diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt deleted file mode 100644 index 4bcf917fa95d..000000000000 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMediaPlayerTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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.keyguard - -import android.app.Notification -import android.graphics.drawable.Icon -import android.media.MediaMetadata -import android.media.session.MediaController -import android.media.session.MediaSession -import android.media.session.PlaybackState -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.View -import android.widget.TextView -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.arch.core.executor.TaskExecutor -import androidx.test.filters.SmallTest - -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.media.MediaControllerFactory -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat - -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -public class KeyguardMediaPlayerTest : SysuiTestCase() { - - private lateinit var keyguardMediaPlayer: KeyguardMediaPlayer - @Mock private lateinit var mockMediaFactory: MediaControllerFactory - @Mock private lateinit var mockMediaController: MediaController - private lateinit var playbackState: PlaybackState - private lateinit var fakeExecutor: FakeExecutor - private lateinit var mediaMetadata: MediaMetadata.Builder - private lateinit var entry: NotificationEntry - @Mock private lateinit var mockView: View - private lateinit var songView: TextView - private lateinit var artistView: TextView - @Mock private lateinit var mockIcon: Icon - - private val taskExecutor: TaskExecutor = object : TaskExecutor() { - public override fun executeOnDiskIO(runnable: Runnable) { - runnable.run() - } - public override fun postToMainThread(runnable: Runnable) { - runnable.run() - } - public override fun isMainThread(): Boolean { - return true - } - } - - @Before - public fun setup() { - playbackState = PlaybackState.Builder().run { - build() - } - mockMediaController = mock(MediaController::class.java) - whenever(mockMediaController.getPlaybackState()).thenReturn(playbackState) - mockMediaFactory = mock(MediaControllerFactory::class.java) - whenever(mockMediaFactory.create(any())).thenReturn(mockMediaController) - - fakeExecutor = FakeExecutor(FakeSystemClock()) - keyguardMediaPlayer = KeyguardMediaPlayer(context, mockMediaFactory, fakeExecutor) - mockIcon = mock(Icon::class.java) - - mockView = mock(View::class.java) - songView = TextView(context) - artistView = TextView(context) - whenever<TextView>(mockView.findViewById(R.id.header_title)).thenReturn(songView) - whenever<TextView>(mockView.findViewById(R.id.header_artist)).thenReturn(artistView) - - mediaMetadata = MediaMetadata.Builder() - entry = NotificationEntryBuilder().build() - entry.getSbn().getNotification().extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token(1, null)) - - ArchTaskExecutor.getInstance().setDelegate(taskExecutor) - - keyguardMediaPlayer.bindView(mockView) - } - - @After - public fun tearDown() { - keyguardMediaPlayer.unbindView() - ArchTaskExecutor.getInstance().setDelegate(null) - } - - @Test - public fun testBind() { - keyguardMediaPlayer.unbindView() - keyguardMediaPlayer.bindView(mockView) - } - - @Test - public fun testUnboundClearControls() { - keyguardMediaPlayer.unbindView() - keyguardMediaPlayer.clearControls() - keyguardMediaPlayer.bindView(mockView) - } - - @Test - public fun testUpdateControls() { - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - FakeExecutor.exhaustExecutors(fakeExecutor) - verify(mockView).setVisibility(View.VISIBLE) - } - - @Test - public fun testClearControls() { - keyguardMediaPlayer.clearControls() - FakeExecutor.exhaustExecutors(fakeExecutor) - verify(mockView).setVisibility(View.GONE) - } - - @Test - public fun testUpdateControlsNullPlaybackState() { - // GIVEN that the playback state is null (ie. the media session was destroyed) - whenever(mockMediaController.getPlaybackState()).thenReturn(null) - // WHEN updated - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - FakeExecutor.exhaustExecutors(fakeExecutor) - // THEN the controls are cleared (ie. visibility is set to GONE) - verify(mockView).setVisibility(View.GONE) - } - - @Test - public fun testSongName() { - val song: String = "Song" - mediaMetadata.putText(MediaMetadata.METADATA_KEY_TITLE, song) - - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - - assertThat(fakeExecutor.runAllReady()).isEqualTo(1) - assertThat(songView.getText()).isEqualTo(song) - } - - @Test - public fun testArtistName() { - val artist: String = "Artist" - mediaMetadata.putText(MediaMetadata.METADATA_KEY_ARTIST, artist) - - keyguardMediaPlayer.updateControls(entry, mockIcon, mediaMetadata.build()) - - assertThat(fakeExecutor.runAllReady()).isEqualTo(1) - assertThat(artistView.getText()).isEqualTo(artist) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java index 92c1d7601106..f70fb4f55a8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java @@ -131,7 +131,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { MediaMetadata metadata = mock(MediaMetadata.class); when(metadata.getText(any())).thenReturn("metadata"); mProvider.onDozingChanged(true); - mProvider.onMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); mProvider.onBindSlice(mProvider.getUri()); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_TITLE)); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_ARTIST)); @@ -144,7 +144,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { when(metadata.getText(any())).thenReturn("metadata"); when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true); when(mDozeParameters.getAlwaysOn()).thenReturn(true); - mProvider.onMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(metadata, PlaybackState.STATE_PLAYING); mProvider.onBindSlice(mProvider.getUri()); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_TITLE)); verify(metadata).getText(eq(MediaMetadata.METADATA_KEY_ARTIST)); @@ -210,7 +210,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { mProvider.onStateChanged(StatusBarState.KEYGUARD); mProvider.onDozingChanged(true); reset(mContentResolver); - mProvider.onMetadataOrStateChanged(mock(MediaMetadata.class), PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class), + PlaybackState.STATE_PLAYING); verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null)); // Hides after waking up @@ -222,7 +223,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { @Test public void onDozingChanged_updatesSliceIfMedia() { mProvider.onStateChanged(StatusBarState.KEYGUARD); - mProvider.onMetadataOrStateChanged(mock(MediaMetadata.class), PlaybackState.STATE_PLAYING); + mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class), + PlaybackState.STATE_PLAYING); reset(mContentResolver); // Show media when dozing mProvider.onDozingChanged(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java index 128d6e5612f1..6c543c73456c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -43,6 +43,7 @@ import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizer; @@ -50,7 +51,6 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.policy.SecurityController; -import com.android.systemui.util.concurrency.DelayableExecutor; import org.junit.Before; import org.junit.Test; @@ -62,7 +62,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; -import java.util.concurrent.Executor; @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -90,9 +89,7 @@ public class QSPanelTest extends SysuiTestCase { @Mock private QSTileView mQSTileView; @Mock - private Executor mForegroundExecutor; - @Mock - private DelayableExecutor mBackgroundExecutor; + private MediaHost mMediaHost; @Mock private LocalBluetoothManager mLocalBluetoothManager; @Mock @@ -116,8 +113,7 @@ public class QSPanelTest extends SysuiTestCase { mTestableLooper.runWithLooper(() -> { mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher, - mQSLogger, mForegroundExecutor, mBackgroundExecutor, - mLocalBluetoothManager, mActivityStarter, mEntryManager, mUiEventLogger); + mQSLogger, mMediaHost, mUiEventLogger); // Provides a parent with non-zero size for QSPanel mParentView = new FrameLayout(mContext); mParentView.addView(mQsPanel); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 646bc9699ff8..f4b5a5b7f3cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -42,9 +42,9 @@ import android.view.ViewGroup; import androidx.test.filters.SmallTest; -import com.android.keyguard.KeyguardMediaPlayer; import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; @@ -74,7 +74,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; @Mock private PeopleHubViewAdapter mPeopleHubAdapter; - @Mock private KeyguardMediaPlayer mKeyguardMediaPlayer; + @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private NotificationSectionsFeatureManager mSectionsFeatureManager; @Mock private NotificationRowComponent mNotificationRowComponent; @Mock private ActivatableNotificationViewController mActivatableNotificationViewController; @@ -93,7 +93,7 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { mStatusBarStateController, mConfigurationController, mPeopleHubAdapter, - mKeyguardMediaPlayer, + mKeyguardMediaController, mSectionsFeatureManager ); // Required in order for the header inflation to work properly @@ -367,38 +367,6 @@ public class NotificationSectionsManagerTest extends SysuiTestCase { verify(mNssl).addView(mSectionsManager.getMediaControlsView(), 1); } - @Test - public void testMediaControls_RemoveWhenExitKeyguard() { - enableMediaControls(); - - // GIVEN a stack with media controls - setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); - - // WHEN we leave the keyguard - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); - mSectionsManager.updateSectionBoundaries(); - - // Then the media controls is removed - verify(mNssl).removeView(mSectionsManager.getMediaControlsView()); - } - - @Test - public void testMediaControls_RemoveWhenPullDownShade() { - enableMediaControls(); - - // GIVEN a stack with media controls - setStackState(ChildType.MEDIA_CONTROLS, ChildType.ALERTING, ChildType.GENTLE_HEADER, - ChildType.GENTLE); - - // WHEN we pull down the shade on the keyguard - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED); - mSectionsManager.updateSectionBoundaries(); - - // Then the media controls is removed - verify(mNssl).removeView(mSectionsManager.getMediaControlsView()); - } - private void enablePeopleFiltering() { when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true); when(mSectionsFeatureManager.getNumberOfBuckets()).thenReturn(4); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java index 57ef05544e7e..b5663d5dd19e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java @@ -55,6 +55,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.doze.DozeLog; +import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.FlingAnimationUtils; @@ -172,6 +173,8 @@ public class NotificationPanelViewTest extends SysuiTestCase { @Mock private ConfigurationController mConfigurationController; @Mock + private MediaHierarchyManager mMediaHiearchyManager; + @Mock private ConversationNotificationManager mConversationNotificationManager; private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder; @@ -228,7 +231,7 @@ public class NotificationPanelViewTest extends SysuiTestCase { mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor, mMetricsLogger, mActivityManager, mZenModeController, mConfigurationController, mFlingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, - mConversationNotificationManager); + mConversationNotificationManager, mMediaHiearchyManager); mNotificationPanelViewController.initDependencies(mStatusBar, mGroupManager, mNotificationShelf, mNotificationAreaController, mScrimController); mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager); |