• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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