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

Commit 52e4ad89 authored by Android (Google) Code Review's avatar Android (Google) Code Review
Browse files

Merge change 7399

* changes:
  Partial implementation of the parsing of Parcel into Metadata.
parents 7ef24576 a5ccb22a
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()));
    }
}