Add QS media player options

This adds a mini player in the QQS, and media carousel in QS
Incorporates the WIP layout changes from ag/9415169

To enable: adb shell settings put system qs_media_player 1
then toggle dark mode, or adb shell stop && adb shell start

Known issues with color overlays not updating until you press a button,
and old sessions do not get automatically removed from the carousel.

Test: manual
Change-Id: Iaeda470a920cb115c28ec98f04d74f255e1d5a12
This commit is contained in:
Beth Thibodeau
2019-10-16 13:45:56 -04:00
parent 7825637f80
commit 07d20c3243
16 changed files with 1061 additions and 6 deletions

View File

@@ -41,6 +41,8 @@
<dimen name="quick_qs_offset_height">48dp</dimen>
<!-- Total height of QQS (quick_qs_offset_height + 128) -->
<dimen name="quick_qs_total_height">176dp</dimen>
<!-- Total height of QQS with two rows to fit media player (quick_qs_offset_height + 176) -->
<dimen name="quick_qs_total_height_with_media">224dp</dimen>
<!-- Height of the bottom navigation / system bar. -->
<dimen name="navigation_bar_height">48dp</dimen>
<!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->

View File

@@ -1774,6 +1774,7 @@
<java-symbol type="dimen" name="display_cutout_touchable_region_size" />
<java-symbol type="dimen" name="quick_qs_offset_height" />
<java-symbol type="dimen" name="quick_qs_total_height" />
<java-symbol type="dimen" name="quick_qs_total_height_with_media" />
<java-symbol type="drawable" name="ic_jog_dial_sound_off" />
<java-symbol type="drawable" name="ic_jog_dial_sound_on" />
<java-symbol type="drawable" name="ic_jog_dial_unlock" />

View File

@@ -0,0 +1,100 @@
<?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:padding="10dp"
>
<!-- Top line: icon + artist name -->
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:gravity="center"
>
<com.android.internal.widget.CachingIconView
android:id="@+id/icon"
android:layout_width="15dp"
android:layout_height="15dp"
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>
<!-- Second line: song name -->
<TextView
android:id="@+id/header_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:fontFamily="@*android:string/config_bodyFontFamily"
android:gravity="center"/>
<!-- 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="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action0"
/>
<ImageButton
style="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action1"
/>
<ImageButton
style="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action2"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,124 @@
<?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 media controls inside QSPanel carousel -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/qs_media_controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal|fill_vertical"
android:padding="10dp"
>
<!-- placeholder for notification header -->
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/header"
android:padding="3dp"
android:layout_marginEnd="-12dp"
/>
<!-- Top line: artist name -->
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
>
<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>
<!-- Second line: song name -->
<TextView
android:id="@+id/header_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:fontFamily="@*android:string/config_bodyFontFamily"
android:gravity="center"/>
<!-- 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="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action0"
/>
<ImageButton
style="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action1"
/>
<ImageButton
style="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action2"
/>
<ImageButton
style="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action3"
/>
<ImageButton
style="@android:style/Widget.Material.Button.Borderless.Small"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:layout_marginEnd="2dp"
android:gravity="center"
android:visibility="gone"
android:id="@+id/action4"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -43,7 +43,7 @@
<com.android.systemui.qs.QuickQSPanel
android:id="@+id/quick_qs_panel"
android:layout_width="match_parent"
android:layout_height="48dp"
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"

View File

@@ -1164,4 +1164,11 @@
<!-- Size of the RAT type for CellularTile -->
<dimen name="celltile_rat_type_size">10sp</dimen>
<dimen name="new_qs_vertical_margin">8dp</dimen>
<!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_height">150dp</dimen>
<dimen name="qs_media_width">350dp</dimen>
<dimen name="qs_media_padding">8dp</dimen>
</resources>

View File

