blob: a86eb531dc0a095642d143450183dfd7e961a675 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.media;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import com.android.car.apps.common.BitmapDownloader;
import com.android.car.apps.common.BitmapWorkerOptions;
import com.android.car.apps.common.ColorChecker;
import com.android.car.apps.common.util.Assert;
import com.android.car.media.util.widgets.PlayPauseStopImageView;
import java.util.List;
import java.util.Objects;
/**
* Fragment that displays the media playback UI.
*/
public class MediaPlaybackFragment extends Fragment implements MediaPlaybackModel.Listener {
private static final String TAG = "MediaPlayback";
/**
* The preferred ordering for bitmap to fetch. The metadata at lower indexes are preferred to
* those at higher indexes.
*/
private static final String[] PREFERRED_BITMAP_TYPE_ORDER = {
MediaMetadata.METADATA_KEY_ALBUM_ART,
MediaMetadata.METADATA_KEY_ART,
MediaMetadata.METADATA_KEY_DISPLAY_ICON
};
/**
* The preferred ordering for metadata URIs to fetch. The metadata at lower indexes are
* preferred to those at higher indexes.
*/
private static final String[] PREFERRED_URI_ORDER = {
MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
MediaMetadata.METADATA_KEY_ART_URI,
MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
};
// The different types of Views that are contained within this Fragment.
private static final int NO_CONTENT_VIEW = 0;
private static final int PLAYBACK_CONTROLS_VIEW = 1;
private static final int LOADING_VIEW = 2;
@IntDef({NO_CONTENT_VIEW, PLAYBACK_CONTROLS_VIEW, LOADING_VIEW})
private @interface ViewType{}
/**
* The amount of time between seek bar updates.
*/
private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 500;
/**
* The delay time before automatically closing the overflow controls view.
*/
private static final long DELAY_CLOSE_OVERFLOW_MS = 3500;
/**
* Delay before showing any content. When the media app cold starts, it usually takes a
* moment to load the last played song from database. So wait for three seconds before showing
* the no content view rather than showing it and immediately switching to the playback view
* when the metadata loads.
*/
private static final long DELAY_SHOW_NO_CONTENT_VIEW_MS = 3000;
private static final long FEEDBACK_MESSAGE_DISPLAY_TIME_MS = 6000;
private static final int MEDIA_SCRIM_FADE_DURATION_MS = 400;
private static final int OVERFLOW_MENU_FADE_DURATION_MS = 250;
private static final int NUM_OF_CUSTOM_ACTION_BUTTONS = 4;
// The default width and height for an image. These are used if the mAlbumArtView has not laid
// out by the time a Bitmap needs to be created to fit in it.
private static final int DEFAULT_ALBUM_ART_WIDTH = 800;
private static final int DEFAULT_ALBUM_ART_HEIGHT = 400;
private MediaPlaybackModel mMediaPlaybackModel;
private final Handler mHandler = new Handler();
private View mScrimView;
private float mDefaultScrimAlpha;
private float mDarkenedScrimAlpha;
private CrossfadeImageView mAlbumArtView;
private TextView mTitleView;
private TextView mArtistView;
private ImageButton mPrevButton;
private PlayPauseStopImageView mPlayPauseStopButton;
private ImageButton mNextButton;
private ImageButton mPlayQueueButton;
private View mMusicPanel;
private View mControlsView;
private View mOverflowView;
private ImageButton mOverflowOnButton;
private ImageButton mOverflowOffButton;
private boolean mIsOverflowVisible;
private final ImageButton[] mCustomActionButtons =
new ImageButton[NUM_OF_CUSTOM_ACTION_BUTTONS];
private SeekBar mSeekBar;
private ProgressBar mSpinner;
private long mStartProgress;
private long mStartTime;
private MediaDescription mCurrentTrack;
private boolean mShowingMessage;
private View mInitialNoContentView;
private View mMetadata;
private View mMusicErrorIcon;
private TextView mTapToSelectText;
private ProgressBar mAppConnectingSpinner;
private boolean mDelayedResetTitleInProgress;
private int mAlbumArtWidth = DEFAULT_ALBUM_ART_WIDTH;
private int mAlbumArtHeight = DEFAULT_ALBUM_ART_HEIGHT;
private int mShowTitleDelayMs;
private TelephonyManager mTelephonyManager;
private boolean mInCall;
private BitmapDownloader mDownloader;
private boolean mReturnFromOnStop;
@ViewType private int mCurrentView;
private PlayQueueRevealer mPlayQueueRevealer;
/**
* An interface that is responsible for displaying a list of the items in the user's currently
* playing queue.
*/
interface PlayQueueRevealer {
void showPlayQueue();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getContext();
mMediaPlaybackModel = new MediaPlaybackModel(context, null /* browserExtras */);
mMediaPlaybackModel.addListener(this);
mTelephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
Resources res = context.getResources();
mShowTitleDelayMs = res.getInteger(R.integer.new_album_art_fade_in_duration);
mDefaultScrimAlpha = res.getFloat(R.dimen.media_scrim_alpha);
mDarkenedScrimAlpha = res.getFloat(R.dimen.media_scrim_darkened_alpha);
}
/**
* Sets the object that is responsible for displaying the current list of items in the user's
* play queue.
*/
void setPlayQueueRevealer(PlayQueueRevealer revealer) {
mPlayQueueRevealer = revealer;
}
@Override
public void onDestroy() {
super.onDestroy();
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
mMediaPlaybackModel = null;
// Calling this with null will clear queue of callbacks and message.
mHandler.removeCallbacksAndMessages(null);
mDelayedResetTitleInProgress = false;
}
@Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.now_playing_screen, container, false);
mScrimView = v.findViewById(R.id.scrim);
mAlbumArtView = v.findViewById(R.id.album_art);
mAlbumArtView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mAlbumArtWidth = mAlbumArtView.getWidth();
mAlbumArtHeight = mAlbumArtView.getHeight();
mAlbumArtView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
setBackgroundColor(getContext().getColor(R.color.music_default_artwork));
mTitleView = v.findViewById(R.id.title);
mArtistView = v.findViewById(R.id.artist);
mSeekBar = v.findViewById(R.id.seek_bar);
mSeekBar.setOnTouchListener((v1, event) -> {
// Eat up touch events from users as we set progress programmatically only.
return true;
});
mControlsView = v.findViewById(R.id.controls);
mOverflowView = v.findViewById(R.id.overflow_items);
mMusicPanel = v.findViewById(R.id.music_panel);
mSpinner = v.findViewById(R.id.spinner);
mInitialNoContentView = v.findViewById(R.id.initial_view);
mMetadata = v.findViewById(R.id.metadata);
mMusicErrorIcon = v.findViewById(R.id.error_icon);
mTapToSelectText = v.findViewById(R.id.tap_to_select_item);
mAppConnectingSpinner = v.findViewById(R.id.loading_spinner);
mCustomActionButtons[0] = v.findViewById(R.id.custom_action_1);
mCustomActionButtons[1] = v.findViewById(R.id.custom_action_2);
mCustomActionButtons[2] = v.findViewById(R.id.custom_action_3);
mCustomActionButtons[3] = v.findViewById(R.id.custom_action_4);
setupMediaButtons(v);
return v;
}
private void setupMediaButtons(View parentView) {
mPlayQueueButton = parentView.findViewById(R.id.play_queue);
mPrevButton = parentView.findViewById(R.id.prev);
mNextButton = parentView.findViewById(R.id.next);
mPlayPauseStopButton = parentView.findViewById(R.id.play_pause);
mOverflowOnButton = parentView.findViewById(R.id.overflow_on);
mOverflowOffButton = parentView.findViewById(R.id.overflow_off);
setActionDrawable(mOverflowOffButton, R.drawable.ic_overflow_activated, getResources());
mPlayQueueButton.setOnClickListener(mControlsClickListener);
mPrevButton.setOnClickListener(mControlsClickListener);
mNextButton.setOnClickListener(mControlsClickListener);
mPlayPauseStopButton.setOnClickListener(mControlsClickListener);
mOverflowOnButton.setOnClickListener(mControlsClickListener);
mOverflowOffButton.setOnClickListener(mControlsClickListener);
}
@Override
public void onPause() {
super.onPause();
mMediaPlaybackModel.stop();
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void onStop() {
super.onStop();
// When switch apps, onStop() will be called. Mark it and don't show fade in/out title and
// background animations when come back.
mReturnFromOnStop = true;
}
@Override
public void onResume() {
super.onResume();
mMediaPlaybackModel.start();
// Note: at registration, TelephonyManager will invoke the callback with the current state.
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void onMediaAppChanged(@Nullable ComponentName currentName,
@Nullable ComponentName newName) {
Assert.isMainThread();
resetTitle();
if (Objects.equals(currentName, newName)) {
return;
}
int accentColor = mMediaPlaybackModel.getAccentColor();
mPlayPauseStopButton.setPrimaryActionColor(accentColor);
mSeekBar.getProgressDrawable().setColorFilter(accentColor, PorterDuff.Mode.SRC_IN);
int overflowViewColor = mMediaPlaybackModel.getPrimaryColorDark();
mOverflowView.getBackground().setColorFilter(overflowViewColor, PorterDuff.Mode.SRC_IN);
// Tint the overflow actions light or dark depending on contrast.
int overflowTintColor = ColorChecker.getTintColor(getContext(), overflowViewColor);
for (ImageView v : mCustomActionButtons) {
v.setColorFilter(overflowTintColor, PorterDuff.Mode.SRC_IN);
}
mOverflowOffButton.setColorFilter(overflowTintColor, PorterDuff.Mode.SRC_IN);
ColorStateList colorStateList = ColorStateList.valueOf(accentColor);
mSpinner.setIndeterminateTintList(colorStateList);
mAppConnectingSpinner.setIndeterminateTintList(ColorStateList.valueOf(accentColor));
showLoadingView();
closeOverflowMenu();
}
@Override
public void onMediaAppStatusMessageChanged(@Nullable String message) {
Assert.isMainThread();
if (message == null) {
resetTitle();
} else {
showMessage(message);
}
}
@Override
public void onMediaConnected() {
Assert.isMainThread();
onMetadataChanged(mMediaPlaybackModel.getMetadata());
onQueueChanged(mMediaPlaybackModel.getQueue());
onPlaybackStateChanged(mMediaPlaybackModel.getPlaybackState());
mReturnFromOnStop = false;
}
@Override
public void onMediaConnectionSuspended() {
Assert.isMainThread();
mReturnFromOnStop = false;
}
@Override
public void onMediaConnectionFailed(CharSequence failedClientName) {
Assert.isMainThread();
showInitialNoContentView(getString(R.string.cannot_connect_to_app, failedClientName),
true /* isError */);
mReturnFromOnStop = false;
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
public void onPlaybackStateChanged(@Nullable PlaybackState state) {
Assert.isMainThread();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "onPlaybackStateChanged; state: "
+ (state == null ? "<< NULL >>" : state.toString()));
}
if (state == null) {
return;
}
if (state.getState() == PlaybackState.STATE_ERROR) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "ERROR: " + state.getErrorMessage());
}
String message = TextUtils.isEmpty(state.getErrorMessage())
? getString(R.string.unknown_error)
: state.getErrorMessage().toString();
showInitialNoContentView(message, true /* isError */);
return;
}
mStartProgress = state.getPosition();
mStartTime = System.currentTimeMillis();
mSeekBar.setProgress((int) mStartProgress);
if (state.getState() == PlaybackState.STATE_PLAYING) {
mHandler.post(mSeekBarRunnable);
} else {
mHandler.removeCallbacks(mSeekBarRunnable);
}
if (!mInCall) {
int playbackState = state.getState();
mPlayPauseStopButton.setPlayState(playbackState);
// Due to the action of PlaybackState will be changed when the state of PlaybackState is
// changed, we set mode every time onPlaybackStateChanged() is called.
if (playbackState == PlaybackState.STATE_PLAYING ||
playbackState == PlaybackState.STATE_BUFFERING) {
mPlayPauseStopButton.setMode(((state.getActions() & PlaybackState.ACTION_STOP) != 0)
? PlayPauseStopImageView.MODE_STOP : PlayPauseStopImageView.MODE_PAUSE);
} else {
mPlayPauseStopButton.setMode(PlayPauseStopImageView.MODE_PAUSE);
}
mPlayPauseStopButton.refreshDrawableState();
}
if (state.getState() == PlaybackState.STATE_BUFFERING) {
mSpinner.setVisibility(View.VISIBLE);
} else {
mSpinner.setVisibility(View.GONE);
}
updateActions(state.getActions(), state.getCustomActions());
if (mMediaPlaybackModel.getMetadata() == null) {
return;
}
showMediaPlaybackControlsView();
}
@Override
public void onMetadataChanged(@Nullable MediaMetadata metadata) {
Assert.isMainThread();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "onMetadataChanged; description: "
+ (metadata == null ? "<< NULL >>" : metadata.getDescription().toString()));
}
if (metadata == null) {
mHandler.postDelayed(mShowNoContentViewRunnable, DELAY_SHOW_NO_CONTENT_VIEW_MS);
return;
} else {
mHandler.removeCallbacks(mShowNoContentViewRunnable);
}
showMediaPlaybackControlsView();
mCurrentTrack = metadata.getDescription();
Bitmap icon = getMetadataBitmap(metadata);
if (!mShowingMessage) {
mHandler.removeCallbacks(mSetTitleRunnable);
// Show the title when the new album art starts to fade in, but don't need to show
// the fade in animation when come back from switching apps.
mHandler.postDelayed(mSetTitleRunnable,
icon == null || mReturnFromOnStop ? 0 : mShowTitleDelayMs);
}
Uri iconUri = getMetadataIconUri(metadata);
if (icon != null) {
Bitmap scaledIcon = cropAlbumArt(icon);
if (scaledIcon != icon && !icon.isRecycled()) {
icon.recycle();
}
// Fade out the old background and then fade in the new one when the new album art
// starts, but don't need to show the fade out and fade in animations when come back
// from switching apps.
setBackgroundBitmap(scaledIcon, !mReturnFromOnStop /* showAnimation */);
} else if (iconUri != null) {
if (mDownloader == null) {
mDownloader = new BitmapDownloader(getContext());
}
final int flags = BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED
| BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Album art size " + mAlbumArtWidth + "x" + mAlbumArtHeight);
}
BitmapWorkerOptions bitmapWorkerOptions = new BitmapWorkerOptions.Builder(getContext())
.resource(iconUri)
.height(mAlbumArtHeight)
.width(mAlbumArtWidth)
.cacheFlag(flags)
.build();
mDownloader.getBitmap(bitmapWorkerOptions,
new BitmapDownloader.BitmapCallback() {
@Override
public void onBitmapRetrieved(Bitmap bitmap) {
setBackgroundBitmap(bitmap, true /* showAnimation */);
}
});
} else {
setBackgroundColor(mMediaPlaybackModel.getPrimaryColorDark());
}
mSeekBar.setMax((int) metadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
}
@Override
public void onQueueChanged(List<MediaSession.QueueItem> queue) {
Assert.isMainThread();
mPlayQueueButton.setVisibility(queue.isEmpty() ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void onSessionDestroyed(CharSequence destroyedMediaClientName) {
Assert.isMainThread();
mHandler.removeCallbacks(mSeekBarRunnable);
showInitialNoContentView(
getString(R.string.cannot_connect_to_app, destroyedMediaClientName), true);
}
/**
* Sets the given {@link Bitmap} as the background of this playback fragment. If
*
* @param showAnimation {@code true} if the bitmap should be faded in.
*/
private void setBackgroundBitmap(Bitmap bitmap, boolean showAnimation) {
mAlbumArtView.setImageBitmap(bitmap, showAnimation);
}
/**
* Sets the given color as the background color of the view.
*/
private void setBackgroundColor(int color) {
mAlbumArtView.setBackgroundColor(color);
}
/**
* Darkens the scrim's alpha level.
*/
private void darkenScrim() {
mScrimView.animate()
.alpha(mDarkenedScrimAlpha)
.setDuration(MEDIA_SCRIM_FADE_DURATION_MS);
}
/**
* Sets whether or not the scrim is visible. The scrim is a semi-transparent View that darkens
* an album art so that does not overpower any text that is over it.
*/
private void setScrimVisible(boolean visible) {
float alpha = visible ? mDefaultScrimAlpha : 0.f;
mScrimView.animate()
.alpha(alpha)
.setDuration(MEDIA_SCRIM_FADE_DURATION_MS);
}
/**
* Displays the given message to the user. The message is displayed in the field that
* normally displays the title of the currently playing media item.
*/
private void showMessage(String message) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "showMessage(); message: " + message);
}
mHandler.removeCallbacks(mResetTitleRunnable);
darkenScrim();
mTitleView.setText(message);
mArtistView.setVisibility(View.GONE);
mShowingMessage = true;
}
/**
* Checks if the user is on the overflow view of the media controls. If they are, then this
* view is closed, and the user is switched back to the usual controls (usually the play
* controls).
*/
void closeOverflowMenu() {
if (mIsOverflowVisible) {
mHandler.removeCallbacks(mCloseOverflowRunnable);
setOverflowMenuVisible(false);
}
}
/**
* Hides the view for overflow controls over the regular media controls. The media controls will
* fade in over the overflow view.
*/
private void hideOverflowView() {
mOverflowView.animate()
.alpha(0f)
.setDuration(OVERFLOW_MENU_FADE_DURATION_MS)
.withStartAction(() -> mControlsView.setVisibility(View.VISIBLE))
.withEndAction(() -> mOverflowView.setVisibility(View.GONE));
}
/**
* Displays the view for overflow controls over the regular media controls. The overflow view
* fades in over the media controls.
*/
private void showOverflowView() {
mOverflowView.animate()
.alpha(1f)
.setDuration(OVERFLOW_MENU_FADE_DURATION_MS)
.withStartAction(() -> mOverflowView.setVisibility(View.VISIBLE))
.withEndAction(() -> mControlsView.setVisibility(View.GONE));
}
private void setOverflowMenuVisible(boolean isVisible) {
if (mIsOverflowVisible == isVisible) {
return;
}
mIsOverflowVisible = isVisible;
if (mIsOverflowVisible) {
showOverflowView();
int tint = ColorChecker.getTintColor(getContext(),
mMediaPlaybackModel.getPrimaryColorDark());
mSeekBar.getProgressDrawable().setColorFilter(tint, PorterDuff.Mode.SRC_IN);
} else {
hideOverflowView();
mSeekBar.getProgressDrawable().setColorFilter(
mMediaPlaybackModel.getAccentColor(), PorterDuff.Mode.SRC_IN);
}
}
/**
* For a given drawer slot, set the proper action of the slot's button,
* based on the slot being reserved and the corresponding action being enabled.
* If the slot is not reserved and the corresponding action is disabled,
* then the next available custom action is assigned to the button.
*
* @param button The button corresponding to the slot
* @param originalResId The drawable resource ID for the original button,
* only used if the original action is not replaced by a custom action.
* @param slotAlwaysReserved True if the slot should be empty when the
* corresponding action is disabled. If false, when the action is disabled
* the slot has its default action replaced by the next custom action, if any.
* @param isOriginalEnabled True if the original action of this button is
* enabled.
* @param customActions A list of custom actions still unassigned to slots.
*/
private void handleSlot(ImageButton button, int originalResId, boolean slotAlwaysReserved,
boolean isOriginalEnabled, List<PlaybackState.CustomAction> customActions) {
if (isOriginalEnabled || slotAlwaysReserved) {
setActionDrawable(button, originalResId, getResources());
button.setVisibility(isOriginalEnabled ? View.VISIBLE : View.INVISIBLE);
button.setTag(null);
return;
}
if (customActions.isEmpty()) {
button.setVisibility(View.INVISIBLE);
return;
}
PlaybackState.CustomAction customAction = customActions.remove(0);
Bundle extras = customAction.getExtras();
boolean repeatedAction = (extras != null && extras.getBoolean(
MediaConstants.EXTRA_REPEATED_CUSTOM_ACTION_BUTTON, false));
if (repeatedAction) {
button.setOnTouchListener(mControlsTouchListener);
} else {
button.setOnClickListener(mControlsClickListener);
}
button.setVisibility(View.VISIBLE);
setActionDrawable(button, customAction.getIcon(),
mMediaPlaybackModel.getPackageResources());
button.setTag(customAction);
}
/**
* Takes a list of custom actions and standard actions and displays them in the media
* controls card (or hides ones that aren't available).
*
* @param actions A bit mask of active actions (android.media.session.PlaybackState#ACTION_*).
* @param customActions A list of custom actions specified by the
* {@link android.media.session.MediaSession}.
*/
private void updateActions(long actions, List<PlaybackState.CustomAction> customActions) {
List<MediaSession.QueueItem> mediaQueue = mMediaPlaybackModel.getQueue();
handleSlot(
mPlayQueueButton, R.drawable.ic_tracklist,
mMediaPlaybackModel.isSlotForActionReserved(
MediaConstants.EXTRA_RESERVED_SLOT_QUEUE),
!mediaQueue.isEmpty(),
customActions);
handleSlot(
mPrevButton, R.drawable.ic_skip_previous,
mMediaPlaybackModel.isSlotForActionReserved(
MediaConstants.EXTRA_RESERVED_SLOT_SKIP_TO_PREVIOUS),
(actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0,
customActions);
handleSlot(
mNextButton, R.drawable.ic_skip_next,
mMediaPlaybackModel.isSlotForActionReserved(
MediaConstants.EXTRA_RESERVED_SLOT_SKIP_TO_NEXT),
(actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0,
customActions);
handleSlot(
mOverflowOnButton, R.drawable.ic_overflow_normal,
customActions.size() > 1,
customActions.size() > 1,
customActions);
for (ImageButton button: mCustomActionButtons) {
handleSlot(button, 0, false, false, customActions);
}
}
private void showInitialNoContentView(String msg, boolean isError) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "showInitialNoContentView()");
}
if (mCurrentView == NO_CONTENT_VIEW) {
return;
}
mCurrentView = NO_CONTENT_VIEW;
mAppConnectingSpinner.setVisibility(View.GONE);
setScrimVisible(false);
if (isError) {
setBackgroundColor(getContext().getColor(R.color.car_error_screen));
mMusicErrorIcon.setVisibility(View.VISIBLE);
} else {
setBackgroundColor(getContext().getColor(R.color.car_dark_blue_grey_800));
mMusicErrorIcon.setVisibility(View.INVISIBLE);
}
mTapToSelectText.setVisibility(View.VISIBLE);
mTapToSelectText.setText(msg);
mInitialNoContentView.setVisibility(View.VISIBLE);
mMetadata.setVisibility(View.GONE);
mMusicPanel.setVisibility(View.GONE);
}
private void showMediaPlaybackControlsView() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "showMediaPlaybackControlsView()");
}
if (mCurrentView == PLAYBACK_CONTROLS_VIEW) {
return;
}
mCurrentView = PLAYBACK_CONTROLS_VIEW;
if (!mShowingMessage) {
setScrimVisible(true);
}
mTapToSelectText.setVisibility(View.GONE);
mInitialNoContentView.setVisibility(View.GONE);
mMetadata.setVisibility(View.VISIBLE);
mMusicPanel.setVisibility(View.VISIBLE);
}
private void showLoadingView() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "showLoadingView()");
}
if (mCurrentView == LOADING_VIEW) {
return;
}
mCurrentView = LOADING_VIEW;
setBackgroundColor(getContext().getColor(R.color.music_loading_view_background));
mAppConnectingSpinner.setVisibility(View.VISIBLE);
mMusicErrorIcon.setVisibility(View.GONE);
mTapToSelectText.setVisibility(View.GONE);
mInitialNoContentView.setVisibility(View.VISIBLE);
mMetadata.setVisibility(View.GONE);
mMusicPanel.setVisibility(View.GONE);
}
private void resetTitle() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "resetTitle()");
}
if (!mShowingMessage) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Message not currently shown; not resetting title");
}
return;
}
// Feedback message is currently being displayed, reset will automatically take place when
// the display interval expires.
if (mDelayedResetTitleInProgress) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Delayed reset title is in progress; not resetting title now");
}
return;
}
setScrimVisible(true);
mArtistView.setVisibility(View.VISIBLE);
if (mCurrentTrack != null) {
mTitleView.setText(mCurrentTrack.getTitle());
mArtistView.setText(mCurrentTrack.getSubtitle());
}
mShowingMessage = false;
}
private Bitmap cropAlbumArt(Bitmap icon) {
if (icon == null) {
return null;
}
int width = icon.getWidth();
int height = icon.getHeight();
int startX = width > mAlbumArtWidth ? (width - mAlbumArtWidth) / 2 : 0;
int startY = height > mAlbumArtHeight ? (height - mAlbumArtHeight) / 2 : 0;
int newWidth = width > mAlbumArtWidth ? mAlbumArtWidth : width;
int newHeight = height > mAlbumArtHeight ? mAlbumArtHeight : height;
return Bitmap.createBitmap(icon, startX, startY, newWidth, newHeight);
}
private Bitmap getMetadataBitmap(MediaMetadata metadata) {
// Get the best art bitmap we can find
for (String bitmapType : PREFERRED_BITMAP_TYPE_ORDER) {
Bitmap bitmap = metadata.getBitmap(bitmapType);
if (bitmap != null) {
return bitmap;
}
}
return null;
}
private Uri getMetadataIconUri(MediaMetadata metadata) {
// Get the best Uri we can find
for (String bitmapUri : PREFERRED_URI_ORDER) {
String iconUri = metadata.getString(bitmapUri);
if (!TextUtils.isEmpty(iconUri)) {
return Uri.parse(iconUri);
}
}
return null;
}
/**
* Sets the drawable given by the {@code resId} on the specified {@link ImageButton}.
*
* @param resources The {@link Resources} to retrieve the Drawable from. This may be different
* from the Resources of this Fragment.
*/
private void setActionDrawable(ImageButton button, @DrawableRes int resId,
Resources resources) {
if (resources == null) {
Log.e(TAG, "Resources is null. Icons will not show up.");
return;
}
Resources myResources = getResources();
// The resources may be from another package. We need to update the configuration using
// the context from the activity so we get the drawable from the correct DPI bucket.
resources.updateConfiguration(myResources.getConfiguration(),
myResources.getDisplayMetrics());
try {
Drawable icon = resources.getDrawable(resId, null);
int inset = myResources.getDimensionPixelSize(R.dimen.music_action_icon_inset);
InsetDrawable insetIcon = new InsetDrawable(icon, inset);
button.setImageDrawable(insetIcon);
} catch (Resources.NotFoundException e) {
Log.w(TAG, "Resource not found: " + resId);
}
}
private void checkAndDisplayFeedbackMessage(PlaybackState.CustomAction ca) {
Bundle extras = ca.getExtras();
if (extras == null) {
return;
}
String feedbackMessage = extras.getString(MediaConstants.EXTRA_CUSTOM_ACTION_STATUS, "");
if (!TextUtils.isEmpty(feedbackMessage)) {
// Show feedback message that appears for a time interval unless a new
// message is shown.
showMessage(feedbackMessage);
mDelayedResetTitleInProgress = true;
mHandler.postDelayed(mResetTitleRunnable, FEEDBACK_MESSAGE_DISPLAY_TIME_MS);
}
}
private final View.OnTouchListener mControlsTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (!mMediaPlaybackModel.isConnected()) {
Log.e(TAG, "Unable to send action for " + v
+ ". The MediaPlaybackModel is not connected.");
return true;
}
boolean onDown;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
onDown = true;
break;
case MotionEvent.ACTION_UP:
onDown = false;
break;
default:
return true;
}
if (v.getTag() instanceof PlaybackState.CustomAction) {
PlaybackState.CustomAction ca = (PlaybackState.CustomAction) v.getTag();
checkAndDisplayFeedbackMessage(ca);
Bundle extras = ca.getExtras();
extras.putBoolean(
MediaConstants.EXTRA_REPEATED_CUSTOM_ACTION_BUTTON_ON_DOWN, onDown);
MediaController.TransportControls transportControls =
mMediaPlaybackModel.getTransportControls();
transportControls.sendCustomAction(ca, extras);
mHandler.removeCallbacks(mCloseOverflowRunnable);
if (!onDown) {
mHandler.postDelayed(mCloseOverflowRunnable, DELAY_CLOSE_OVERFLOW_MS);
}
}
return true;
}
};
private final View.OnClickListener mControlsClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!mMediaPlaybackModel.isConnected()) {
Log.e(TAG, "Unable to send action for " + v
+ ". The MediaPlaybackModel is not connected.");
return;
}
MediaController.TransportControls transportControls =
mMediaPlaybackModel.getTransportControls();
if (v.getTag() instanceof PlaybackState.CustomAction) {
PlaybackState.CustomAction ca = (PlaybackState.CustomAction) v.getTag();
checkAndDisplayFeedbackMessage(ca);
transportControls.sendCustomAction(ca, ca.getExtras());
mHandler.removeCallbacks(mCloseOverflowRunnable);
mHandler.postDelayed(mCloseOverflowRunnable, DELAY_CLOSE_OVERFLOW_MS);
return;
}
switch (v.getId()) {
case R.id.play_queue:
if (mPlayQueueRevealer != null) {
mPlayQueueRevealer.showPlayQueue();
}
break;
case R.id.prev:
transportControls.skipToPrevious();
break;
case R.id.play_pause:
case R.id.play_pause_container:
handlePlaybackStateForPlay(mMediaPlaybackModel.getPlaybackState(),
transportControls);
break;
case R.id.next:
transportControls.skipToNext();
break;
case R.id.overflow_off:
closeOverflowMenu();
break;
case R.id.overflow_on:
setOverflowMenuVisible(true);
break;
default:
throw new IllegalStateException("Unknown button press: " + v);
}
}
/**
* Plays, pauses or stops the music playback depending on the state given in
* {@link PlaybackState}.
*/
private void handlePlaybackStateForPlay(PlaybackState playbackState,
MediaController.TransportControls transportControls) {
if (playbackState == null) {
return;
}
switch (playbackState.getState()) {
// Only if the music is currently playing does this method need to handle pausing
// and stopping of media.
case PlaybackState.STATE_PLAYING:
case PlaybackState.STATE_BUFFERING:
long actions = playbackState.getActions();
if ((actions & PlaybackState.ACTION_PAUSE) != 0) {
transportControls.pause();
} else if ((actions & PlaybackState.ACTION_STOP) != 0) {
transportControls.stop();
}
break;
default:
transportControls.play();
}
}
};
private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
case TelephonyManager.CALL_STATE_OFFHOOK:
mPlayPauseStopButton
.setPlayState(PlayPauseStopImageView.PLAYBACKSTATE_DISABLED);
mPlayPauseStopButton.setMode(PlayPauseStopImageView.MODE_PAUSE);
mPlayPauseStopButton.refreshDrawableState();
mInCall = true;
break;
case TelephonyManager.CALL_STATE_IDLE:
if (mInCall) {
PlaybackState playbackState = mMediaPlaybackModel.getPlaybackState();
if (playbackState != null) {
mPlayPauseStopButton.setPlayState(playbackState.getState());
boolean isStopAction =
(playbackState.getActions() & PlaybackState.ACTION_STOP) != 0;
mPlayPauseStopButton.setMode(isStopAction
? PlayPauseStopImageView.MODE_STOP
: PlayPauseStopImageView.MODE_PAUSE);
mPlayPauseStopButton.refreshDrawableState();
}
mInCall = false;
}
break;
default:
Log.w(TAG, "TelephonyManager reports an unknown call state: " + state);
}
}
};
private final Runnable mSeekBarRunnable = new Runnable() {
@Override
public void run() {
mSeekBar.setProgress((int) (System.currentTimeMillis() - mStartTime + mStartProgress));
mHandler.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS);
}
};
private final Runnable mCloseOverflowRunnable = () -> setOverflowMenuVisible(false);
private final Runnable mShowNoContentViewRunnable =
() -> showInitialNoContentView(getString(R.string.nothing_to_play), false);
private final Runnable mResetTitleRunnable = () -> {
mDelayedResetTitleInProgress = false;
resetTitle();
};
private final Runnable mSetTitleRunnable = () -> {
mTitleView.setText(mCurrentTrack.getTitle());
mArtistView.setText(mCurrentTrack.getSubtitle());
};
}