Merge changes from topic "media_refactor" into rvc-dev am: 4b510ab9b8

Change-Id: Id247270d0ff13888a5b9c09bf5f433f15777cab3
This commit is contained in:
Selim Cinek
2020-05-12 05:39:55 +00:00
committed by Automerger Merge Worker
56 changed files with 3091 additions and 2084 deletions

View File

@@ -19,4 +19,4 @@
systemui:rippleMinSize="30dp"
systemui:rippleMaxSize="135dp"
systemui:highlight="15"
systemui:cornerRadius="@dimen/qs_media_corner_radius" />
systemui:cornerRadius="?android:attr/dialogCornerRadius" />

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
<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"
/>
<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:layout_alignParentLeft="true"
android:fontFamily="@*android:string/config_bodyFontFamily"
android:gravity="left"
android:textSize="14sp" />
<TextView
android:id="@+id/media_total_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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:fontFamily="@*android:string/config_bodyFontFamily"
android:singleLine="true"
android:text="@*android:string/ext_media_seamless_action"
android:textSize="14sp" />
</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"/>
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/view_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/media_guts"
<androidx.constraintlayout.widget.Guideline
android:id="@+id/view_width"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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"
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"
/>
<FrameLayout
android:id="@+id/notification_media_progress_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_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>
</LinearLayout>
</LinearLayout>
app:layout_constraintGuide_begin="300dp" />
</androidx.constraintlayout.motion.widget.MotionLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,40 +168,64 @@ 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);
}
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 */);
}
// Hide any unused buttons
for (; i < ACTION_IDS.length; i++) {
setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
}
// Seek Bar
final MediaController controller = getController();
mBackgroundExecutor.execute(
() -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
// Set up long press menu
// TODO: b/156036025 bring back media guts
makeActive();
// App title (not in mini player)
TextView appName = mMediaNotifView.findViewById(R.id.app_name);
if (appName != null) {
appName.setText(appNameString);
appName.setTextColor(mForegroundColor);
// 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;
}
// Can be null!
MediaMetadata mediaMetadata = mController.getMetadata();
ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
if (albumView != null) {
// Resize art in a background thread
mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
// 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);
}
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
String songName = "";
if (mediaMetadata != null) {
songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
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);
}
titleText.setText(songName);
titleText.setTextColor(mForegroundColor);
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;
}
// 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);
/**
* 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);
}
}
@@ -420,120 +523,6 @@ public class MediaControlPanel {
return (state.getState() == PlaybackState.STATE_PLAYING);
}
/**
* 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

@@ -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;
}
}
protected void onFinishInflate() {
super.onFinishInflate();
// Add media carousel at the end
if (useQsMediaPlayer(getContext())) {
addMediaHostView();
}
}
/**
* 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 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {
}
/**

View File

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

View File

@@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.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

View File

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

View File

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

View File

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

View File

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

View File

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