Home | History | Annotate | Download | only in media
      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