| /** |
| * Copyright (C) 2018 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.radio.media; |
| |
| import android.content.Context; |
| import android.hardware.radio.ProgramSelector; |
| import android.hardware.radio.RadioManager.ProgramInfo; |
| import android.media.Rating; |
| import android.media.session.MediaController; |
| import android.media.session.MediaSession; |
| import android.media.session.PlaybackState; |
| import android.net.Uri; |
| import android.os.Bundle; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.car.broadcastradio.support.Program; |
| import com.android.car.broadcastradio.support.media.BrowseTree; |
| import com.android.car.broadcastradio.support.platform.ImageResolver; |
| import com.android.car.broadcastradio.support.platform.ProgramInfoExt; |
| import com.android.car.broadcastradio.support.platform.ProgramSelectorExt; |
| import com.android.car.radio.R; |
| import com.android.car.radio.service.RadioAppServiceWrapper; |
| import com.android.car.radio.service.RadioAppServiceWrapper.ConnectionState; |
| import com.android.car.radio.storage.RadioStorage; |
| import com.android.car.radio.util.Log; |
| |
| import java.util.Objects; |
| |
| /** |
| * Implementation of tuner's MediaSession. |
| */ |
| public class TunerSession { |
| private static final String TAG = "BcRadioApp.media"; |
| |
| private final Object mLock = new Object(); |
| private final MediaSession mSession; |
| |
| private final Context mContext; |
| private final BrowseTree mBrowseTree; |
| @Nullable private final ImageResolver mImageResolver; |
| private final RadioAppServiceWrapper mAppService; |
| |
| private final RadioStorage mRadioStorage; |
| |
| private final PlaybackState.Builder mPlaybackStateBuilder = |
| new PlaybackState.Builder(); |
| @Nullable private ProgramInfo mCurrentProgram; |
| |
| public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree, |
| @NonNull RadioAppServiceWrapper appService, @Nullable ImageResolver imageResolver) { |
| mSession = new MediaSession(context, TAG); |
| |
| mContext = Objects.requireNonNull(context); |
| mBrowseTree = Objects.requireNonNull(browseTree); |
| mImageResolver = imageResolver; |
| mAppService = Objects.requireNonNull(appService); |
| |
| mRadioStorage = RadioStorage.getInstance(context); |
| |
| // ACTION_PAUSE is reserved for time-shifted playback |
| mPlaybackStateBuilder.setActions( |
| PlaybackState.ACTION_STOP |
| | PlaybackState.ACTION_PLAY |
| | PlaybackState.ACTION_SKIP_TO_PREVIOUS |
| | PlaybackState.ACTION_SKIP_TO_NEXT |
| | PlaybackState.ACTION_SET_RATING |
| | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
| | PlaybackState.ACTION_PLAY_FROM_URI); |
| mSession.setRatingType(Rating.RATING_HEART); |
| onPlaybackStateChanged(PlaybackState.STATE_NONE); |
| mSession.setCallback(new TunerSessionCallback()); |
| |
| // TunerSession is a part of RadioAppService, so observeForever is fine here. |
| appService.getPlaybackState().observeForever(this::onPlaybackStateChanged); |
| appService.getCurrentProgram().observeForever(this::updateMetadata); |
| mRadioStorage.getFavorites().observeForever( |
| favorites -> updateMetadata(mAppService.getCurrentProgram().getValue())); |
| |
| mSession.setActive(true); |
| |
| mAppService.getConnectionState().observeForever(this::onSelfStateChanged); |
| } |
| |
| private void onSelfStateChanged(@ConnectionState int state) { |
| if (state == RadioAppServiceWrapper.STATE_ERROR) { |
| mSession.setActive(false); |
| } |
| } |
| |
| private void updateMetadata(@Nullable ProgramInfo info) { |
| synchronized (mLock) { |
| if (info == null) return; |
| boolean fav = mRadioStorage.isFavorite(info.getSelector()); |
| mSession.setMetadata(ProgramInfoExt.toMediaMetadata(info, fav, mImageResolver)); |
| } |
| } |
| |
| private void onPlaybackStateChanged(@PlaybackState.State int state) { |
| synchronized (mPlaybackStateBuilder) { |
| mPlaybackStateBuilder.setState(state, |
| PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| mSession.setPlaybackState(mPlaybackStateBuilder.build()); |
| } |
| } |
| |
| private void selectionError() { |
| mAppService.setMuted(true); |
| mPlaybackStateBuilder.setErrorMessage(mContext.getString(R.string.invalid_selection)); |
| onPlaybackStateChanged(PlaybackState.STATE_ERROR); |
| mPlaybackStateBuilder.setErrorMessage(null); |
| } |
| |
| /** See {@link MediaSession#getSessionToken}. */ |
| public MediaSession.Token getSessionToken() { |
| return mSession.getSessionToken(); |
| } |
| |
| /** See {@link MediaSession#getController}. */ |
| public MediaController getController() { |
| return mSession.getController(); |
| } |
| |
| /** See {@link MediaSession#release}. */ |
| public void release() { |
| mSession.release(); |
| } |
| |
| private class TunerSessionCallback extends MediaSession.Callback { |
| @Override |
| public void onStop() { |
| mAppService.setMuted(true); |
| } |
| |
| @Override |
| public void onPlay() { |
| mAppService.setMuted(false); |
| } |
| |
| @Override |
| public void onSkipToNext() { |
| mAppService.skip(true); |
| } |
| |
| @Override |
| public void onSkipToPrevious() { |
| mAppService.skip(false); |
| } |
| |
| @Override |
| public void onSetRating(Rating rating) { |
| synchronized (mLock) { |
| ProgramInfo info = mAppService.getCurrentProgram().getValue(); |
| if (info == null) return; |
| |
| if (rating.hasHeart()) { |
| mRadioStorage.addFavorite(Program.fromProgramInfo(info)); |
| } else { |
| mRadioStorage.removeFavorite(info.getSelector()); |
| } |
| } |
| } |
| |
| @Override |
| public void onPlayFromMediaId(String mediaId, Bundle extras) { |
| if (mBrowseTree.getRoot().getRootId().equals(mediaId)) { |
| // general play command |
| onPlay(); |
| return; |
| } |
| |
| ProgramSelector selector = mBrowseTree.parseMediaId(mediaId); |
| if (selector != null) { |
| mAppService.tune(selector); |
| } else { |
| Log.w(TAG, "Invalid media ID: " + mediaId); |
| selectionError(); |
| } |
| } |
| |
| @Override |
| public void onPlayFromUri(Uri uri, Bundle extras) { |
| ProgramSelector selector = ProgramSelectorExt.fromUri(uri); |
| if (selector != null) { |
| mAppService.tune(selector); |
| } else { |
| Log.w(TAG, "Invalid URI: " + uri); |
| selectionError(); |
| } |
| } |
| } |
| } |