diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index a01bbe38f2968..9791241a52af0 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -41,6 +41,8 @@ 48dp 176dp + + 224dp 48dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e702c68d443e6..e2f57fda5bc55 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1774,6 +1774,7 @@ + diff --git a/packages/SystemUI/res/layout/qqs_media_panel.xml b/packages/SystemUI/res/layout/qqs_media_panel.xml new file mode 100644 index 0000000000000..1189371fc7f13 --- /dev/null +++ b/packages/SystemUI/res/layout/qqs_media_panel.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml new file mode 100644 index 0000000000000..dd422766c1536 --- /dev/null +++ b/packages/SystemUI/res/layout/qs_media_panel.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index ed18dc728402a..e99b91787072d 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -43,7 +43,7 @@ 10sp + + 8dp + + + 150dp + 350dp + 8dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt new file mode 100644 index 0000000000000..f710f7fc47e2b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/DoubleLineTileLayout.kt @@ -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() + 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 +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java index 9221b68521123..a267bbb92ee79 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java @@ -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(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java new file mode 100644 index 0000000000000..af418f6308a0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -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; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 2e24403d460f3..20600596c3a14 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -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 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(), diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java new file mode 100644 index 0000000000000..ae66cd5767656 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java @@ -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; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 85aafa06961a8..dcd4633a79d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 4013586d41973..592e3881ea973 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -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( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 095ca54f3bb2b..d6b87afc53b93 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -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. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index 5d5c09e98f113..9bc0ca440893c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 89051cda15ab0..30fe68a28ef24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -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) {