/** * 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(); } } } }