@@ -0,0 +1,133 @@
/*
* 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.content.Context
import android.content.res.Configuration
import android.view.View
import android.view.ViewGroup
import com.android.systemui.R
import com.android.systemui.qs.TileLayout.exactly
class DoubleLineTileLayout(context: Context) : ViewGroup(context), QSPanel.QSTileLayout {
protected val mRecords = ArrayList<QSPanel.TileRecord>()
private var _listening = false
private var smallTileSize = 0
private val twoLineHeight
get() = smallTileSize * 2 + cellMarginVertical
private var cellMarginHorizontal = 0
private var cellMarginVertical = 0
init {
isFocusableInTouchMode = true
clipChildren = false
clipToPadding = false
updateResources()
}
override fun addTile(tile: QSPanel.TileRecord) {
mRecords.add(tile)
tile.tile.setListening(this, _listening)
addTileView(tile)
}
protected fun addTileView(tile: QSPanel.TileRecord) {
addView(tile.tileView)
}
override fun removeTile(tile: QSPanel.TileRecord) {
mRecords.remove(tile)
tile.tile.setListening(this, false)
removeView(tile.tileView)
}
override fun removeAllViews() {
mRecords.forEach { it.tile.setListening(this, false) }
mRecords.clear()
super.removeAllViews()
}
override fun getOffsetTop(tile: QSPanel.TileRecord?) = top
override fun updateResources(): Boolean {
with(mContext.resources) {
smallTileSize = getDimensionPixelSize(R.dimen.qs_quick_tile_size)
cellMarginHorizontal = getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal)
cellMarginVertical = getDimensionPixelSize(R.dimen.new_qs_vertical_margin)
}
requestLayout()
return false
}
override fun setListening(listening: Boolean) {
if (_listening == listening) return
_listening = listening
for (record in mRecords) {
record.tile.setListening(this, listening)
}
}
override fun getNumVisibleTiles() = mRecords.size
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateResources()
}
override fun onFinishInflate() {
updateResources()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var previousView: View = this
var tiles = 0
mRecords.forEach {
val tileView = it.tileView
if (tileView.visibility != View.GONE) {
tileView.updateAccessibilityOrder(previousView)
previousView = tileView
tiles++
tileView.measure(exactly(smallTileSize), exactly(smallTileSize))
}
}
val height = twoLineHeight
val columns = tiles / 2
val width = paddingStart + paddingEnd +
columns * smallTileSize +
(columns - 1) * cellMarginHorizontal
setMeasuredDimension(width, height)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val tiles = mRecords.filter { it.tileView.visibility != View.GONE }
tiles.forEachIndexed {
index, tile ->
val column = index % (tiles.size / 2)
val left = getLeftForColumn(column)
val top = if (index < tiles.size / 2) 0 else getTopBottomRow()
tile.tileView.layout(left, top, left + smallTileSize, top + smallTileSize)
}
}
private fun getLeftForColumn(column: Int) = column * (smallTileSize + cellMarginHorizontal)
private fun getTopBottomRow() = smallTileSize + cellMarginVertical
}

View File

@@ -14,6 +14,7 @@
package com.android.systemui.qs;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
@@ -267,6 +268,17 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha
mAllViews.add(tileView);
count++;
}
int flag = Settings.System.getInt(mQsPanel.getContext().getContentResolver(),
"qs_media_player", 0);
if (flag == 1) {
View qsMediaView = mQsPanel.getMediaPanel();
View qqsMediaView = mQuickQsPanel.getMediaPlayer().getView();
translationXBuilder.addFloat(qsMediaView, "alpha", 0, 1);
translationXBuilder.addFloat(qqsMediaView, "alpha", 1, 0);
}
if (mAllowFancy) {
// Make brightness appear static position and alpha in through second half.
View brightness = mQsPanel.getBrightnessView();

View File

@@ -0,0 +1,301 @@
/*
* 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.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.RippleDrawable;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RemoteViews;
import android.widget.TextView;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.MediaTransferManager;
/**
* Single media player for carousel in QSPanel
*/
public class QSMediaPlayer {
private static final String TAG = "QSMediaPlayer";
private Context mContext;
private LinearLayout mMediaNotifView;
private MediaSession.Token mToken;
private MediaController mController;
private int mWidth;
private int mHeight;
/**
*
* @param context
* @param parent
* @param width
* @param height
*/
public QSMediaPlayer(Context context, ViewGroup parent, int width, int height) {
mContext = context;
LayoutInflater inflater = LayoutInflater.from(mContext);
mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
mWidth = width;
mHeight = height;
}
public View getView() {
return mMediaNotifView;
}
/**
*
* @param token token for this media session
* @param icon app notification icon
* @param iconColor foreground color (for text, icons)
* @param bgColor background color
* @param actionsContainer a LinearLayout containing the media action buttons
* @param notif
*/
public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
View actionsContainer, Notification notif) {
Log.d(TAG, "got media session: " + token);
mToken = token;
mController = new MediaController(mContext, token);
MediaMetadata mMediaMetadata = mController.getMetadata();
Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif);
// Album art
addAlbumArtBackground(mMediaMetadata, bgColor, mWidth, mHeight);
// Reuse notification header instead of reimplementing everything
RemoteViews headerRemoteView = builder.makeNotificationHeader();
LinearLayout headerView = mMediaNotifView.findViewById(R.id.header);
View result = headerRemoteView.apply(mContext, headerView);
result.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, 75);
result.setLayoutParams(lp);
headerView.removeAllViews();
headerView.addView(result);
View seamless = headerView.findViewById(com.android.internal.R.id.media_seamless);
seamless.setVisibility(View.VISIBLE);
// App icon
ImageView appIcon = headerView.findViewById(com.android.internal.R.id.icon);
Drawable iconDrawable = icon.loadDrawable(mContext);
iconDrawable.setTint(iconColor);
appIcon.setImageDrawable(iconDrawable);
// App title
TextView appName = headerView.findViewById(com.android.internal.R.id.app_name_text);
String appNameString = builder.loadHeaderAppName();
appName.setText(appNameString);
appName.setTextColor(iconColor);
// Action
mMediaNotifView.setOnClickListener(v -> {
try {
notif.contentIntent.send();
// Also close shade
mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent was canceled");
e.printStackTrace();
}
});
// Separator
TextView separator = headerView.findViewById(com.android.internal.R.id.header_text_divider);
separator.setTextColor(iconColor);
// Album name
TextView albumName = headerView.findViewById(com.android.internal.R.id.header_text);
String albumString = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
if (!albumString.isEmpty()) {
albumName.setText(albumString);
albumName.setTextColor(iconColor);
albumName.setVisibility(View.VISIBLE);
separator.setVisibility(View.VISIBLE);
} else {
albumName.setVisibility(View.GONE);
separator.setVisibility(View.GONE);
}
// Transfer chip
MediaTransferManager mediaTransferManager = new MediaTransferManager(mContext);
View transferBackgroundView = headerView.findViewById(
com.android.internal.R.id.media_seamless);
LinearLayout viewLayout = (LinearLayout) transferBackgroundView;
RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
rect.setStroke(2, iconColor);
rect.setColor(bgColor);
ImageView transferIcon = headerView.findViewById(
com.android.internal.R.id.media_seamless_image);
transferIcon.setBackgroundColor(bgColor);
transferIcon.setImageTintList(ColorStateList.valueOf(iconColor));
TextView transferText = headerView.findViewById(
com.android.internal.R.id.media_seamless_text);
transferText.setTextColor(iconColor);
ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
transferBackgroundView.setOnClickListener(v -> {
final Intent intent = new Intent()
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT);
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
});
// Artist name
TextView artistText = mMediaNotifView.findViewById(R.id.header_title);
String artistName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
artistText.setText(artistName);
artistText.setTextColor(iconColor);
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_text);
String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
titleText.setText(songName);
titleText.setTextColor(iconColor);
// Media controls
LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
final int[] actionIds = {
R.id.action0,
R.id.action1,
R.id.action2,
R.id.action3,
R.id.action4
};
final int[] notifActionIds = {
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
};
for (int i = 0; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]);
if (thatBtn == null || thatBtn.getDrawable() == null) {
thisBtn.setVisibility(View.GONE);
continue;
}
Drawable thatIcon = thatBtn.getDrawable();
thisBtn.setImageDrawable(thatIcon.mutate());
thisBtn.setVisibility(View.VISIBLE);
thisBtn.setOnClickListener(v -> {
Log.d(TAG, "clicking on other button");
thatBtn.performClick();
});
}
}
public MediaSession.Token getMediaSessionToken() {
return mToken;
}
public String getMediaPlayerPackage() {
return mController.getPackageName();
}
/**
* Check whether the media controlled by this player is currently playing
* @return whether it is playing, or false if no controller information
*/
public boolean isPlaying() {
if (mController == null) {
return false;
}
PlaybackState state = mController.getPlaybackState();
if (state == null) {
return false;
}
return (state.getState() == PlaybackState.STATE_PLAYING);
}
private void addAlbumArtBackground(MediaMetadata metadata, int bgColor, int width, int height) {
Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
if (albumArt != null) {
Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
Bitmap scaled = scaleBitmap(original, width, height);
Canvas canvas = new Canvas(scaled);
// Add translucent layer over album art to improve contrast
Paint p = new Paint();
p.setStyle(Paint.Style.FILL);
p.setColor(bgColor);
p.setAlpha(200);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), p);
RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
mContext.getResources(), scaled);
roundedDrawable.setCornerRadius(20);
mMediaNotifView.setBackground(roundedDrawable);
} else {
Log.e(TAG, "No album art available");
}
}
private Bitmap scaleBitmap(Bitmap original, int width, int height) {
Bitmap cropped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(cropped);
float scale = (float) cropped.getWidth() / (float) original.getWidth();
float dy = (cropped.getHeight() - original.getHeight() * scale) / 2.0f;
Matrix transformation = new Matrix();
transformation.postTranslate(0, dy);
transformation.preScale(scale, scale);
Paint paint = new Paint();
paint.setFilterBitmap(true);
canvas.drawBitmap(original, transformation, paint);
return cropped;
}
}

