Merge change 7399

* changes:
  Partial implementation of the parsing of Parcel into Metadata.
This commit is contained in:
Android (Google) Code Review
2009-07-16 11:55:45 -07:00
3 changed files with 424 additions and 38 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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()));
}
}