diff --git a/media/java/android/media/Metadata.java b/media/java/android/media/Metadata.java index 6525109945ff2..a345ef88b08d1 100644 --- a/media/java/android/media/Metadata.java +++ b/media/java/android/media/Metadata.java @@ -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 MATCH_NONE = Collections.EMPTY_SET; public static final Set 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 mKeyToPosMap = new HashMap(); /** * 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: +
+                         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 ....                     |
+      |                                                               |
+      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     
+ * @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: +
+                         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 ....                     |
+      |                                                               |
+      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     
+ * + * @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 iterator() { - // FIXME: Implement. - return new java.util.HashSet().iterator(); + public Set 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), ""); } + + // @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; + } } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java index 81d59da5c4928..a203adc71c318 100755 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java @@ -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); } } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerMetadataParserTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerMetadataParserTest.java new file mode 100644 index 0000000000000..f51b29e073ea4 --- /dev/null +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerMetadataParserTest.java @@ -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())); + } +}