1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.notification.row.wrapper; 18 19 import static com.android.systemui.Dependency.MAIN_HANDLER; 20 21 import android.annotation.Nullable; 22 import android.app.Notification; 23 import android.content.Context; 24 import android.content.res.ColorStateList; 25 import android.media.MediaMetadata; 26 import android.media.session.MediaController; 27 import android.media.session.MediaSession; 28 import android.media.session.PlaybackState; 29 import android.metrics.LogMaker; 30 import android.os.Handler; 31 import android.text.format.DateUtils; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewStub; 35 import android.widget.SeekBar; 36 import android.widget.TextView; 37 38 import com.android.internal.R; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.logging.MetricsLogger; 41 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 42 import com.android.internal.widget.MediaNotificationView; 43 import com.android.systemui.Dependency; 44 import com.android.systemui.statusbar.NotificationMediaManager; 45 import com.android.systemui.statusbar.TransformableView; 46 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 47 48 import java.util.Timer; 49 import java.util.TimerTask; 50 51 /** 52 * Wraps a notification containing a media template 53 */ 54 public class NotificationMediaTemplateViewWrapper extends NotificationTemplateViewWrapper { 55 56 private static final long PROGRESS_UPDATE_INTERVAL = 1000; // 1s 57 private static final String COMPACT_MEDIA_TAG = "media"; 58 private final Handler mHandler = Dependency.get(MAIN_HANDLER); 59 private Timer mSeekBarTimer; 60 private View mActions; 61 private SeekBar mSeekBar; 62 private TextView mSeekBarElapsedTime; 63 private TextView mSeekBarTotalTime; 64 private long mDuration = 0; 65 private MediaController mMediaController; 66 private MediaMetadata mMediaMetadata; 67 private NotificationMediaManager mMediaManager; 68 private View mSeekBarView; 69 private Context mContext; 70 private MetricsLogger mMetricsLogger; 71 private boolean mIsViewVisible; 72 73 @VisibleForTesting 74 protected SeekBar.OnSeekBarChangeListener mSeekListener = 75 new SeekBar.OnSeekBarChangeListener() { 76 @Override 77 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 78 } 79 80 @Override 81 public void onStartTrackingTouch(SeekBar seekBar) { 82 } 83 84 @Override 85 public void onStopTrackingTouch(SeekBar seekBar) { 86 if (mMediaController != null) { 87 mMediaController.getTransportControls().seekTo(mSeekBar.getProgress()); 88 mMetricsLogger.write(newLog(MetricsEvent.TYPE_UPDATE)); 89 } 90 } 91 }; 92 93 private MediaNotificationView.VisibilityChangeListener mVisibilityListener = 94 new MediaNotificationView.VisibilityChangeListener() { 95 @Override 96 public void onAggregatedVisibilityChanged(boolean isVisible) { 97 mIsViewVisible = isVisible; 98 if (isVisible && mMediaController != null) { 99 // Restart timer if we're currently playing and didn't already have one going 100 PlaybackState state = mMediaController.getPlaybackState(); 101 if (state != null && state.getState() == PlaybackState.STATE_PLAYING 102 && mSeekBarTimer == null && mSeekBarView != null 103 && mSeekBarView.getVisibility() != View.GONE) { 104 startTimer(); 105 } 106 } else { 107 clearTimer(); 108 } 109 } 110 }; 111 112 private View.OnAttachStateChangeListener mAttachStateListener = 113 new View.OnAttachStateChangeListener() { 114 @Override 115 public void onViewAttachedToWindow(View v) { 116 } 117 118 @Override 119 public void onViewDetachedFromWindow(View v) { 120 mIsViewVisible = false; 121 } 122 }; 123 124 private MediaController.Callback mMediaCallback = new MediaController.Callback() { 125 @Override 126 public void onSessionDestroyed() { 127 clearTimer(); 128 mMediaController.unregisterCallback(this); 129 if (mView instanceof MediaNotificationView) { 130 ((MediaNotificationView) mView).removeVisibilityListener(mVisibilityListener); 131 mView.removeOnAttachStateChangeListener(mAttachStateListener); 132 } 133 } 134 135 @Override 136 public void onPlaybackStateChanged(@Nullable PlaybackState state) { 137 if (state == null) { 138 return; 139 } 140 141 if (state.getState() != PlaybackState.STATE_PLAYING) { 142 // Update the UI once, in case playback info changed while we were paused 143 updatePlaybackUi(state); 144 clearTimer(); 145 } else if (mSeekBarTimer == null && mSeekBarView != null 146 && mSeekBarView.getVisibility() != View.GONE) { 147 startTimer(); 148 } 149 } 150 151 @Override 152 public void onMetadataChanged(@Nullable MediaMetadata metadata) { 153 if (mMediaMetadata == null || !mMediaMetadata.equals(metadata)) { 154 mMediaMetadata = metadata; 155 updateDuration(); 156 } 157 } 158 }; 159 NotificationMediaTemplateViewWrapper(Context ctx, View view, ExpandableNotificationRow row)160 protected NotificationMediaTemplateViewWrapper(Context ctx, View view, 161 ExpandableNotificationRow row) { 162 super(ctx, view, row); 163 mContext = ctx; 164 mMediaManager = Dependency.get(NotificationMediaManager.class); 165 mMetricsLogger = Dependency.get(MetricsLogger.class); 166 167 if (mView instanceof MediaNotificationView) { 168 MediaNotificationView mediaView = (MediaNotificationView) mView; 169 mediaView.addVisibilityListener(mVisibilityListener); 170 mView.addOnAttachStateChangeListener(mAttachStateListener); 171 } 172 } 173 resolveViews()174 private void resolveViews() { 175 mActions = mView.findViewById(com.android.internal.R.id.media_actions); 176 mIsViewVisible = mView.isShown(); 177 178 final MediaSession.Token token = mRow.getEntry().notification.getNotification().extras 179 .getParcelable(Notification.EXTRA_MEDIA_SESSION); 180 181 boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar(); 182 if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) { 183 if (mSeekBarView != null) { 184 mSeekBarView.setVisibility(View.GONE); 185 } 186 return; 187 } 188 189 // Check for existing media controller and clean up / create as necessary 190 boolean controllerUpdated = false; 191 if (mMediaController == null || !mMediaController.getSessionToken().equals(token)) { 192 if (mMediaController != null) { 193 mMediaController.unregisterCallback(mMediaCallback); 194 } 195 mMediaController = new MediaController(mContext, token); 196 controllerUpdated = true; 197 } 198 199 mMediaMetadata = mMediaController.getMetadata(); 200 if (mMediaMetadata != null) { 201 long duration = mMediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); 202 if (duration <= 0) { 203 // Don't include the seekbar if this is a livestream 204 if (mSeekBarView != null && mSeekBarView.getVisibility() != View.GONE) { 205 mSeekBarView.setVisibility(View.GONE); 206 mMetricsLogger.write(newLog(MetricsEvent.TYPE_CLOSE)); 207 clearTimer(); 208 } else if (mSeekBarView == null && controllerUpdated) { 209 // Only log if the controller changed, otherwise we would log multiple times for 210 // the same notification when user pauses/resumes 211 mMetricsLogger.write(newLog(MetricsEvent.TYPE_CLOSE)); 212 } 213 return; 214 } else if (mSeekBarView != null && mSeekBarView.getVisibility() == View.GONE) { 215 // Otherwise, make sure the seekbar is visible 216 mSeekBarView.setVisibility(View.VISIBLE); 217 mMetricsLogger.write(newLog(MetricsEvent.TYPE_OPEN)); 218 updateDuration(); 219 startTimer(); 220 } 221 } 222 223 // Inflate the seekbar template 224 ViewStub stub = mView.findViewById(R.id.notification_media_seekbar_container); 225 if (stub instanceof ViewStub) { 226 LayoutInflater layoutInflater = LayoutInflater.from(stub.getContext()); 227 stub.setLayoutInflater(layoutInflater); 228 stub.setLayoutResource(R.layout.notification_material_media_seekbar); 229 mSeekBarView = stub.inflate(); 230 mMetricsLogger.write(newLog(MetricsEvent.TYPE_OPEN)); 231 232 mSeekBar = mSeekBarView.findViewById(R.id.notification_media_progress_bar); 233 mSeekBar.setOnSeekBarChangeListener(mSeekListener); 234 235 mSeekBarElapsedTime = mSeekBarView.findViewById(R.id.notification_media_elapsed_time); 236 mSeekBarTotalTime = mSeekBarView.findViewById(R.id.notification_media_total_time); 237 238 if (mSeekBarTimer == null) { 239 if (mMediaController != null && canSeekMedia(mMediaController.getPlaybackState())) { 240 // Log initial state, since it will not be updated 241 mMetricsLogger.write(newLog(MetricsEvent.TYPE_DETAIL, 1)); 242 } else { 243 setScrubberVisible(false); 244 } 245 updateDuration(); 246 startTimer(); 247 mMediaController.registerCallback(mMediaCallback); 248 } 249 } 250 updateSeekBarTint(mSeekBarView); 251 } 252 startTimer()253 private void startTimer() { 254 clearTimer(); 255 if (mIsViewVisible) { 256 mSeekBarTimer = new Timer(true /* isDaemon */); 257 mSeekBarTimer.schedule(new TimerTask() { 258 @Override 259 public void run() { 260 mHandler.post(mOnUpdateTimerTick); 261 } 262 }, 0, PROGRESS_UPDATE_INTERVAL); 263 } 264 } 265 clearTimer()266 private void clearTimer() { 267 if (mSeekBarTimer != null) { 268 mSeekBarTimer.cancel(); 269 mSeekBarTimer.purge(); 270 mSeekBarTimer = null; 271 } 272 } 273 274 @Override setRemoved()275 public void setRemoved() { 276 clearTimer(); 277 if (mMediaController != null) { 278 mMediaController.unregisterCallback(mMediaCallback); 279 } 280 if (mView instanceof MediaNotificationView) { 281 ((MediaNotificationView) mView).removeVisibilityListener(mVisibilityListener); 282 mView.removeOnAttachStateChangeListener(mAttachStateListener); 283 } 284 } 285 canSeekMedia(@ullable PlaybackState state)286 private boolean canSeekMedia(@Nullable PlaybackState state) { 287 if (state == null) { 288 return false; 289 } 290 291 long actions = state.getActions(); 292 return ((actions & PlaybackState.ACTION_SEEK_TO) != 0); 293 } 294 setScrubberVisible(boolean isVisible)295 private void setScrubberVisible(boolean isVisible) { 296 if (mSeekBar == null || mSeekBar.isEnabled() == isVisible) { 297 return; 298 } 299 300 mSeekBar.getThumb().setAlpha(isVisible ? 255 : 0); 301 mSeekBar.setEnabled(isVisible); 302 mMetricsLogger.write(newLog(MetricsEvent.TYPE_DETAIL, isVisible ? 1 : 0)); 303 } 304 updateDuration()305 private void updateDuration() { 306 if (mMediaMetadata != null && mSeekBar != null) { 307 long duration = mMediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); 308 if (mDuration != duration) { 309 mDuration = duration; 310 mSeekBar.setMax((int) mDuration); 311 mSeekBarTotalTime.setText(millisecondsToTimeString(duration)); 312 } 313 } 314 } 315 316 protected final Runnable mOnUpdateTimerTick = new Runnable() { 317 @Override 318 public void run() { 319 if (mMediaController != null && mSeekBar != null) { 320 PlaybackState playbackState = mMediaController.getPlaybackState(); 321 if (playbackState != null) { 322 updatePlaybackUi(playbackState); 323 } else { 324 clearTimer(); 325 } 326 } else { 327 clearTimer(); 328 } 329 } 330 }; 331 updatePlaybackUi(PlaybackState state)332 private void updatePlaybackUi(PlaybackState state) { 333 if (mSeekBar == null || mSeekBarElapsedTime == null) { 334 return; 335 } 336 337 long position = state.getPosition(); 338 mSeekBar.setProgress((int) position); 339 340 mSeekBarElapsedTime.setText(millisecondsToTimeString(position)); 341 342 // Update scrubber in case available actions have changed 343 setScrubberVisible(canSeekMedia(state)); 344 } 345 millisecondsToTimeString(long milliseconds)346 private String millisecondsToTimeString(long milliseconds) { 347 long seconds = milliseconds / 1000; 348 String text = DateUtils.formatElapsedTime(seconds); 349 return text; 350 } 351 352 @Override onContentUpdated(ExpandableNotificationRow row)353 public void onContentUpdated(ExpandableNotificationRow row) { 354 // Reinspect the notification. Before the super call, because the super call also updates 355 // the transformation types and we need to have our values set by then. 356 resolveViews(); 357 super.onContentUpdated(row); 358 } 359 updateSeekBarTint(View seekBarContainer)360 private void updateSeekBarTint(View seekBarContainer) { 361 if (seekBarContainer == null) { 362 return; 363 } 364 365 if (this.getNotificationHeader() == null) { 366 return; 367 } 368 369 int tintColor = getNotificationHeader().getOriginalIconColor(); 370 mSeekBarElapsedTime.setTextColor(tintColor); 371 mSeekBarTotalTime.setTextColor(tintColor); 372 mSeekBarTotalTime.setShadowLayer(1.5f, 1.5f, 1.5f, mBackgroundColor); 373 374 ColorStateList tintList = ColorStateList.valueOf(tintColor); 375 mSeekBar.setThumbTintList(tintList); 376 tintList = tintList.withAlpha(192); // 75% 377 mSeekBar.setProgressTintList(tintList); 378 tintList = tintList.withAlpha(128); // 50% 379 mSeekBar.setProgressBackgroundTintList(tintList); 380 } 381 382 @Override updateTransformedTypes()383 protected void updateTransformedTypes() { 384 // This also clears the existing types 385 super.updateTransformedTypes(); 386 if (mActions != null) { 387 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ACTIONS, 388 mActions); 389 } 390 } 391 392 @Override isDimmable()393 public boolean isDimmable() { 394 return getCustomBackgroundColor() == 0; 395 } 396 397 @Override shouldClipToRounding(boolean topRounded, boolean bottomRounded)398 public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { 399 return true; 400 } 401 402 /** 403 * Returns an initialized LogMaker for logging changes to the seekbar 404 * @return new LogMaker 405 */ newLog(int event)406 private LogMaker newLog(int event) { 407 String packageName = mRow.getEntry().notification.getPackageName(); 408 409 return new LogMaker(MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR) 410 .setType(event) 411 .setPackageName(packageName); 412 } 413 414 /** 415 * Returns an initialized LogMaker for logging changes with subtypes 416 * @return new LogMaker 417 */ newLog(int event, int subtype)418 private LogMaker newLog(int event, int subtype) { 419 String packageName = mRow.getEntry().notification.getPackageName(); 420 return new LogMaker(MetricsEvent.MEDIA_NOTIFICATION_SEEKBAR) 421 .setType(event) 422 .setSubtype(subtype) 423 .setPackageName(packageName); 424 } 425 } 426