From cb395358c55b4aea3604f09fb46153388a3e49fb Mon Sep 17 00:00:00 2001 From: Beth Thibodeau Date: Fri, 25 Jan 2019 15:39:54 -0500 Subject: [PATCH] Adding seekbar to media notifications Test: manually tested, screenshots at https://drive.google.com/open?id=1rbiNM_2PG_Kyd4MWnDFUdQy-K3dAVKz3 Bug: 123698590 Change-Id: Ieb7bc6b379f78d1611f7a97615a13a3c0a993f9e --- .../widget/MediaNotificationView.java | 4 +- .../notification_material_media_seekbar.xml | 64 +++++ ...tification_template_material_big_media.xml | 5 + .../notification_template_material_media.xml | 82 ++++--- core/res/res/values/symbols.xml | 7 + packages/SystemUI/res/values/dimens.xml | 3 + .../row/ExpandableNotificationRow.java | 11 + .../NotificationMediaTemplateViewWrapper.java | 225 +++++++++++++++++- 8 files changed, 361 insertions(+), 40 deletions(-) create mode 100644 core/res/res/layout/notification_material_media_seekbar.xml diff --git a/core/java/com/android/internal/widget/MediaNotificationView.java b/core/java/com/android/internal/widget/MediaNotificationView.java index 7609b67e492bd..498bc5a7d598a 100644 --- a/core/java/com/android/internal/widget/MediaNotificationView.java +++ b/core/java/com/android/internal/widget/MediaNotificationView.java @@ -39,6 +39,7 @@ public class MediaNotificationView extends FrameLayout { private View mActions; private View mHeader; private View mMainColumn; + private View mMediaContent; private int mImagePushIn; public MediaNotificationView(Context context) { @@ -70,7 +71,7 @@ public class MediaNotificationView extends FrameLayout { (MarginLayoutParams) mRightIcon.getLayoutParams(); int imageEndMargin = layoutParams.getMarginEnd(); size -= imageEndMargin; - int fullHeight = getMeasuredHeight(); + int fullHeight = mMediaContent.getMeasuredHeight(); if (size > fullHeight) { size = fullHeight; } else if (size < fullHeight) { @@ -154,5 +155,6 @@ public class MediaNotificationView extends FrameLayout { mActions = findViewById(com.android.internal.R.id.media_actions); mHeader = findViewById(com.android.internal.R.id.notification_header); mMainColumn = findViewById(com.android.internal.R.id.notification_main_column); + mMediaContent = findViewById(com.android.internal.R.id.notification_media_content); } } diff --git a/core/res/res/layout/notification_material_media_seekbar.xml b/core/res/res/layout/notification_material_media_seekbar.xml new file mode 100644 index 0000000000000..1b691d68a2013 --- /dev/null +++ b/core/res/res/layout/notification_material_media_seekbar.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/layout/notification_template_material_big_media.xml b/core/res/res/layout/notification_template_material_big_media.xml index 56f7a593cc439..3267f726c69d0 100644 --- a/core/res/res/layout/notification_template_material_big_media.xml +++ b/core/res/res/layout/notification_template_material_big_media.xml @@ -39,6 +39,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" + android:id="@+id/notification_media_content" > + diff --git a/core/res/res/layout/notification_template_material_media.xml b/core/res/res/layout/notification_template_material_media.xml index 01b0866f428c4..64d91ad867f92 100644 --- a/core/res/res/layout/notification_template_material_media.xml +++ b/core/res/res/layout/notification_template_material_media.xml @@ -34,49 +34,61 @@ android:layout_width="match_parent" android:layout_height="@dimen/media_notification_header_height" /> - - - - - - - + + + + + + + + + + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c9098736fdc9e..1973bf31053b2 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -195,6 +195,12 @@ + + + + + + @@ -1573,6 +1579,7 @@ + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1060211cebf53..e23fda2b15c8e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -83,6 +83,9 @@ 146dp + + 160dp + 64dp diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index bed2426021a15..b8e33a8e0d2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationMediaTemplateViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; @@ -144,6 +145,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mNotificationMinHeightBeforeP; private int mNotificationMinHeight; private int mNotificationMinHeightLarge; + private int mNotificationMinHeightMedia; private int mNotificationMaxHeight; private int mIncreasedPaddingBetweenElements; private int mNotificationLaunchHeight; @@ -652,8 +654,15 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N; boolean beforeP = mEntry.targetSdk < Build.VERSION_CODES.P; int minHeight; + + View expandedView = layout.getExpandedChild(); + boolean isMediaLayout = expandedView != null + && expandedView.findViewById(com.android.internal.R.id.media_actions) != null; + if (customView && beforeP && !mIsSummaryWithChildren) { minHeight = beforeN ? mNotificationMinHeightBeforeN : mNotificationMinHeightBeforeP; + } else if (isMediaLayout && !NotificationMediaTemplateViewWrapper.HIDE_COMPACT_SCRUBBER) { + minHeight = mNotificationMinHeightMedia; } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) { minHeight = mNotificationMinHeightLarge; } else { @@ -1642,6 +1651,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView R.dimen.notification_min_height); mNotificationMinHeightLarge = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_min_height_increased); + mNotificationMinHeightMedia = NotificationUtils.getFontScaledHeight(mContext, + R.dimen.notification_min_height_media); mNotificationMaxHeight = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_height); mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, 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 5a9a56865a5ce..ddda3e50fc2c9 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 @@ -16,26 +16,222 @@ package com.android.systemui.statusbar.notification.row.wrapper; -import android.content.Context; -import android.view.View; +import static com.android.systemui.Dependency.MAIN_HANDLER; +import android.app.Notification; +import android.content.Context; +import android.content.res.ColorStateList; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Handler; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.systemui.Dependency; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import java.util.Timer; +import java.util.TimerTask; + /** * Wraps a notification containing a media template */ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateViewWrapper { + private static final String TAG = "NotificationMediaTVW"; + private static final long PROGRESS_UPDATE_INTERVAL = 1000; // 1s + private static final String COMPACT_MEDIA_TAG = "media"; + private final Handler mHandler = Dependency.get(MAIN_HANDLER); + private Timer mSeekBarTimer; + private View mActions; + private SeekBar mSeekBar; + private TextView mSeekBarElapsedTime; + private TextView mSeekBarTotalTime; + private long mDuration = 0; + private MediaController mMediaController; + private View mSeekBarView; + private Context mContext; + + // TODO: implement as phenotype flag + public static final boolean HIDE_COMPACT_SCRUBBER = true; + + private SeekBar.OnSeekBarChangeListener mSeekListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mMediaController != null && canSeekMedia()) { + mMediaController.getTransportControls().seekTo(mSeekBar.getProgress()); + } + } + }; + + private MediaController.Callback mMediaCallback = new MediaController.Callback() { + @Override + public void onSessionDestroyed() { + clearTimer(); + mMediaController.unregisterCallback(this); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (state.getState() != PlaybackState.STATE_PLAYING) { + clearTimer(); + } else if (mSeekBarTimer == null) { + startTimer(); + } + } + }; + protected NotificationMediaTemplateViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); + mContext = ctx; } - View mActions; - private void resolveViews() { mActions = mView.findViewById(com.android.internal.R.id.media_actions); + + final MediaSession.Token token = mRow.getEntry().notification.getNotification().extras + .getParcelable(Notification.EXTRA_MEDIA_SESSION); + + if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && HIDE_COMPACT_SCRUBBER)) { + if (mSeekBarView != null) { + mSeekBarView.setVisibility(View.GONE); + } + return; + } + + // Check for existing media controller and clean up / create as necessary + if (mMediaController == null || !mMediaController.getSessionToken().equals(token)) { + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaCallback); + } + mMediaController = new MediaController(mContext, token); + } + + if (mMediaController.getMetadata() != null) { + long duration = mMediaController.getMetadata().getLong( + MediaMetadata.METADATA_KEY_DURATION); + if (duration <= 0) { + // Don't include the seekbar if this is a livestream + Log.d(TAG, "removing seekbar"); + if (mSeekBarView != null) { + mSeekBarView.setVisibility(View.GONE); + } + return; + } else { + // Otherwise, make sure the seekbar is visible + if (mSeekBarView != null) { + mSeekBarView.setVisibility(View.VISIBLE); + } + } + } + + // Inflate the seekbar template + ViewStub stub = mView.findViewById(R.id.notification_media_seekbar_container); + if (stub instanceof ViewStub) { + LayoutInflater layoutInflater = LayoutInflater.from(stub.getContext()); + stub.setLayoutInflater(layoutInflater); + stub.setLayoutResource(R.layout.notification_material_media_seekbar); + mSeekBarView = stub.inflate(); + + mSeekBar = mSeekBarView.findViewById(R.id.notification_media_progress_bar); + mSeekBar.setOnSeekBarChangeListener(mSeekListener); + + mSeekBarElapsedTime = mSeekBarView.findViewById(R.id.notification_media_elapsed_time); + mSeekBarTotalTime = mSeekBarView.findViewById(R.id.notification_media_total_time); + + if (mSeekBarTimer == null) { + // Disable seeking if it is not supported for this media session + if (!canSeekMedia()) { + mSeekBar.getThumb().setAlpha(0); + mSeekBar.setEnabled(false); + } else { + mSeekBar.getThumb().setAlpha(255); + mSeekBar.setEnabled(true); + } + + startTimer(); + + mMediaController.registerCallback(mMediaCallback); + } + } + updateSeekBarTint(mSeekBarView); + } + + private void startTimer() { + clearTimer(); + mSeekBarTimer = new Timer(true /* isDaemon */); + mSeekBarTimer.schedule(new TimerTask() { + @Override + public void run() { + mHandler.post(mUpdatePlaybackUi); + } + }, 0, PROGRESS_UPDATE_INTERVAL); + } + + private void clearTimer() { + if (mSeekBarTimer != null) { + // TODO: also trigger this when the notification panel is collapsed + mSeekBarTimer.cancel(); + mSeekBarTimer.purge(); + mSeekBarTimer = null; + } + } + + private boolean canSeekMedia() { + if (mMediaController == null || mMediaController.getPlaybackState() == null) { + return false; + } + + long actions = mMediaController.getPlaybackState().getActions(); + return (actions == 0 || (actions & PlaybackState.ACTION_SEEK_TO) != 0); + } + + protected final Runnable mUpdatePlaybackUi = new Runnable() { + @Override + public void run() { + if (mMediaController != null && mMediaController.getMetadata() != null + && mSeekBar != null) { + long position = mMediaController.getPlaybackState().getPosition(); + long duration = mMediaController.getMetadata().getLong( + MediaMetadata.METADATA_KEY_DURATION); + + if (mDuration != duration) { + mDuration = duration; + mSeekBar.setMax((int) mDuration); + mSeekBarTotalTime.setText(millisecondsToTimeString(duration)); + } + mSeekBar.setProgress((int) position); + + mSeekBarElapsedTime.setText(millisecondsToTimeString(position)); + } else { + // We no longer have a media session / notification + clearTimer(); + } + } + }; + + private String millisecondsToTimeString(long milliseconds) { + long seconds = milliseconds / 1000; + String text = DateUtils.formatElapsedTime(seconds); + return text; } @Override @@ -46,6 +242,27 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi super.onContentUpdated(row); } + private void updateSeekBarTint(View seekBarContainer) { + if (seekBarContainer == null) { + return; + } + + if (this.getNotificationHeader() == null) { + return; + } + + int tintColor = getNotificationHeader().getOriginalIconColor(); + mSeekBarElapsedTime.setTextColor(tintColor); + mSeekBarTotalTime.setTextColor(tintColor); + + ColorStateList tintList = ColorStateList.valueOf(tintColor); + mSeekBar.setThumbTintList(tintList); + tintList = tintList.withAlpha(192); // 75% + mSeekBar.setProgressTintList(tintList); + tintList = tintList.withAlpha(128); // 50% + mSeekBar.setProgressBackgroundTintList(tintList); + } + @Override protected void updateTransformedTypes() { // This also clears the existing types