570 lines
17 KiB
Java
570 lines
17 KiB
Java
![]() |
/*
|
||
|
* 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); /**
|
||
|
* {@hide}
|
||
|
*/
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
}
|