View File

@@ -24,16 +24,22 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.metrics.LogMaker;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
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.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import com.android.internal.logging.MetricsLogger;
@@ -82,6 +88,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final QSTileRevealController mQsTileRevealController;
private final LinearLayout mMediaCarousel;
private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
protected boolean mExpanded;
protected boolean mListening;
@@ -140,6 +149,27 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
addDivider();
// Add media carousel
int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_player", 0);
if (flag == 1) {
HorizontalScrollView mediaScrollView = new HorizontalScrollView(mContext);
mediaScrollView.setHorizontalScrollBarEnabled(false);
int playerHeight = (int) getResources().getDimension(R.dimen.qs_media_height);
int padding = (int) getResources().getDimension(R.dimen.qs_media_padding);
LayoutParams lpView = new LayoutParams(LayoutParams.MATCH_PARENT, playerHeight);
lpView.setMarginStart(padding);
lpView.setMarginEnd(padding);
addView(mediaScrollView, lpView);
LayoutParams lpCarousel = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
mMediaCarousel = new LinearLayout(mContext);
mMediaCarousel.setOrientation(LinearLayout.HORIZONTAL);
mediaScrollView.addView(mMediaCarousel, lpCarousel);
} else {
mMediaCarousel = null;
}
mFooter = new QSSecurityFooter(this, context);
addView(mFooter.getView());
@@ -159,6 +189,72 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
}
/**
* Add or update a player for the associated media session
* @param token
* @param icon
* @param iconColor
* @param bgColor
* @param actionsContainer
* @param notif
*/
public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
View actionsContainer, StatusBarNotification notif) {
int flag = Settings.System.getInt(mContext.getContentResolver(), "qs_media_player", 0);
if (flag != 1) {
// Shouldn't happen, but just in case
Log.e(TAG, "Tried to add media session without player!");
return;
}
QSMediaPlayer player = null;
String packageName = notif.getPackageName();
for (QSMediaPlayer p : mMediaPlayers) {
if (p.getMediaSessionToken().equals(token)) {
Log.d(TAG, "a player for this session already exists");
player = p;
break;
}
if (packageName.equals(p.getMediaPlayerPackage())) {
Log.d(TAG, "found an old session for this app");
player = p;
break;
}
}
int playerHeight = (int) getResources().getDimension(R.dimen.qs_media_height);
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");
player = new QSMediaPlayer(mContext, this, playerWidth, playerHeight);
if (player.isPlaying()) {
mMediaCarousel.addView(player.getView(), 0, lp); // add in front
} else {
mMediaCarousel.addView(player.getView(), lp); // add at end
}
} else if (player.isPlaying()) {
// move it to the front
mMediaCarousel.removeView(player.getView());
mMediaCarousel.addView(player.getView(), 0, lp);
}
Log.d(TAG, "setting player session");
player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
notif.getNotification());
mMediaPlayers.add(player);
}
protected View getMediaPanel() {
return mMediaCarousel;
}
protected void addDivider() {
mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),

