1 /*
2  * Copyright (C) 2013 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 androidx.mediarouter.media;
18 
19 import android.app.PendingIntent;
20 import android.os.Bundle;
21 import android.os.SystemClock;
22 
23 import androidx.core.util.TimeUtils;
24 
25 /**
26  * Describes the playback status of a media item.
27  * <p>
28  * This class is part of the remote playback protocol described by the
29  * {@link MediaControlIntent MediaControlIntent} class.
30  * </p><p>
31  * As a media item is played, it transitions through a sequence of states including:
32  * {@link #PLAYBACK_STATE_PENDING pending}, {@link #PLAYBACK_STATE_BUFFERING buffering},
33  * {@link #PLAYBACK_STATE_PLAYING playing}, {@link #PLAYBACK_STATE_PAUSED paused},
34  * {@link #PLAYBACK_STATE_FINISHED finished}, {@link #PLAYBACK_STATE_CANCELED canceled},
35  * {@link #PLAYBACK_STATE_INVALIDATED invalidated}, and
36  * {@link #PLAYBACK_STATE_ERROR error}.  Refer to the documentation of each state
37  * for an explanation of its meaning.
38  * </p><p>
39  * While the item is playing, the playback status may also include progress information
40  * about the {@link #getContentPosition content position} and
41  * {@link #getContentDuration content duration} although not all route destinations
42  * will report it.
43  * </p><p>
44  * To monitor playback status, the application should supply a {@link PendingIntent} to use as the
45  * {@link MediaControlIntent#EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receiver}
46  * for a given {@link MediaControlIntent#ACTION_PLAY playback request}.  Note that
47  * the status update receiver will only be invoked for major status changes such as a
48  * transition from playing to finished.
49  * </p><p class="note">
50  * The status update receiver will not be invoked for minor progress updates such as
51  * changes to playback position or duration.  If the application wants to monitor
52  * playback progress, then it must use the
53  * {@link MediaControlIntent#ACTION_GET_STATUS get status request} to poll for changes
54  * periodically and estimate the playback position while playing.  Note that there may
55  * be a significant power impact to polling so the application is advised only
56  * to poll when the screen is on and never more than about once every 5 seconds or so.
57  * </p><p>
58  * This object is immutable once created using a {@link Builder} instance.
59  * </p>
60  */
61 public final class MediaItemStatus {
62     static final String KEY_TIMESTAMP = "timestamp";
63     static final String KEY_PLAYBACK_STATE = "playbackState";
64     static final String KEY_CONTENT_POSITION = "contentPosition";
65     static final String KEY_CONTENT_DURATION = "contentDuration";
66     static final String KEY_EXTRAS = "extras";
67 
68     final Bundle mBundle;
69 
70     /**
71      * Playback state: Pending.
72      * <p>
73      * Indicates that the media item has not yet started playback but will be played eventually.
74      * </p>
75      */
76     public static final int PLAYBACK_STATE_PENDING = 0;
77 
78     /**
79      * Playback state: Playing.
80      * <p>
81      * Indicates that the media item is currently playing.
82      * </p>
83      */
84     public static final int PLAYBACK_STATE_PLAYING = 1;
85 
86     /**
87      * Playback state: Paused.
88      * <p>
89      * Indicates that playback of the media item has been paused.  Playback can be
90      * resumed using the {@link MediaControlIntent#ACTION_RESUME resume} action.
91      * </p>
92      */
93     public static final int PLAYBACK_STATE_PAUSED = 2;
94 
95     /**
96      * Playback state: Buffering or seeking to a new position.
97      * <p>
98      * Indicates that the media item has been temporarily interrupted
99      * to fetch more content.  Playback will continue automatically
100      * when enough content has been buffered.
101      * </p>
102      */
103     public static final int PLAYBACK_STATE_BUFFERING = 3;
104 
105     /**
106      * Playback state: Finished.
107      * <p>
108      * Indicates that the media item played to the end of the content and finished normally.
109      * </p><p>
110      * A finished media item cannot be resumed.  To play the content again, the application
111      * must send a new {@link MediaControlIntent#ACTION_PLAY play} or
112      * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
113      * </p>
114      */
115     public static final int PLAYBACK_STATE_FINISHED = 4;
116 
117     /**
118      * Playback state: Canceled.
119      * <p>
120      * Indicates that the media item was explicitly removed from the queue by the
121      * application.  Items may be canceled and removed from the queue using
122      * the {@link MediaControlIntent#ACTION_REMOVE remove} or
123      * {@link MediaControlIntent#ACTION_STOP stop} action or by issuing
124      * another {@link MediaControlIntent#ACTION_PLAY play} action that has the
125      * side-effect of clearing the queue.
126      * </p><p>
127      * A canceled media item cannot be resumed.  To play the content again, the
128      * application must send a new {@link MediaControlIntent#ACTION_PLAY play} or
129      * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
130      * </p>
131      */
132     public static final int PLAYBACK_STATE_CANCELED = 5;
133 
134     /**
135      * Playback state: Invalidated.
136      * <p>
137      * Indicates that the media item was invalidated permanently and involuntarily.
138      * This state is used to indicate that the media item was invalidated and removed
139      * from the queue because the session to which it belongs was invalidated
140      * (typically by another application taking control of the route).
141      * </p><p>
142      * When invalidation occurs, the application should generally wait for the user
143      * to perform an explicit action, such as clicking on a play button in the UI,
144      * before creating a new media session to avoid unnecessarily interrupting
145      * another application that may have just started using the route.
146      * </p><p>
147      * An invalidated media item cannot be resumed.  To play the content again, the application
148      * must send a new {@link MediaControlIntent#ACTION_PLAY play} or
149      * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
150      * </p>
151      */
152     public static final int PLAYBACK_STATE_INVALIDATED = 6;
153 
154     /**
155      * Playback state: Playback halted or aborted due to an error.
156      * <p>
157      * Examples of errors are no network connectivity when attempting to retrieve content
158      * from a server, or expired user credentials when trying to play subscription-based
159      * content.
160      * </p><p>
161      * A media item in the error state cannot be resumed.  To play the content again,
162      * the application must send a new {@link MediaControlIntent#ACTION_PLAY play} or
163      * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
164      * </p>
165      */
166     public static final int PLAYBACK_STATE_ERROR = 7;
167 
168     /**
169      * Integer extra: HTTP status code.
170      * <p>
171      * Specifies the HTTP status code that was encountered when the content
172      * was requested after all redirects were followed.  This key only needs to
173      * specified when the content uri uses the HTTP or HTTPS scheme and an error
174      * occurred.  This key may be omitted if the content was able to be played
175      * successfully; there is no need to report a 200 (OK) status code.
176      * </p><p>
177      * The value is an integer HTTP status code, such as 401 (Unauthorized),
178      * 404 (Not Found), or 500 (Server Error), or 0 if none.
179      * </p>
180      */
181     public static final String EXTRA_HTTP_STATUS_CODE =
182             "android.media.status.extra.HTTP_STATUS_CODE";
183 
184     /**
185      * Bundle extra: HTTP response headers.
186      * <p>
187      * Specifies the HTTP response headers that were returned when the content was
188      * requested from the network.  The headers may include additional information
189      * about the content or any errors conditions that were encountered while
190      * trying to fetch the content.
191      * </p><p>
192      * The value is a {@link android.os.Bundle} of string based key-value pairs
193      * that describe the HTTP response headers.
194      * </p>
195      */
196     public static final String EXTRA_HTTP_RESPONSE_HEADERS =
197             "android.media.status.extra.HTTP_RESPONSE_HEADERS";
198 
MediaItemStatus(Bundle bundle)199     MediaItemStatus(Bundle bundle) {
200         mBundle = bundle;
201     }
202 
203     /**
204      * Gets the timestamp associated with the status information in
205      * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
206      *
207      * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
208      */
getTimestamp()209     public long getTimestamp() {
210         return mBundle.getLong(KEY_TIMESTAMP);
211     }
212 
213     /**
214      * Gets the playback state of the media item.
215      *
216      * @return The playback state.  One of {@link #PLAYBACK_STATE_PENDING},
217      * {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED},
218      * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_FINISHED},
219      * {@link #PLAYBACK_STATE_CANCELED}, {@link #PLAYBACK_STATE_INVALIDATED},
220      * or {@link #PLAYBACK_STATE_ERROR}.
221      */
getPlaybackState()222     public int getPlaybackState() {
223         return mBundle.getInt(KEY_PLAYBACK_STATE, PLAYBACK_STATE_ERROR);
224     }
225 
226     /**
227      * Gets the content playback position as a long integer number of milliseconds
228      * from the beginning of the content.
229      *
230      * @return The content playback position in milliseconds, or -1 if unknown.
231      */
getContentPosition()232     public long getContentPosition() {
233         return mBundle.getLong(KEY_CONTENT_POSITION, -1);
234     }
235 
236     /**
237      * Gets the total duration of the content to be played as a long integer number of
238      * milliseconds.
239      *
240      * @return The content duration in milliseconds, or -1 if unknown.
241      */
getContentDuration()242     public long getContentDuration() {
243         return mBundle.getLong(KEY_CONTENT_DURATION, -1);
244     }
245 
246     /**
247      * Gets a bundle of extras for this status object.
248      * The extras will be ignored by the media router but they may be used
249      * by applications.
250      */
getExtras()251     public Bundle getExtras() {
252         return mBundle.getBundle(KEY_EXTRAS);
253     }
254 
255     @Override
toString()256     public String toString() {
257         StringBuilder result = new StringBuilder();
258         result.append("MediaItemStatus{ ");
259         result.append("timestamp=");
260         TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
261         result.append(" ms ago");
262         result.append(", playbackState=").append(playbackStateToString(getPlaybackState()));
263         result.append(", contentPosition=").append(getContentPosition());
264         result.append(", contentDuration=").append(getContentDuration());
265         result.append(", extras=").append(getExtras());
266         result.append(" }");
267         return result.toString();
268     }
269 
playbackStateToString(int playbackState)270     private static String playbackStateToString(int playbackState) {
271         switch (playbackState) {
272             case PLAYBACK_STATE_PENDING:
273                 return "pending";
274             case PLAYBACK_STATE_BUFFERING:
275                 return "buffering";
276             case PLAYBACK_STATE_PLAYING:
277                 return "playing";
278             case PLAYBACK_STATE_PAUSED:
279                 return "paused";
280             case PLAYBACK_STATE_FINISHED:
281                 return "finished";
282             case PLAYBACK_STATE_CANCELED:
283                 return "canceled";
284             case PLAYBACK_STATE_INVALIDATED:
285                 return "invalidated";
286             case PLAYBACK_STATE_ERROR:
287                 return "error";
288         }
289         return Integer.toString(playbackState);
290     }
291 
292     /**
293      * Converts this object to a bundle for serialization.
294      *
295      * @return The contents of the object represented as a bundle.
296      */
asBundle()297     public Bundle asBundle() {
298         return mBundle;
299     }
300 
301     /**
302      * Creates an instance from a bundle.
303      *
304      * @param bundle The bundle, or null if none.
305      * @return The new instance, or null if the bundle was null.
306      */
fromBundle(Bundle bundle)307     public static MediaItemStatus fromBundle(Bundle bundle) {
308         return bundle != null ? new MediaItemStatus(bundle) : null;
309     }
310 
311     /**
312      * Builder for {@link MediaItemStatus media item status objects}.
313      */
314     public static final class Builder {
315         private final Bundle mBundle;
316 
317         /**
318          * Creates a media item status builder using the current time as the
319          * reference timestamp.
320          *
321          * @param playbackState The item playback state.
322          */
Builder(int playbackState)323         public Builder(int playbackState) {
324             mBundle = new Bundle();
325             setTimestamp(SystemClock.elapsedRealtime());
326             setPlaybackState(playbackState);
327         }
328 
329         /**
330          * Creates a media item status builder whose initial contents are
331          * copied from an existing status.
332          */
Builder(MediaItemStatus status)333         public Builder(MediaItemStatus status) {
334             if (status == null) {
335                 throw new IllegalArgumentException("status must not be null");
336             }
337 
338             mBundle = new Bundle(status.mBundle);
339         }
340 
341         /**
342          * Sets the timestamp associated with the status information in
343          * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
344          */
setTimestamp(long elapsedRealtimeTimestamp)345         public Builder setTimestamp(long elapsedRealtimeTimestamp) {
346             mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
347             return this;
348         }
349 
350         /**
351          * Sets the playback state of the media item.
352          */
setPlaybackState(int playbackState)353         public Builder setPlaybackState(int playbackState) {
354             mBundle.putInt(KEY_PLAYBACK_STATE, playbackState);
355             return this;
356         }
357 
358         /**
359          * Sets the content playback position as a long integer number of milliseconds
360          * from the beginning of the content.
361          */
setContentPosition(long positionMilliseconds)362         public Builder setContentPosition(long positionMilliseconds) {
363             mBundle.putLong(KEY_CONTENT_POSITION, positionMilliseconds);
364             return this;
365         }
366 
367         /**
368          * Sets the total duration of the content to be played as a long integer number
369          * of milliseconds.
370          */
setContentDuration(long durationMilliseconds)371         public Builder setContentDuration(long durationMilliseconds) {
372             mBundle.putLong(KEY_CONTENT_DURATION, durationMilliseconds);
373             return this;
374         }
375 
376         /**
377          * Sets a bundle of extras for this status object.
378          * The extras will be ignored by the media router but they may be used
379          * by applications.
380          */
setExtras(Bundle extras)381         public Builder setExtras(Bundle extras) {
382             mBundle.putBundle(KEY_EXTRAS, extras);
383             return this;
384         }
385 
386         /**
387          * Builds the {@link MediaItemStatus media item status object}.
388          */
build()389         public MediaItemStatus build() {
390             return new MediaItemStatus(mBundle);
391         }
392     }
393 }
394