blob: 0cda1f3cfa6eff0202d1d7bed408b59395dc371b [file] [log] [blame]
/**
* 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();
}
}
}
}