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 */
16package com.android.car.stream.media;
17
18import android.content.Context;
19import android.media.MediaMetadata;
20import android.media.session.MediaController;
21import android.media.session.MediaSessionManager;
22import android.media.session.PlaybackState;
23import android.os.Handler;
24import android.os.Looper;
25import android.support.annotation.MainThread;
26import android.support.annotation.NonNull;
27import android.support.annotation.Nullable;
28import android.util.Log;
29import android.view.KeyEvent;
30import com.android.car.apps.common.util.Assert;
31
32import java.util.LinkedHashSet;
33import java.util.List;
34import 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 */
40public 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 {
52        void onMediaSessionConnected(PlaybackState playbackState, MediaMetadata metaData,
53                MediaAppInfo appInfo);
54
55        void onPlaybackStateChanged(@Nullable PlaybackState state);
56
57        void onMetadataChanged(@Nullable MediaMetadata metadata);
58
59        void onSessionDestroyed();
60    }
61
62    public MediaStateManager(@NonNull Context context) {
63        mContext = context;
64        mHandler = new Handler(Looper.getMainLooper());
65        mListeners = new LinkedHashSet<>();
66    }
67
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
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
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
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
142    public void addListener(@NonNull Listener listener) {
143        mListeners.add(listener);
144    }
145
146    public void removeListener(@NonNull Listener listener) {
147        mListeners.remove(listener);
148    }
149
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
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