• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MediaPlaybackFragment()137     public MediaPlaybackFragment() {
138       super();
139     }
140 
141     @Override
onCreate(Bundle savedInstanceState)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
onDestroy()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
onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)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
onViewCreated(View view, Bundle savedInstanceState)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
onPause()236     public void onPause() {
237         super.onPause();
238         mMediaPlaybackModel.stop();
239         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
240     }
241 
242     @Override
onStop()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
onResume()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
onMediaAppChanged(@ullable ComponentName currentName, @Nullable ComponentName newName)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
onMediaAppStatusMessageChanged(@ullable String message)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
onMediaConnected()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
onMediaConnectionSuspended()304     public void onMediaConnectionSuspended() {
305         Assert.isMainThread();
306         mReturnFromOnStop = false;
307     }
308 
309     @Override
onMediaConnectionFailed(CharSequence failedClientName)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)
onPlaybackStateChanged(@ullable PlaybackState state)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
onMetadataChanged(@ullable MediaMetadata metadata)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
onQueueChanged(List<MediaSession.QueueItem> queue)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
onSessionDestroyed(CharSequence destroyedMediaClientName)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 
showMessage(String msg)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 
isOverflowMenuVisible()471     boolean isOverflowMenuVisible() {
472         return mOverflowVisibility;
473     }
474 
closeOverflowMenu()475     void closeOverflowMenu() {
476         mHandler.removeCallbacks(mCloseOverflowRunnable);
477         setOverflowMenuVisibility(false);
478     }
479 
setOverflowMenuVisibility(boolean visibility)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 
setControlsFocusability(boolean focusable)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 
setOverflowFocusability(boolean focusable)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      */
handleSlot(ImageButton button, int originalResId, boolean slotAlwaysReserved, boolean isOriginalEnabled, List<PlaybackState.CustomAction> customActions)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      */
updateActions(long actions, List<PlaybackState.CustomAction> customActions)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 
setCustomAction(ImageButton imageButton, PlaybackState.CustomAction customAction)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 
showInitialNoContentView(String msg, boolean isError)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 
showMediaPlaybackControlsView()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 
showLoadingView()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 
needViewChange(ViewType newView)695     private boolean needViewChange(ViewType newView) {
696         if (mCurrentView != null && mCurrentView == newView) {
697             return false;
698         }
699         mCurrentView = newView;
700         return true;
701     }
702 
resetTitle()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 
cropAlbumArt(Bitmap icon)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 
getMetadataBitmap(MediaMetadata metadata)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 
getMetadataIconUri(MediaMetadata metadata)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 
setActionDrawable(ImageButton button, int resId, Resources resources)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 
checkAndDisplayFeedbackMessage(PlaybackState.CustomAction ca)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