Added Android code
[wl-app.git] / Android / app / src / main / java / com / moiseum / wolnelektury / view / player / service / MediaBrowserHelper.java
1 /*
2  * Copyright 2017 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.moiseum.wolnelektury.view.player.service;
18
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.os.Bundle;
22 import android.os.RemoteException;
23 import android.support.annotation.NonNull;
24 import android.support.annotation.Nullable;
25 import android.support.v4.media.MediaBrowserCompat;
26 import android.support.v4.media.MediaBrowserServiceCompat;
27 import android.support.v4.media.MediaMetadataCompat;
28 import android.support.v4.media.session.MediaControllerCompat;
29 import android.support.v4.media.session.MediaControllerCompat.Callback;
30 import android.support.v4.media.session.MediaSessionCompat;
31 import android.support.v4.media.session.PlaybackStateCompat;
32 import android.util.Log;
33
34 import java.util.ArrayList;
35 import java.util.List;
36
37 /**
38  * Helper class for a MediaBrowser that handles connecting, disconnecting,
39  * and basic browsing with simplified callbacks.
40  */
41 public class MediaBrowserHelper {
42
43     private static final String TAG = MediaBrowserHelper.class.getSimpleName();
44
45     private final Context mContext;
46     private final Class<? extends MediaBrowserServiceCompat> mMediaBrowserServiceClass;
47
48     private final List<Callback> mCallbackList = new ArrayList<>();
49
50     private final MediaBrowserConnectionCallback mMediaBrowserConnectionCallback;
51     private final MediaControllerCallback mMediaControllerCallback;
52     private final MediaBrowserSubscriptionCallback mMediaBrowserSubscriptionCallback;
53
54     private MediaBrowserCompat mMediaBrowser;
55
56     @Nullable
57     private MediaControllerCompat mMediaController;
58
59     public MediaBrowserHelper(Context context,
60                               Class<? extends MediaBrowserServiceCompat> serviceClass) {
61         mContext = context;
62         mMediaBrowserServiceClass = serviceClass;
63
64         mMediaBrowserConnectionCallback = new MediaBrowserConnectionCallback();
65         mMediaControllerCallback = new MediaControllerCallback();
66         mMediaBrowserSubscriptionCallback = new MediaBrowserSubscriptionCallback();
67     }
68
69     public void onStart() {
70         if (mMediaBrowser == null) {
71             mMediaBrowser =
72                     new MediaBrowserCompat(
73                             mContext,
74                             new ComponentName(mContext, mMediaBrowserServiceClass),
75                             mMediaBrowserConnectionCallback,
76                             null);
77             mMediaBrowser.connect();
78         }
79         Log.d(TAG, "onStart: Creating MediaBrowser, and connecting");
80     }
81
82     public void onStop() {
83         if (mMediaController != null) {
84             mMediaController.unregisterCallback(mMediaControllerCallback);
85             mMediaController = null;
86         }
87         if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
88             mMediaBrowser.disconnect();
89             mMediaBrowser = null;
90         }
91         resetState();
92         Log.d(TAG, "onStop: Releasing MediaController, Disconnecting from MediaBrowser");
93     }
94
95     /**
96      * Called after connecting with a {@link MediaBrowserServiceCompat}.
97      * <p>
98      * Override to perform processing after a connection is established.
99      *
100      * @param mediaController {@link MediaControllerCompat} associated with the connected
101      *                        MediaSession.
102      */
103     protected void onConnected(@NonNull MediaControllerCompat mediaController) {
104     }
105
106     /**
107      * Called after loading a browsable {@link MediaBrowserCompat.MediaItem}
108      *
109      * @param parentId The media ID of the parent item.
110      * @param children List (possibly empty) of child items.
111      */
112     protected void onChildrenLoaded(@NonNull String parentId,
113                                     @NonNull List<MediaBrowserCompat.MediaItem> children) {
114     }
115
116     /**
117      * Called when the {@link MediaBrowserServiceCompat} connection is lost.
118      */
119     protected void onDisconnected() {
120     }
121
122     @NonNull
123     protected final MediaControllerCompat getMediaController() {
124         if (mMediaController == null) {
125             throw new IllegalStateException("MediaController is null!");
126         }
127         return mMediaController;
128     }
129
130     /**
131      * The internal state of the app needs to revert to what it looks like when it started before
132      * any connections to the {@link AudiobookService} happens via the {@link MediaSessionCompat}.
133      */
134     private void resetState() {
135         performOnAllCallbacks(callback -> callback.onPlaybackStateChanged(null));
136         Log.d(TAG, "resetState: ");
137     }
138
139     public MediaControllerCompat.TransportControls getTransportControls() {
140         if (mMediaController == null) {
141             Log.d(TAG, "getTransportControls: MediaController is null!");
142             throw new IllegalStateException("MediaController is null!");
143         }
144         return mMediaController.getTransportControls();
145     }
146
147     public void registerCallback(Callback callback) {
148         if (callback != null) {
149             mCallbackList.add(callback);
150
151             // Update with the latest metadata/playback state.
152             if (mMediaController != null) {
153                 final MediaMetadataCompat metadata = mMediaController.getMetadata();
154                 if (metadata != null) {
155                     callback.onMetadataChanged(metadata);
156                 }
157
158                 final PlaybackStateCompat playbackState = mMediaController.getPlaybackState();
159                 if (playbackState != null) {
160                     callback.onPlaybackStateChanged(playbackState);
161                 }
162             }
163         }
164     }
165
166     private void performOnAllCallbacks(@NonNull CallbackCommand command) {
167         for (Callback callback : mCallbackList) {
168             if (callback != null) {
169                 command.perform(callback);
170             }
171         }
172     }
173
174     /**
175      * Helper for more easily performing operations on all listening clients.
176      */
177     private interface CallbackCommand {
178         void perform(@NonNull Callback callback);
179     }
180
181     // Receives callbacks from the MediaBrowser when it has successfully connected to the
182     // MediaBrowserService (AudiobookService).
183     private class MediaBrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
184
185         // Happens as a result of onStart().
186         @Override
187         public void onConnected() {
188             try {
189                 // Get a MediaController for the MediaSession.
190                 mMediaController =
191                         new MediaControllerCompat(mContext, mMediaBrowser.getSessionToken());
192                 mMediaController.registerCallback(mMediaControllerCallback);
193
194                 // Sync existing MediaSession state to the UI.
195                 mMediaControllerCallback.onMetadataChanged(mMediaController.getMetadata());
196                 mMediaControllerCallback.onPlaybackStateChanged(
197                         mMediaController.getPlaybackState());
198
199                 MediaBrowserHelper.this.onConnected(mMediaController);
200             } catch (RemoteException e) {
201                 Log.d(TAG, String.format("onConnected: Problem: %s", e.toString()));
202                 throw new RuntimeException(e);
203             }
204
205             mMediaBrowser.subscribe(mMediaBrowser.getRoot(), mMediaBrowserSubscriptionCallback);
206         }
207     }
208
209     // Receives callbacks from the MediaBrowser when the MediaBrowserService has loaded new media
210     // that is ready for playback.
211     public class MediaBrowserSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
212
213         @Override
214         public void onChildrenLoaded(@NonNull String parentId,
215                                      @NonNull List<MediaBrowserCompat.MediaItem> children) {
216             MediaBrowserHelper.this.onChildrenLoaded(parentId, children);
217         }
218     }
219
220     // Receives callbacks from the MediaController and updates the UI state,
221     // i.e.: Which is the current item, whether it's playing or paused, etc.
222     private class MediaControllerCallback extends MediaControllerCompat.Callback {
223
224         @Override
225         public void onMetadataChanged(final MediaMetadataCompat metadata) {
226             performOnAllCallbacks(callback -> callback.onMetadataChanged(metadata));
227         }
228
229         @Override
230         public void onPlaybackStateChanged(@Nullable final PlaybackStateCompat state) {
231             performOnAllCallbacks(callback -> callback.onPlaybackStateChanged(state));
232         }
233
234             @Override
235             public void onExtrasChanged(Bundle extras) {
236                 performOnAllCallbacks(callback -> callback.onExtrasChanged(extras));
237             }
238
239             // This might happen if the AudiobookService is killed while the Activity is in the
240         // foreground and onStart() has been called (but not onStop()).
241         @Override
242         public void onSessionDestroyed() {
243             resetState();
244             onPlaybackStateChanged(null);
245
246             MediaBrowserHelper.this.onDisconnected();
247         }
248     }
249 }