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