Skip to content

Most visited

Recently visited

navigation
MediaBrowserService / src / com.example.android.mediabrowserservice /

MediaNotificationManager.java

1
/*
2
 * Copyright (C) 2014 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.example.android.mediabrowserservice;
18
 
19
import android.app.Notification;
20
import android.app.NotificationManager;
21
import android.app.PendingIntent;
22
import android.content.BroadcastReceiver;
23
import android.content.Context;
24
import android.content.Intent;
25
import android.content.IntentFilter;
26
import android.graphics.Bitmap;
27
import android.graphics.BitmapFactory;
28
import android.graphics.Color;
29
import android.media.MediaDescription;
30
import android.media.MediaMetadata;
31
import android.media.session.MediaController;
32
import android.media.session.MediaSession;
33
import android.media.session.PlaybackState;
34
 
35
import com.example.android.mediabrowserservice.utils.LogHelper;
36
import com.example.android.mediabrowserservice.utils.ResourceHelper;
37
 
38
/**
39
 * Keeps track of a notification and updates it automatically for a given
40
 * MediaSession. Maintaining a visible notification (usually) guarantees that the music service
41
 * won't be killed during playback.
42
 */
43
public class MediaNotificationManager extends BroadcastReceiver {
44
    private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class);
45
 
46
    private static final int NOTIFICATION_ID = 412;
47
    private static final int REQUEST_CODE = 100;
48
 
49
    public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause";
50
    public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play";
51
    public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev";
52
    public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next";
53
 
54
    private final MusicService mService;
55
    private MediaSession.Token mSessionToken;
56
    private MediaController mController;
57
    private MediaController.TransportControls mTransportControls;
58
 
59
    private PlaybackState mPlaybackState;
60
    private MediaMetadata mMetadata;
61
 
62
    private NotificationManager mNotificationManager;
63
 
64
    private PendingIntent mPauseIntent;
65
    private PendingIntent mPlayIntent;
66
    private PendingIntent mPreviousIntent;
67
    private PendingIntent mNextIntent;
68
 
69
    private int mNotificationColor;
70
 
71
    private boolean mStarted = false;
72
 
73
    public MediaNotificationManager(MusicService service) {
74
        mService = service;
75
        updateSessionToken();
76
 
77
        mNotificationColor = ResourceHelper.getThemeColor(mService,
78
            android.R.attr.colorPrimary, Color.DKGRAY);
79
 
80
        mNotificationManager = (NotificationManager) mService
81
                .getSystemService(Context.NOTIFICATION_SERVICE);
82
 
83
        String pkg = mService.getPackageName();
84
        mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
85
                new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
86
        mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
87
                new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
88
        mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
89
                new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
90
        mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
91
                new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
92
 
93
        // Cancel all notifications to handle the case where the Service was killed and
94
        // restarted by the system.
95
        mNotificationManager.cancelAll();
96
    }
97
 
98
    /**
99
     * Posts the notification and starts tracking the session to keep it
100
     * updated. The notification will automatically be removed if the session is
101
     * destroyed before {@link #stopNotification} is called.
102
     */
103
    public void startNotification() {
104
        if (!mStarted) {
105
            mMetadata = mController.getMetadata();
106
            mPlaybackState = mController.getPlaybackState();
107
 
108
            // The notification must be updated after setting started to true
109
            Notification notification = createNotification();
110
            if (notification != null) {
111
                mController.registerCallback(mCb);
112
                IntentFilter filter = new IntentFilter();
113
                filter.addAction(ACTION_NEXT);
114
                filter.addAction(ACTION_PAUSE);
115
                filter.addAction(ACTION_PLAY);
116
                filter.addAction(ACTION_PREV);
117
                mService.registerReceiver(this, filter);
118
 
119
                mService.startForeground(NOTIFICATION_ID, notification);
120
                mStarted = true;
121
            }
122
        }
123
    }
124
 
125
    /**
126
     * Removes the notification and stops tracking the session. If the session
127
     * was destroyed this has no effect.
128
     */
129
    public void stopNotification() {
130
        if (mStarted) {
131
            mStarted = false;
132
            mController.unregisterCallback(mCb);
133
            try {
134
                mNotificationManager.cancel(NOTIFICATION_ID);
135
                mService.unregisterReceiver(this);
136
            } catch (IllegalArgumentException ex) {
137
                // ignore if the receiver is not registered.
138
            }
139
            mService.stopForeground(true);
140
        }
141
    }
142
 
143
    @Override
144
    public void onReceive(Context context, Intent intent) {
145
        final String action = intent.getAction();
146
        LogHelper.d(TAG, "Received intent with action " + action);
147
        switch (action) {
148
            case ACTION_PAUSE:
149
                mTransportControls.pause();
150
                break;
151
            case ACTION_PLAY:
152
                mTransportControls.play();
153
                break;
154
            case ACTION_NEXT:
155
                mTransportControls.skipToNext();
156
                break;
157
            case ACTION_PREV:
158
                mTransportControls.skipToPrevious();
159
                break;
160
            default:
161
                LogHelper.w(TAG, "Unknown intent ignored. Action=", action);
162
        }
163
    }
164
 
165
    /**
166
     * Update the state based on a change on the session token. Called either when
167
     * we are running for the first time or when the media session owner has destroyed the session
168
     * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()})
169
     */