View File

@@ -0,0 +1,203 @@
/*
* 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.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
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.media.session.PlaybackState;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.systemui.R;
/**
* QQS mini media player
*/
public class QuickQSMediaPlayer {
private static final String TAG = "QQSMediaPlayer";
private Context mContext;
private LinearLayout mMediaNotifView;
private MediaSession.Token mToken;
private MediaController mController;
/**
*
* @param context
* @param parent
*/
public QuickQSMediaPlayer(Context context, ViewGroup parent) {
mContext = context;
LayoutInflater inflater = LayoutInflater.from(mContext);
mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qqs_media_panel, parent, false);
}
public View getView() {
return mMediaNotifView;
}
/**
*
* @param token token for this media session
* @param icon app notification icon
* @param iconColor foreground color (for text, icons)
* @param bgColor background color
* @param actionsContainer a LinearLayout containing the media action buttons
*/
public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
View actionsContainer) {
Log.d(TAG, "Setting media session: " + token);
mToken = token;
mController = new MediaController(mContext, token);
MediaMetadata mMediaMetadata = mController.getMetadata();
// Album art
addAlbumArtBackground(mMediaMetadata, bgColor);
// App icon
ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Drawable iconDrawable = icon.loadDrawable(mContext);
iconDrawable.setTint(iconColor);
appIcon.setImageDrawable(iconDrawable);
// Artist name
TextView appText = mMediaNotifView.findViewById(R.id.header_title);
String artistName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
appText.setText(artistName);
appText.setTextColor(iconColor);
// Song name
TextView titleText = mMediaNotifView.findViewById(R.id.header_text);
String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
titleText.setText(songName);
titleText.setTextColor(iconColor);
// Action buttons
LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2};
// TODO some apps choose different buttons to show in compact mode
final int[] notifActionIds = {
com.android.internal.R.id.action1,
com.android.internal.R.id.action2,
com.android.internal.R.id.action3
};
for (int i = 0; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {
ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]);
if (thatBtn == null || thatBtn.getDrawable() == null) {
thisBtn.setVisibility(View.GONE);
continue;
}
Drawable thatIcon = thatBtn.getDrawable();
thisBtn.setImageDrawable(thatIcon.mutate());
thisBtn.setVisibility(View.VISIBLE);
thisBtn.setOnClickListener(v -> {
Log.d(TAG, "clicking on other button");
thatBtn.performClick();
});
}
}
public MediaSession.Token getMediaSessionToken() {
return mToken;
}
/**
* Check whether the media controlled by this player is currently playing
* @return whether it is playing, or false if no controller information
*/
public boolean isPlaying() {
if (mController == null) {
return false;
}
PlaybackState state = mController.getPlaybackState();
if (state == null) {
return false;
}
return (state.getState() == PlaybackState.STATE_PLAYING);
}
private void addAlbumArtBackground(MediaMetadata metadata, int bgColor) {
Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
if (albumArt != null) {
Rect bounds = new Rect();
mMediaNotifView.getBoundsOnScreen(bounds);
int width = bounds.width();
int height = bounds.height();
Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
Bitmap scaled = scaleBitmap(original, width, height);
Canvas canvas = new Canvas(scaled);
// Add translucent layer over album art to improve contrast
Paint p = new Paint();
p.setStyle(Paint.Style.FILL);
p.setColor(bgColor);
p.setAlpha(200);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), p);
RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
mContext.getResources(), scaled);
roundedDrawable.setCornerRadius(20);
mMediaNotifView.setBackground(roundedDrawable);
} else {
Log.e(TAG, "No album art available");
}
}
private Bitmap scaleBitmap(Bitmap original, int width, int height) {
Bitmap cropped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(cropped);
float scale = (float) cropped.getWidth() / (float) original.getWidth();
float dy = (cropped.getHeight() - original.getHeight() * scale) / 2.0f;
Matrix transformation = new Matrix();
transformation.postTranslate(0, dy);
transformation.preScale(scale, scale);
Paint paint = new Paint();
paint.setFilterBitmap(true);
canvas.drawBitmap(original, transformation, paint);
return cropped;
}
}

