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
This commit is contained in:
Beth Thibodeau
2019-01-25 15:39:54 -05:00
parent 22f25a926a
commit cb395358c5
8 changed files with 361 additions and 40 deletions

View File

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

View File

@@ -0,0 +1,64 @@
<?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
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/notification_media_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentBottom="true"
>
<SeekBar android:id="@+id/notification_media_progress_bar"
style="@style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="3dp"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:layout_marginBottom="-24dp"
android:layout_marginTop="-12dp"
android:splitTrack="false"
/>
<FrameLayout
android:id="@+id/notification_media_progress_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="11dp"
>
<!-- width is set to "match_parent" to avoid extra layout calls -->
<TextView android:id="@+id/notification_media_elapsed_time"
style="@style/Widget.DeviceDefault.Notification.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginStart="@dimen/notification_content_margin_start"
android:gravity="left"
/>
<TextView android:id="@+id/notification_media_total_time"
style="@style/Widget.DeviceDefault.Notification.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginEnd="@dimen/notification_content_margin_end"
android:gravity="right"
/>
</FrameLayout>
</LinearLayout>

View File

@@ -39,6 +39,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/notification_media_content"
>
<LinearLayout
android:id="@+id/notification_main_column"
@@ -84,5 +85,9 @@
android:id="@+id/media_seamless"
/>
</LinearLayout>
<ViewStub android:id="@+id/notification_media_seekbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
</com.android.internal.widget.MediaNotificationView>

View File

@@ -34,49 +34,61 @@
android:layout_width="match_parent"
android:layout_height="@dimen/media_notification_header_height" />
<LinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="@dimen/notification_content_margin_start"
android:layout_marginTop="@dimen/notification_content_margin_top"
android:tag="media"
android:orientation="vertical"
android:id="@+id/notification_media_content"
>
<LinearLayout
android:id="@+id/notification_content_container"
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical"
android:layout_weight="1"
android:minHeight="@dimen/notification_min_content_height"
android:paddingBottom="@dimen/notification_content_margin"
android:orientation="vertical"
>
<include layout="@layout/notification_template_part_line1"/>
<include layout="@layout/notification_template_text"/>
</LinearLayout>
<LinearLayout
android:id="@+id/media_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="10dp"
android:layout_marginBottom="@dimen/media_notification_actions_padding_bottom"
android:layoutDirection="ltr"
android:orientation="horizontal"
android:layout_marginStart="@dimen/notification_content_margin_start"
android:layout_marginTop="@dimen/notification_content_margin_top"
android:layout_alignParentTop="true"
android:tag="media"
>
<include
layout="@layout/notification_material_media_action"
android:id="@+id/action0"
/>
<include
layout="@layout/notification_material_media_action"
android:id="@+id/action1"
/>
<include
layout="@layout/notification_material_media_action"
android:id="@+id/action2"
/>
<LinearLayout
android:id="@+id/notification_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical"
android:layout_weight="1"
android:minHeight="@dimen/notification_min_content_height"
android:paddingBottom="@dimen/notification_content_margin"
android:orientation="vertical"
>
<include layout="@layout/notification_template_part_line1"/>
<include layout="@layout/notification_template_text"/>
</LinearLayout>
<LinearLayout
android:id="@+id/media_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="10dp"
android:layoutDirection="ltr"
android:orientation="horizontal"
>
<include
layout="@layout/notification_material_media_action"
android:id="@+id/action0"
/>
<include
layout="@layout/notification_material_media_action"
android:id="@+id/action1"
/>
<include
layout="@layout/notification_material_media_action"
android:id="@+id/action2"
/>
</LinearLayout>
</LinearLayout>
<ViewStub android:id="@+id/notification_media_seekbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
/>
</LinearLayout>
</FrameLayout>

View File

@@ -195,6 +195,12 @@
<java-symbol type="id" name="action3" />
<java-symbol type="id" name="action4" />
<java-symbol type="id" name="media_seamless" />
<java-symbol type="id" name="notification_media_seekbar_container" />
<java-symbol type="id" name="notification_media_content" />
<java-symbol type="id" name="notification_media_progress" />
<java-symbol type="id" name="notification_media_progress_bar" />
<java-symbol type="id" name="notification_media_elapsed_time" />
<java-symbol type="id" name="notification_media_total_time" />
<java-symbol type="id" name="big_picture" />
<java-symbol type="id" name="big_text" />
<java-symbol type="id" name="chronometer" />
@@ -1573,6 +1579,7 @@
<java-symbol type="layout" name="immersive_mode_cling" />
<java-symbol type="layout" name="user_switching_dialog" />
<java-symbol type="layout" name="common_tab_settings" />
<java-symbol type="layout" name="notification_material_media_seekbar" />
<java-symbol type="anim" name="slide_in_child_bottom" />
<java-symbol type="anim" name="slide_in_right" />

View File

@@ -83,6 +83,9 @@
<!-- Increased height of a small notification in the status bar -->
<dimen name="notification_min_height_increased">146dp</dimen>
<!-- Increased height of a collapsed media notification in the status bar -->
<dimen name="notification_min_height_media">160dp</dimen>
<!-- Height of a small notification in the status bar which was used before android N -->
<dimen name="notification_min_height_legacy">64dp</dimen>

View File

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

View File

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