1/*
2 * Copyright (C) 2009 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 android.media;
18
19import android.graphics.Bitmap;
20import android.os.Parcel;
21import android.util.Log;
22
23import java.util.Calendar;
24import java.util.Collections;
25import java.util.Date;
26import java.util.HashMap;
27import java.util.Set;
28import java.util.TimeZone;
29
30
31/**
32   Class to hold the media's metadata.  Metadata are used
33   for human consumption and can be embedded in the media (e.g
34   shoutcast) or available from an external source. The source can be
35   local (e.g thumbnail stored in the DB) or remote (e.g caption
36   server).
37
38   Metadata is like a Bundle. It is sparse and each key can occur at
39   most once. The key is an integer and the value is the actual metadata.
40
41   The caller is expected to know the type of the metadata and call
42   the right get* method to fetch its value.
43
44   // FIXME: unhide.
45   {@hide}
46 */
47public class Metadata
48{
49    // The metadata are keyed using integers rather than more heavy
50    // weight strings. We considered using Bundle to ship the metadata
51    // between the native layer and the java layer but dropped that
52    // option since keeping in sync a native implementation of Bundle
53    // and the java one would be too burdensome. Besides Bundle uses
54    // String for its keys.
55    // The key range [0 8192) is reserved for the system.
56    //
57    // We manually serialize the data in Parcels. For large memory
58    // blob (bitmaps, raw pictures) we use MemoryFile which allow the
59    // client to make the data purge-able once it is done with it.
60    //
61
62    public static final int ANY = 0;  // Never used for metadata returned, only for filtering.
63                                      // Keep in sync with kAny in MediaPlayerService.cpp
64
65    // TODO: Should we use numbers compatible with the metadata retriever?
66    public static final int TITLE = 1;           // String
67    public static final int COMMENT = 2;         // String
68    public static final int COPYRIGHT = 3;       // String
69    public static final int ALBUM = 4;           // String
70    public static final int ARTIST = 5;          // String
71    public static final int AUTHOR = 6;          // String
72    public static final int COMPOSER = 7;        // String
73    public static final int GENRE = 8;           // String
74    public static final int DATE = 9;            // Date
75    public static final int DURATION = 10;       // Integer(millisec)
76    public static final int CD_TRACK_NUM = 11;   // Integer 1-based
77    public static final int CD_TRACK_MAX = 12;   // Integer
78    public static final int RATING = 13;         // String
79    public static final int ALBUM_ART = 14;      // byte[]
80    public static final int VIDEO_FRAME = 15;    // Bitmap
81    public static final int CAPTION = 16;        // TimedText
82
83    public static final int BIT_RATE = 17;       // Integer, Aggregate rate of
84                                                 // all the streams in bps.
85
86    public static final int AUDIO_BIT_RATE = 18; // Integer, bps
87    public static final int VIDEO_BIT_RATE = 19; // Integer, bps
88    public static final int AUDIO_SAMPLE_RATE = 20; // Integer, Hz
89    public static final int VIDEO_FRAME_RATE = 21;  // Integer, Hz
90
91    // See RFC2046 and RFC4281.
92    public static final int MIME_TYPE = 22;      // String
93    public static final int AUDIO_CODEC = 23;    // String
94    public static final int VIDEO_CODEC = 24;    // String
95
96    public static final int VIDEO_HEIGHT = 25;   // Integer
97    public static final int VIDEO_WIDTH = 26;    // Integer
98    public static final int NUM_TRACKS = 27;     // Integer
99    public static final int DRM_CRIPPLED = 28;   // Boolean
100
101    // Playback capabilities.
102    public static final int PAUSE_AVAILABLE = 29;         // Boolean
103    public static final int SEEK_BACKWARD_AVAILABLE = 30; // Boolean
104    public static final int SEEK_FORWARD_AVAILABLE = 31;  // Boolean
105
106    private static final int LAST_SYSTEM = 31;
107    private static final int FIRST_CUSTOM = 8192;
108
109    // Shorthands to set the MediaPlayer's metadata filter.
110    public static final Set<Integer> MATCH_NONE = Collections.EMPTY_SET;
111    public static final Set<Integer> MATCH_ALL = Collections.singleton(ANY);
112
113    public static final int STRING_VAL     = 1;
114    public static final int INTEGER_VAL    = 2;
115    public static final int BOOLEAN_VAL    = 3;
116    public static final int LONG_VAL       = 4;
117    public static final int DOUBLE_VAL     = 5;
118    public static final int TIMED_TEXT_VAL = 6;
119    public static final int DATE_VAL       = 7;
120    public static final int BYTE_ARRAY_VAL = 8;
121    // FIXME: misses a type for shared heap is missing (MemoryFile).
122    // FIXME: misses a type for bitmaps.
123    private static final int LAST_TYPE = 8;
124
125    private static final String TAG = "media.Metadata";
126    private static final int kInt32Size = 4;
127    private static final int kMetaHeaderSize = 2 * kInt32Size; //  size + marker
128    private static final int kRecordHeaderSize = 3 * kInt32Size; // size + id + type
129
130    private static final int kMetaMarker = 0x4d455441;  // 'M' 'E' 'T' 'A'
131
132    // After a successful parsing, set the parcel with the serialized metadata.
133    private Parcel mParcel;
134
135    // Map to associate a Metadata key (e.g TITLE) with the offset of
136    // the record's payload in the parcel.
137    // Used to look up if a key was present too.
138    // Key: Metadata ID
139    // Value: Offset of the metadata type field in the record.
140    private final HashMap<Integer, Integer> mKeyToPosMap =
141            new HashMap<Integer, Integer>();
142
143    /**
144     * Helper class to hold a triple (time, duration, text). Can be used to
145     * implement caption.
146     */
147    public class TimedText {
148        private Date mTime;
149        private int mDuration;  // millisec
150        private String mText;
151
152        public TimedText(Date time, int duration, String text) {
153            mTime = time;
154            mDuration = duration;
155            mText = text;
156        }
157
158        public String toString() {
159            StringBuilder res = new StringBuilder(80);
160            res.append(mTime).append("-").append(mDuration)
161                    .append(":").append(mText);
162            return res.toString();
163        }
164    }
165
166    public Metadata() { }
167
168    /**
169     * Go over all the records, collecting metadata keys and records'
170     * type field offset in the Parcel. These are stored in
171     * mKeyToPosMap for latter retrieval.
172     * Format of a metadata record:
173     <pre>
174                         1                   2                   3
175      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
176      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
177      |                     record size                               |
178      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
179      |                     metadata key                              |  // TITLE
180      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
181      |                     metadata type                             |  // STRING_VAL
182      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
183      |                                                               |
184      |                .... metadata payload ....                     |
185      |                                                               |
186      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
187     </pre>
188     * @param parcel With the serialized records.
189     * @param bytesLeft How many bytes in the parcel should be processed.
190     * @return false if an error occurred during parsing.
191     */
192    private boolean scanAllRecords(Parcel parcel, int bytesLeft) {
193        int recCount = 0;
194        boolean error = false;
195
196        mKeyToPosMap.clear();
197        while (bytesLeft > kRecordHeaderSize) {
198            final int start = parcel.dataPosition();
199            // Check the size.
200            final int size = parcel.readInt();
201
202            if (size <= kRecordHeaderSize) {  // at least 1 byte should be present.
203                Log.e(TAG, "Record is too short");
204                error = true;
205                break;
206            }
207
208            // Check the metadata key.
209            final int metadataId = parcel.readInt();
210            if (!checkMetadataId(metadataId)) {
211                error = true;
212                break;
213            }
214
215            // Store the record offset which points to the type
216            // field so we can later on read/unmarshall the record
217            // payload.
218            if (mKeyToPosMap.containsKey(metadataId)) {
219                Log.e(TAG, "Duplicate metadata ID found");
220                error = true;
221                break;
222            }
223
224            mKeyToPosMap.put(metadataId, parcel.dataPosition());
225
226            // Check the metadata type.
227            final int metadataType = parcel.readInt();
228            if (metadataType <= 0 || metadataType > LAST_TYPE) {
229                Log.e(TAG, "Invalid metadata type " + metadataType);
230                error = true;
231                break;
232            }
233
234            // Skip to the next one.
235            parcel.setDataPosition(start + size);
236            bytesLeft -= size;
237            ++recCount;
238        }
239
240        if (0 != bytesLeft || error) {
241            Log.e(TAG, "Ran out of data or error on record " + recCount);
242            mKeyToPosMap.clear();
243            return false;
244        } else {
245            return true;
246        }
247    }
248
249    /**
250     * Check a parcel containing metadata is well formed. The header
251     * is checked as well as the individual records format. However, the
252     * data inside the record is not checked because we do lazy access
253     * (we check/unmarshall only data the user asks for.)
254     *
255     * Format of a metadata parcel:
256     <pre>
257                         1                   2                   3
258      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
259      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
260      |                     metadata total size                       |
261      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
262      |     'M'       |     'E'       |     'T'       |     'A'       |
263      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
264      |                                                               |
265      |                .... metadata records ....                     |
266      |                                                               |
267      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
268     </pre>
269     *
270     * @param parcel With the serialized data. Metadata keeps a
271     *               reference on it to access it later on. The caller
272     *               should not modify the parcel after this call (and
273     *               not call recycle on it.)
274     * @return false if an error occurred.
275     */
276    public boolean parse(Parcel parcel) {
277        if (parcel.dataAvail() < kMetaHeaderSize) {
278            Log.e(TAG, "Not enough data " + parcel.dataAvail());
279            return false;
280        }
281
282        final int pin = parcel.dataPosition();  // to roll back in case of errors.
283        final int size = parcel.readInt();
284
285        // The extra kInt32Size below is to account for the int32 'size' just read.
286        if (parcel.dataAvail() + kInt32Size < size || size < kMetaHeaderSize) {
287            Log.e(TAG, "Bad size " + size + " avail " + parcel.dataAvail() + " position " + pin);
288            parcel.setDataPosition(pin);
289            return false;
290        }
291
292        // Checks if the 'M' 'E' 'T' 'A' marker is present.
293        final int kShouldBeMetaMarker = parcel.readInt();
294        if (kShouldBeMetaMarker != kMetaMarker ) {
295            Log.e(TAG, "Marker missing " + Integer.toHexString(kShouldBeMetaMarker));
296            parcel.setDataPosition(pin);
297            return false;
298        }
299
300        // Scan the records to collect metadata ids and offsets.
301        if (!scanAllRecords(parcel, size - kMetaHeaderSize)) {
302            parcel.setDataPosition(pin);
303            return false;
304        }
305        mParcel = parcel;
306        return true;
307    }
308
309    /**
310     * @return The set of metadata ID found.
311     */
312    public Set<Integer> keySet() {
313        return mKeyToPosMap.keySet();
314    }
315
316    /**
317     * @return true if a value is present for the given key.
318     */
319    public boolean has(final int metadataId) {
320        if (!checkMetadataId(metadataId)) {
321            throw new IllegalArgumentException("Invalid key: " + metadataId);
322        }
323        return mKeyToPosMap.containsKey(metadataId);
324    }
325
326    // Accessors.
327    // Caller must make sure the key is present using the {@code has}
328    // method otherwise a RuntimeException will occur.
329
330    public String getString(final int key) {
331        checkType(key, STRING_VAL);
332        return mParcel.readString();
333    }
334
335    public int getInt(final int key) {
336        checkType(key, INTEGER_VAL);
337        return mParcel.readInt();
338    }
339
340    public boolean getBoolean(final int key) {
341        checkType(key, BOOLEAN_VAL);
342        return mParcel.readInt() == 1;
343    }
344
345    public long getLong(final int key) {
346        checkType(key, LONG_VAL);
347        return mParcel.readLong();
348    }
349
350    public double getDouble(final int key) {
351        checkType(key, DOUBLE_VAL);
352        return mParcel.readDouble();
353    }
354
355    public byte[] getByteArray(final int key) {
356        checkType(key, BYTE_ARRAY_VAL);
357        return mParcel.createByteArray();
358    }
359
360    public Date getDate(final int key) {
361        checkType(key, DATE_VAL);
362        final long timeSinceEpoch = mParcel.readLong();
363        final String timeZone = mParcel.readString();
364
365        if (timeZone.length() == 0) {
366            return new Date(timeSinceEpoch);
367        } else {
368            TimeZone tz = TimeZone.getTimeZone(timeZone);
369            Calendar cal = Calendar.getInstance(tz);
370
371            cal.setTimeInMillis(timeSinceEpoch);
372            return cal.getTime();
373        }
374    }
375
376    public TimedText getTimedText(final int key) {
377        checkType(key, TIMED_TEXT_VAL);
378        final Date startTime = new Date(mParcel.readLong());  // epoch
379        final int duration = mParcel.readInt();  // millisec
380
381        return new TimedText(startTime,
382                             duration,
383                             mParcel.readString());
384    }
385
386    // @return the last available system metadata id. Ids are
387    // 1-indexed.
388    public static int lastSytemId() { return LAST_SYSTEM; }
389
390    // @return the first available cutom metadata id.
391    public static int firstCustomId() { return FIRST_CUSTOM; }
392
393    // @return the last value of known type. Types are 1-indexed.
394    public static int lastType() { return LAST_TYPE; }
395
396    // Check val is either a system id or a custom one.
397    // @param val Metadata key to test.
398    // @return true if it is in a valid range.
399    private boolean checkMetadataId(final int val) {
400        if (val <= ANY || (LAST_SYSTEM < val && val < FIRST_CUSTOM)) {
401            Log.e(TAG, "Invalid metadata ID " + val);
402            return false;
403        }
404        return true;
405    }
406
407    // Check the type of the data match what is expected.
408    private void checkType(final int key, final int expectedType) {
409        final int pos = mKeyToPosMap.get(key);
410
411        mParcel.setDataPosition(pos);
412
413        final int type = mParcel.readInt();
414        if (type != expectedType) {
415            throw new IllegalStateException("Wrong type " + expectedType + " but got " + type);
416        }
417    }
418}
419