diff options
author | Selim Cinek <cinek@google.com> | 2020-05-12 05:33:10 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-05-12 05:33:10 +0000 |
commit | 4b510ab9b8bebea3e7aca985e7e3d7af42c55ef9 (patch) | |
tree | 8a982982033ce76c4ad70fa7f4d6a89f485e0fdb | |
parent | 954820b047c97924df3537d239cd7f754901a364 (diff) | |
parent | f418bb0275c5234a44d5d138f70b1dfad529be7d (diff) |
Merge changes from topic "media_refactor" into rvc-dev
* changes:
Handling multiple players better
Fixed an issue where GONE Views would show during transitions
Fixing some issues where view was too short when created
Made the animation logic more stable
Measuring the Media Views now properly the first time its created
When animating between states, animate the view width
Factored out the media view manager from the hiearchyManager
Migrated away from view detaching as a signal
Work back in media uri loading that was changed in the refactor
Media views now dynamically transition between media hosts
Changed the quick settings layout for media
Refactored the Media Player management
Changing media to MotionLayout
Made sure that we're only updating the media notification when inflated
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); |