Merge change 7399
* changes: Partial implementation of the parsing of Parcel into Metadata.
This commit is contained in:
@@ -22,8 +22,8 @@ import android.util.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ public class Metadata
|
||||
//
|
||||
// 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 purgeable once it is done with it.
|
||||
// client to make the data purge-able once it is done with it.
|
||||
//
|
||||
|
||||
public static final int ANY = 0; // Never used for metadata returned, only for filtering.
|
||||
@@ -95,18 +95,34 @@ public class Metadata
|
||||
public static final int VIDEO_WIDTH = 26; // Integer
|
||||
public static final int NUM_TRACKS = 27; // Integer
|
||||
public static final int DRM_CRIPPLED = 28; // Boolean
|
||||
public static final int LAST_SYSTEM = 29;
|
||||
public static final int FIRST_CUSTOM = 8092;
|
||||
private static final int LAST_SYSTEM = 28;
|
||||
private static final int FIRST_CUSTOM = 8092;
|
||||
|
||||
// Shorthands to set the MediaPlayer's metadata filter.
|
||||
public static final Set<Integer> MATCH_NONE = Collections.EMPTY_SET;
|
||||
public static final Set<Integer> MATCH_ALL = Collections.singleton(ANY);
|
||||
|
||||
private static final int STRING_VAL = 1;
|
||||
private static final int INTEGER_VAL = 2;
|
||||
private static final int LONG_VAL = 3;
|
||||
private static final int DOUBLE_VAL = 4;
|
||||
private static final int TIMED_TEXT_VAL = 2;
|
||||
public static final int STRING_VAL = 1;
|
||||
public static final int INTEGER_VAL = 2;
|
||||
public static final int LONG_VAL = 3;
|
||||
public static final int DOUBLE_VAL = 4;
|
||||
public static final int TIMED_TEXT_VAL = 5;
|
||||
private static final int LAST_TYPE = 5;
|
||||
|
||||
private static final String TAG = "media.Metadata";
|
||||
private static final int kMetaHeaderSize = 8; // 8 bytes for the size + the marker
|
||||
private static final int kMetaMarker = 0x4d455441; // 'M' 'E' 'T' 'A'
|
||||
private static final int kRecordHeaderSize = 12; // size + id + type
|
||||
|
||||
// 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>();
|
||||
|
||||
/**
|
||||
* Helper class to hold a pair (time, text). Can be used to implement caption.
|
||||
@@ -125,41 +141,163 @@ public class Metadata
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ Metadata() {}
|
||||
public Metadata() { }
|
||||
|
||||
/* package */ boolean parse(Parcel data) {
|
||||
// FIXME: Implement.
|
||||
/**
|
||||
* 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.
|
||||
parcel.setDataPosition(start + size);
|
||||
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.
|
||||
*/
|
||||
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();
|
||||
|
||||
if (parcel.dataAvail() < size || size < kMetaHeaderSize) {
|
||||
Log.e(TAG, "Bad size " + size);
|
||||
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 number of element in this metadata set.
|
||||
* @return The set of metadata ID found.
|
||||
*/
|
||||
public int size() {
|
||||
// FIXME: Implement.
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an iterator over the keys.
|
||||
*/
|
||||
public Iterator<Integer> iterator() {
|
||||
// FIXME: Implement.
|
||||
return new java.util.HashSet<Integer>().iterator();
|
||||
public Set<Integer> keySet() {
|
||||
return mKeyToPosMap.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a value is present for the given key.
|
||||
*/
|
||||
public boolean has(final int key) {
|
||||
if (key <= ANY) {
|
||||
throw new IllegalArgumentException("Invalid key: " + key);
|
||||
public boolean has(final int metadataId) {
|
||||
if (!checkMetadataId(metadataId)) {
|
||||
throw new IllegalArgumentException("Invalid key: " + metadataId);
|
||||
}
|
||||
if (LAST_SYSTEM <= key && key < FIRST_CUSTOM) {
|
||||
throw new IllegalArgumentException("Key in reserved range: " + key);
|
||||
}
|
||||
// FIXME: Implement.
|
||||
return true;
|
||||
return mKeyToPosMap.containsKey(metadataId);
|
||||
}
|
||||
|
||||
// Accessors
|
||||
@@ -201,4 +339,25 @@ public class Metadata
|
||||
// FIXME: Implement.
|
||||
return new TimedText(new Date(0), "<missing>");
|
||||
}
|
||||
|
||||
// @return the last available system metadata id. Ids are
|
||||
// 1-indexed.
|
||||
public static int lastSytemId() { return LAST_SYSTEM; }
|
||||
|
||||
// @return the first available cutom metadata id.
|
||||
public static int firstCustomId() { return FIRST_CUSTOM; }
|
||||
|
||||
// @return the last value of known type. Types are 1-indexed.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,16 +24,16 @@ import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Instrumentation Test Runner for all media framework unit tests.
|
||||
*
|
||||
*
|
||||
* Make sure that MediaFrameworkUnitTestRunner has been added to
|
||||
* AndroidManifest.xml file, and then "make -j4 mediaframeworktest; adb sync"
|
||||
* to build and upload mediaframeworktest to the phone or emulator.
|
||||
*
|
||||
*
|
||||
* Example on running all unit tests for a single class:
|
||||
* adb shell am instrument -e class \
|
||||
* com.android.mediaframeworktest.unit.MediaMetadataRetrieverUnitTest \
|
||||
* com.android.mediaframeworktest.unit.MediaMetadataRetrieverUnitTest \
|
||||
* -w com.android.mediaframeworktest/.MediaFrameworkUnitTestRunner
|
||||
*
|
||||
*
|
||||
* Example on running all unit tests for the media framework:
|
||||
* adb shell am instrument \
|
||||
* -w com.android.mediaframeworktest/.MediaFrameworkUnitTestRunner
|
||||
@@ -54,12 +54,12 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner {
|
||||
public ClassLoader getLoader() {
|
||||
return MediaFrameworkUnitTestRunner.class.getClassLoader();
|
||||
}
|
||||
|
||||
|
||||
// Running all unit tests checking the state machine may be time-consuming.
|
||||
private void addMediaMetadataRetrieverStateUnitTests(TestSuite suite) {
|
||||
suite.addTestSuite(MediaMetadataRetrieverTest.class);
|
||||
}
|
||||
|
||||
|
||||
// Running all unit tests checking the state machine may be time-consuming.
|
||||
private void addMediaRecorderStateUnitTests(TestSuite suite) {
|
||||
suite.addTestSuite(MediaRecorderPrepareStateUnitTest.class);
|
||||
@@ -87,5 +87,6 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner {
|
||||
suite.addTestSuite(MediaPlayerSetLoopingStateUnitTest.class);
|
||||
suite.addTestSuite(MediaPlayerSetAudioStreamTypeStateUnitTest.class);
|
||||
suite.addTestSuite(MediaPlayerSetVolumeStateUnitTest.class);
|
||||
suite.addTestSuite(MediaPlayerMetadataParserTest.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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 com.android.mediaframeworktest.unit;
|
||||
import android.media.Metadata;
|
||||
import android.os.Parcel;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.util.Log;
|
||||
|
||||
/*
|
||||
* Check the Java layer that parses serialized metadata in Parcel
|
||||
* works as expected.
|
||||
*
|
||||
*/
|
||||
|
||||
public class MediaPlayerMetadataParserTest extends AndroidTestCase {
|
||||
private static final String TAG = "MediaPlayerMetadataTest";
|
||||
private static final int kToken = 0xdeadbeef;
|
||||
private static final int kMarker = 0x4d455441; // 'M' 'E' 'T' 'A'
|
||||
private static final int kHeaderSize = 8;
|
||||
|
||||
private Metadata mMetadata = null;
|
||||
private Parcel mParcel = null;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMetadata = new Metadata();
|
||||
mParcel = Parcel.obtain();
|
||||
|
||||
resetParcel();
|
||||
}
|
||||
|
||||
// Check parsing of the parcel fails. Make sure the parser rewind
|
||||
// the parcel properly.
|
||||
private void assertParseFail() throws Exception {
|
||||
mParcel.setDataPosition(0);
|
||||
assertFalse(mMetadata.parse(mParcel));
|
||||
assertEquals(0, mParcel.dataPosition());
|
||||
}
|
||||
|
||||
// Check parsing of the parcel is successful. Before the
|
||||
// invocation of the parser a token is inserted. When the parser
|
||||
// returns, the parcel should be positioned at the token (check it
|
||||
// does not read too much data).
|
||||
private void assertParse() throws Exception {
|
||||
mParcel.writeInt(kToken);
|
||||
mParcel.setDataPosition(0);
|
||||
assertTrue(mMetadata.parse(mParcel));
|
||||
assertEquals(kToken, mParcel.readInt());
|
||||
}
|
||||
|
||||
// Write the number of bytes from the start of the parcel to the
|
||||
// current position at the beginning of the parcel (offset 0).
|
||||
private void adjustSize() {
|
||||
adjustSize(0);
|
||||
}
|
||||
|
||||
// Write the number of bytes from the offset to the current
|
||||
// position at position pointed by offset.
|
||||
private void adjustSize(int offset) {
|
||||
final int pos = mParcel.dataPosition();
|
||||
|
||||
mParcel.setDataPosition(offset);
|
||||
mParcel.writeInt(pos - offset);
|
||||
mParcel.setDataPosition(pos);
|
||||
}
|
||||
|
||||
// Rewind the parcel and insert the header.
|
||||
private void resetParcel() {
|
||||
mParcel.setDataPosition(0);
|
||||
// Most tests will use a properly formed parcel with a size
|
||||
// and the meta marker so we add them by default.
|
||||
mParcel.writeInt(-1); // Placeholder for the size
|
||||
mParcel.writeInt(kMarker);
|
||||
}
|
||||
|
||||
// Insert a string record at the current position.
|
||||
private void writeStringRecord(int metadataId, String val) {
|
||||
final int start = mParcel.dataPosition();
|
||||
mParcel.writeInt(-1); // Placeholder for the length
|
||||
mParcel.writeInt(metadataId);
|
||||
mParcel.writeInt(Metadata.STRING_VAL);
|
||||
mParcel.writeString(val);
|
||||
adjustSize(start);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// START OF THE TESTS
|
||||
|
||||
|
||||
// There should be at least 8 bytes in the parcel, 4 for the size
|
||||
// and 4 for the 'M' 'E' 'T' 'A' marker.
|
||||
@SmallTest
|
||||
public void testMissingSizeAndMarker() throws Exception {
|
||||
for (int i = 0; i < kHeaderSize; ++i) {
|
||||
mParcel.setDataPosition(0);
|
||||
mParcel.setDataSize(i);
|
||||
|
||||
assertEquals(i, mParcel.dataAvail());
|
||||
assertParseFail();
|
||||
}
|
||||
}
|
||||
|
||||
// There should be at least 'size' bytes in the parcel.
|
||||
@SmallTest
|
||||
public void testMissingData() throws Exception {
|
||||
final int size = 20;
|
||||
|
||||
mParcel.writeInt(size);
|
||||
mParcel.setDataSize(size - 1);
|
||||
assertParseFail();
|
||||
}
|
||||
|
||||
// Empty parcel is fine
|
||||
@SmallTest
|
||||
public void testEmptyIsOk() throws Exception {
|
||||
adjustSize();
|
||||
assertParse();
|
||||
}
|
||||
|
||||
// RECORDS
|
||||
|
||||
// A record header should be at least 12 bytes long
|
||||
@SmallTest
|
||||
public void testRecordMissingId() throws Exception {
|
||||
mParcel.writeInt(13); // record length
|
||||
// misses metadata id and metadata type.
|
||||
adjustSize();
|
||||
assertParseFail();
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testRecordMissingType() throws Exception {
|
||||
mParcel.writeInt(13); // record length lies
|
||||
mParcel.writeInt(Metadata.TITLE);
|
||||
// misses metadata type
|
||||
adjustSize();
|
||||
assertParseFail();
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testRecordWithZeroPayload() throws Exception {
|
||||
mParcel.writeInt(0);
|
||||
adjustSize();
|
||||
assertParseFail();
|
||||
}
|
||||
|
||||
// A record cannot be empty.
|
||||
@SmallTest
|
||||
public void testRecordMissingPayload() throws Exception {
|
||||
mParcel.writeInt(12);
|
||||
mParcel.writeInt(Metadata.TITLE);
|
||||
mParcel.writeInt(Metadata.STRING_VAL);
|
||||
// misses payload
|
||||
adjustSize();
|
||||
assertParseFail();
|
||||
}
|
||||
|
||||
// Check records can be found.
|
||||
@SmallTest
|
||||
public void testRecordsFound() throws Exception {
|
||||
writeStringRecord(Metadata.TITLE, "a title");
|
||||
writeStringRecord(Metadata.GENRE, "comedy");
|
||||
writeStringRecord(Metadata.firstCustomId(), "custom");
|
||||
adjustSize();
|
||||
assertParse();
|
||||
assertTrue(mMetadata.has(Metadata.TITLE));
|
||||
assertTrue(mMetadata.has(Metadata.GENRE));
|
||||
assertTrue(mMetadata.has(Metadata.firstCustomId()));
|
||||
assertFalse(mMetadata.has(Metadata.DRM_CRIPPLED));
|
||||
assertEquals(3, mMetadata.keySet().size());
|
||||
}
|
||||
|
||||
// Detects bad metadata type
|
||||
@SmallTest
|
||||
public void testBadMetadataType() throws Exception {
|
||||
final int start = mParcel.dataPosition();
|
||||
mParcel.writeInt(-1); // Placeholder for the length
|
||||
mParcel.writeInt(Metadata.TITLE);
|
||||
mParcel.writeInt(0); // Invalid type.
|
||||
mParcel.writeString("dummy");
|
||||
adjustSize(start);
|
||||
|
||||
adjustSize();
|
||||
assertParseFail();
|
||||
}
|
||||
|
||||
// Check a Metadata instance can be reused, i.e the parse method
|
||||
// wipes out the existing states/keys.
|
||||
@SmallTest
|
||||
public void testParseClearState() throws Exception {
|
||||
writeStringRecord(Metadata.TITLE, "a title");
|
||||
writeStringRecord(Metadata.GENRE, "comedy");
|
||||
writeStringRecord(Metadata.firstCustomId(), "custom");
|
||||
adjustSize();
|
||||
assertParse();
|
||||
|
||||
resetParcel();
|
||||
writeStringRecord(Metadata.MIME_TYPE, "audio/mpg");
|
||||
adjustSize();
|
||||
assertParse();
|
||||
|
||||
// Only the mime type metadata should be present.
|
||||
assertEquals(1, mMetadata.keySet().size());
|
||||
assertTrue(mMetadata.has(Metadata.MIME_TYPE));
|
||||
|
||||
assertFalse(mMetadata.has(Metadata.TITLE));
|
||||
assertFalse(mMetadata.has(Metadata.GENRE));
|
||||
assertFalse(mMetadata.has(Metadata.firstCustomId()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user