| /* | |
| * Copyright (C) 2009 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package android.media; | |
| import android.compat.annotation.UnsupportedAppUsage; | |
| import android.os.Parcel; | |
| import android.util.Log; | |
| import android.util.MathUtils; | |
| import java.util.Calendar; | |
| import java.util.Collections; | |
| import java.util.Date; | |
| import java.util.HashMap; | |
| import java.util.Set; | |
| import java.util.TimeZone; | |
| /** | |
| Class to hold the media's metadata. Metadata are used | |
| for human consumption and can be embedded in the media (e.g | |
| shoutcast) or available from an external source. The source can be | |
| local (e.g thumbnail stored in the DB) or remote. | |
| Metadata is like a Bundle. It is sparse and each key can occur at | |
| most once. The key is an integer and the value is the actual metadata. | |
| The caller is expected to know the type of the metadata and call | |
| the right get* method to fetch its value. | |
| @hide | |
| @deprecated Use {@link MediaMetadata}. | |
| */ | |
| @Deprecated public class Metadata | |
| { | |
| // The metadata are keyed using integers rather than more heavy | |
| // weight strings. We considered using Bundle to ship the metadata | |
| // between the native layer and the java layer but dropped that | |
| // option since keeping in sync a native implementation of Bundle | |
| // and the java one would be too burdensome. Besides Bundle uses | |
| // String for its keys. | |
| // The key range [0 8192) is reserved for the system. | |
| // | |
| // We manually serialize the data in Parcels. For large memory | |
| // blob (bitmaps, raw pictures) we use MemoryFile which allow the | |
| // client to make the data purge-able once it is done with it. | |
| // | |
| /** | |
| * @hide | |
| */ | |
| public static final int ANY = 0; // Never used for metadata returned, only for filtering. | |
| // Keep in sync with kAny in MediaPlayerService.cpp | |
| // Playback capabilities. | |
| /** | |
| * Indicate whether the media can be paused | |
| */ | |
| @UnsupportedAppUsage | |
| public static final int PAUSE_AVAILABLE = 1; // Boolean | |
| /** | |
| * Indicate whether the media can be backward seeked | |
| */ | |
| @UnsupportedAppUsage | |
| public static final int SEEK_BACKWARD_AVAILABLE = 2; // Boolean | |
| /** | |
| * Indicate whether the media can be forward seeked | |
| */ | |
| @UnsupportedAppUsage | |
| public static final int SEEK_FORWARD_AVAILABLE = 3; // Boolean | |
| /** | |
| * Indicate whether the media can be seeked | |
| */ | |
| @UnsupportedAppUsage | |
| public static final int SEEK_AVAILABLE = 4; // Boolean | |
| // TODO: Should we use numbers compatible with the metadata retriever? | |
| /** | |
| * @hide | |
| */ | |
| public static final int TITLE = 5; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int COMMENT = 6; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int COPYRIGHT = 7; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int ALBUM = 8; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int ARTIST = 9; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int AUTHOR = 10; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int COMPOSER = 11; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int GENRE = 12; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int DATE = 13; // Date | |
| /** | |
| * @hide | |
| */ | |
| public static final int DURATION = 14; // Integer(millisec) | |
| /** | |
| * @hide | |
| */ | |
| public static final int CD_TRACK_NUM = 15; // Integer 1-based | |
| /** | |
| * @hide | |
| */ | |
| public static final int CD_TRACK_MAX = 16; // Integer | |
| /** | |
| * @hide | |
| */ | |
| public static final int RATING = 17; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int ALBUM_ART = 18; // byte[] | |
| /** | |
| * @hide | |
| */ | |
| public static final int VIDEO_FRAME = 19; // Bitmap | |
| /** | |
| * @hide | |
| */ | |
| public static final int BIT_RATE = 20; // Integer, Aggregate rate of | |
| // all the streams in bps. | |
| /** | |
| * @hide | |
| */ | |
| public static final int AUDIO_BIT_RATE = 21; // Integer, bps | |
| /** | |
| * @hide | |
| */ | |
| public static final int VIDEO_BIT_RATE = 22; // Integer, bps | |
| /** | |
| * @hide | |
| */ | |
| public static final int AUDIO_SAMPLE_RATE = 23; // Integer, Hz | |
| /** | |
| * @hide | |
| */ | |
| public static final int VIDEO_FRAME_RATE = 24; // Integer, Hz | |
| // See RFC2046 and RFC4281. | |
| /** | |
| * @hide | |
| */ | |
| public static final int MIME_TYPE = 25; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int AUDIO_CODEC = 26; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int VIDEO_CODEC = 27; // String | |
| /** | |
| * @hide | |
| */ | |
| public static final int VIDEO_HEIGHT = 28; // Integer | |
| /** | |
| * @hide | |
| */ | |
| public static final int VIDEO_WIDTH = 29; // Integer | |
| /** | |
| * @hide | |
| */ | |
| public static final int NUM_TRACKS = 30; // Integer | |
| /** | |
| * @hide | |
| */ | |
| public static final int DRM_CRIPPLED = 31; // Boolean | |
| private static final int LAST_SYSTEM = 31; | |
| private static final int FIRST_CUSTOM = 8192; | |
| // Shorthands to set the MediaPlayer's metadata filter. | |
| /** | |
| * @hide | |
| */ | |
| public static final Set<Integer> MATCH_NONE = Collections.EMPTY_SET; | |
| /** | |
| * @hide | |
| */ | |
| public static final Set<Integer> MATCH_ALL = Collections.singleton(ANY); | |
| /** | |
| * @hide | |
| */ | |
| public static final int STRING_VAL = 1; | |
| /** | |
| * @hide | |
| */ | |
| public static final int INTEGER_VAL = 2; | |
| /** | |
| * @hide | |
| */ | |
| public static final int BOOLEAN_VAL = 3; | |
| /** | |
| * @hide | |
| */ | |
| public static final int LONG_VAL = 4; | |
| /** | |
| * @hide | |
| */ | |
| public static final int DOUBLE_VAL = 5; | |
| /** | |
| * @hide | |
| */ | |
| public static final int DATE_VAL = 6; | |
| /** | |
| * @hide | |
| */ | |
| public static final int BYTE_ARRAY_VAL = 7; | |
| // FIXME: misses a type for shared heap is missing (MemoryFile). | |
| // FIXME: misses a type for bitmaps. | |
| private static final int LAST_TYPE = 7; | |
| private static final String TAG = "media.Metadata"; | |
| private static final int kInt32Size = 4; | |
| private static final int kMetaHeaderSize = 2 * kInt32Size; // size + marker | |
| private static final int kRecordHeaderSize = 3 * kInt32Size; // size + id + type | |
| private static final int kMetaMarker = 0x4d455441; // 'M' 'E' 'T' 'A' | |
| // After a successful parsing, set the parcel with the serialized metadata. | |
| private Parcel mParcel; | |
| // Map to associate a Metadata key (e.g TITLE) with the offset of | |
| // the record's payload in the parcel. | |
| // Used to look up if a key was present too. | |
| // Key: Metadata ID | |
| // Value: Offset of the metadata type field in the record. | |
| private final HashMap<Integer, Integer> mKeyToPosMap = | |
| new HashMap<Integer, Integer>(); | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public Metadata() { } | |
| /** | |
| * Go over all the records, collecting metadata keys and records' | |
| * type field offset in the Parcel. These are stored in | |
| * mKeyToPosMap for latter retrieval. | |
| * Format of a metadata record: | |
| <pre> | |
| 1 2 3 | |
| 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 | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | record size | | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | metadata key | // TITLE | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | metadata type | // STRING_VAL | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | | | |
| | .... metadata payload .... | | |
| | | | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| </pre> | |
| * @param parcel With the serialized records. | |
| * @param bytesLeft How many bytes in the parcel should be processed. | |
| * @return false if an error occurred during parsing. | |
| */ | |
| private boolean scanAllRecords(Parcel parcel, int bytesLeft) { | |
| int recCount = 0; | |
| boolean error = false; | |
| mKeyToPosMap.clear(); | |
| while (bytesLeft > kRecordHeaderSize) { | |
| final int start = parcel.dataPosition(); | |
| // Check the size. | |
| final int size = parcel.readInt(); | |
| if (size <= kRecordHeaderSize) { // at least 1 byte should be present. | |
| Log.e(TAG, "Record is too short"); | |
| error = true; | |
| break; | |
| } | |
| // Check the metadata key. | |
| final int metadataId = parcel.readInt(); | |
| if (!checkMetadataId(metadataId)) { | |
| error = true; | |
| break; | |
| } | |
| // Store the record offset which points to the type | |
| // field so we can later on read/unmarshall the record | |
| // payload. | |
| if (mKeyToPosMap.containsKey(metadataId)) { | |
| Log.e(TAG, "Duplicate metadata ID found"); | |
| error = true; | |
| break; | |
| } | |
| mKeyToPosMap.put(metadataId, parcel.dataPosition()); | |
| // Check the metadata type. | |
| final int metadataType = parcel.readInt(); | |
| if (metadataType <= 0 || metadataType > LAST_TYPE) { | |
| Log.e(TAG, "Invalid metadata type " + metadataType); | |
| error = true; | |
| break; | |
| } | |
| // Skip to the next one. | |
| try { | |
| parcel.setDataPosition(MathUtils.addOrThrow(start, size)); | |
| } catch (IllegalArgumentException e) { | |
| Log.e(TAG, "Invalid size: " + e.getMessage()); | |
| error = true; | |
| break; | |
| } | |
| bytesLeft -= size; | |
| ++recCount; | |
| } | |
| if (0 != bytesLeft || error) { | |
| Log.e(TAG, "Ran out of data or error on record " + recCount); | |
| mKeyToPosMap.clear(); | |
| return false; | |
| } else { | |
| return true; | |
| } | |
| } | |
| /** | |
| * Check a parcel containing metadata is well formed. The header | |
| * is checked as well as the individual records format. However, the | |
| * data inside the record is not checked because we do lazy access | |
| * (we check/unmarshall only data the user asks for.) | |
| * | |
| * Format of a metadata parcel: | |
| <pre> | |
| 1 2 3 | |
| 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 | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | metadata total size | | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | 'M' | 'E' | 'T' | 'A' | | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| | | | |
| | .... metadata records .... | | |
| | | | |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | |
| </pre> | |
| * | |
| * @param parcel With the serialized data. Metadata keeps a | |
| * reference on it to access it later on. The caller | |
| * should not modify the parcel after this call (and | |
| * not call recycle on it.) | |
| * @return false if an error occurred. | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public boolean parse(Parcel parcel) { | |
| if (parcel.dataAvail() < kMetaHeaderSize) { | |
| Log.e(TAG, "Not enough data " + parcel.dataAvail()); | |
| return false; | |
| } | |
| final int pin = parcel.dataPosition(); // to roll back in case of errors. | |
| final int size = parcel.readInt(); | |
| // The extra kInt32Size below is to account for the int32 'size' just read. | |
| if (parcel.dataAvail() + kInt32Size < size || size < kMetaHeaderSize) { | |
| Log.e(TAG, "Bad size " + size + " avail " + parcel.dataAvail() + " position " + pin); | |
| parcel.setDataPosition(pin); | |
| return false; | |
| } | |
| // Checks if the 'M' 'E' 'T' 'A' marker is present. | |
| final int kShouldBeMetaMarker = parcel.readInt(); | |
| if (kShouldBeMetaMarker != kMetaMarker ) { | |
| Log.e(TAG, "Marker missing " + Integer.toHexString(kShouldBeMetaMarker)); | |
| parcel.setDataPosition(pin); | |
| return false; | |
| } | |
| // Scan the records to collect metadata ids and offsets. | |
| if (!scanAllRecords(parcel, size - kMetaHeaderSize)) { | |
| parcel.setDataPosition(pin); | |
| return false; | |
| } | |
| mParcel = parcel; | |
| return true; | |
| } | |
| /** | |
| * @return The set of metadata ID found. | |
| */ | |
| @UnsupportedAppUsage | |
| public Set<Integer> keySet() { | |
| return mKeyToPosMap.keySet(); | |
| } | |
| /** | |
| * @return true if a value is present for the given key. | |
| */ | |
| @UnsupportedAppUsage | |
| public boolean has(final int metadataId) { | |
| if (!checkMetadataId(metadataId)) { | |
| throw new IllegalArgumentException("Invalid key: " + metadataId); | |
| } | |
| return mKeyToPosMap.containsKey(metadataId); | |
| } | |
| // Accessors. | |
| // Caller must make sure the key is present using the {@code has} | |
| // method otherwise a RuntimeException will occur. | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public String getString(final int key) { | |
| checkType(key, STRING_VAL); | |
| return mParcel.readString(); | |
| } | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public int getInt(final int key) { | |
| checkType(key, INTEGER_VAL); | |
| return mParcel.readInt(); | |
| } | |
| /** | |
| * Get the boolean value indicated by key | |
| */ | |
| @UnsupportedAppUsage | |
| public boolean getBoolean(final int key) { | |
| checkType(key, BOOLEAN_VAL); | |
| return mParcel.readInt() == 1; | |
| } | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public long getLong(final int key) { | |
| checkType(key, LONG_VAL); | |
| return mParcel.readLong(); | |
| } | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public double getDouble(final int key) { | |
| checkType(key, DOUBLE_VAL); | |
| return mParcel.readDouble(); | |
| } | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public byte[] getByteArray(final int key) { | |
| checkType(key, BYTE_ARRAY_VAL); | |
| return mParcel.createByteArray(); | |
| } | |
| /** | |
| * @hide | |
| */ | |
| @UnsupportedAppUsage | |
| public Date getDate(final int key) { | |
| checkType(key, DATE_VAL); | |
| final long timeSinceEpoch = mParcel.readLong(); | |
| final String timeZone = mParcel.readString(); | |
| if (timeZone.length() == 0) { | |
| return new Date(timeSinceEpoch); | |
| } else { | |
| TimeZone tz = TimeZone.getTimeZone(timeZone); | |
| Calendar cal = Calendar.getInstance(tz); | |
| cal.setTimeInMillis(timeSinceEpoch); | |
| return cal.getTime(); | |
| } | |
| } | |
| /** | |
| * @return the last available system metadata id. Ids are | |
| * 1-indexed. | |
| * @hide | |
| */ | |
| public static int lastSytemId() { return LAST_SYSTEM; } | |
| /** | |
| * @return the first available cutom metadata id. | |
| * @hide | |
| */ | |
| public static int firstCustomId() { return FIRST_CUSTOM; } | |
| /** | |
| * @return the last value of known type. Types are 1-indexed. | |
| * @hide | |
| */ | |
| public static int lastType() { return LAST_TYPE; } | |
| /** | |
| * Check val is either a system id or a custom one. | |
| * @param val Metadata key to test. | |
| * @return true if it is in a valid range. | |
| **/ | |
| private boolean checkMetadataId(final int val) { | |
| if (val <= ANY || (LAST_SYSTEM < val && val < FIRST_CUSTOM)) { | |
| Log.e(TAG, "Invalid metadata ID " + val); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Check the type of the data match what is expected. | |
| */ | |
| private void checkType(final int key, final int expectedType) { | |
| final int pos = mKeyToPosMap.get(key); | |
| mParcel.setDataPosition(pos); | |
| final int type = mParcel.readInt(); | |
| if (type != expectedType) { | |
| throw new IllegalStateException("Wrong type " + expectedType + " but got " + type); | |
| } | |
| } | |
| } |