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 package com.android.car.media; 17 18 import android.annotation.TargetApi; 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.Resources; 23 import android.graphics.Bitmap; 24 import android.graphics.PorterDuff; 25 import android.graphics.drawable.Drawable; 26 import android.graphics.drawable.InsetDrawable; 27 import android.media.MediaDescription; 28 import android.media.MediaMetadata; 29 import android.media.session.MediaController; 30 import android.media.session.MediaSession; 31 import android.media.session.PlaybackState; 32 import android.net.Uri; 33 import android.os.BadParcelableException; 34 import android.os.Build; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.support.annotation.Nullable; 38 import android.support.car.ui.ColorChecker; 39 import android.support.v4.app.Fragment; 40 import android.telephony.PhoneStateListener; 41 import android.telephony.TelephonyManager; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.util.Pair; 45 import android.view.LayoutInflater; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.widget.ImageButton; 50 import android.widget.ImageView; 51 import android.widget.LinearLayout; 52 import android.widget.ProgressBar; 53 import android.widget.SeekBar; 54 import android.widget.TextView; 55 56 import com.android.car.apps.common.BitmapDownloader; 57 import com.android.car.apps.common.BitmapWorkerOptions; 58 import com.android.car.apps.common.util.Assert; 59 import com.android.car.media.util.widgets.MusicPanelLayout; 60 import com.android.car.media.util.widgets.PlayPauseStopImageView; 61 62 import java.util.List; 63 import java.util.Objects; 64 65 /** 66 * Fragment that displays the media playback UI. 67 */ 68 public class MediaPlaybackFragment extends Fragment implements MediaPlaybackModel.Listener { 69 private static final String TAG = "MediaPlayback"; 70 71 private static final String[] PREFERRED_BITMAP_ORDER = { 72 MediaMetadata.METADATA_KEY_ALBUM_ART, 73 MediaMetadata.METADATA_KEY_ART, 74 MediaMetadata.METADATA_KEY_DISPLAY_ICON 75 }; 76 77 private static final String[] PREFERRED_URI_ORDER = { 78 MediaMetadata.METADATA_KEY_ALBUM_ART_URI, 79 MediaMetadata.METADATA_KEY_ART_URI, 80 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI 81 }; 82 83 private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 500; 84 private static final long DELAY_CLOSE_OVERFLOW_MS = 3500; 85 // delay showing the no content view for 3 second -- when the media app cold starts, it 86 // usually takes a moment to load the last played song from database. So we will wait for 87 // 3 sec, before we show the no content view, instead of showing it and immediately 88 // switch to playback view when the metadata loads. 89 private static final long DELAY_SHOW_NO_CONTENT_VIEW_MS = 3000; 90 private static final long FEEDBACK_MESSAGE_DISPLAY_TIME_MS = 6000; 91 92 private MediaActivity mActivity; 93 private MediaPlaybackModel mMediaPlaybackModel; 94 private final Handler mHandler = new Handler(); 95 96 private TextView mTitleView; 97 private TextView mArtistView; 98 private ImageButton mPrevButton; 99 private PlayPauseStopImageView mPlayPauseStopButton; 100 private ImageButton mNextButton; 101 private ImageButton mPlayQueueButton; 102 private MusicPanelLayout mMusicPanel; 103 private LinearLayout mControlsView; 104 private LinearLayout mOverflowView; 105 private ImageButton mOverflowOnButton; 106 private ImageButton mOverflowOffButton; 107 private final ImageButton[] mCustomActionButtons = new ImageButton[4]; 108 private SeekBar mSeekBar; 109 private ProgressBar mSpinner; 110 private boolean mOverflowVisibility; 111 private long mStartProgress; 112 private long mStartTime; 113 private MediaDescription mCurrentTrack; 114 private boolean mShowingMessage; 115 private View mInitialNoContentView; 116 private View mMetadata; 117 private ImageView mMusicErrorIcon; 118 private TextView mTapToSelectText; 119 private ProgressBar mAppConnectingSpinner; 120 private boolean mDelayedResetTitleInProgress; 121 private int mAlbumArtWidth = 800; 122 private int mAlbumArtHeight = 400; 123 private int mShowTitleDelayMs = 250; 124 private TelephonyManager mTelephonyManager; 125 private boolean mInCall = false; 126 private BitmapDownloader mDownloader; 127 private boolean mReturnFromOnStop = false; 128 129 private enum ViewType { 130 NO_CONTENT_VIEW, 131 PLAYBACK_CONTROLS_VIEW, 132 LOADING_VIEW, 133 } 134 135 private ViewType mCurrentView; 136 137 public MediaPlaybackFragment() { 138 super(); 139 } 140 141 @Override 142 public void onCreate(Bundle savedInstanceState) { 143 super.onCreate(savedInstanceState); 144 mActivity = (MediaActivity) getHost(); 145 mShowTitleDelayMs = 146 mActivity.getResources().getInteger(R.integer.new_album_art_fade_in_offset); 147 mMediaPlaybackModel = new MediaPlaybackModel(mActivity, null /* browserExtras */); 148 mMediaPlaybackModel.addListener(this); 149 mTelephonyManager = 150 (TelephonyManager) mActivity.getSystemService(Context.TELEPHONY_SERVICE); 151 } 152 153 @Override 154 public void onDestroy() { 155 super.onDestroy(); 156 mCurrentView = null; 157 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); 158 mMediaPlaybackModel = null; 159 mActivity = null; 160 // Calling this with null will clear queue of callbacks and message. 161 mHandler.removeCallbacksAndMessages(null); 162 mDelayedResetTitleInProgress = false; 163 } 164 165 @Override 166 public View onCreateView(LayoutInflater inflater, final ViewGroup container, 167 Bundle savedInstanceState) { 168 View v = inflater.inflate(R.layout.now_playing_screen, container, false); 169 mTitleView = (TextView) v.findViewById(R.id.title); 170 mArtistView = (TextView) v.findViewById(R.id.artist); 171 mSeekBar = (SeekBar) v.findViewById(R.id.seek_bar); 172 // In L setEnabled(false) will make the tint color wrong, but not in M. 173 mSeekBar.setOnTouchListener(new View.OnTouchListener() { 174 @Override 175 public boolean onTouch(View v, MotionEvent event) { 176 // Eat up touch events from users as we set progress programmatically only. 177 return true; 178 } 179 }); 180 mControlsView = (LinearLayout) v.findViewById(R.id.controls); 181 mPlayQueueButton = (ImageButton) v.findViewById(R.id.play_queue); 182 mPrevButton = (ImageButton) v.findViewById(R.id.prev); 183 mPlayPauseStopButton = (PlayPauseStopImageView) v.findViewById(R.id.play_pause); 184 mNextButton = (ImageButton) v.findViewById(R.id.next); 185 mOverflowOnButton = (ImageButton) v.findViewById(R.id.overflow_on); 186 mOverflowView = (LinearLayout) v.findViewById(R.id.overflow_items); 187 mOverflowOffButton = (ImageButton) v.findViewById(R.id.overflow_off); 188 setActionDrawable(mOverflowOffButton, R.drawable.ic_overflow_activated, getResources()); 189 mMusicPanel = (MusicPanelLayout) v.findViewById(R.id.music_panel); 190 mMusicPanel.setDefaultFocus(mPlayPauseStopButton); 191 mSpinner = (ProgressBar) v.findViewById(R.id.spinner); 192 mInitialNoContentView = v.findViewById(R.id.initial_view); 193 mMetadata = v.findViewById(R.id.metadata); 194 195 mMusicErrorIcon = (ImageView) v.findViewById(R.id.error_icon); 196 mTapToSelectText = (TextView) v.findViewById(R.id.tap_to_select_item); 197 mAppConnectingSpinner = (ProgressBar) v.findViewById(R.id.loading_spinner); 198 199 mCustomActionButtons[0] = (ImageButton) v.findViewById(R.id.custom_action_1); 200 mCustomActionButtons[1] = (ImageButton) v.findViewById(R.id.custom_action_2); 201 mCustomActionButtons[2] = (ImageButton) v.findViewById(R.id.custom_action_3); 202 mCustomActionButtons[3] = (ImageButton) v.findViewById(R.id.custom_action_4); 203 204 mPrevButton.setOnClickListener(mControlsClickListener); 205 mNextButton.setOnClickListener(mControlsClickListener); 206 // Yes they both need it. The layout is not focusable so it will never get the click. 207 // You can't make the layout focusable because then the button wont highlight. 208 v.findViewById(R.id.play_pause_container).setOnClickListener(mControlsClickListener); 209 mPlayPauseStopButton.setOnClickListener(mControlsClickListener); 210 mPlayQueueButton.setOnClickListener(mControlsClickListener); 211 mOverflowOnButton.setOnClickListener(mControlsClickListener); 212 mOverflowOffButton.setOnClickListener(mControlsClickListener); 213 214 // If touch mode is enabled, we disable focus from buttons. 215 if (getResources().getBoolean(R.bool.has_touch)) { 216 setControlsFocusability(false); 217 setOverflowFocusability(false); 218 } 219 220 return v; 221 } 222 223 @Override 224 public void onViewCreated(View view, Bundle savedInstanceState) { 225 super.onViewCreated(view, savedInstanceState); 226 Pair<Integer, Integer> albumArtSize = mActivity.getAlbumArtSize(); 227 if (albumArtSize != null) { 228 if (albumArtSize.first > 0 && albumArtSize.second > 0) { 229 mAlbumArtWidth = albumArtSize.first; 230 mAlbumArtHeight = albumArtSize.second; 231 } 232 } 233 } 234 235 @Override 236 public void onPause() { 237 super.onPause(); 238 mMediaPlaybackModel.stop(); 239 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 240 } 241 242 @Override 243 public void onStop() { 244 super.onStop(); 245 // When switch apps, onStop() will be called. Mark it and don't show fade in/out title and 246 // background animations when come back. 247 mReturnFromOnStop = true; 248 } 249 250 @Override 251 public void onResume() { 252 super.onResume(); 253 mMediaPlaybackModel.start(); 254 // Note: at registration, TelephonyManager will invoke the callback with the current state. 255 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 256 } 257 258 @Override 259 public void onMediaAppChanged(@Nullable ComponentName currentName, 260 @Nullable ComponentName newName) { 261 Assert.isMainThread(); 262 resetTitle(); 263 if (Objects.equals(currentName, newName)) { 264 return; 265 } 266 int accentColor = mMediaPlaybackModel.getAccentColor(); 267 mPlayPauseStopButton.setPrimaryActionColor(accentColor); 268 mSeekBar.getProgressDrawable().setColorFilter(accentColor, PorterDuff.Mode.SRC_IN); 269 int overflowViewColor = mMediaPlaybackModel.getPrimaryColorDark(); 270 mOverflowView.getBackground().setColorFilter(overflowViewColor, PorterDuff.Mode.SRC_IN); 271 // Tint the overflow actions light or dark depending on contrast. 272 int overflowTintColor = ColorChecker.getTintColor(mActivity, overflowViewColor); 273 for (ImageView v : mCustomActionButtons) { 274 v.setColorFilter(overflowTintColor, PorterDuff.Mode.SRC_IN); 275 } 276 mOverflowOffButton.setColorFilter(overflowTintColor, PorterDuff.Mode.SRC_IN); 277 ColorStateList colorStateList = ColorStateList.valueOf(accentColor); 278 mSpinner.setIndeterminateTintList(colorStateList); 279 mAppConnectingSpinner.setIndeterminateTintList(ColorStateList.valueOf(accentColor)); 280 showLoadingView(); 281 closeOverflowMenu(); 282 } 283 284 @Override 285 public void onMediaAppStatusMessageChanged(@Nullable String message) { 286 Assert.isMainThread(); 287 if (message == null) { 288 resetTitle(); 289 } else { 290 showMessage(message); 291 } 292 } 293 294 @Override 295 public void onMediaConnected() { 296 Assert.isMainThread(); 297 onMetadataChanged(mMediaPlaybackModel.getMetadata()); 298 onQueueChanged(mMediaPlaybackModel.getQueue()); 299 onPlaybackStateChanged(mMediaPlaybackModel.getPlaybackState()); 300 mReturnFromOnStop = false; 301 } 302 303 @Override 304 public void onMediaConnectionSuspended() { 305 Assert.isMainThread(); 306 mReturnFromOnStop = false; 307 } 308 309 @Override 310 public void onMediaConnectionFailed(CharSequence failedClientName) { 311 Assert.isMainThread(); 312 showInitialNoContentView(getString(R.string.cannot_connect_to_app, failedClientName), true); 313 mReturnFromOnStop = false; 314 } 315 316 @Override 317 @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) 318 public void onPlaybackStateChanged(@Nullable PlaybackState state) { 319 Assert.isMainThread(); 320 if (Log.isLoggable(TAG, Log.VERBOSE)) { 321 Log.v(TAG, "onPlaybackStateChanged; state: " 322 + (state == null ? "<< NULL >>" : state.toString())); 323 } 324 MediaMetadata metadata = mMediaPlaybackModel.getMetadata(); 325 if (state == null) { 326 return; 327 } 328 329 if (state.getState() == PlaybackState.STATE_ERROR) { 330 if (Log.isLoggable(TAG, Log.DEBUG)) { 331 Log.d(TAG, "ERROR: " + state.getErrorMessage()); 332 } 333 showInitialNoContentView(state.getErrorMessage() != null ? 334 state.getErrorMessage().toString() : 335 mActivity.getString(R.string.unknown_error), true); 336 return; 337 } 338 339 mStartProgress = state.getPosition(); 340 mStartTime = System.currentTimeMillis(); 341 mSeekBar.setProgress((int) mStartProgress); 342 if (state.getState() == PlaybackState.STATE_PLAYING) { 343 mHandler.post(mSeekBarRunnable); 344 } else { 345 mHandler.removeCallbacks(mSeekBarRunnable); 346 } 347 if (!mInCall) { 348 int playbackState = state.getState(); 349 mPlayPauseStopButton.setPlayState(playbackState); 350 // Due to the action of PlaybackState will be changed when the state of PlaybackState is 351 // changed, we set mode every time onPlaybackStateChanged() is called. 352 if (playbackState == PlaybackState.STATE_PLAYING || 353 playbackState == PlaybackState.STATE_BUFFERING) { 354 mPlayPauseStopButton.setMode(((state.getActions() & PlaybackState.ACTION_STOP) != 0) 355 ? PlayPauseStopImageView.MODE_STOP : PlayPauseStopImageView.MODE_PAUSE); 356 } else { 357 mPlayPauseStopButton.setMode(PlayPauseStopImageView.MODE_PAUSE); 358 } 359 mPlayPauseStopButton.refreshDrawableState(); 360 } 361 if (state.getState() == PlaybackState.STATE_BUFFERING) { 362 mSpinner.setVisibility(View.VISIBLE); 363 } else { 364 mSpinner.setVisibility(View.GONE); 365 } 366 367 updateActions(state.getActions(), state.getCustomActions()); 368 369 if (metadata == null) { 370 return; 371 } 372 showMediaPlaybackControlsView(); 373 } 374 375 @Override 376 public void onMetadataChanged(@Nullable MediaMetadata metadata) { 377 Assert.isMainThread(); 378 if (Log.isLoggable(TAG, Log.VERBOSE)) { 379 Log.v(TAG, "onMetadataChanged; description: " 380 + (metadata == null ? "<< NULL >>" : metadata.getDescription().toString())); 381 } 382 if (metadata == null) { 383 mHandler.postDelayed(mShowNoContentViewRunnable, DELAY_SHOW_NO_CONTENT_VIEW_MS); 384 return; 385 } else { 386 mHandler.removeCallbacks(mShowNoContentViewRunnable); 387 } 388 389 showMediaPlaybackControlsView(); 390 mCurrentTrack = metadata.getDescription(); 391 Bitmap icon = getMetadataBitmap(metadata); 392 if (!mShowingMessage) { 393 mHandler.removeCallbacks(mSetTitleRunnable); 394 // Show the title when the new album art starts to fade in, but don't need to show 395 // the fade in animation when come back from switching apps. 396 mHandler.postDelayed(mSetTitleRunnable, 397 icon == null || mReturnFromOnStop ? 0 : mShowTitleDelayMs); 398 } 399 Uri iconUri = getMetadataIconUri(metadata); 400 if (icon != null) { 401 Bitmap scaledIcon = cropAlbumArt(icon); 402 if (scaledIcon != icon && !icon.isRecycled()) { 403 icon.recycle(); 404 } 405 // Fade out the old background and then fade in the new one when the new album art 406 // starts, but don't need to show the fade out and fade in animations when come back 407 // from switching apps. 408 mActivity.setBackgroundBitmap(scaledIcon, !mReturnFromOnStop /* showAnimation */); 409 } else if (iconUri != null) { 410 if (mDownloader == null) { 411 mDownloader = new BitmapDownloader(mActivity); 412 } 413 final int flags = BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED 414 | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED; 415 if (Log.isLoggable(TAG, Log.VERBOSE)) { 416 Log.v(TAG, "Album art size " + mAlbumArtWidth + "x" + mAlbumArtHeight); 417 } 418 419 mDownloader.getBitmap(new BitmapWorkerOptions.Builder(mActivity).resource(iconUri) 420 .height(mAlbumArtHeight).width(mAlbumArtWidth).cacheFlag(flags).build(), 421 new BitmapDownloader.BitmapCallback() { 422 @Override 423 public void onBitmapRetrieved(Bitmap bitmap) { 424 if (mActivity != null) { 425 mActivity.setBackgroundBitmap(bitmap, true /* showAnimation */); 426 } 427 } 428 }); 429 } else { 430 mActivity.setBackgroundColor(mMediaPlaybackModel.getPrimaryColorDark()); 431 } 432 433 mSeekBar.setMax((int) metadata.getLong(MediaMetadata.METADATA_KEY_DURATION)); 434 } 435 436 @Override 437 public void onQueueChanged(List<MediaSession.QueueItem> queue) { 438 Assert.isMainThread(); 439 if (queue.isEmpty()) { 440 mPlayQueueButton.setVisibility(View.INVISIBLE); 441 } else { 442 mPlayQueueButton.setVisibility(View.VISIBLE); 443 } 444 } 445 446 @Override 447 public void onSessionDestroyed(CharSequence destroyedMediaClientName) { 448 Assert.isMainThread(); 449 mHandler.removeCallbacks(mSeekBarRunnable); 450 if (mActivity != null) { 451 showInitialNoContentView( 452 getString(R.string.cannot_connect_to_app, destroyedMediaClientName), true); 453 } 454 } 455 456 457 public void showMessage(String msg) { 458 if (Log.isLoggable(TAG, Log.VERBOSE)) { 459 Log.v(TAG, "showMessage(); msg: " + msg); 460 } 461 // New messages will always be displayed regardless of if a feedback message is being shown. 462 mHandler.removeCallbacks(mResetTitleRunnable); 463 mActivity.darkenScrim(true); 464 mTitleView.setSingleLine(false); 465 mTitleView.setMaxLines(2); 466 mArtistView.setVisibility(View.GONE); 467 mTitleView.setText(msg); 468 mShowingMessage = true; 469 } 470 471 boolean isOverflowMenuVisible() { 472 return mOverflowVisibility; 473 } 474 475 void closeOverflowMenu() { 476 mHandler.removeCallbacks(mCloseOverflowRunnable); 477 setOverflowMenuVisibility(false); 478 } 479 480 void setOverflowMenuVisibility(boolean visibility) { 481 if (mOverflowVisibility == visibility) { 482 return; 483 } 484 mOverflowVisibility = visibility; 485 if (visibility) { 486 // Make the view invisible to let request focus work. Or else it will make b/23679226. 487 mOverflowView.setVisibility(View.INVISIBLE); 488 if (!getResources().getBoolean(R.bool.has_touch)) { 489 setOverflowFocusability(true); 490 setControlsFocusability(false); 491 } 492 mMusicPanel.setDefaultFocus(mOverflowOffButton); 493 mOverflowOffButton.requestFocus(); 494 // After requesting focus is done, make the view to be visible. 495 mOverflowView.setVisibility(View.VISIBLE); 496 mOverflowView.animate().alpha(1f).setDuration(250) 497 .withEndAction(new Runnable() { 498 @Override 499 public void run() { 500 mControlsView.setVisibility(View.GONE); 501 } 502 }); 503 504 int tint = ColorChecker.getTintColor(mActivity, 505 mMediaPlaybackModel.getPrimaryColorDark()); 506 mSeekBar.getProgressDrawable().setColorFilter(tint, PorterDuff.Mode.SRC_IN); 507 } else { 508 mControlsView.setVisibility(View.INVISIBLE); 509 if (!getResources().getBoolean(R.bool.has_touch)) { 510 setControlsFocusability(true); 511 setOverflowFocusability(false); 512 } 513 mMusicPanel.setDefaultFocus(mPlayPauseStopButton); 514 mOverflowOnButton.requestFocus(); 515 mControlsView.setVisibility(View.VISIBLE); 516 mOverflowView.animate().alpha(0f).setDuration(250) 517 .withEndAction(new Runnable() { 518 @Override 519 public void run() { 520 mOverflowView.setVisibility(View.GONE); 521 } 522 }); 523 mSeekBar.getProgressDrawable().setColorFilter( 524 mMediaPlaybackModel.getAccentColor(), PorterDuff.Mode.SRC_IN); 525 } 526 } 527 528 private void setControlsFocusability(boolean focusable) { 529 mPlayQueueButton.setFocusable(focusable); 530 mPrevButton.setFocusable(focusable); 531 mPlayPauseStopButton.setFocusable(focusable); 532 mNextButton.setFocusable(focusable); 533 mOverflowOnButton.setFocusable(focusable); 534 } 535 536 private void setOverflowFocusability(boolean focusable) { 537 mCustomActionButtons[0].setFocusable(focusable); 538 mCustomActionButtons[1].setFocusable(focusable); 539 mCustomActionButtons[2].setFocusable(focusable); 540 mCustomActionButtons[3].setFocusable(focusable); 541 mOverflowOffButton.setFocusable(focusable); 542 } 543 544 /** 545 * For a given drawer slot, set the proper action of the slot's button, 546 * based on the slot being reserved and the corresponding action being enabled. 547 * If the slot is not reserved and the corresponding action is disabled, 548 * then the next available custom action is assigned to the button. 549 * @param button The button corresponding to the slot 550 * @param originalResId The drawable resource ID for the original button, 551 * only used if the original action is not replaced by a custom action. 552 * @param slotAlwaysReserved True if the slot should be empty when the 553 * corresponding action is disabled. If false, when the action is disabled 554 * the slot has its default action replaced by the next custom action, if any. 555 * @param isOriginalEnabled True if the original action of this button is 556 * enabled. 557 * @param customActions A list of custom actions still unassigned to slots. 558 */ 559 private void handleSlot(ImageButton button, int originalResId, boolean slotAlwaysReserved, 560 boolean isOriginalEnabled, List<PlaybackState.CustomAction> customActions) { 561 if (isOriginalEnabled || slotAlwaysReserved) { 562 setActionDrawable(button, originalResId, getResources()); 563 button.setVisibility(isOriginalEnabled ? View.VISIBLE : View.INVISIBLE); 564 button.setTag(null); 565 } else { 566 if (customActions.isEmpty()) { 567 button.setVisibility(View.INVISIBLE); 568 } else { 569 PlaybackState.CustomAction customAction = customActions.remove(0); 570 Bundle extras = customAction.getExtras(); 571 boolean repeatedAction = false; 572 try { 573 repeatedAction = (extras != null && extras.getBoolean( 574 MediaConstants.EXTRA_REPEATED_CUSTOM_ACTION_BUTTON, false)); 575 } catch (BadParcelableException e) { 576 Log.e(TAG, "custom parcelable in custom action extras.", e); 577 } 578 if (repeatedAction) { 579 button.setOnTouchListener(mControlsTouchListener); 580 } else { 581 button.setOnClickListener(mControlsClickListener); 582 } 583 setCustomAction(button, customAction); 584 } 585 } 586 } 587 588 /** 589 * Takes a list of custom actions and standard actions and displays them in the media 590 * controls card (or hides ones that aren't available). 591 * 592 * @param actions A bit mask of active actions (android.media.session.PlaybackState#ACTION_*). 593 * @param customActions A list of custom actions specified by the {@link android.media.session.MediaSession}. 594 */ 595 private void updateActions(long actions, List<PlaybackState.CustomAction> customActions) { 596 List<MediaSession.QueueItem> mediaQueue = mMediaPlaybackModel.getQueue(); 597 handleSlot( 598 mPlayQueueButton, R.drawable.ic_tracklist, 599 mMediaPlaybackModel.isSlotForActionReserved( 600 MediaConstants.EXTRA_RESERVED_SLOT_QUEUE), 601 !mediaQueue.isEmpty(), 602 customActions); 603 604 handleSlot( 605 mPrevButton, R.drawable.ic_skip_previous, 606 mMediaPlaybackModel.isSlotForActionReserved( 607 MediaConstants.EXTRA_RESERVED_SLOT_SKIP_TO_PREVIOUS), 608 (actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0, 609 customActions); 610 611 handleSlot( 612 mNextButton, R.drawable.ic_skip_next, 613 mMediaPlaybackModel.isSlotForActionReserved( 614 MediaConstants.EXTRA_RESERVED_SLOT_SKIP_TO_NEXT), 615 (actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0, 616 customActions); 617 618 handleSlot( 619 mOverflowOnButton, R.drawable.ic_overflow_normal, 620 customActions.size() > 1, 621 customActions.size() > 1, 622 customActions); 623 624 for (ImageButton button: mCustomActionButtons) { 625 handleSlot(button, 0, false, false, customActions); 626 } 627 } 628 629 private void setCustomAction(ImageButton imageButton, PlaybackState.CustomAction customAction) { 630 imageButton.setVisibility(View.VISIBLE); 631 setActionDrawable(imageButton, customAction.getIcon(), 632 mMediaPlaybackModel.getPackageResources()); 633 imageButton.setTag(customAction); 634 } 635 636 private void showInitialNoContentView(String msg, boolean isError) { 637 if (Log.isLoggable(TAG, Log.VERBOSE)) { 638 Log.v(TAG, "showInitialNoContentView()"); 639 } 640 if (!needViewChange(ViewType.NO_CONTENT_VIEW)) { 641 return; 642 } 643 mAppConnectingSpinner.setVisibility(View.GONE); 644 mActivity.setScrimVisibility(false); 645 if (isError) { 646 mActivity.setBackgroundColor(getResources().getColor(R.color.car_error_screen)); 647 mMusicErrorIcon.setVisibility(View.VISIBLE); 648 } else { 649 mActivity.setBackgroundColor(getResources().getColor(R.color.car_dark_blue_grey_800)); 650 mMusicErrorIcon.setVisibility(View.INVISIBLE); 651 } 652 mTapToSelectText.setVisibility(View.VISIBLE); 653 mTapToSelectText.setText(msg); 654 mInitialNoContentView.setVisibility(View.VISIBLE); 655 mMetadata.setVisibility(View.GONE); 656 mMusicPanel.setVisibility(View.GONE); 657 } 658 659 private void showMediaPlaybackControlsView() { 660 if (Log.isLoggable(TAG, Log.VERBOSE)) { 661 Log.v(TAG, "showMediaPlaybackControlsView()"); 662 } 663 if (!needViewChange(ViewType.PLAYBACK_CONTROLS_VIEW)) { 664 return; 665 } 666 if (mPlayPauseStopButton != null && getResources().getBoolean(R.bool.has_wheel)) { 667 mPlayPauseStopButton.requestFocusFromTouch(); 668 } 669 670 if (!mShowingMessage) { 671 mActivity.setScrimVisibility(true); 672 } 673 mInitialNoContentView.setVisibility(View.GONE); 674 mMetadata.setVisibility(View.VISIBLE); 675 mMusicPanel.setVisibility(View.VISIBLE); 676 } 677 678 private void showLoadingView() { 679 if (Log.isLoggable(TAG, Log.VERBOSE)) { 680 Log.v(TAG, "showLoadingView()"); 681 } 682 if (!needViewChange(ViewType.LOADING_VIEW)) { 683 return; 684 } 685 mActivity.setBackgroundColor( 686 getResources().getColor(R.color.music_loading_view_background)); 687 mAppConnectingSpinner.setVisibility(View.VISIBLE); 688 mMusicErrorIcon.setVisibility(View.GONE); 689 mTapToSelectText.setVisibility(View.GONE); 690 mInitialNoContentView.setVisibility(View.VISIBLE); 691 mMetadata.setVisibility(View.GONE); 692 mMusicPanel.setVisibility(View.GONE); 693 } 694 695 private boolean needViewChange(ViewType newView) { 696 if (mCurrentView != null && mCurrentView == newView) { 697 return false; 698 } 699 mCurrentView = newView; 700 return true; 701 } 702 703 private void resetTitle() { 704 if (Log.isLoggable(TAG, Log.VERBOSE)) { 705 Log.v(TAG, "resetTitle()"); 706 } 707 if (!mShowingMessage) { 708 if (Log.isLoggable(TAG, Log.DEBUG)) { 709 Log.d(TAG, "message not currently shown, not resetting title"); 710 } 711 return; 712 } 713 // Feedback message is currently being displayed, reset will automatically take place when 714 // the display interval expires. 715 if (mDelayedResetTitleInProgress) { 716 if (Log.isLoggable(TAG, Log.DEBUG)) { 717 Log.d(TAG, "delay reset title is in progress, not resetting title now"); 718 } 719 return; 720 } 721 // This will set scrim visible and alpha value back to normal. 722 mActivity.setScrimVisibility(true); 723 mTitleView.setSingleLine(true); 724 mArtistView.setVisibility(View.VISIBLE); 725 if (mCurrentTrack != null) { 726 mTitleView.setText(mCurrentTrack.getTitle()); 727 mArtistView.setText(mCurrentTrack.getSubtitle()); 728 } 729 mShowingMessage = false; 730 } 731 732 private Bitmap cropAlbumArt(Bitmap icon) { 733 if (icon == null) { 734 return null; 735 } 736 int width = icon.getWidth(); 737 int height = icon.getHeight(); 738 int startX = width > mAlbumArtWidth ? (width - mAlbumArtWidth) / 2 : 0; 739 int startY = height > mAlbumArtHeight ? (height - mAlbumArtHeight) / 2 : 0; 740 int newWidth = width > mAlbumArtWidth ? mAlbumArtWidth : width; 741 int newHeight = height > mAlbumArtHeight ? mAlbumArtHeight : height; 742 return Bitmap.createBitmap(icon, startX, startY, newWidth, newHeight); 743 } 744 745 private Bitmap getMetadataBitmap(MediaMetadata metadata) { 746 // Get the best art bitmap we can find 747 for (int i = 0; i < PREFERRED_BITMAP_ORDER.length; i++) { 748 Bitmap bitmap = metadata.getBitmap(PREFERRED_BITMAP_ORDER[i]); 749 if (bitmap != null) { 750 return bitmap; 751 } 752 } 753 return null; 754 } 755 756 private Uri getMetadataIconUri(MediaMetadata metadata) { 757 // Get the best Uri we can find 758 for (int i = 0; i < PREFERRED_URI_ORDER.length; i++) { 759 String iconUri = metadata.getString(PREFERRED_URI_ORDER[i]); 760 if (!TextUtils.isEmpty(iconUri)) { 761 return Uri.parse(iconUri); 762 } 763 } 764 return null; 765 } 766 767 private void setActionDrawable(ImageButton button, int resId, Resources resources) { 768 if (resources == null) { 769 Log.e(TAG, "Resources is null. Icons will not show up."); 770 return; 771 } 772 773 Resources myResources = getResources(); 774 // the resources may be from another package. we need to update the configuration using 775 // the context from the activity so we get the drawable from the correct DPI bucket. 776 resources.updateConfiguration( 777 myResources.getConfiguration(), myResources.getDisplayMetrics()); 778 try { 779 Drawable icon = resources.getDrawable(resId, null); 780 int inset = myResources.getDimensionPixelSize(R.dimen.music_action_icon_inset); 781 InsetDrawable insetIcon = new InsetDrawable(icon, inset); 782 button.setImageDrawable(insetIcon); 783 } catch (Resources.NotFoundException e) { 784 Log.w(TAG, "Resource not found: " + resId); 785 } 786 } 787 788 private void checkAndDisplayFeedbackMessage(PlaybackState.CustomAction ca) { 789 try { 790 Bundle extras = ca.getExtras(); 791 if (extras != null) { 792 String feedbackMessage = extras.getString( 793 MediaConstants.EXTRA_CUSTOM_ACTION_STATUS, ""); 794 if (!TextUtils.isEmpty(feedbackMessage)) { 795 // Show feedback message that appears for a time interval unless a new 796 // message is shown. 797 showMessage(feedbackMessage); 798 mDelayedResetTitleInProgress = true; 799 mHandler.postDelayed(mResetTitleRunnable, FEEDBACK_MESSAGE_DISPLAY_TIME_MS); 800 } 801 } 802 } catch (BadParcelableException e) { 803 Log.e(TAG, "Custom parcelable was added to extras, unable " + 804 "to check for feedback message.", e); 805 } 806 } 807 808 private final View.OnTouchListener mControlsTouchListener = new View.OnTouchListener() { 809 @Override 810 public boolean onTouch(View v, MotionEvent event) { 811 if (!mMediaPlaybackModel.isConnected()) { 812 Log.e(TAG, "Unable to send action for " + v 813 + ". The MediaPlaybackModel is not connected."); 814 return true; 815 } 816 boolean onDown; 817 switch (event.getAction() & MotionEvent.ACTION_MASK) { 818 case MotionEvent.ACTION_DOWN: 819 onDown = true; 820 break; 821 case MotionEvent.ACTION_UP: 822 onDown = false; 823 break; 824 default: 825 return true; 826 } 827 828 if (v.getTag() != null && v.getTag() instanceof PlaybackState.CustomAction) { 829 PlaybackState.CustomAction ca = (PlaybackState.CustomAction) v.getTag(); 830 checkAndDisplayFeedbackMessage(ca); 831 Bundle extras = ca.getExtras(); 832 try { 833 extras.putBoolean( 834 MediaConstants.EXTRA_REPEATED_CUSTOM_ACTION_BUTTON_ON_DOWN, onDown); 835 } catch (BadParcelableException e) { 836 Log.e(TAG, "unable to on down notification for custom action.", e); 837 } 838 MediaController.TransportControls transportControls = 839 mMediaPlaybackModel.getTransportControls(); 840 transportControls.sendCustomAction(ca, extras); 841 mHandler.removeCallbacks(mCloseOverflowRunnable); 842 if (!onDown) { 843 mHandler.postDelayed(mCloseOverflowRunnable, DELAY_CLOSE_OVERFLOW_MS); 844 } 845 } 846 return true; 847 } 848 }; 849 850 private final View.OnClickListener mControlsClickListener = new View.OnClickListener() { 851 @Override 852 public void onClick(View v) { 853 if (!mMediaPlaybackModel.isConnected()) { 854 Log.e(TAG, "Unable to send action for " + v 855 + ". The MediaPlaybackModel is not connected."); 856 return; 857 } 858 MediaController.TransportControls transportControls = 859 mMediaPlaybackModel.getTransportControls(); 860 if (v.getTag() != null && v.getTag() instanceof PlaybackState.CustomAction) { 861 PlaybackState.CustomAction ca = (PlaybackState.CustomAction) v.getTag(); 862 checkAndDisplayFeedbackMessage(ca); 863 transportControls.sendCustomAction(ca, ca.getExtras()); 864 mHandler.removeCallbacks(mCloseOverflowRunnable); 865 mHandler.postDelayed(mCloseOverflowRunnable, DELAY_CLOSE_OVERFLOW_MS); 866 } else { 867 switch (v.getId()) { 868 case R.id.play_queue: 869 mActivity.showQueueInDrawer(); 870 break; 871 case R.id.prev: 872 transportControls.skipToPrevious(); 873 break; 874 case R.id.play_pause: 875 case R.id.play_pause_container: 876 PlaybackState playbackState = mMediaPlaybackModel.getPlaybackState(); 877 if (playbackState == null) { 878 break; 879 } 880 long transportControlFlags = playbackState.getActions(); 881 if (playbackState.getState() == PlaybackState.STATE_PLAYING) { 882 if ((transportControlFlags & PlaybackState.ACTION_PAUSE) != 0) { 883 transportControls.pause(); 884 } else if ((transportControlFlags & PlaybackState.ACTION_STOP) != 0) { 885 transportControls.stop(); 886 } 887 } else if (playbackState.getState() == PlaybackState.STATE_BUFFERING) { 888 if ((transportControlFlags & PlaybackState.ACTION_STOP) != 0) { 889 transportControls.stop(); 890 } else if ((transportControlFlags & PlaybackState.ACTION_PAUSE) != 0) { 891 transportControls.pause(); 892 } 893 } else { 894 transportControls.play(); 895 } 896 break; 897 case R.id.next: 898 transportControls.skipToNext(); 899 break; 900 case R.id.overflow_off: 901 mHandler.removeCallbacks(mCloseOverflowRunnable); 902 setOverflowMenuVisibility(false); 903 break; 904 case R.id.overflow_on: 905 setOverflowMenuVisibility(true); 906 break; 907 default: 908 throw new IllegalStateException("Unknown button press: " + v); 909 } 910 } 911 } 912 }; 913 914 private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { 915 @Override 916 public void onCallStateChanged(int state, String incomingNumber) { 917 switch (state) { 918 case TelephonyManager.CALL_STATE_RINGING: // falls through 919 case TelephonyManager.CALL_STATE_OFFHOOK: 920 mPlayPauseStopButton 921 .setPlayState(PlayPauseStopImageView.PLAYBACKSTATE_DISABLED); 922 mPlayPauseStopButton.setMode(PlayPauseStopImageView.MODE_PAUSE); 923 mPlayPauseStopButton.refreshDrawableState(); 924 mInCall = true; 925 break; 926 case TelephonyManager.CALL_STATE_IDLE: 927 if (mInCall) { 928 PlaybackState playbackState = mMediaPlaybackModel.getPlaybackState(); 929 if (playbackState != null) { 930 mPlayPauseStopButton.setPlayState(playbackState.getState()); 931 mPlayPauseStopButton.setMode(( 932 (playbackState.getActions() & PlaybackState.ACTION_STOP) != 0) ? 933 PlayPauseStopImageView.MODE_STOP : 934 PlayPauseStopImageView.MODE_PAUSE); 935 mPlayPauseStopButton.refreshDrawableState(); 936 } 937 mInCall = false; 938 } 939 break; 940 default: 941 Log.w(TAG, "TelephonyManager reports an unknown call state: " + state); 942 } 943 } 944 }; 945 946 private final Runnable mSeekBarRunnable = new Runnable() { 947 @Override 948 public void run() { 949 mSeekBar.setProgress((int) (System.currentTimeMillis() - mStartTime + mStartProgress)); 950 mHandler.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS); 951 } 952 }; 953 954 private final Runnable mCloseOverflowRunnable = new Runnable() { 955 @Override 956 public void run() { 957 setOverflowMenuVisibility(false); 958 } 959 }; 960 961 private final Runnable mShowNoContentViewRunnable = new Runnable() { 962 @Override 963 public void run() { 964 showInitialNoContentView(getString(R.string.nothing_to_play), false); 965 } 966 }; 967 968 private final Runnable mResetTitleRunnable = new Runnable() { 969 @Override 970 public void run() { 971 mDelayedResetTitleInProgress = false; 972 resetTitle(); 973 } 974 }; 975 976 private final Runnable mSetTitleRunnable = new Runnable() { 977 @Override 978 public void run() { 979 mTitleView.setText(mCurrentTrack.getTitle()); 980 mArtistView.setText(mCurrentTrack.getSubtitle()); 981 } 982 }; 983 } 984