1 /*
2  * Copyright (c) 2016, 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 package com.android.car.stream.media;
17 
18 import android.content.Context;
19 import android.media.MediaMetadata;
20 import android.media.session.MediaController;
21 import android.media.session.MediaSessionManager;
22 import android.media.session.PlaybackState;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.support.annotation.MainThread;
26 import android.support.annotation.NonNull;
27 import android.support.annotation.Nullable;
28 import android.util.Log;
29 import android.view.KeyEvent;
30 import com.android.car.apps.common.util.Assert;
31 
32 import java.util.LinkedHashSet;
33 import java.util.List;
34 import java.util.Set;
35 
36 /**
37  * A class to listen for changes in sessions from {@link MediaSessionManager}. It also notifies
38  * listeners of changes in the playback state or metadata.
39  */
40 public class MediaStateManager {
41     private static final String TAG = "MediaStateManager";
42     private static final String TELECOM_PACKAGE = "com.android.server.telecom";
43 
44     private final Context mContext;
45 
46     private MediaAppInfo mConnectedAppInfo;
47     private MediaController mController;
48     private Handler mHandler;
49     private final Set<Listener> mListeners;
50 
51     public interface Listener {
onMediaSessionConnected(PlaybackState playbackState, MediaMetadata metaData, MediaAppInfo appInfo)52         void onMediaSessionConnected(PlaybackState playbackState, MediaMetadata metaData,
53                 MediaAppInfo appInfo);
54 
onPlaybackStateChanged(@ullable PlaybackState state)55         void onPlaybackStateChanged(@Nullable PlaybackState state);
56 
onMetadataChanged(@ullable MediaMetadata metadata)57         void onMetadataChanged(@Nullable MediaMetadata metadata);
58 
onSessionDestroyed()59         void onSessionDestroyed();
60     }
61 
MediaStateManager(@onNull Context context)62     public MediaStateManager(@NonNull Context context) {
63         mContext = context;
64         mHandler = new Handler(Looper.getMainLooper());
65         mListeners = new LinkedHashSet<>();
66     }
67 
start()68     public void start() {
69         if (Log.isLoggable(TAG, Log.DEBUG)) {
70             Log.d(TAG, "Starting MediaStateManager");
71         }
72         MediaSessionManager sessionManager
73                 = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
74 
75         try {
76             sessionManager.addOnActiveSessionsChangedListener(mSessionChangedListener, null);
77 
78             List<MediaController> controllers = sessionManager.getActiveSessions(null);
79             updateMediaController(controllers);
80         } catch (SecurityException e) {
81             // User hasn't granted the permission so we should just go away silently.
82         }
83     }
84 
85     @MainThread
destroy()86     public void destroy() {
87         Assert.isMainThread();
88         if (Log.isLoggable(TAG, Log.DEBUG)) {
89             Log.d(TAG, "destroy()");
90         }
91         stop();
92         mListeners.clear();
93         mHandler = null;
94     }
95 
96     @MainThread
stop()97     public void stop() {
98         Assert.isMainThread();
99         if (Log.isLoggable(TAG, Log.DEBUG)) {
100             Log.d(TAG, "stop()");
101         }
102 
103         if (mController != null) {
104             mController.unregisterCallback(mMediaControllerCallback);
105             mController = null;
106         }
107         // Calling this with null will clear queue of callbacks and message. This needs to be done
108         // here because prior to the above lines to disconnect and unregister the
109         // controller a posted runnable to do work maybe have happened and thus we need to clear it
110         // out to prevent race conditions.
111         mHandler.removeCallbacksAndMessages(null);
112     }
113 
dispatchMediaButton(KeyEvent keyEvent)114     public void dispatchMediaButton(KeyEvent keyEvent) {
115         if (mController != null) {
116             MediaController.TransportControls transportControls
117                     = mController.getTransportControls();
118             int eventId = keyEvent.getKeyCode();
119 
120             switch (eventId) {
121                 case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD:
122                     transportControls.skipToPrevious();
123                     break;
124                 case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD:
125                     transportControls.skipToNext();
126                     break;
127                 case KeyEvent.KEYCODE_MEDIA_PLAY:
128                     transportControls.play();
129                     break;
130                 case KeyEvent.KEYCODE_MEDIA_PAUSE:
131                     transportControls.pause();
132                     break;
133                 case KeyEvent.KEYCODE_MEDIA_STOP:
134                     transportControls.stop();
135                     break;
136                 default:
137                     mController.dispatchMediaButtonEvent(keyEvent);
138             }
139         }
140     }
141 
addListener(@onNull Listener listener)142     public void addListener(@NonNull Listener listener) {
143         mListeners.add(listener);
144     }
145 
removeListener(@onNull Listener listener)146     public void removeListener(@NonNull Listener listener) {
147         mListeners.remove(listener);
148     }
149 
updateMediaController(List<MediaController> controllers)150     private void updateMediaController(List<MediaController> controllers) {
151         if (controllers.size() > 0) {
152             // If the telecom package is trying to onStart a media session, ignore it
153             // so that the existing media item continues to appear in the stream.
154             if (TELECOM_PACKAGE.equals(controllers.get(0).getPackageName())) {
155                 return;
156             }
157 
158             if (mController != null) {
159                 mController.unregisterCallback(mMediaControllerCallback);
160             }
161             // Currently the first controller is the active one playing music.
162             // If this is no longer the case, consider checking notification listener
163             // for a MediaStyle notification to get currently playing media app.
164             mController = controllers.get(0);
165             mController.registerCallback(mMediaControllerCallback);
166 
167             mConnectedAppInfo = new MediaAppInfo(mContext, mController.getPackageName());
168 
169             if (Log.isLoggable(TAG, Log.DEBUG)) {
170                 Log.d(TAG, "updating media controller");
171             }
172 
173             for (Listener listener : mListeners) {
174                 listener.onMediaSessionConnected(mController.getPlaybackState(),
175                         mController.getMetadata(), mConnectedAppInfo);
176             }
177         } else {
178             Log.w(TAG, "Updating controllers with an empty list!");
179         }
180     }
181 
isMainThread()182     public static boolean isMainThread() {
183         return Looper.myLooper() == Looper.getMainLooper();
184     }
185 
186     private final MediaSessionManager.OnActiveSessionsChangedListener
187             mSessionChangedListener = new MediaSessionManager.OnActiveSessionsChangedListener() {
188         @Override
189         public void onActiveSessionsChanged(List<MediaController> controllers) {
190             updateMediaController(controllers);
191         }
192     };
193 
194     private final MediaController.Callback mMediaControllerCallback =
195             new MediaController.Callback() {
196                 @Override
197                 public void onPlaybackStateChanged(@NonNull final PlaybackState state) {
198                     mHandler.post(new Runnable() {
199                         @Override
200                         public void run() {
201                             if (Log.isLoggable(TAG, Log.DEBUG)) {
202                                 Log.d(TAG, "onPlaybackStateChanged(" + state + ")");
203                             }
204                             for (Listener listener : mListeners) {
205                                 listener.onPlaybackStateChanged(state);
206                             }
207                         }
208                     });
209                 }
210 
211                 @Override
212                 public void onMetadataChanged(@Nullable final MediaMetadata metadata) {
213                     mHandler.post(new Runnable() {
214                         @Override
215                         public void run() {
216                             if (Log.isLoggable(TAG, Log.DEBUG)) {
217                                 Log.d(TAG, "onMetadataChanged(" + metadata + ")");
218                             }
219                             for (Listener listener : mListeners) {
220                                 listener.onMetadataChanged(metadata);
221                             }
222                         }
223                     });
224                 }
225 
226                 @Override
227                 public void onSessionDestroyed() {
228                     mHandler.post(new Runnable() {
229                         @Override
230                         public void run() {
231                             if (Log.isLoggable(TAG, Log.DEBUG)) {
232                                 Log.d(TAG, "onSessionDestroyed()");
233                             }
234 
235                             mConnectedAppInfo = null;
236                             if (mController != null) {
237                                 mController.unregisterCallback(mMediaControllerCallback);
238                                 mController = null;
239                             }
240 
241                             for (Listener listener : mListeners) {
242                                 listener.onSessionDestroyed();
243                             }
244                         }
245                     });
246                 }
247             };
248 }
249