Merge changes from topic "media_refactor" into rvc-dev am: 4b510ab9b8
Change-Id: Id247270d0ff13888a5b9c09bf5f433f15777cab3
This commit is contained in:
@@ -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" />
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
447
packages/SystemUI/res/xml/media_scene.xml
Normal file
447
packages/SystemUI/res/xml/media_scene.xml
Normal 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>
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
159
packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
Normal file
159
packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/** */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user