View File

@@ -21,6 +21,7 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
@@ -55,6 +56,7 @@ public class QuickQSPanel extends QSPanel {
private boolean mDisabledByPolicy;
private int mMaxTiles;
protected QSPanel mFullPanel;
private QuickQSMediaPlayer mMediaPlayer;
@Inject
public QuickQSPanel(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
@@ -69,11 +71,43 @@ public class QuickQSPanel extends QSPanel {
}
removeView((View) mTileLayout);
}
sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns);
mTileLayout = new HeaderTileLayout(context);
mTileLayout.setListening(mListening);
addView((View) mTileLayout, 0 /* Between brightness and footer */);
super.setPadding(0, 0, 0, 0);
int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_player", 0);
if (flag == 1) {
LinearLayout mHorizontalLinearLayout = new LinearLayout(mContext);
mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
mHorizontalLinearLayout.setClipChildren(false);
mHorizontalLinearLayout.setClipToPadding(false);
LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
mTileLayout = new DoubleLineTileLayout(context);
lp.setMarginEnd(10);
lp.setMarginStart(0);
mHorizontalLinearLayout.addView((View) mTileLayout, lp);
mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout);
lp.setMarginEnd(0);
lp.setMarginStart(10);
mHorizontalLinearLayout.addView(mMediaPlayer.getView(), lp);
sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns);
mTileLayout.setListening(mListening);
addView(mHorizontalLinearLayout, 0 /* Between brightness and footer */);
super.setPadding(0, 0, 0, 0);
} else {
sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns);
mTileLayout = new HeaderTileLayout(context);
mTileLayout.setListening(mListening);
addView((View) mTileLayout, 0 /* Between brightness and footer */);
super.setPadding(0, 0, 0, 0);
}
}
public QuickQSMediaPlayer getMediaPlayer() {
return mMediaPlayer;
}
@Override