170
    private void updateSessionToken() {
171
        MediaSession.Token freshToken = mService.getSessionToken();
172
        if (mSessionToken == null || !mSessionToken.equals(freshToken)) {
173
            if (mController != null) {
174
                mController.unregisterCallback(mCb);
175
            }
176
            mSessionToken = freshToken;
177
            mController = new MediaController(mService, mSessionToken);
178
            mTransportControls = mController.getTransportControls();
179
            if (mStarted) {
180
                mController.registerCallback(mCb);
181
            }
182
        }
183
    }
184
 
185
    private PendingIntent createContentIntent() {
186
        Intent openUI = new Intent(mService, MusicPlayerActivity.class);
187
        openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
188
        return PendingIntent.getActivity(mService, REQUEST_CODE, openUI,
189
                PendingIntent.FLAG_CANCEL_CURRENT);
190
    }
191
 
192
    private final MediaController.Callback mCb = new MediaController.Callback() {
193
        @Override
194
        public void onPlaybackStateChanged(PlaybackState state) {
195
            mPlaybackState = state;
196
            LogHelper.d(TAG, "Received new playback state", state);
197
            if (state != null && (state.getState() == PlaybackState.STATE_STOPPED ||
198
                    state.getState() == PlaybackState.STATE_NONE)) {
199
                stopNotification();
200
            } else {
201
                Notification notification = createNotification();
202
                if (notification != null) {
203
                    mNotificationManager.notify(NOTIFICATION_ID, notification);
204
                }
205
            }
206
        }
207
 
208
        @Override
209
        public void onMetadataChanged(MediaMetadata metadata) {
210
            mMetadata = metadata;
211
            LogHelper.d(TAG, "Received new metadata ", metadata);
212
            Notification notification = createNotification();
213
            if (notification != null) {
214
                mNotificationManager.notify(NOTIFICATION_ID, notification);
215
            }
216
        }
217
 
218
        @Override
219
        public void onSessionDestroyed() {
220
            super.onSessionDestroyed();
221
            LogHelper.d(TAG, "Session was destroyed, resetting to the new session token");
222
            updateSessionToken();
223
        }
224
    };
225
 
226
    private Notification createNotification() {
227
        LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
228
        if (mMetadata == null || mPlaybackState == null) {
229
            return null;
230
        }
231
 
232
        Notification.Builder notificationBuilder = new Notification.Builder(mService);
233
        int playPauseButtonPosition = 0;
234
 
235
        // If skip to previous action is enabled
236
        if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
237
            notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp,
238
                        mService.getString(R.string.label_previous), mPreviousIntent);
239
 
240
            // If there is a "skip to previous" button, the play/pause button will
241
            // be the second one. We need to keep track of it, because the MediaStyle notification
242
            // requires to specify the index of the buttons (actions) that should be visible
243
            // when in compact view.
244
            playPauseButtonPosition = 1;
245
        }
246
 
247
        addPlayPauseAction(notificationBuilder);
248
 
249
        // If skip to next action is enabled
250
        if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
251
            notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
252
                mService.getString(R.string.label_next), mNextIntent);
253
        }
254
 
255
        MediaDescription description = mMetadata.getDescription();
256
 
257
        String fetchArtUrl = null;
258
        Bitmap art = null;
259
        if (description.getIconUri() != null) {
260
            // This sample assumes the iconUri will be a valid URL formatted String, but
261
            // it can actually be any valid Android Uri formatted String.
262
            // async fetch the album art icon
263
            String artUrl = description.getIconUri().toString();
264
            art = AlbumArtCache.getInstance().getBigImage(artUrl);
265
            if (art == null) {
266
                fetchArtUrl = artUrl;
267
                // use a placeholder art while the remote art is being downloaded
268
                art = BitmapFactory.decodeResource(mService.getResources(),
269
                    R.drawable.ic_default_art);
270
            }
271
        }
272
 
273
        notificationBuilder
274
                .setStyle(new Notification.MediaStyle()
275
                    .setShowActionsInCompactView(
276
                        new int[]{playPauseButtonPosition})  // show only play/pause in compact view
277
                    .setMediaSession(mSessionToken))
278
                .setColor(mNotificationColor)
279
                .setSmallIcon(R.drawable.ic_notification)
280
                .setVisibility(Notification.VISIBILITY_PUBLIC)
281
                .setUsesChronometer(true)
282
                .setContentIntent(createContentIntent())
283
                .setContentTitle(description.getTitle())
284
                .setContentText(description.getSubtitle())
285
                .setLargeIcon(art);
286
 
287
        setNotificationPlaybackState(notificationBuilder);
288
        if (fetchArtUrl != null) {
289
            fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder);
290
        }
291
 
292
        return notificationBuilder.build();
293
    }
294
 
295
    private void addPlayPauseAction(Notification.Builder builder) {
296
        LogHelper.d(TAG, "updatePlayPauseAction");
297
        String label;
298
        int icon;
299
        PendingIntent intent;
300
        if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
301
            label = mService.getString(R.string.label_pause);
302
            icon = R.drawable.ic_pause_white_24dp;
303
            intent = mPauseIntent;
304
        } else {
305
            label = mService.getString(R.string.label_play);
306
            icon = R.drawable.ic_play_arrow_white_24dp;
307
            intent = mPlayIntent;
308
        }
309
        builder.addAction(new Notification.Action(icon, label, intent));
310
    }
311
 
312
    private void setNotificationPlaybackState(Notification.Builder builder) {
313
        LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState);
314
        if (