1/*
2 * Copyright 2018 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
17package androidx.media;
18
19import android.content.Context;
20import android.net.Uri;
21
22import androidx.annotation.NonNull;
23import androidx.annotation.Nullable;
24import androidx.core.util.Preconditions;
25
26import java.io.FileDescriptor;
27import java.net.CookieHandler;
28import java.net.CookieManager;
29import java.net.HttpCookie;
30import java.util.ArrayList;
31import java.util.HashMap;
32import java.util.List;
33import java.util.Map;
34
35/**
36 * Structure for data source descriptor. Used by {@link MediaItem2}.
37 * <p>
38 * Users should use {@link Builder} to change {@link DataSourceDesc}.
39 *
40 * @see MediaItem2
41 */
42public final class DataSourceDesc {
43    /* No data source has been set yet */
44    public static final int TYPE_NONE     = 0;
45    /* data source is type of MediaDataSource */
46    public static final int TYPE_CALLBACK = 1;
47    /* data source is type of FileDescriptor */
48    public static final int TYPE_FD       = 2;
49    /* data source is type of Uri */
50    public static final int TYPE_URI      = 3;
51
52    // intentionally less than long.MAX_VALUE.
53    // Declare this first to avoid 'illegal forward reference'.
54    private static final long LONG_MAX = 0x7ffffffffffffffL;
55
56    /**
57     * Used when a position is unknown.
58     *
59     * @see #getEndPosition()
60     */
61    public static final long POSITION_UNKNOWN = LONG_MAX;
62
63    /**
64     * Used when the length of file descriptor is unknown.
65     *
66     * @see #getFileDescriptorLength()
67     */
68    public static final long FD_LENGTH_UNKNOWN = LONG_MAX;
69
70    private int mType = TYPE_NONE;
71
72    private Media2DataSource mMedia2DataSource;
73
74    private FileDescriptor mFD;
75    private long mFDOffset = 0;
76    private long mFDLength = FD_LENGTH_UNKNOWN;
77
78    private Uri mUri;
79    private Map<String, String> mUriHeader;
80    private List<HttpCookie> mUriCookies;
81    private Context mUriContext;
82
83    private String mMediaId;
84    private long mStartPositionMs = 0;
85    private long mEndPositionMs = POSITION_UNKNOWN;
86
87    private DataSourceDesc() {
88    }
89
90    /**
91     * Return the media Id of data source.
92     * @return the media Id of data source
93     */
94    public @Nullable String getMediaId() {
95        return mMediaId;
96    }
97
98    /**
99     * Return the position in milliseconds at which the playback will start.
100     * @return the position in milliseconds at which the playback will start
101     */
102    public long getStartPosition() {
103        return mStartPositionMs;
104    }
105
106    /**
107     * Return the position in milliseconds at which the playback will end.
108     * {@link #POSITION_UNKNOWN} means ending at the end of source content.
109     * @return the position in milliseconds at which the playback will end
110     */
111    public long getEndPosition() {
112        return mEndPositionMs;
113    }
114
115    /**
116     * Return the type of data source.
117     * @return the type of data source
118     */
119    public int getType() {
120        return mType;
121    }
122
123    /**
124     * Return the Media2DataSource of this data source.
125     * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
126     * @return the Media2DataSource of this data source
127     */
128    public @Nullable Media2DataSource getMedia2DataSource() {
129        return mMedia2DataSource;
130    }
131
132    /**
133     * Return the FileDescriptor of this data source.
134     * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
135     * @return the FileDescriptor of this data source
136     */
137    public @Nullable FileDescriptor getFileDescriptor() {
138        return mFD;
139    }
140
141    /**
142     * Return the offset associated with the FileDescriptor of this data source.
143     * It's meaningful only when {@code getType} returns {@link #TYPE_FD} and it has
144     * been set by the {@link Builder}.
145     * @return the offset associated with the FileDescriptor of this data source
146     */
147    public long getFileDescriptorOffset() {
148        return mFDOffset;
149    }
150
151    /**
152     * Return the content length associated with the FileDescriptor of this data source.
153     * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
154     * {@link #FD_LENGTH_UNKNOWN} means same as the length of source content.
155     * @return the content length associated with the FileDescriptor of this data source
156     */
157    public long getFileDescriptorLength() {
158        return mFDLength;
159    }
160
161    /**
162     * Return the Uri of this data source.
163     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
164     * @return the Uri of this data source
165     */
166    public @Nullable Uri getUri() {
167        return mUri;
168    }
169
170    /**
171     * Return the Uri headers of this data source.
172     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
173     * @return the Uri headers of this data source
174     */
175    public @Nullable Map<String, String> getUriHeaders() {
176        if (mUriHeader == null) {
177            return null;
178        }
179        return new HashMap<String, String>(mUriHeader);
180    }
181
182    /**
183     * Return the Uri cookies of this data source.
184     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
185     * @return the Uri cookies of this data source
186     */
187    public @Nullable List<HttpCookie> getUriCookies() {
188        if (mUriCookies == null) {
189            return null;
190        }
191        return new ArrayList<HttpCookie>(mUriCookies);
192    }
193
194    /**
195     * Return the Context used for resolving the Uri of this data source.
196     * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
197     * @return the Context used for resolving the Uri of this data source
198     */
199    public @Nullable Context getUriContext() {
200        return mUriContext;
201    }
202
203    /**
204     * Builder class for {@link DataSourceDesc} objects.
205     */
206    public static class Builder {
207        private int mType = TYPE_NONE;
208
209        private Media2DataSource mMedia2DataSource;
210
211        private FileDescriptor mFD;
212        private long mFDOffset = 0;
213        private long mFDLength = FD_LENGTH_UNKNOWN;
214
215        private Uri mUri;
216        private Map<String, String> mUriHeader;
217        private List<HttpCookie> mUriCookies;
218        private Context mUriContext;
219
220        private String mMediaId;
221        private long mStartPositionMs = 0;
222        private long mEndPositionMs = POSITION_UNKNOWN;
223
224        /**
225         * Constructs a new Builder with the defaults.
226         */
227        public Builder() {
228        }
229
230        /**
231         * Constructs a new Builder from a given {@link DataSourceDesc} instance
232         * @param dsd the {@link DataSourceDesc} object whose data will be reused
233         * in the new Builder.
234         */
235        public Builder(@NonNull DataSourceDesc dsd) {
236            mType = dsd.mType;
237            mMedia2DataSource = dsd.mMedia2DataSource;
238            mFD = dsd.mFD;
239            mFDOffset = dsd.mFDOffset;
240            mFDLength = dsd.mFDLength;
241            mUri = dsd.mUri;
242            mUriHeader = dsd.mUriHeader;
243            mUriCookies = dsd.mUriCookies;
244            mUriContext = dsd.mUriContext;
245
246            mMediaId = dsd.mMediaId;
247            mStartPositionMs = dsd.mStartPositionMs;
248            mEndPositionMs = dsd.mEndPositionMs;
249        }
250
251        /**
252         * Combines all of the fields that have been set and return a new
253         * {@link DataSourceDesc} object. <code>IllegalStateException</code> will be
254         * thrown if there is conflict between fields.
255         *
256         * @return a new {@link DataSourceDesc} object
257         */
258        public @NonNull DataSourceDesc build() {
259            if (mType != TYPE_CALLBACK
260                    && mType != TYPE_FD
261                    && mType != TYPE_URI) {
262                throw new IllegalStateException("Illegal type: " + mType);
263            }
264            if (mStartPositionMs > mEndPositionMs) {
265                throw new IllegalStateException("Illegal start/end position: "
266                        + mStartPositionMs + " : " + mEndPositionMs);
267            }
268
269            DataSourceDesc dsd = new DataSourceDesc();
270            dsd.mType = mType;
271            dsd.mMedia2DataSource = mMedia2DataSource;
272            dsd.mFD = mFD;
273            dsd.mFDOffset = mFDOffset;
274            dsd.mFDLength = mFDLength;
275            dsd.mUri = mUri;
276            dsd.mUriHeader = mUriHeader;
277            dsd.mUriCookies = mUriCookies;
278            dsd.mUriContext = mUriContext;
279
280            dsd.mMediaId = mMediaId;
281            dsd.mStartPositionMs = mStartPositionMs;
282            dsd.mEndPositionMs = mEndPositionMs;
283
284            return dsd;
285        }
286
287        /**
288         * Sets the media Id of this data source.
289         *
290         * @param mediaId the media Id of this data source
291         * @return the same Builder instance.
292         */
293        public @NonNull Builder setMediaId(String mediaId) {
294            mMediaId = mediaId;
295            return this;
296        }
297
298        /**
299         * Sets the start position in milliseconds at which the playback will start.
300         * Any negative number is treated as 0.
301         *
302         * @param position the start position in milliseconds at which the playback will start
303         * @return the same Builder instance.
304         *
305         */
306        public @NonNull Builder setStartPosition(long position) {
307            if (position < 0) {
308                position = 0;
309            }
310            mStartPositionMs = position;
311            return this;
312        }
313
314        /**
315         * Sets the end position in milliseconds at which the playback will end.
316         * Any negative number is treated as maximum length of the data source.
317         *
318         * @param position the end position in milliseconds at which the playback will end
319         * @return the same Builder instance.
320         */
321        public @NonNull Builder setEndPosition(long position) {
322            if (position < 0) {
323                position = POSITION_UNKNOWN;
324            }
325            mEndPositionMs = position;
326            return this;
327        }
328
329        /**
330         * Sets the data source (Media2DataSource) to use.
331         *
332         * @param m2ds the Media2DataSource for the media you want to play
333         * @return the same Builder instance.
334         * @throws NullPointerException if m2ds is null.
335         */
336        public @NonNull Builder setDataSource(@NonNull Media2DataSource m2ds) {
337            Preconditions.checkNotNull(m2ds);
338            resetDataSource();
339            mType = TYPE_CALLBACK;
340            mMedia2DataSource = m2ds;
341            return this;
342        }
343
344        /**
345         * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
346         * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
347         * to close the file descriptor after the source has been used.
348         *
349         * @param fd the FileDescriptor for the file you want to play
350         * @return the same Builder instance.
351         * @throws NullPointerException if fd is null.
352         */
353        public @NonNull Builder setDataSource(@NonNull FileDescriptor fd) {
354            Preconditions.checkNotNull(fd);
355            resetDataSource();
356            mType = TYPE_FD;
357            mFD = fd;
358            return this;
359        }
360
361        /**
362         * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
363         * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
364         * to close the file descriptor after the source has been used.
365         *
366         * Any negative number for offset is treated as 0.
367         * Any negative number for length is treated as maximum length of the data source.
368         *
369         * @param fd the FileDescriptor for the file you want to play
370         * @param offset the offset into the file where the data to be played starts, in bytes
371         * @param length the length in bytes of the data to be played
372         * @return the same Builder instance.
373         * @throws NullPointerException if fd is null.
374         */
375        public @NonNull Builder setDataSource(@NonNull FileDescriptor fd, long offset,
376                long length) {
377            Preconditions.checkNotNull(fd);
378            if (offset < 0) {
379                offset = 0;
380            }
381            if (length < 0) {
382                length = FD_LENGTH_UNKNOWN;
383            }
384            resetDataSource();
385            mType = TYPE_FD;
386            mFD = fd;
387            mFDOffset = offset;
388            mFDLength = length;
389            return this;
390        }
391
392        /**
393         * Sets the data source as a content Uri.
394         *
395         * @param context the Context to use when resolving the Uri
396         * @param uri the Content URI of the data you want to play
397         * @return the same Builder instance.
398         * @throws NullPointerException if context or uri is null.
399         */
400        public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
401            Preconditions.checkNotNull(context, "context cannot be null");
402            Preconditions.checkNotNull(uri, "uri cannot be null");
403            resetDataSource();
404            mType = TYPE_URI;
405            mUri = uri;
406            mUriContext = context;
407            return this;
408        }
409
410        /**
411         * Sets the data source as a content Uri.
412         *
413         * To provide cookies for the subsequent HTTP requests, you can install your own default
414         * cookie handler and use other variants of setDataSource APIs instead.
415         *
416         *  <p><strong>Note</strong> that the cross domain redirection is allowed by default,
417         * but that can be changed with key/value pairs through the headers parameter with
418         * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
419         * disallow or allow cross domain redirection.
420         *
421         * @param context the Context to use when resolving the Uri
422         * @param uri the Content URI of the data you want to play
423         * @param headers the headers to be sent together with the request for the data
424         *                The headers must not include cookies. Instead, use the cookies param.
425         * @param cookies the cookies to be sent together with the request
426         * @return the same Builder instance.
427         * @throws NullPointerException if context or uri is null.
428         * @throws IllegalArgumentException if the cookie handler is not of CookieManager type
429         *                                  when cookies are provided.
430         */
431        public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
432                @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
433            Preconditions.checkNotNull(context, "context cannot be null");
434            Preconditions.checkNotNull(uri);
435            if (cookies != null) {
436                CookieHandler cookieHandler = CookieHandler.getDefault();
437                if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
438                    throw new IllegalArgumentException(
439                            "The cookie handler has to be of CookieManager type "
440                                    + "when cookies are provided.");
441                }
442            }
443
444            resetDataSource();
445            mType = TYPE_URI;
446            mUri = uri;
447            if (headers != null) {
448                mUriHeader = new HashMap<String, String>(headers);
449            }
450            if (cookies != null) {
451                mUriCookies = new ArrayList<HttpCookie>(cookies);
452            }
453            mUriContext = context;
454            return this;
455        }
456
457        private void resetDataSource() {
458            mType = TYPE_NONE;
459            mMedia2DataSource = null;
460            mFD = null;
461            mFDOffset = 0;
462            mFDLength = FD_LENGTH_UNKNOWN;
463            mUri = null;
464            mUriHeader = null;
465            mUriCookies = null;
466            mUriContext = null;
467        }
468    }
469}