Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a5ccb22a authored by Nicolas Catania's avatar Nicolas Catania
Browse files

Partial implementation of the parsing of Parcel into Metadata.

In this first cut, a raw parcel is parsed to check that the overall
format is correct.

At the same time, we record the metadata seen and their position in
the parcel for later retrieval using the get* methods.

This means that the 'has' method to check the existence of a metadata
should work.

Removed size and iterator methods. Instead, I added a new method
to give access to the set of keys. The user can make use of the set
to call size(), empty(), iterator() etc...
parent 851da848
Loading
Loading
Loading
Loading
+188 −29
Original line number Diff line number Diff line
@@ -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;
        }
    }

    /**
     * @return the number of element in this metadata set.
     * 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 int size() {
        // FIXME: Implement.
        return 0;
    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 an iterator over the keys.
     * @return The set of metadata ID found.
     */
    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);
        }
        if (LAST_SYSTEM <= key && key < FIRST_CUSTOM) {
            throw new IllegalArgumentException("Key in reserved range: " + key);
    public boolean has(final int metadataId) {
        if (!checkMetadataId(metadataId)) {
            throw new IllegalArgumentException("Invalid key: " + metadataId);
        }
        // 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;
    }
}
+7 −6
Original line number Diff line number Diff line
@@ -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);
    }
}
+226 −0
Original line number Diff line number Diff line
/*
 * 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()));
    }
}