/* * 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.NonNull; import android.car.Car; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.media.session.MediaController; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.widget.DrawerLayout; import android.transition.Fade; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import com.android.car.media.common.ActiveMediaSourceManager; import com.android.car.media.common.CrossfadeImageView; import com.android.car.media.common.MediaItemMetadata; import com.android.car.media.common.MediaSource; import com.android.car.media.common.MediaSourcesManager; import com.android.car.media.common.PlaybackControls; import com.android.car.media.common.PlaybackModel; import com.android.car.media.drawer.MediaDrawerController; import com.android.car.media.widgets.AppBarView; import com.android.car.media.widgets.MetadataView; import com.android.car.media.widgets.ViewUtils; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import androidx.car.drawer.CarDrawerActivity; import androidx.car.drawer.CarDrawerAdapter; /** * This activity controls the UI of media. It also updates the connection status for the media app * by broadcast. Drawer menu is controlled by {@link MediaDrawerController}. */ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.Callbacks, AppSelectionFragment.Callbacks, PlaybackFragment.Callbacks { private static final String TAG = "MediaActivity"; /** Intent extra specifying the package with the MediaBrowser */ public static final String KEY_MEDIA_PACKAGE = "media_package"; /** Shared preferences files */ public static final String SHARED_PREF = "com.android.car.media"; /** Shared preference containing the last controlled source */ public static final String LAST_MEDIA_SOURCE_SHARED_PREF_KEY = "last_media_source"; /** Configuration (controlled from resources) */ private boolean mContentForwardBrowseEnabled; private float mBackgroundBlurRadius; private float mBackgroundBlurScale; private int mFadeDuration; /** Models */ private MediaDrawerController mDrawerController; private ActiveMediaSourceManager mActiveMediaSourceManager; private MediaSource mMediaSource; private PlaybackModel mPlaybackModel; private MediaSourcesManager mMediaSourcesManager; private SharedPreferences mSharedPreferences; /** Layout views */ private AppBarView mAppBarView; private CrossfadeImageView mAlbumBackground; private PlaybackFragment mPlaybackFragment; private AppSelectionFragment mAppSelectionFragment; private PlaybackControls mPlaybackControls; private MetadataView mMetadataView; private ViewGroup mBrowseControlsContainer; private EmptyFragment mEmptyFragment; private ViewGroup mBrowseContainer; private ViewGroup mPlaybackContainer; /** Current state */ private Fragment mCurrentFragment; private Mode mMode = Mode.BROWSING; private boolean mIsAppSelectorOpen; private MediaItemMetadata mCurrentMetadata; private MediaSource.Observer mMediaSourceObserver = new MediaSource.Observer() { @Override protected void onBrowseConnected(boolean success) { MediaActivity.this.onBrowseConnected(success); } @Override protected void onBrowseDisconnected() { MediaActivity.this.onBrowseConnected(false); } }; private PlaybackModel.PlaybackObserver mPlaybackObserver = new PlaybackModel.PlaybackObserver() { @Override protected void onSourceChanged() { updateMetadata(); } @Override public void onMetadataChanged() { mCurrentMetadata = null; updateMetadata(); } }; private ActiveMediaSourceManager.Observer mActiveSourceObserver = () -> { // If the active media source changes and it is the one currently being browsed, then // we should capture the controller. MediaController controller = mActiveMediaSourceManager.getMediaController(); if (mPlaybackModel.getMediaController() == null && mMediaSource != null && controller != null && Objects.equals(controller.getPackageName(), mMediaSource.getPackageName())) { mPlaybackModel.setMediaController(controller); } }; private MediaSource.ItemsSubscription mItemsSubscription = new MediaSource.ItemsSubscription() { @Override public void onChildrenLoaded(MediaSource mediaSource, String parentId, List items) { if (mediaSource == mMediaSource) { updateTabs(items); } else { Log.w(TAG, "Received items for a wrong source: " + mediaSource.getPackageName()); } } }; private AppBarView.AppBarListener mAppBarListener = new AppBarView.AppBarListener() { @Override public void onTabSelected(MediaItemMetadata item) { updateBrowseFragment(BrowseState.LOADED, item); switchToMode(Mode.BROWSING); } @Override public void onBack() { if (mCurrentFragment != null && mCurrentFragment instanceof BrowseFragment) { BrowseFragment fragment = (BrowseFragment) mCurrentFragment; fragment.navigateBack(); } } @Override public void onCollapse() { switchToMode(Mode.BROWSING); } @Override public void onAppSelection() { Log.d(TAG, "onAppSelection clicked"); if (mIsAppSelectorOpen) { closeAppSelector(); } else { openAppSelector(); } } }; private MediaSourcesManager.Observer mMediaSourcesManagerObserver = () -> { mAppBarView.setAppSelection(!mMediaSourcesManager.getMediaSources().isEmpty()); mAppSelectionFragment.refresh(); }; private DrawerLayout.DrawerListener mDrawerListener = new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@android.support.annotation.NonNull View view, float v) { } @Override public void onDrawerOpened(@android.support.annotation.NonNull View view) { closeAppSelector(); } @Override public void onDrawerClosed(@android.support.annotation.NonNull View view) { } @Override public void onDrawerStateChanged(int i) { } }; /** * Possible modes of the application UI */ private enum Mode { /** The user is browsing a media source */ BROWSING, /** The user is interacting with the full screen playback UI */ PLAYBACK } /** * Possible states of the application UI */ public enum BrowseState { /** There is no content to show */ EMPTY, /** We are still in the process of obtaining data */ LOADING, /** Data has been loaded */ LOADED, /** The content can't be shown due an error */ ERROR } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setMainContent(R.layout.media_activity); setToolbarElevation(0f); mContentForwardBrowseEnabled = getResources() .getBoolean(R.bool.forward_content_browse_enabled); mDrawerController = new MediaDrawerController(this, getDrawerController()); getDrawerController().setRootAdapter(getRootAdapter()); getDrawerController().addDrawerListener(mDrawerListener); if (mContentForwardBrowseEnabled) { getSupportActionBar().hide(); } mAppBarView = findViewById(R.id.app_bar); mAppBarView.setListener(mAppBarListener); mAppBarView.setContentForwardEnabled(mContentForwardBrowseEnabled); mPlaybackFragment = new PlaybackFragment(); mAppSelectionFragment = new AppSelectionFragment(); int fadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration); mAppSelectionFragment.setEnterTransition(new Fade().setDuration(fadeDuration)); mAppSelectionFragment.setExitTransition(new Fade().setDuration(fadeDuration)); mActiveMediaSourceManager = new ActiveMediaSourceManager(this); mPlaybackModel = new PlaybackModel(this); mMediaSourcesManager = new MediaSourcesManager(this); mAlbumBackground = findViewById(R.id.media_background); mPlaybackControls = findViewById(R.id.browse_controls); mPlaybackControls.setModel(mPlaybackModel); mMetadataView = findViewById(R.id.browse_metadata); mMetadataView.setModel(mPlaybackModel); mBrowseControlsContainer = findViewById(R.id.browse_controls_container); mBrowseControlsContainer.setOnClickListener(view -> switchToMode(Mode.PLAYBACK)); TypedValue outValue = new TypedValue(); getResources().getValue(R.dimen.playback_background_blur_radius, outValue, true); mBackgroundBlurRadius = outValue.getFloat(); getResources().getValue(R.dimen.playback_background_blur_scale, outValue, true); mBackgroundBlurScale = outValue.getFloat(); mSharedPreferences = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE); mFadeDuration = getResources().getInteger( R.integer.new_album_art_fade_in_duration); mEmptyFragment = new EmptyFragment(); mBrowseContainer = findViewById(R.id.fragment_container); mPlaybackContainer = findViewById(R.id.playback_container); getSupportFragmentManager().beginTransaction() .replace(R.id.playback_container, mPlaybackFragment) .commit(); } @Override public void onResume() { super.onResume(); mPlaybackModel.registerObserver(mPlaybackObserver); mActiveMediaSourceManager.registerObserver(mActiveSourceObserver); mMediaSourcesManager.registerObserver(mMediaSourcesManagerObserver); handleIntent(); } @Override public void onPause() { super.onPause(); mPlaybackModel.unregisterObserver(mPlaybackObserver); mActiveMediaSourceManager.unregisterObserver(mActiveSourceObserver); mMediaSourcesManager.unregisterObserver(mMediaSourcesManagerObserver); } @Override public void onDestroy() { super.onDestroy(); mDrawerController.cleanup(); mPlaybackControls.setModel(null); mMetadataView.setModel(null); } @Override protected CarDrawerAdapter getRootAdapter() { return mDrawerController == null ? null : mDrawerController.getRootAdapter(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onNewIntent(); intent: " + (intent == null ? "<< NULL >>" : intent)); } setIntent(intent); handleIntent(); } @Override public void onBackPressed() { mPlaybackFragment.closeOverflowMenu(); super.onBackPressed(); } private void onBrowseConnected(boolean success) { if (!success) { updateTabs(null); mMediaSource.unsubscribeChildren(null, mItemsSubscription); mMediaSource.unsubscribe(mMediaSourceObserver); updateBrowseFragment(BrowseState.ERROR, null); return; } mMediaSource.subscribeChildren(null, mItemsSubscription); if (mPlaybackModel.getMediaController() == null) { mPlaybackModel.setMediaController(mMediaSource.getMediaController()); } } private void handleIntent() { Intent intent = getIntent(); String action = intent != null ? intent.getAction() : null; getDrawerController().closeDrawer(); if (Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE.equals(action)) { // The user either wants to browse a particular media source or switch to the // playback UI. String packageName = intent.getStringExtra(KEY_MEDIA_PACKAGE); if (packageName != null) { // We were told to navigate to a particular package: we open browse for it. closeAppSelector(); changeMediaSource(new MediaSource(this, packageName), null); switchToMode(Mode.BROWSING); return; } // If we didn't receive a package name and we are playing something: show the playback // UI for the playing media source. MediaController controller = mActiveMediaSourceManager.getMediaController(); if (controller != null) { closeAppSelector(); changeMediaSource(new MediaSource(this, controller.getPackageName()), controller); switchToMode(Mode.PLAYBACK); return; } } // In any other case, if we were already browsing something: just close drawers/overlays // and display what we have. if (mMediaSource != null) { closeAppSelector(); return; } // If we don't have a current media source, we try with the last one we remember. MediaSource lastMediaSource = getLastMediaSource(); if (lastMediaSource != null) { closeAppSelector(); changeMediaSource(lastMediaSource, null); switchToMode(Mode.BROWSING); } else { // If we don't have anything from before: open the app selector. openAppSelector(); } } /** * Sets the media source being browsed. * * @param mediaSource the media source we are going to try to browse * @param controller a controller we can use to control the playback state of the given * source. If not provided, we will try to obtain it from the session manager. * Otherwise, we will obtain a controller once the media browser is connected. */ private void changeMediaSource(MediaSource mediaSource, MediaController controller) { if (Objects.equals(mediaSource, mMediaSource)) { // No change, nothing to do. return; } if (mMediaSource != null) { mMediaSource.unsubscribeChildren(null, mItemsSubscription); mMediaSource.unsubscribe(mMediaSourceObserver); updateTabs(new ArrayList<>()); } mMediaSource = mediaSource; mPlaybackModel.setMediaController(controller != null ? controller : mActiveMediaSourceManager.getControllerForPackage(mediaSource.getPackageName())); setLastMediaSource(mMediaSource); if (mMediaSource != null) { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Browsing: " + mediaSource.getName()); } // Prepare the media source for playback mPlaybackModel.onPrepare(); // Make the drawer display browse information of the selected source ComponentName component = mMediaSource.getBrowseServiceComponentName(); MediaManager.getInstance(this).setMediaClientComponent(component); // If content forward browsing is disabled, then no need to subscribe to this media // source, we will use the drawer instead. if (mContentForwardBrowseEnabled) { Log.i(TAG, "Content forward is enabled: subscribing to " + mMediaSource.getPackageName()); updateBrowseFragment(BrowseState.LOADING, null); mMediaSource.subscribe(mMediaSourceObserver); } mAppBarView.setAppIcon(mMediaSource.getRoundPackageIcon()); mAppBarView.setTitle(mMediaSource.getName()); } else { mAppBarView.setAppIcon(null); mAppBarView.setTitle(null); } } private boolean isCurrentMediaSourcePlaying() { return mMediaSource != null && mActiveMediaSourceManager.isPlaying(mMediaSource.getPackageName()); } /** * Updates the tabs displayed on the app bar, based on the top level items on the browse tree. * If there is at least one browsable item, we show the browse content of that node. * If there are only playable items, then we show those items. * If there are not items at all, we show the empty message. * If we receive null, we show the error message. * * @param items top level items, or null if there was an error trying load those items. */ private void updateTabs(List items) { if (items == null || items.isEmpty()) { mAppBarView.setItems(null); updateBrowseFragment(items == null ? BrowseState.ERROR : BrowseState.EMPTY, null); return; } items = customizeTabs(mMediaSource, items); List browsableTopLevel = items.stream() .filter(item -> item.isBrowsable()) .collect(Collectors.toList()); if (!browsableTopLevel.isEmpty()) { // If we have at least a few browsable items, we show the tabs mAppBarView.setItems(browsableTopLevel); updateBrowseFragment(BrowseState.LOADED, browsableTopLevel.get(0)); } else { // Otherwise, we show the top of the tree with no fabs mAppBarView.setItems(null); updateBrowseFragment(BrowseState.LOADED, null); } } /** * Extension point used to customize media items displayed on the tabs. * * @param mediaSource media source these items belong to. * @param items items to override. * @return an updated list of items. * @deprecated This method will be removed on b/79089344 */ @Deprecated protected List customizeTabs(MediaSource mediaSource, List items) { return items; } private void switchToMode(Mode mode) { // If content forward is not enable, then we always show the playback UI (browse will be // done in the drawer) mMode = mContentForwardBrowseEnabled ? mode : Mode.PLAYBACK; updateMetadata(); switch (mMode) { case PLAYBACK: ViewUtils.showViewAnimated(mPlaybackContainer, mFadeDuration); ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration); mAppBarView.setState(AppBarView.State.PLAYING); break; case BROWSING: ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration); ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration); mAppBarView.setState(AppBarView.State.BROWSING); break; } } /** * Updates the browse area with either a loading state, the root node content, or the * content of a particular media item. * * @param state state in the process of loading browse information. * @param topItem if state == IDLE, this will contain the item to display, * or null to display the root node. */ private void updateBrowseFragment(BrowseState state, MediaItemMetadata topItem) { switch(state) { case LOADED: if (topItem != null) { mCurrentFragment = BrowseFragment.newInstance(mMediaSource, topItem); mAppBarView.setActiveItem(topItem); } else { mCurrentFragment = BrowseFragment.newInstance(mMediaSource, null); mAppBarView.setActiveItem(null); } break; case EMPTY: case LOADING: case ERROR: mCurrentFragment = mEmptyFragment; mEmptyFragment.setState(state, mMediaSource); mAppBarView.setActiveItem(null); break; } getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, mCurrentFragment) .commitAllowingStateLoss(); } private void updateMetadata() { if (isCurrentMediaSourcePlaying()) { if (mMode == Mode.PLAYBACK) { ViewUtils.hideViewAnimated(mBrowseControlsContainer, mFadeDuration); } else { ViewUtils.showViewAnimated(mBrowseControlsContainer, mFadeDuration); } MediaItemMetadata metadata = mPlaybackModel.getMetadata(); if (Objects.equals(mCurrentMetadata, metadata)) { return; } mCurrentMetadata = metadata; mUpdateAlbumArtRunnable.run(); } else { mAlbumBackground.setImageBitmap(null, true); ViewUtils.hideViewAnimated(mBrowseControlsContainer, mFadeDuration); } } /** * We might receive new album art before we are ready to display it. If that situation happens * we will retrieve and render the album art when the views are already laid out. */ private Runnable mUpdateAlbumArtRunnable = new Runnable() { @Override public void run() { MediaItemMetadata metadata = mPlaybackModel.getMetadata(); if (metadata != null) { if (mAlbumBackground.getWidth() == 0 || mAlbumBackground.getHeight() == 0) { // We need to wait for the view to be measured before we can render this // album art. mAlbumBackground.setImageBitmap(null, false); mAlbumBackground.post(this); } else { mAlbumBackground.removeCallbacks(this); metadata.getAlbumArt(MediaActivity.this, mAlbumBackground.getWidth(), mAlbumBackground.getHeight(), false) .thenAccept(bitmap -> setBackgroundImage(bitmap)); } } else { mAlbumBackground.removeCallbacks(this); mAlbumBackground.setImageBitmap(null, true); } } }; private void setBackgroundImage(Bitmap bitmap) { // TODO(b/77551865): Implement image blurring once the following issue is solved: // b/77551557 // bitmap = ImageUtils.blur(getContext(), bitmap, mBackgroundBlurScale, // mBackgroundBlurRadius); mAlbumBackground.setImageBitmap(bitmap, true); } @Override public MediaSource getMediaSource(String packageName) { if (mMediaSource != null && mMediaSource.getPackageName().equals(packageName)) { return mMediaSource; } return new MediaSource(this, packageName); } @Override public void onBackStackChanged() { // TODO: Update ActionBar } @Override public void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item) { mPlaybackModel.onStop(); if (!Objects.equals(mediaSource, mPlaybackModel.getMediaSource())) { Log.w(TAG, "Trying to play an item from a different source " + "(expected: " + mPlaybackModel.getMediaSource() + ", received" + mediaSource + ")"); changeMediaSource(mediaSource, mediaSource.getMediaController()); } mPlaybackModel.onPlayItem(item.getId()); setIntent(null); } private void openAppSelector() { mIsAppSelectorOpen = true; FragmentManager manager = getSupportFragmentManager(); mAppBarView.setState(AppBarView.State.APP_SELECTION); manager.beginTransaction() .replace(R.id.app_selection_container, mAppSelectionFragment) .commit(); } private void closeAppSelector() { mIsAppSelectorOpen = false; FragmentManager manager = getSupportFragmentManager(); mAppBarView.setState(mMode == Mode.PLAYBACK ? AppBarView.State.PLAYING : AppBarView.State.BROWSING); manager.beginTransaction() .remove(mAppSelectionFragment) .commit(); } @Override public List getMediaSources() { return mMediaSourcesManager.getMediaSources() .stream() .filter(source -> source.getMediaBrowser() != null || source.isCustom()) .collect(Collectors.toList()); } @Override public void onMediaSourceSelected(MediaSource mediaSource) { closeAppSelector(); if (mediaSource.getMediaBrowser() != null && !mediaSource.isCustom()) { mCurrentMetadata = null; changeMediaSource(mediaSource, null); switchToMode(Mode.BROWSING); } else { String packageName = mediaSource.getPackageName(); Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); startActivity(intent); } } private MediaSource getLastMediaSource() { String packageName = mSharedPreferences.getString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, null); if (packageName == null) { return null; } // Verify that the stored package name corresponds to a currently installed media source. for (MediaSource mediaSource : mMediaSourcesManager.getMediaSources()) { if (mediaSource.getPackageName().equals(packageName)) { return mediaSource; } } return null; } private void setLastMediaSource(@NonNull MediaSource mediaSource) { mSharedPreferences.edit() .putString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, mediaSource.getPackageName()) .apply(); } @Override public PlaybackModel getPlaybackModel() { return mPlaybackModel; } @Override public void onQueueButtonClicked() { if (mContentForwardBrowseEnabled) { mPlaybackFragment.toggleQueueVisibility(); } else { mDrawerController.showPlayQueue(); } } }