blob: a565046b7f52d82461b288f101ad26da4a7259c9 [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.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager.ProgramInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
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.audio.IPlaybackStateListener;
import com.android.car.radio.service.IRadioManager;
import com.android.car.radio.utils.LocalInterface;
import com.android.car.radio.utils.ThrowingRunnable;
import java.util.Objects;
/**
* Implementation of tuner's MediaSession.
*/
public class TunerSession extends MediaSessionCompat
implements IPlaybackStateListener, LocalInterface {
private static final String TAG = "BcRadioApp.msess";
private final Object mLock = new Object();
private final Context mContext;
private final BrowseTree mBrowseTree;
@Nullable private final ImageResolver mImageResolver;
private final IRadioManager mUiSession;
private final PlaybackStateCompat.Builder mPlaybackStateBuilder =
new PlaybackStateCompat.Builder();
@Nullable private ProgramInfo mCurrentProgram;
public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree,
@NonNull IRadioManager uiSession, @Nullable ImageResolver imageResolver) {
super(context, TAG);
mContext = Objects.requireNonNull(context);
mBrowseTree = Objects.requireNonNull(browseTree);
mImageResolver = imageResolver;
mUiSession = Objects.requireNonNull(uiSession);
// ACTION_PAUSE is reserved for time-shifted playback
mPlaybackStateBuilder.setActions(
PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SET_RATING
| PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
| PlaybackStateCompat.ACTION_PLAY_FROM_URI);
setRatingType(RatingCompat.RATING_HEART);
onPlaybackStateChanged(PlaybackStateCompat.STATE_NONE);
setCallback(new TunerSessionCallback());
setActive(true);
}
private void updateMetadata() {
synchronized (mLock) {
if (mCurrentProgram == null) return;
boolean fav = mBrowseTree.isFavorite(mCurrentProgram.getSelector());
setMetadata(MediaMetadataCompat.fromMediaMetadata(
ProgramInfoExt.toMediaMetadata(mCurrentProgram, fav, mImageResolver)));
}
}
public void notifyProgramInfoChanged(@NonNull ProgramInfo info) {
synchronized (mLock) {
mCurrentProgram = info;
updateMetadata();
}
}
@Override
public void onPlaybackStateChanged(@PlaybackStateCompat.State int state) {
synchronized (mPlaybackStateBuilder) {
mPlaybackStateBuilder.setState(state,
PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f);
setPlaybackState(mPlaybackStateBuilder.build());
}
}
public void notifyFavoritesChanged() {
updateMetadata();
}
private void selectionError() {
exec(() -> mUiSession.mute());
mPlaybackStateBuilder.setErrorMessage(mContext.getString(R.string.invalid_selection));
onPlaybackStateChanged(PlaybackStateCompat.STATE_ERROR);
mPlaybackStateBuilder.setErrorMessage(null);
}
private void exec(ThrowingRunnable<RemoteException> func) {
try {
func.run();
} catch (RemoteException ex) {
Log.e(TAG, "Failed to execute MediaSession callback", ex);
}
}
private class TunerSessionCallback extends MediaSessionCompat.Callback {
@Override
public void onStop() {
exec(() -> mUiSession.mute());
}
@Override
public void onPlay() {
exec(() -> mUiSession.unMute());
}
@Override
public void onSkipToNext() {
exec(() -> mUiSession.seekForward());
}
@Override
public void onSkipToPrevious() {
exec(() -> mUiSession.seekBackward());
}
@Override
public void onSetRating(RatingCompat rating) {
synchronized (mLock) {
if (mCurrentProgram == null) return;
if (rating.hasHeart()) {
Program fav = Program.fromProgramInfo(mCurrentProgram);
exec(() -> mUiSession.addFavorite(fav));
} else {
ProgramSelector fav = mCurrentProgram.getSelector();
exec(() -> mUiSession.removeFavorite(fav));
}
}
}
@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) {
exec(() -> mUiSession.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) {
exec(() -> mUiSession.tune(selector));
} else {
Log.w(TAG, "Invalid URI: " + uri);
selectionError();
}
}
}
}