View File

@@ -392,9 +392,15 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
int flag = Settings.System.getInt(mContext.getContentResolver(), "qs_media_player", 0);
if (mQsDisabled) {
lp.height = resources.getDimensionPixelSize(
com.android.internal.R.dimen.quick_qs_offset_height);
} else if (flag == 1) {
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(

View File

@@ -21,6 +21,7 @@ import android.content.res.Resources;
import android.os.Handler;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -86,6 +87,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
private final BubbleController mBubbleController;
private final DynamicPrivacyController mDynamicPrivacyController;
private final KeyguardBypassController mBypassController;
private final Context mContext;
private NotificationPresenter mPresenter;
private NotificationListContainer mListContainer;
@@ -107,6 +109,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
KeyguardBypassController bypassController,
BubbleController bubbleController,
DynamicPrivacyController privacyController) {
mContext = context;
mHandler = mainHandler;
mLockscreenUserManager = notificationLockscreenUserManager;
mBypassController = bypassController;
@@ -143,7 +146,11 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle
final int N = activeNotifications.size();
for (int i = 0; i < N; i++) {
NotificationEntry ent = activeNotifications.get(i);
int flag = Settings.System.getInt(mContext.getContentResolver(),
"qs_media_player", 0);
boolean hideMedia = (flag == 1);
if (ent.isRowDismissed() || ent.isRowRemoved()
|| (ent.isMediaNotification() && hideMedia)
|| mBubbleController.isBubbleNotificationSuppressedFromShade(ent.getKey())) {
// we don't want to update removed notifications because they could
// temporarily become children if they were isolated before.

View File

@@ -28,6 +28,7 @@ import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.metrics.LogMaker;
import android.os.Handler;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -41,9 +42,12 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.widget.MediaNotificationView;
import com.android.systemui.Dependency;
import com.android.systemui.qs.QSPanel;
import com.android.systemui.qs.QuickQSPanel;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.TransformableView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import java.util.Timer;
import java.util.TimerTask;
@@ -178,6 +182,26 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi
final MediaSession.Token token = mRow.getEntry().getSbn().getNotification().extras
.getParcelable(Notification.EXTRA_MEDIA_SESSION);
int flag = Settings.System.getInt(mContext.getContentResolver(), "qs_media_player", 0);
if (flag == 1) {
StatusBarWindowController ctrl = Dependency.get(StatusBarWindowController.class);
QuickQSPanel panel = ctrl.getStatusBarView().findViewById(
com.android.systemui.R.id.quick_qs_panel);
panel.getMediaPlayer().setMediaSession(token,
mRow.getStatusBarNotification().getNotification().getSmallIcon(),
getNotificationHeader().getOriginalIconColor(),
mRow.getCurrentBackgroundTint(),
mActions);
QSPanel bigPanel = ctrl.getStatusBarView().findViewById(
com.android.systemui.R.id.quick_settings_panel);
bigPanel.addMediaSession(token,
mRow.getStatusBarNotification().getNotification().getSmallIcon(),
getNotificationHeader().getOriginalIconColor(),
mRow.getCurrentBackgroundTint(),
mActions,
mRow.getStatusBarNotification());
}
boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar();
if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) {
if (mSeekBarView != null) {

View File

@@ -770,6 +770,11 @@ public class NotificationPanelView extends PanelView implements
int sideMargin = res.getDimensionPixelOffset(R.dimen.notification_side_paddings);
int topMargin =
res.getDimensionPixelOffset(com.android.internal.R.dimen.quick_qs_total_height);
int flag = Settings.System.getInt(mContext.getContentResolver(), "qs_media_player", 0);
if (flag == 1) {
topMargin = res.getDimensionPixelOffset(
com.android.internal.R.dimen.quick_qs_total_height_with_media);
}
lp = (FrameLayout.LayoutParams) mPluginFrame.getLayoutParams();
if (lp.width != qsWidth || lp.gravity != panelGravity || lp.leftMargin != sideMargin
|| lp.rightMargin != sideMargin || lp.topMargin != topMargin) {