blob: 5becb9f451b5871e245b8d665fbb9fbbe3d32650 [file] [log] [blame]
/*
* Copyright (C) 2020 Google Inc.
*
* 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.carlauncher.homescreen.audio;
import static android.car.media.CarMediaIntents.EXTRA_MEDIA_COMPONENT;
import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
import android.app.Application;
import android.car.media.CarMediaIntents;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.Observer;
import com.android.car.apps.common.imaging.ImageBinder;
import com.android.car.carlauncher.homescreen.HomeCardInterface;
import com.android.car.carlauncher.homescreen.ui.CardContent;
import com.android.car.carlauncher.homescreen.ui.CardHeader;
import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView;
import com.android.car.carlauncher.homescreen.ui.SeekBarViewModel;
import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.R;
import com.android.car.media.common.playback.PlaybackProgress;
import com.android.car.media.common.playback.PlaybackViewModel;
import com.android.car.media.common.source.MediaSource;
import com.android.car.media.common.source.MediaSourceColors;
import com.android.car.media.common.source.MediaSourceViewModel;
import com.android.internal.annotations.VisibleForTesting;
/**
* ViewModel for media. Uses both a {@link MediaSourceViewModel} and a {@link PlaybackViewModel}
* for data on the audio source and audio metadata (such as song title), respectively.
*/
public class MediaViewModel extends AndroidViewModel implements AudioModel {
private static final String TAG = "MediaViewModel";
private static final String EMPTY_TIME = "";
private HomeCardInterface.Presenter mAudioPresenter;
// MediaSourceViewModel is for the current or last played media app
private MediaSourceViewModel mSourceViewModel;
// PlaybackViewModel has the media's metadata
private PlaybackViewModel mPlaybackViewModel;
private PlaybackViewModel.PlaybackController mPlaybackController;
private Context mContext;
private CardHeader mCardHeader;
private CharSequence mAppName;
private Drawable mAppIcon;
private CharSequence mSongTitle;
private CharSequence mArtistName;
private CharSequence mTimes;
private CharSequence mTimesSeparator;
private boolean mIsSeekEnabled;
private boolean mUseMediaSourceColor;
private int mDefaultSeekBarColor;
private int mSeekBarColor;
/**
* Use int value for progress and seekbar max value from config since {@link
* android.widget.SeekBar} only works with int. Handling the long & int conversion in {@link
* MediaViewModel}
*/
private int mProgress;
private int mSeekBarMax;
private long mRealMaxProgress;
private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
private Drawable mAlbumImageBitmap;
private Drawable mMediaBackground;
private OnModelUpdateListener mOnModelUpdateListener;
private OnProgressUpdateListener mOnProgressUpdateListener;
private Observer<Object> mMediaSourceColorObserver = x -> updateMediaSourceColor();
private Observer<Object> mMetadataObserver = x -> updateModelMetadata();
private Observer<Object> mPlaybackControllerObserver = controller -> updatePlaybackController();
private PlaybackCallback mPlaybackCallback = new PlaybackCallback() {
@Override
public void seekTo(int pos) {
if (mPlaybackController != null) {
double fraction = (double) pos / (double) mSeekBarMax;
Double realPos = mRealMaxProgress * fraction;
mPlaybackController.seekTo(realPos.longValue());
}
}
};
private Observer<Object> mMediaSourceObserver = x -> updateModel();
private Observer<Object> mProgressObserver = x -> updateProgress();
private Observer<PlaybackViewModel.PlaybackStateWrapper> mPlaybackStateWrapperObserver =
playbackStateWrapper -> {
if (playbackStateWrapper != null
&& mIsSeekEnabled != playbackStateWrapper.isSeekToEnabled()) {
mIsSeekEnabled = playbackStateWrapper.isSeekToEnabled();
if (mOnProgressUpdateListener != null) {
mOnProgressUpdateListener.onProgressUpdate(/* model = */
this, /* updateProgress = */
false);
}
}
};
public MediaViewModel(Application application) {
super(application);
}
@VisibleForTesting
MediaViewModel(Application application, MediaSourceViewModel sourceViewModel,
PlaybackViewModel playbackViewModel) {
super(application);
mSourceViewModel = sourceViewModel;
mPlaybackViewModel = playbackViewModel;
}
@Override
public void onCreate(@NonNull Context context) {
if (mSourceViewModel == null) {
mSourceViewModel = MediaSourceViewModel.get(getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
}
if (mPlaybackViewModel == null) {
mPlaybackViewModel = PlaybackViewModel.get(getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
}
mContext = context;
Resources resources = mContext.getResources();
int max = resources.getInteger(R.integer.media_items_bitmap_max_size_px);
mMediaBackground = resources
.getDrawable(R.drawable.control_bar_image_background);
Size maxArtSize = new Size(max, max);
mAlbumArtBinder = new ImageBinder<>(ImageBinder.PlaceholderType.FOREGROUND, maxArtSize,
drawable -> {
mAlbumImageBitmap = drawable;
mOnModelUpdateListener.onModelUpdate(/* model = */ this);
});
mSourceViewModel.getPrimaryMediaSource().observeForever(mMediaSourceObserver);
mPlaybackViewModel.getMetadata().observeForever(mMetadataObserver);
mPlaybackViewModel.getMediaSourceColors().observeForever(mMediaSourceColorObserver);
mPlaybackViewModel.getProgress().observeForever(mProgressObserver);
mPlaybackViewModel.getPlaybackController().observeForever(mPlaybackControllerObserver);
mPlaybackViewModel.getPlaybackStateWrapper().observeForever(mPlaybackStateWrapperObserver);
mSeekBarColor = mDefaultSeekBarColor = resources.getColor(
com.android.car.carlauncher.R.color.seek_bar_color, null);
mSeekBarMax = resources.getInteger(
com.android.car.carlauncher.R.integer.optional_seekbar_max);
mUseMediaSourceColor = resources.getBoolean(R.bool.use_media_source_color_for_seek_bar);
mTimesSeparator = resources.getString(com.android.car.carlauncher.R.string.times_separator);
mOnModelUpdateListener.onModelUpdate(/* model = */ this);
updateModel(); // Make sure the name of the media source properly reflects the locale.
}
@Override
protected void onCleared() {
super.onCleared();
mSourceViewModel.getPrimaryMediaSource().removeObserver(mMediaSourceObserver);
mPlaybackViewModel.getMetadata().removeObserver(mMetadataObserver);
mPlaybackViewModel.getPlaybackStateWrapper().removeObserver(mPlaybackStateWrapperObserver);
}
@Override
public Intent getIntent() {
MediaSource mediaSource = getMediaSourceViewModel().getPrimaryMediaSource().getValue();
Intent intent = new Intent(CarMediaIntents.ACTION_MEDIA_TEMPLATE);
if (mediaSource != null) {
intent.putExtra(EXTRA_MEDIA_COMPONENT,
mediaSource.getBrowseServiceComponentName().flattenToString());
}
return intent;
}
@Override
public void setOnModelUpdateListener(OnModelUpdateListener onModelUpdateListener) {
mOnModelUpdateListener = onModelUpdateListener;
}
@Override
public void setOnProgressUpdateListener(OnProgressUpdateListener onProgressUpdateListener) {
mOnProgressUpdateListener = onProgressUpdateListener;
}
@Override
public CardHeader getCardHeader() {
return mCardHeader;
}
@Override
public CardContent getCardContent() {
return new DescriptiveTextWithControlsView(
new CardContent.CardBackgroundImage(mAlbumImageBitmap, mMediaBackground),
mSongTitle,
mArtistName,
new SeekBarViewModel(
mTimes,
mIsSeekEnabled,
mSeekBarColor,
mProgress,
mPlaybackCallback)
);
}
/**
* Allows the {@link HomeAudioCardPresenter} to access the model to
* initialize the {@link com.android.car.media.common.PlaybackControlsActionBar}
*/
public PlaybackViewModel getPlaybackViewModel() {
return mPlaybackViewModel;
}
protected MediaSourceViewModel getMediaSourceViewModel() {
return mSourceViewModel;
}
/**
* Callback for the observer of the MediaSourceViewModel
*/
private void updateModel() {
MediaSource mediaSource = mSourceViewModel.getPrimaryMediaSource().getValue();
if (mediaSourceChanged()) {
if (mediaSource != null) {
if (Log.isLoggable(TAG, Log.INFO)) {
Log.i(TAG, "Setting Media view to source "
+ mediaSource.getDisplayName(mContext));
}
mAppName = mediaSource.getDisplayName(mContext);
mAppIcon = mediaSource.getIcon();
mCardHeader = new CardHeader(mAppName, mAppIcon);
updateMetadata();
updateProgress();
updateMediaSourceColor();
mOnModelUpdateListener.onModelUpdate(/* model = */ this);
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Not resetting media widget for apps "
+ "that do not support media browse");
}
}
}
}
/**
* Callback for the observer of the PlaybackViewModel
*/
private void updateModelMetadata() {
if (metadataChanged()) {
updateMetadata();
if (mCardHeader != null) {
mOnModelUpdateListener.onModelUpdate(/* model = */ this);
}
}
}
private void updateMediaSourceColor() {
MediaSourceColors mediaSourceColors = mPlaybackViewModel.getMediaSourceColors().getValue();
mSeekBarColor = (mediaSourceColors == null || !mUseMediaSourceColor)
? mDefaultSeekBarColor
: mediaSourceColors.getAccentColor(mDefaultSeekBarColor);
mOnProgressUpdateListener.onProgressUpdate(/* model = */ this, /* updateProgress = */
false);
}
private void updateProgress() {
PlaybackProgress playbackProgress = mPlaybackViewModel.getProgress().getValue();
if (playbackProgress == null) {
return;
}
mTimes = playbackProgress.hasTime() ? new StringBuilder(
playbackProgress.getCurrentTimeText()).append(
mTimesSeparator).append(playbackProgress.getMaxTimeText()).toString() : EMPTY_TIME;
mRealMaxProgress = playbackProgress.getMaxProgress();
int progress = playbackProgress.getProgressFraction() < 0 ? 0
: (int) (mSeekBarMax * playbackProgress.getProgressFraction());
if (mProgress != progress) {
mProgress = progress;
mOnProgressUpdateListener.onProgressUpdate(/* model = */ this, /* updateProgress = */
true);
}
}
private void updateMetadata() {
MediaItemMetadata metadata = mPlaybackViewModel.getMetadata().getValue();
if (metadata == null) {
clearMetadata();
} else {
mSongTitle = metadata.getTitle();
mArtistName = metadata.getSubtitle();
mAlbumArtBinder.setImage(mContext, metadata.getArtworkKey());
}
}
private void updatePlaybackController() {
mPlaybackController = mPlaybackViewModel.getPlaybackController().getValue();
}
private void clearMetadata() {
mSongTitle = mContext.getString(R.string.default_media_song_title);
mArtistName = null;
mAlbumArtBinder.setImage(mContext, /* newArtRef = */ null);
}
/**
* Helper method to check for a change in the media's metadata
*/
private boolean metadataChanged() {
MediaItemMetadata metadata = mPlaybackViewModel.getMetadata().getValue();
if (metadata == null && (mSongTitle != null || mArtistName != null)) {
return true;
}
if (metadata != null && (mSongTitle != metadata.getTitle()
|| mArtistName != metadata.getSubtitle())) {
return true;
}
return false;
}
/**
* Helper method to check for a change in the media source
*/
private boolean mediaSourceChanged() {
MediaSource mediaSource = mSourceViewModel.getPrimaryMediaSource().getValue();
if (mediaSource == null && (mAppName != null || mAppIcon != null)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "new media source is null...");
}
return true;
}
if (mediaSource != null && (mAppName != mediaSource.getDisplayName(mContext)
|| mAppIcon != mediaSource.getIcon())) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "new media source is " + mediaSource.toString());
}
return true;
}
return false;
}
/**
* Callback for {@link com.android.car.carlauncher.homescreen.HomeCardFragment} pass the seekbar
* info back.
*/
public interface PlaybackCallback {
/**
* Moves to a new location in the media stream
*/
void seekTo(int pos);
}
}