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.content.Context; 20 import android.hardware.radio.ProgramSelector; 21 import android.hardware.radio.RadioManager.ProgramInfo; 22 import android.hardware.radio.RadioMetadata; 23 import android.media.MediaMetadata; 24 import android.media.Rating; 25 import android.media.session.MediaController; 26 import android.media.session.MediaSession; 27 import android.media.session.PlaybackState; 28 import android.net.Uri; 29 import android.os.Bundle; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 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.service.RadioAppServiceWrapper; 41 import com.android.car.radio.service.RadioAppServiceWrapper.ConnectionState; 42 import com.android.car.radio.storage.RadioStorage; 43 import com.android.car.radio.util.Log; 44 45 import java.util.Objects; 46 47 /** 48 * Implementation of tuner's MediaSession. 49 */ 50 public class TunerSession { 51 private static final String TAG = "BcRadioApp.media"; 52 53 private final Object mLock = new Object(); 54 private final MediaSession mSession; 55 56 private final Context mContext; 57 private final BrowseTree mBrowseTree; 58 @Nullable private final ImageResolver mImageResolver; 59 private final RadioAppServiceWrapper mAppService; 60 61 private final RadioStorage mRadioStorage; 62 63 private final PlaybackState.Builder mPlaybackStateBuilder = 64 new PlaybackState.Builder(); 65 @Nullable private ProgramInfo mCurrentProgram; 66 67 /** 68 * Custom order that puts RDS_RT ahead of RDS_PS. 69 * RDS_PS is often used to scroll data in RDS_RT in some regions, and for the interests of 70 * driver distration we want to prevent RDS_PS scrolling from updating UI so frequently. 71 */ 72 public static final String[] PROGRAM_NAME_ORDER = new String[] { 73 RadioMetadata.METADATA_KEY_PROGRAM_NAME, 74 RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME, 75 RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME, 76 RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME, 77 RadioMetadata.METADATA_KEY_RDS_RT, 78 RadioMetadata.METADATA_KEY_RDS_PS 79 }; 80 TunerSession(@onNull Context context, @NonNull BrowseTree browseTree, @NonNull RadioAppServiceWrapper appService, @Nullable ImageResolver imageResolver)81 public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree, 82 @NonNull RadioAppServiceWrapper appService, @Nullable ImageResolver imageResolver) { 83 mSession = new MediaSession(context, TAG); 84 85 mContext = Objects.requireNonNull(context); 86 mBrowseTree = Objects.requireNonNull(browseTree); 87 mImageResolver = imageResolver; 88 mAppService = Objects.requireNonNull(appService); 89 90 mRadioStorage = RadioStorage.getInstance(context); 91 92 // ACTION_PAUSE is reserved for time-shifted playback 93 mPlaybackStateBuilder.setActions( 94 PlaybackState.ACTION_STOP 95 | PlaybackState.ACTION_PLAY 96 | PlaybackState.ACTION_SKIP_TO_PREVIOUS 97 | PlaybackState.ACTION_SKIP_TO_NEXT 98 | PlaybackState.ACTION_SET_RATING 99 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID 100 | PlaybackState.ACTION_PLAY_FROM_URI); 101 mSession.setRatingType(Rating.RATING_HEART); 102 onPlaybackStateChanged(PlaybackState.STATE_NONE); 103 mSession.setCallback(new TunerSessionCallback()); 104 105 // TunerSession is a part of RadioAppService, so observeForever is fine here. 106 appService.getPlaybackState().observeForever(this::onPlaybackStateChanged); 107 appService.getCurrentProgram().observeForever(this::updateMetadata); 108 mRadioStorage.getFavorites().observeForever( 109 favorites -> updateMetadata(mAppService.getCurrentProgram().getValue())); 110 111 mSession.setActive(true); 112 113 mAppService.getConnectionState().observeForever(this::onSelfStateChanged); 114 } 115 onSelfStateChanged(@onnectionState int state)116 private void onSelfStateChanged(@ConnectionState int state) { 117 if (state == RadioAppServiceWrapper.STATE_ERROR) { 118 mSession.setActive(false); 119 } 120 } 121 updateMetadata(@ullable ProgramInfo info)122 private void updateMetadata(@Nullable ProgramInfo info) { 123 synchronized (mLock) { 124 if (info == null) return; 125 boolean fav = mRadioStorage.isFavorite(info.getSelector()); 126 MediaMetadata currMetaData = mSession.getController().getMetadata(); 127 MediaMetadata newMetaData = ProgramInfoExt.toMediaDisplayMetadata(info, fav, 128 mImageResolver, PROGRAM_NAME_ORDER); 129 if (!Objects.equals(currMetaData, newMetaData)) { 130 mSession.setMetadata(newMetaData); 131 } 132 } 133 } 134 onPlaybackStateChanged(@laybackState.State int state)135 private void onPlaybackStateChanged(@PlaybackState.State int state) { 136 synchronized (mPlaybackStateBuilder) { 137 mPlaybackStateBuilder.setState(state, 138 PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f); 139 mSession.setPlaybackState(mPlaybackStateBuilder.build()); 140 } 141 } 142 selectionError()143 private void selectionError() { 144 mAppService.setMuted(true); 145 mPlaybackStateBuilder.setErrorMessage(mContext.getString(R.string.invalid_selection)); 146 onPlaybackStateChanged(PlaybackState.STATE_ERROR); 147 mPlaybackStateBuilder.setErrorMessage(null); 148 } 149 150 /** See {@link MediaSession#getSessionToken}. */ getSessionToken()151 public MediaSession.Token getSessionToken() { 152 return mSession.getSessionToken(); 153 } 154 155 /** See {@link MediaSession#getController}. */ getController()156 public MediaController getController() { 157 return mSession.getController(); 158 } 159 160 /** See {@link MediaSession#release}. */ release()161 public void release() { 162 mSession.release(); 163 } 164 165 private class TunerSessionCallback extends MediaSession.Callback { 166 @Override onStop()167 public void onStop() { 168 mAppService.setMuted(true); 169 } 170 171 @Override onPlay()172 public void onPlay() { 173 mAppService.tuneToDefaultIfNeeded(); 174 mAppService.setMuted(false); 175 } 176 177 @Override onSkipToNext()178 public void onSkipToNext() { 179 mAppService.skip(true); 180 } 181 182 @Override onSkipToPrevious()183 public void onSkipToPrevious() { 184 mAppService.skip(false); 185 } 186 187 @Override onSetRating(Rating rating)188 public void onSetRating(Rating rating) { 189 synchronized (mLock) { 190 ProgramInfo info = mAppService.getCurrentProgram().getValue(); 191 if (info == null) return; 192 193 if (rating.hasHeart()) { 194 mRadioStorage.addFavorite(Program.fromProgramInfo(info)); 195 } else { 196 mRadioStorage.removeFavorite(info.getSelector()); 197 } 198 } 199 } 200 201 @Override onPlayFromMediaId(String mediaId, Bundle extras)202 public void onPlayFromMediaId(String mediaId, Bundle extras) { 203 if (mBrowseTree.getRoot().getRootId().equals(mediaId)) { 204 // general play command 205 onPlay(); 206 return; 207 } 208 209 ProgramSelector selector = mBrowseTree.parseMediaId(mediaId); 210 if (selector != null) { 211 mAppService.tune(selector); 212 } else { 213 Log.w(TAG, "Invalid media ID: " + mediaId); 214 selectionError(); 215 } 216 } 217 218 @Override onPlayFromUri(Uri uri, Bundle extras)219 public void onPlayFromUri(Uri uri, Bundle extras) { 220 ProgramSelector selector = ProgramSelectorExt.fromUri(uri); 221 if (selector != null) { 222 mAppService.tune(selector); 223 } else { 224 Log.w(TAG, "Invalid URI: " + uri); 225 selectionError(); 226 } 227 } 228 } 229 } 230