1 /** 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.radio.media; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.hardware.radio.ProgramSelector; 23 import android.hardware.radio.RadioManager.ProgramInfo; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.os.IBinder; 27 import android.os.RemoteException; 28 import android.support.v4.media.MediaMetadataCompat; 29 import android.support.v4.media.RatingCompat; 30 import android.support.v4.media.session.MediaSessionCompat; 31 import android.support.v4.media.session.PlaybackStateCompat; 32 import android.util.Log; 33 34 import com.android.car.broadcastradio.support.Program; 35 import com.android.car.broadcastradio.support.media.BrowseTree; 36 import com.android.car.broadcastradio.support.platform.ImageResolver; 37 import com.android.car.broadcastradio.support.platform.ProgramInfoExt; 38 import com.android.car.broadcastradio.support.platform.ProgramSelectorExt; 39 import com.android.car.radio.R; 40 import com.android.car.radio.audio.IPlaybackStateListener; 41 import com.android.car.radio.service.IRadioManager; 42 import com.android.car.radio.utils.ThrowingRunnable; 43 44 import java.util.Objects; 45 46 /** 47 * Implementation of tuner's MediaSession. 48 */ 49 public class TunerSession extends MediaSessionCompat implements IPlaybackStateListener { 50 private static final String TAG = "BcRadioApp.msess"; 51 52 private final Object mLock = new Object(); 53 54 private final Context mContext; 55 private final BrowseTree mBrowseTree; 56 @Nullable private final ImageResolver mImageResolver; 57 private final IRadioManager mUiSession; 58 private final PlaybackStateCompat.Builder mPlaybackStateBuilder = 59 new PlaybackStateCompat.Builder(); 60 @Nullable private ProgramInfo mCurrentProgram; 61 TunerSession(@onNull Context context, @NonNull BrowseTree browseTree, @NonNull IRadioManager uiSession, @Nullable ImageResolver imageResolver)62 public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree, 63 @NonNull IRadioManager uiSession, @Nullable ImageResolver imageResolver) { 64 super(context, TAG); 65 66 mContext = Objects.requireNonNull(context); 67 mBrowseTree = Objects.requireNonNull(browseTree); 68 mImageResolver = imageResolver; 69 mUiSession = Objects.requireNonNull(uiSession); 70 71 // ACTION_PAUSE is reserved for time-shifted playback 72 mPlaybackStateBuilder.setActions( 73 PlaybackStateCompat.ACTION_STOP 74 | PlaybackStateCompat.ACTION_PLAY 75 | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS 76 | PlaybackStateCompat.ACTION_SKIP_TO_NEXT 77 | PlaybackStateCompat.ACTION_SET_RATING 78 | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID 79 | PlaybackStateCompat.ACTION_PLAY_FROM_URI); 80 setRatingType(RatingCompat.RATING_HEART); 81 onPlaybackStateChanged(PlaybackStateCompat.STATE_NONE); 82 setCallback(new TunerSessionCallback()); 83 84 setActive(true); 85 } 86 updateMetadata()87 private void updateMetadata() { 88 synchronized (mLock) { 89 if (mCurrentProgram == null) return; 90 boolean fav = mBrowseTree.isFavorite(mCurrentProgram.getSelector()); 91 setMetadata(MediaMetadataCompat.fromMediaMetadata( 92 ProgramInfoExt.toMediaMetadata(mCurrentProgram, fav, mImageResolver))); 93 } 94 } 95 notifyProgramInfoChanged(@onNull ProgramInfo info)96 public void notifyProgramInfoChanged(@NonNull ProgramInfo info) { 97 synchronized (mLock) { 98 mCurrentProgram = info; 99 updateMetadata(); 100 } 101 } 102 103 @Override onPlaybackStateChanged(@laybackStateCompat.State int state)104 public void onPlaybackStateChanged(@PlaybackStateCompat.State int state) { 105 synchronized (mPlaybackStateBuilder) { 106 mPlaybackStateBuilder.setState(state, 107 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); 108 setPlaybackState(mPlaybackStateBuilder.build()); 109 } 110 } 111 notifyFavoritesChanged()112 public void notifyFavoritesChanged() { 113 updateMetadata(); 114 } 115 selectionError()116 private void selectionError() { 117 exec(() -> mUiSession.mute()); 118 mPlaybackStateBuilder.setErrorMessage(mContext.getString(R.string.invalid_selection)); 119 onPlaybackStateChanged(PlaybackStateCompat.STATE_ERROR); 120 mPlaybackStateBuilder.setErrorMessage(null); 121 } 122 exec(ThrowingRunnable<RemoteException> func)123 private void exec(ThrowingRunnable<RemoteException> func) { 124 try { 125 func.run(); 126 } catch (RemoteException ex) { 127 Log.e(TAG, "Failed to execute MediaSession callback", ex); 128 } 129 } 130 131 private class TunerSessionCallback extends MediaSessionCompat.Callback { 132 @Override onStop()133 public void onStop() { 134 exec(() -> mUiSession.mute()); 135 } 136 137 @Override onPlay()138 public void onPlay() { 139 exec(() -> mUiSession.unMute()); 140 } 141 142 @Override onSkipToNext()143 public void onSkipToNext() { 144 exec(() -> mUiSession.seekForward()); 145 } 146 147 @Override onSkipToPrevious()148 public void onSkipToPrevious() { 149 exec(() -> mUiSession.seekBackward()); 150 } 151 152 @Override onSetRating(RatingCompat rating)153 public void onSetRating(RatingCompat rating) { 154 synchronized (mLock) { 155 if (mCurrentProgram == null) return; 156 if (rating.hasHeart()) { 157 Program fav = Program.fromProgramInfo(mCurrentProgram); 158 exec(() -> mUiSession.addFavorite(fav)); 159 } else { 160 ProgramSelector fav = mCurrentProgram.getSelector(); 161 exec(() -> mUiSession.removeFavorite(fav)); 162 } 163 } 164 } 165 166 @Override onPlayFromMediaId(String mediaId, Bundle extras)167 public void onPlayFromMediaId(String mediaId, Bundle extras) { 168 if (mBrowseTree.getRoot().getRootId().equals(mediaId)) { 169 // general play command 170 onPlay(); 171 return; 172 } 173 174 ProgramSelector selector = mBrowseTree.parseMediaId(mediaId); 175 if (selector != null) { 176 exec(() -> mUiSession.tune(selector)); 177 } else { 178 Log.w(TAG, "Invalid media ID: " + mediaId); 179 selectionError(); 180 } 181 } 182 183 @Override onPlayFromUri(Uri uri, Bundle extras)184 public void onPlayFromUri(Uri uri, Bundle extras) { 185 ProgramSelector selector = ProgramSelectorExt.fromUri(uri); 186 if (selector != null) { 187 exec(() -> mUiSession.tune(selector)); 188 } else { 189 Log.w(TAG, "Invalid URI: " + uri); 190 selectionError(); 191 } 192 } 193 } 194 195 @Override asBinder()196 public IBinder asBinder() { 197 throw new UnsupportedOperationException("Not a binder"); 198 } 199 } 200