host side MediaBitstreamsTest

Bug: 30268664
Test: run cts everything -m CtsMediaBitstreamsTestCases
Change-Id: Iae347d44a27bcc2290dd93f1db6638011b884ef9
diff --git a/hostsidetests/media/bitstreams/Android.mk b/hostsidetests/media/bitstreams/Android.mk
new file mode 100644
index 0000000..b07309c
--- /dev/null
+++ b/hostsidetests/media/bitstreams/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES += $(call all-java-files-under, common/src)
+
+LOCAL_JAVA_LIBRARIES := compatibility-host-util cts-tradefed tradefed
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := CtsMediaBitstreamsTestCases
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/hostsidetests/media/bitstreams/AndroidTest.xml b/hostsidetests/media/bitstreams/AndroidTest.xml
new file mode 100644
index 0000000..fe06a09
--- /dev/null
+++ b/hostsidetests/media/bitstreams/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<configuration description="Config for CTS Sample host test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsMediaBitstreamsDeviceSideTestApp.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
+        <option name="target" value="device" />
+        <option name="config-filename" value="CtsMediaBitstreamsTestCases" />
+    </target_preparer>
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsMediaBitstreamsTestCases.jar" />
+        <option name="runtime-hint" value="6m" />
+    </test>
+</configuration>
diff --git a/hostsidetests/media/bitstreams/app/Android.mk b/hostsidetests/media/bitstreams/app/Android.mk
new file mode 100644
index 0000000..df8e6a4
--- /dev/null
+++ b/hostsidetests/media/bitstreams/app/Android.mk
@@ -0,0 +1,39 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Don't include this package in any target
+LOCAL_MODULE_TAGS := tests
+# When built, explicitly put it in the data partition.
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_DEX_PREOPT := false
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test compatibility-device-util media-bitstreams-common-devicesidelib
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+LOCAL_PACKAGE_NAME := CtsMediaBitstreamsDeviceSideTestApp
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/hostsidetests/media/bitstreams/app/AndroidManifest.xml b/hostsidetests/media/bitstreams/app/AndroidManifest.xml
new file mode 100644
index 0000000..d5565fd
--- /dev/null
+++ b/hostsidetests/media/bitstreams/app/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2017 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.media.cts.bitstreams.app">
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="android.media.cts.bitstreams.app"
+            android:label="Device-side CTS media bitstreams preparation" />
+</manifest>
diff --git a/hostsidetests/media/bitstreams/app/src/android/media/cts/bitstreams/app/MediaBitstreamsDeviceSideTest.java b/hostsidetests/media/bitstreams/app/src/android/media/cts/bitstreams/app/MediaBitstreamsDeviceSideTest.java
new file mode 100644
index 0000000..d7965b7
--- /dev/null
+++ b/hostsidetests/media/bitstreams/app/src/android/media/cts/bitstreams/app/MediaBitstreamsDeviceSideTest.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2017 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 android.media.cts.bitstreams.app;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.cts.bitstreams.MediaBitstreams;
+import android.os.Bundle;
+import android.os.Debug;
+import android.support.test.InstrumentationRegistry;
+import android.util.Xml;
+import com.android.compatibility.common.util.DynamicConfigDeviceSide;
+import com.android.compatibility.common.util.MediaUtils;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * Test class that uses device-side media APIs to determine up to which resolution MediaPreparer
+ * should copy media files for CtsMediaStressTestCases.
+ */
+@RunWith(JUnit4.class)
+public class MediaBitstreamsDeviceSideTest {
+
+    private static final String KEY_SIZE = "size";
+    private static final String UTF_8 = "utf-8";
+    private static final String DYNAMIC_CONFIG = "dynamicConfig";
+    private static final String DYNAMIC_CONFIG_ENTRY = "entry";
+    private static final String DYNAMIC_CONFIG_KEY = "key";
+    private static final String DYNAMIC_CONFIG_VALUE = "value";
+
+    /** Instrumentation status code used to write resolution to metrics */
+    private static final int INST_STATUS_IN_PROGRESS = 2;
+
+    private static File mAppCache = InstrumentationRegistry.getContext().getExternalCacheDir();
+    private static String mDeviceBitstreamsPath = InstrumentationRegistry.getArguments().getString(
+            MediaBitstreams.OPT_DEVICE_BITSTEAMS_PATH,
+            MediaBitstreams.DEFAULT_DEVICE_BITSTEAMS_PATH);
+
+    @BeforeClass
+    public static void setUp() {
+        Bundle args = InstrumentationRegistry.getArguments();
+        String debugStr = args.getString(MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, "false");
+        boolean debug = Boolean.parseBoolean(debugStr);
+        if (debug && !Debug.isDebuggerConnected()) {
+            Debug.waitForDebugger();
+        }
+    }
+
+    static interface ReportCallback {
+        void run(OutputStream out) throws Exception;
+    }
+
+    static class GenerateBitstreamsFormatsXml implements ReportCallback {
+        @Override
+        public void run(OutputStream out) throws Exception {
+
+            String[] keys = new String[] {
+                    MediaFormat.KEY_WIDTH,
+                    MediaFormat.KEY_HEIGHT,
+                    MediaFormat.KEY_FRAME_RATE,
+                    MediaFormat.KEY_PROFILE,
+                    MediaFormat.KEY_LEVEL,
+                    MediaFormat.KEY_BIT_RATE};
+
+            XmlSerializer formats = Xml.newSerializer();
+            formats.setOutput(out, UTF_8);
+            formats.startDocument(UTF_8, true);
+            formats.startTag(null, DYNAMIC_CONFIG);
+
+            DynamicConfigDeviceSide config = new DynamicConfigDeviceSide(MediaBitstreams.K_MODULE);
+            for (String path : config.keySet()) {
+
+                formats.startTag(null, DYNAMIC_CONFIG_ENTRY);
+                formats.attribute(null, DYNAMIC_CONFIG_KEY, path);
+                formats.startTag(null, DYNAMIC_CONFIG_VALUE);
+
+                String formatStr = config.getValue(path);
+                if (formatStr != null && !formatStr.isEmpty()) {
+                    formats.text(formatStr);
+                } else {
+                    File media = new File(mDeviceBitstreamsPath, path);
+                    String fullPath = media.getPath();
+                    MediaFormat format = MediaUtils.getTrackFormatForPath(null, fullPath, "video");
+                    StringBuilder formatStringBuilder = new StringBuilder(MediaFormat.KEY_MIME);
+                    formatStringBuilder.append('=').append(format.getString(MediaFormat.KEY_MIME));
+                    formatStringBuilder.append(',').append(KEY_SIZE)
+                            .append('=').append(media.length());
+                    for (String key : keys) {
+                        formatStringBuilder.append(',').append(key).append('=');
+                        if (format.containsKey(key)) {
+                            formatStringBuilder.append(format.getInteger(key));
+                        }
+                    }
+                    formats.text(formatStringBuilder.toString());
+                }
+
+                formats.endTag(null, DYNAMIC_CONFIG_VALUE);
+                formats.endTag(null, DYNAMIC_CONFIG_ENTRY);
+
+            }
+
+            formats.endTag(null, DYNAMIC_CONFIG);
+            formats.endDocument();
+
+        }
+    }
+
+    static class GenerateSupportedBitstreamsFormatsTxt implements ReportCallback {
+
+        @Override
+        public void run(OutputStream out) throws Exception {
+
+            PrintStream ps = new PrintStream(out);
+            Bundle args = InstrumentationRegistry.getArguments();
+            String prefix = args.getString(MediaBitstreams.OPT_BITSTREAMS_PREFIX, "");
+            DynamicConfigDeviceSide config = new DynamicConfigDeviceSide(MediaBitstreams.K_MODULE);
+
+            for (String path : config.keySet()) {
+
+                if (!path.startsWith(prefix)) {
+                    continue;
+                }
+
+                String formatStr = config.getValue(path);
+                if (formatStr == null || formatStr.isEmpty()) {
+                    continue;
+                }
+
+                MediaFormat format = parseTrackFormat(formatStr);
+                if (!MediaUtils.checkDecoderForFormat(format)) {
+                    continue;
+                }
+
+                ps.println(path);
+            }
+            ps.flush();
+        }
+    }
+
+    static class TestBitstreamsConformance implements ReportCallback {
+
+        ExecutorService mExecutorService;
+
+        private SharedPreferences getSettings() {
+            Context ctx = InstrumentationRegistry.getContext();
+            SharedPreferences settings = ctx.getSharedPreferences(MediaBitstreams.K_MODULE, 0);
+            return settings;
+        }
+
+        private void setup() {
+            Bundle args = InstrumentationRegistry.getArguments();
+            String lastCrash = args.getString(MediaBitstreams.OPT_LAST_CRASH);
+            if (lastCrash != null) {
+                SharedPreferences settings = getSettings();
+                int n = settings.getInt(lastCrash, 0);
+                Editor editor = settings.edit();
+                editor.putInt(lastCrash, n + 1);
+                editor.commit();
+            }
+        }
+
+        @Override
+        public void run(OutputStream out) throws Exception {
+            setup();
+            mExecutorService = Executors.newFixedThreadPool(3);
+            try (
+                Scanner sc = new Scanner(
+                        new File(mDeviceBitstreamsPath, MediaBitstreams.K_BITSTREAMS_LIST_TXT));
+                PrintStream ps = new PrintStream(out, true)
+            ) {
+                while (sc.hasNextLine()) {
+                    verifyBitstream(ps, sc.nextLine());
+                }
+            } finally {
+                mExecutorService.shutdown();
+            }
+        }
+
+        private List<String> getDecodersForPath(String path) throws IOException {
+            List<String> decoders = new ArrayList<>();
+            MediaExtractor ex = new MediaExtractor();
+            try {
+                ex.setDataSource(path);
+                MediaFormat format = ex.getTrackFormat(0);
+                boolean[] vendors = new boolean[] {false, true};
+                for (boolean v : vendors) {
+                    for (String name : MediaUtils.getDecoderNames(v, format)) {
+                        decoders.add(name);
+                    }
+                }
+            } finally {
+                ex.release();
+            }
+            return decoders;
+        }
+
+        private List<String> getFrameChecksumsForPath(String path) throws IOException {
+            String md5Path = MediaBitstreams.getMd5Path(path);
+            List<String> frameMD5Sums = Files.readAllLines(
+                    new File(mDeviceBitstreamsPath, md5Path).toPath());
+            for (int i = 0; i < frameMD5Sums.size(); i++) {
+                String line = frameMD5Sums.get(i);
+                frameMD5Sums.set(i, line.split(" ")[0]);
+            }
+            return frameMD5Sums;
+        }
+
+        private void verifyBitstream(PrintStream ps, String relativePath) {
+            ps.println(relativePath);
+
+            List<String> decoders = new ArrayList<>();
+            List<String> frameChecksums = new ArrayList<>();
+            SharedPreferences settings = getSettings();
+            String fullPath = new File(mDeviceBitstreamsPath, relativePath).toString();
+            try {
+                String lastCrash = MediaBitstreams.generateCrashSignature(relativePath, "");
+                if (settings.getInt(lastCrash, 0) >= 3) {
+                    ps.println(MediaBitstreams.K_NATIVE_CRASH);
+                    return;
+                }
+                decoders = getDecodersForPath(fullPath);
+                frameChecksums = getFrameChecksumsForPath(relativePath);
+                ps.println(false);
+            } catch (Exception e) {
+                ps.println(true);
+                ps.println(e.toString());
+                return;
+            }
+
+            ps.println(decoders.size());
+            for (String name : decoders) {
+                ps.println(name);
+                String lastCrash = MediaBitstreams.generateCrashSignature(relativePath, name);
+                if (settings.getInt(lastCrash, 0) >= 3) {
+                    ps.println(MediaBitstreams.K_NATIVE_CRASH);
+                } else {
+                    ps.println(verifyBitstream(fullPath, name, frameChecksums));
+                }
+            }
+
+        }
+
+        private String verifyBitstream(String path, String name, List<String> frameChecksums)  {
+            MediaExtractor ex = new MediaExtractor();
+            MediaCodec d = null;
+            try {
+                MediaCodec decoder = d = MediaCodec.createByCodecName(name);
+                ex.setDataSource(path);
+                ex.selectTrack(0);
+                ex.seekTo(0, MediaExtractor.SEEK_TO_NEXT_SYNC);
+                Future<Boolean> conform = mExecutorService.submit(new Callable<Boolean>() {
+                    @Override
+                    public Boolean call() throws Exception {
+                        return MediaUtils.verifyDecoder(decoder, ex, frameChecksums);
+                    }
+                });
+                return conform.get(15, TimeUnit.SECONDS).toString();
+            } catch (Exception e) {
+                return e.toString();
+            } finally {
+                ex.release();
+                if (d != null) {
+                    d.release();
+                }
+            }
+        }
+
+    }
+
+    private void generateReportFile(String suffix, String reportKey, ReportCallback callback)
+            throws IOException, FileNotFoundException, Exception {
+
+        OutputStream out = new ByteArrayOutputStream(0);
+
+        try {
+
+            File tmpf = File.createTempFile(getClass().getSimpleName(), suffix, mAppCache);
+            Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+            Bundle bundle = new Bundle();
+            bundle.putString(MediaBitstreams.KEY_APP_CACHE_DIR, mAppCache.getCanonicalPath());
+            bundle.putString(reportKey, tmpf.getCanonicalPath());
+            inst.sendStatus(INST_STATUS_IN_PROGRESS, bundle);
+
+            out = new FileOutputStream(tmpf);
+            callback.run(out);
+            out.flush();
+
+        } finally {
+
+            out.close();
+
+        }
+    }
+
+    @Test
+    public void testGetBitstreamsFormats() throws Exception {
+        generateReportFile(".xml",
+                MediaBitstreams.KEY_BITSTREAMS_FORMATS_XML,
+                new GenerateBitstreamsFormatsXml());
+    }
+
+    @Test
+    public void testGetSupportedBitstreams() throws Exception {
+        generateReportFile(".txt",
+                MediaBitstreams.KEY_SUPPORTED_BITSTREAMS_TXT,
+                new GenerateSupportedBitstreamsFormatsTxt());
+    }
+
+    @Test
+    public void testBitstreamsConformance() throws Exception {
+        generateReportFile(".txt",
+                MediaBitstreams.KEY_BITSTREAMS_VALIDATION_TXT,
+                new TestBitstreamsConformance());
+    }
+
+    /**
+     * Converts a single media track format string into a MediaFormat object
+     *
+     * @param trackFormatString a string representation of the format of one media track
+     * @return a MediaFormat
+     */
+    private static MediaFormat parseTrackFormat(String trackFormatString) {
+        MediaFormat format = new MediaFormat();
+        format.setString(MediaFormat.KEY_MIME, "");
+        for (String entry : trackFormatString.split(",")) {
+            String[] kv = entry.split("=");
+            if (kv.length < 2 || kv[1].isEmpty()) {
+                continue;
+            }
+            String k = kv[0];
+            String v = kv[1];
+            try {
+                format.setInteger(k, Integer.parseInt(v));
+            } catch (NumberFormatException e) {
+                format.setString(k, v);
+            }
+        }
+        return format;
+    }
+}
diff --git a/hostsidetests/media/bitstreams/common/Android.mk b/hostsidetests/media/bitstreams/common/Android.mk
new file mode 100644
index 0000000..1691257
--- /dev/null
+++ b/hostsidetests/media/bitstreams/common/Android.mk
@@ -0,0 +1,31 @@
+# Copyright (C) 2017 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.
+
+###############################################################################
+# Build the common library for use device-side
+###############################################################################
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := media-bitstreams-common-devicesidelib
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/hostsidetests/media/bitstreams/common/src/android/media/cts/bitstreams/MediaBitstreams.java b/hostsidetests/media/bitstreams/common/src/android/media/cts/bitstreams/MediaBitstreams.java
new file mode 100644
index 0000000..2266254
--- /dev/null
+++ b/hostsidetests/media/bitstreams/common/src/android/media/cts/bitstreams/MediaBitstreams.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 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 android.media.cts.bitstreams;
+
+/**
+ * This class provides constants and utilities shared between the host-side and device-side test
+ * components.
+ */
+public class MediaBitstreams {
+
+    /* options */
+    public static final String OPT_HOST_BITSTEAMS_PATH = "host-bitsteams-path";
+    public static final String OPT_DEVICE_BITSTEAMS_PATH = "device-bitsteams-path";
+    public static final String OPT_DOWNLOAD_BITSTREAMS = "download-bitstreams";
+    public static final String OPT_DEBUG_TARGET_DEVICE = "debug-target-device";
+    public static final String OPT_BITSTREAMS_TO_TEST_TXT = "bitstreams-to-test-txt";
+    public static final String OPT_UTILIZATION_RATE = "utilization-rate";
+    public static final String OPT_NUM_BATCHES = "num-batches";
+    public static final String OPT_LAST_CRASH = "last-crash";
+    public static final String OPT_BITSTREAMS_PREFIX = "prefix";
+
+    /* defaults */
+    public static final String DEFAULT_HOST_BITSTREAMS_PATH = "TestVectorsIttiam";
+    public static final String DEFAULT_DEVICE_BITSTEAMS_PATH = "/data/local/tmp/TestVectorsIttiam";
+
+    /* metric keys */
+    public static final String KEY_BITSTREAMS_FORMATS_XML = "bitstreams-formats-xml";
+    public static final String KEY_SUPPORTED_BITSTREAMS_TXT = "supported-bitstreams-txt";
+    public static final String KEY_BITSTREAMS_VALIDATION_TXT = "bitstreams-validation-txt";
+    public static final String KEY_APP_CACHE_DIR = "app-cache-dir";
+    public static final String KEY_ERR_MSG = "err-msg";
+
+    /* constants */
+    public static final String K_MODULE = "CtsMediaBitstreamsTestCases";
+    public static final String K_BITSTREAMS_LIST_TXT = "bitstreamsFile.txt";
+    public static final String K_TEST_GET_SUPPORTED_BITSTREAMS = "testGetSupportedBitstreams";
+    public static final String K_NATIVE_CRASH = "native crash";
+
+    /* utilities */
+    /**
+     * @param bitstreamPath path of individual bitstream relative to bitstreams root,
+     * e.g. {@code h264/../../../../*.mp4}
+     * @return checksum file path for {@code bitstreamPath}, e.g. {@code h264/../../../../*_md5}.
+     */
+    public static String getMd5Path(String bitstreamPath) {
+        String base = bitstreamPath.split("\\.")[0];
+        String codec = bitstreamPath.split("/", 2)[0];
+        String md5Path = String.format("%s_%s_md5", base, codec);
+        return md5Path;
+    }
+
+    /**
+     * @param path relative bitstream path, e.g. {@code h264/../../../../*.mp4}
+     * @param name codec name, e.g. {@code OMX.google.h264.decoder}
+     * @return crash signature for a crashed device decoding session,
+     * in the form of {@code <bitstream path>:<codec name>}
+     */
+    public static String generateCrashSignature(String path, String name) {
+        return String.format("%s:%s", path, name);
+    }
+
+
+}
diff --git a/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/MediaBitstreamsTest.java b/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/MediaBitstreamsTest.java
new file mode 100644
index 0000000..5f5e4cd
--- /dev/null
+++ b/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/MediaBitstreamsTest.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright (C) 2017 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 android.media.cts.bitstreams;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.util.MetricsReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.util.FileUtil;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that verifies video bitstreams decode pixel perfectly
+ */
+@OptionClass(alias="media-bitstreams-test")
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class MediaBitstreamsTest implements IDeviceTest, IBuildReceiver, IAbiReceiver {
+
+    @Option(name = MediaBitstreams.OPT_HOST_BITSTEAMS_PATH,
+            description = "Absolute path of Ittiam bitstreams (host)",
+            mandatory = true)
+    private File mHostBitstreamsPath = new File(MediaBitstreams.DEFAULT_HOST_BITSTREAMS_PATH);
+
+    @Option(name = MediaBitstreams.OPT_DEVICE_BITSTEAMS_PATH,
+            description = "Absolute path of Ittiam bitstreams (device)")
+    private String mDeviceBitstreamsPath = MediaBitstreams.DEFAULT_DEVICE_BITSTEAMS_PATH;
+
+    @Option(name = MediaBitstreams.OPT_DOWNLOAD_BITSTREAMS,
+            description = "Whether to download the bitstreams files")
+    private boolean mDownloadBitstreams = false;
+
+    @Option(name = MediaBitstreams.OPT_UTILIZATION_RATE,
+            description = "Percentage of external storage space used for test")
+    private int mUtilizationRate = 80;
+
+    @Option(name = MediaBitstreams.OPT_NUM_BATCHES,
+            description = "Number of batches to test;"
+                    + " each batch uses external storage up to utilization rate")
+    private int mNumBatches = Integer.MAX_VALUE;
+
+    @Option(name = MediaBitstreams.OPT_DEBUG_TARGET_DEVICE,
+            description = "Whether to debug target device under test")
+    private boolean mDebugTargetDevice = false;
+
+    @Option(name = MediaBitstreams.OPT_BITSTREAMS_PREFIX,
+            description = "Only test bitstreams in this sub-directory")
+    private String mPrefix = "";
+
+    /**
+     * A helper to access resources in the build.
+     */
+    private CompatibilityBuildHelper mBuildHelper;
+
+    private IAbi mAbi;
+    private ITestDevice mDevice;
+
+    private MediaBitstreamsTest(String prefix) {
+        mPrefix = prefix;
+    }
+
+    public MediaBitstreamsTest() {
+        this("");
+    }
+
+    @Override
+    public void setBuild(IBuildInfo buildInfo) {
+        // Get the build, this is used to access the APK.
+        mBuildHelper = new CompatibilityBuildHelper(buildInfo);
+    }
+
+    @Override
+    public void setAbi(IAbi abi) {
+        mAbi = abi;
+    }
+
+    @Override
+    public void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+
+    @Override
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    /*
+     * Returns true if all necessary media files exist on the device, and false otherwise.
+     *
+     * This method is exposed for unit testing.
+     */
+    private boolean bitstreamsExistOnDevice(ITestDevice device)
+            throws DeviceNotAvailableException {
+        return device.doesFileExist(mDeviceBitstreamsPath)
+                && device.isDirectory(mDeviceBitstreamsPath);
+    }
+
+    private String getCurrentMethod() {
+        return Thread.currentThread().getStackTrace()[2].getMethodName();
+    }
+
+    Map<String, String> getArgs() {
+        Map<String, String> args = new HashMap<>();
+        args.put(MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, Boolean.toString(mDebugTargetDevice));
+        args.put(MediaBitstreams.OPT_DEVICE_BITSTEAMS_PATH, mDeviceBitstreamsPath);
+        return args;
+    }
+
+    private class ProcessBitstreamsFormats extends ReportProcessor {
+
+        @Override
+        void setUp(ITestDevice device) throws DeviceNotAvailableException {
+            if (mDownloadBitstreams || !bitstreamsExistOnDevice(device)) {
+                device.pushDir(mHostBitstreamsPath, mDeviceBitstreamsPath);
+            }
+        }
+
+        @Override
+        Map<String, String> getArgs() {
+            return MediaBitstreamsTest.this.getArgs();
+        }
+
+        @Override
+        void process(ITestDevice device, String reportPath)
+                throws DeviceNotAvailableException, IOException {
+            File testDir = mBuildHelper.getTestsDir();
+            File dynamicConfigFile = new File(testDir, MediaBitstreams.K_MODULE + ".dynamic");
+            device.pullFile(reportPath, dynamicConfigFile);
+            CLog.i("Pulled bitstreams formats to %s", dynamicConfigFile.getPath());
+        }
+
+    }
+
+    private class ProcessBitstreamsValidation extends ReportProcessor {
+
+        Set<String> mBitstreams;
+        Deque<String> mProcessedBitstreams = new ArrayDeque<>();
+        private final String mMethodName;
+        private final String mBitstreamsListTxt = new File(
+                mDeviceBitstreamsPath,
+                MediaBitstreams.K_BITSTREAMS_LIST_TXT).toString();
+        private String mLastCrash;
+
+        ProcessBitstreamsValidation(Set<String> bitstreams, String methodName) {
+            mBitstreams = bitstreams;
+            mMethodName = methodName;
+        }
+
+        private String getBitstreamsListString() {
+            OutputStream baos = new ByteArrayOutputStream();
+            PrintStream ps = new PrintStream(baos, true);
+            try {
+                for (String b : mBitstreams) {
+                    ps.println(b);
+                }
+                return baos.toString();
+            } finally {
+                ps.close();
+            }
+        }
+
+        private void pushBitstreams(ITestDevice device)
+                throws IOException, DeviceNotAvailableException {
+            File tmp = null;
+            try {
+                CLog.i("Pushing %d bitstream(s) from %s to %s",
+                        mBitstreams.size(),
+                        mHostBitstreamsPath,
+                        mDeviceBitstreamsPath);
+                tmp = Files.createTempDirectory(null).toFile();
+                for (String b : mBitstreams) {
+                    String m = MediaBitstreams.getMd5Path(b);
+                    for (String f : new String[] {m, b}) {
+                        File tmpf = new File(tmp, f);
+                        new File(tmpf.getParent()).mkdirs();
+                        FileUtil.copyFile(new File(mHostBitstreamsPath, f), tmpf);
+                    }
+                }
+                device.executeShellCommand(String.format("rm -rf %s", mDeviceBitstreamsPath));
+                device.pushDir(tmp, mDeviceBitstreamsPath);
+                device.pushString(getBitstreamsListString(), mBitstreamsListTxt);
+            } finally {
+                FileUtil.recursiveDelete(tmp);
+            }
+        }
+
+        @Override
+        void setUp(ITestDevice device) throws DeviceNotAvailableException, IOException {
+            pushBitstreams(device);
+        }
+
+        @Override
+        Map<String, String> getArgs() {
+            Map<String, String> args = MediaBitstreamsTest.this.getArgs();
+            if (mLastCrash != null) {
+                args.put(MediaBitstreams.OPT_LAST_CRASH, mLastCrash);
+            }
+            return args;
+        }
+
+        private void parse(ITestDevice device, String reportPath)
+                throws DeviceNotAvailableException {
+            String[] lines = getReportLines(device, reportPath);
+            mProcessedBitstreams.clear();
+            for (int i = 0; i < lines.length;) {
+
+                String path = lines[i++];
+                mProcessedBitstreams.add(path);
+                String className = MediaBitstreamsTest.class.getCanonicalName();
+                MetricsReportLog report = new MetricsReportLog(
+                        mBuildHelper.getBuildInfo(), mAbi.getName(),
+                        String.format("%s#%s", className, mMethodName),
+                        getClass().getSimpleName(), path);
+
+                boolean failedEarly;
+                String errMsg;
+                if (i < lines.length) {
+                    failedEarly = Boolean.parseBoolean(lines[i++]);
+                    errMsg = failedEarly ? lines[i++] : "";
+                } else {
+                    failedEarly = true;
+                    errMsg = MediaBitstreams.K_NATIVE_CRASH;
+                    mLastCrash = MediaBitstreams.generateCrashSignature(path, "");
+                    mProcessedBitstreams.removeLast();
+                }
+                if (failedEarly) {
+                    String keyErrMsg = MediaBitstreams.KEY_ERR_MSG;
+                    report.addValue(keyErrMsg, errMsg, ResultType.NEUTRAL, ResultUnit.NONE);
+                    report.submit();
+                    continue;
+                }
+
+                int n = Integer.parseInt(lines[i++]);
+                for (int j = 0; j < n && i < lines.length; j++) {
+                    String name = lines[i++];
+                    String result;
+                    if (i < lines.length) {
+                        result = lines[i++];
+                    } else {
+                        result = MediaBitstreams.K_NATIVE_CRASH;
+                        mLastCrash = MediaBitstreams.generateCrashSignature(path, name);
+                        mProcessedBitstreams.removeLast();
+                    }
+                    report.addValue(name, result, ResultType.NEUTRAL, ResultUnit.NONE);
+                }
+                report.submit();
+
+            }
+        }
+
+        @Override
+        void process(ITestDevice device, String reportPath)
+                throws DeviceNotAvailableException, IOException {
+            parse(device, reportPath);
+        }
+
+        @Override
+        boolean recover(ITestDevice device, String reportPath)
+                throws DeviceNotAvailableException, IOException {
+            try {
+                parse(device, reportPath);
+                mBitstreams.removeAll(mProcessedBitstreams);
+                device.pushString(getBitstreamsListString(), mBitstreamsListTxt);
+                return true;
+            } catch (RuntimeException e) {
+                CLog.e("Error parsing report; saving report to %s", device.pullFile(reportPath));
+                CLog.e(e);
+                return false;
+            }
+        }
+
+    }
+
+    @Ignore
+    @Test
+    public void testGetBitstreamsFormats() throws DeviceNotAvailableException, IOException {
+        ReportProcessor processor = new ProcessBitstreamsFormats();
+        processor.processDeviceReport(
+                getDevice(),
+                mBuildHelper.getTestsDir(),
+                getCurrentMethod(), MediaBitstreams.KEY_BITSTREAMS_FORMATS_XML);
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpBitrate() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/bitrate");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpLevels() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/levels");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpParamsCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/params/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpParamsCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/params/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpParamsCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/params/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpParamsCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/params/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpParamsCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/params/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpResolutions() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/resolutions");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpSlicesCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/slices/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpSlicesCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/slices/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpSlicesCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/slices/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpSlicesCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/slices/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitBpSlicesCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/bp/slices/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpBitrate() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/bitrate");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpGopCrowd_640x360p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/gop/crowd_640x360p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpGopCrowd_854x480p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/gop/crowd_854x480p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpGopCrowd_1280x720p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/gop/crowd_1280x720p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpGopCrowd_1920x1080p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/gop/crowd_1920x1080p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpGopCrowd_3840x2160p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/gop/crowd_3840x2160p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpLevels() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/levels");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpParamsCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/params/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpParamsCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/params/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpParamsCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/params/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpParamsCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/params/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpParamsCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/params/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpResolutions() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/resolutions");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpSlicesCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/slices/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpSlicesCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/slices/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpSlicesCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/slices/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpSlicesCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/slices/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitMpSlicesCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/mp/slices/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpBitrate() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/bitrate");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpGopCrowd_640x360p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/gop/crowd_640x360p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpGopCrowd_854x480p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/gop/crowd_854x480p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpGopCrowd_1280x720p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/gop/crowd_1280x720p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpGopCrowd_1920x1080p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/gop/crowd_1920x1080p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpGopCrowd_3840x2160p50() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/gop/crowd_3840x2160p50");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpLevels() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/levels");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpParamsCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/params/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpParamsCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/params/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpParamsCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/params/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpParamsCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/params/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpParamsCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/params/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpResolutions() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/resolutions");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpScalingmatrixCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/scalingmatrix/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpScalingmatrixCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/scalingmatrix/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpScalingmatrixCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/scalingmatrix/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpScalingmatrixCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/scalingmatrix/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpScalingmatrixCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/scalingmatrix/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpSlicesCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/slices/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpSlicesCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/slices/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpSlicesCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/slices/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpSlicesCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/slices/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testH264Yuv420_8bitHpSlicesCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("h264/yuv420/8bit/hp/slices/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitBitrate() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/bitrate");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitParamsCrowd_640x360p50f32() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/params/crowd_640x360p50f32");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitParamsCrowd_854x480p50f32() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/params/crowd_854x480p50f32");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitParamsCrowd_1280x720p50f32() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/params/crowd_1280x720p50f32");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitParamsCrowd_1920x1080p50f32() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/params/crowd_1920x1080p50f32");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitParamsCrowd_3840x2160p50f32() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/params/crowd_3840x2160p50f32");
+    }
+
+    @Test
+    public void testVp8Yuv420_8bitResolution() throws Exception {
+        testBitstreamsConformance("vp8/yuv420/8bit/resolution");
+    }
+
+    @Ignore
+    @Test
+    public void testBitstreamsConformance()
+            throws DeviceNotAvailableException, IOException {
+        testBitstreamsConformance(mPrefix);
+    }
+
+    private void testBitstreamsConformance(String prefix)
+            throws DeviceNotAvailableException, IOException {
+
+        ITestDevice device = getDevice();
+        SupportedBitstreamsProcessor preparer;
+        preparer = new SupportedBitstreamsProcessor(prefix, mDebugTargetDevice);
+        preparer.processDeviceReport(
+                device,
+                mBuildHelper.getTestsDir(),
+                MediaBitstreams.K_TEST_GET_SUPPORTED_BITSTREAMS,
+                MediaBitstreams.KEY_SUPPORTED_BITSTREAMS_TXT);
+        Set<String> supportedBitstreams = preparer.getSupportedBitstreams();
+        CLog.i("%d supported bitstreams under %s", supportedBitstreams.size(), prefix);
+
+        int n = 0;
+        long size = 0;
+        long limit = device.getExternalStoreFreeSpace() * mUtilizationRate * 1024 / 100;
+
+        String currentMethod = getCurrentMethod();
+        Set<String> bitstreams = new LinkedHashSet<>();
+        Iterator<String> iter = supportedBitstreams.iterator();
+
+        for (int i = 0; i < supportedBitstreams.size(); i++) {
+
+            if (n >= mNumBatches) {
+                break;
+            }
+
+            String bitstreamPath = iter.next();
+            File bitstreamFile = new File(mHostBitstreamsPath, bitstreamPath);
+            String md5Path = MediaBitstreams.getMd5Path(bitstreamPath);
+            File md5File = new File(mHostBitstreamsPath, md5Path);
+
+            if (md5File.exists() && bitstreamFile.exists()) {
+                size += md5File.length();
+                size += bitstreamFile.length();
+                bitstreams.add(bitstreamPath);
+            }
+
+            if (size > limit || i + 1 == supportedBitstreams.size()) {
+                ReportProcessor processor;
+                processor = new ProcessBitstreamsValidation(bitstreams, currentMethod);
+                processor.processDeviceReport(
+                        device,
+                        mBuildHelper.getTestsDir(),
+                        currentMethod, MediaBitstreams.KEY_BITSTREAMS_VALIDATION_TXT);
+                bitstreams.clear();
+                size = 0;
+                n++;
+            }
+
+        }
+
+    }
+
+
+}
diff --git a/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/ReportProcessor.java b/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/ReportProcessor.java
new file mode 100644
index 0000000..8c41347
--- /dev/null
+++ b/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/ReportProcessor.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017 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 android.media.cts.bitstreams;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.testtype.AndroidJUnitTest;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A {@link ReportProcessor} installs {@code CtsMediaBitstreamsDeviceSideTestApp.apk}
+ * onto a test device, invokes a report-generating test, then pulls and processes
+ * the generated report.
+ */
+abstract class ReportProcessor {
+
+    private final Map<String, String> mMetrics = new HashMap<>();
+    private String mFailureStackTrace = null;
+
+    private static final String APP_APK = "CtsMediaBitstreamsDeviceSideTestApp.apk";
+    private static final String APP_CLS_NAME = "MediaBitstreamsDeviceSideTest";
+    private static final String APP_PKG_NAME = "android.media.cts.bitstreams.app";
+
+    /**
+     * Setup {@code device} before test.
+     *
+     * @param device device under test
+     * @throws DeviceNotAvailableException
+     * @throws IOException
+     */
+    void setUp(ITestDevice device) throws DeviceNotAvailableException, IOException {}
+
+    Map<String, String> getArgs() {
+        return Collections.emptyMap();
+    }
+
+    /**
+     * Process test report.
+     *
+     * @param device device under test
+     * @param reportPath path to test report on {@code device}
+     * @throws DeviceNotAvailableException
+     * @throws IOException
+     */
+    void process(ITestDevice device, String reportPath)
+            throws DeviceNotAvailableException, IOException {}
+
+    /**
+     * Attempt to recover from a crash during test on {@code device}.
+     *
+     * @param device device under test
+     * @param reportPath path to test report on {@code device}
+     * @throws DeviceNotAvailableException
+     * @throws IOException
+     * @return true if successfully recovered from test crash, false otherwise
+     */
+    boolean recover(ITestDevice device, String reportPath)
+            throws DeviceNotAvailableException, IOException {
+        return false;
+    }
+
+    /**
+     * Cleanup {@code device} after test
+     * @param device device under test
+     * @param reportPath path to test report on {@code device}
+     */
+    void cleanup(ITestDevice device, String reportPath) {
+        try {
+            device.executeShellCommand(String.format("rm %s", reportPath));
+        } catch (DeviceNotAvailableException e) {
+            CLog.e(e);
+        }
+    }
+
+    /**
+     * @param device device under test
+     * @param reportPath path to test report on {@code device}
+     * @return array of lines in report, sans newline
+     * @throws DeviceNotAvailableException
+     */
+    static String[] getReportLines(ITestDevice device, String reportPath)
+            throws DeviceNotAvailableException {
+        String cat = String.format("cat %s", reportPath);
+        String output = device.executeShellCommand(cat);
+        return output.isEmpty() ? new String[0] : output.split("\n");
+    }
+
+    /* Special listener for setting MediaPreparer instance variable values */
+    private class MediaBitstreamsListener implements ITestInvocationListener {
+
+        @Override
+        public void testEnded(TestIdentifier test, Map<String, String> metrics) {
+            mMetrics.putAll(metrics);
+        }
+
+        @Override
+        public void testFailed(TestIdentifier test, String trace) {
+            mFailureStackTrace = trace;
+        }
+
+    }
+
+    private boolean runDeviceTest(
+            ITestDevice device, File testDir, String method, String reportKey,
+            int testTimeout, long shellTimeout)
+            throws DeviceNotAvailableException {
+
+        File apkFile = new File(testDir, APP_APK);
+        device.installPackage(apkFile, true, true);
+
+        String fullTestName = String.format("%s.%s#%s", APP_PKG_NAME, APP_CLS_NAME, method);
+        AndroidJUnitTest instrTest = new AndroidJUnitTest();
+        instrTest.setDevice(device);
+        instrTest.setPackageName(APP_PKG_NAME);
+        instrTest.addIncludeFilter(fullTestName);
+        instrTest.setTestTimeout(testTimeout);
+        instrTest.setShellTimeout(shellTimeout);
+        for (Entry<String, String> e : getArgs().entrySet()) {
+            instrTest.addInstrumentationArg(e.getKey(), e.getValue());
+        }
+        instrTest.run(new MediaBitstreamsListener());
+
+        return checkFile(reportKey);
+
+    }
+
+    private boolean checkFile(String reportKey) {
+        if (mFailureStackTrace != null) {
+            CLog.w("Retrieving bitstreams formats failed with trace:\n%s", mFailureStackTrace);
+            mFailureStackTrace = null;
+            return false;
+        } else if (!mMetrics.containsKey(reportKey)) {
+            CLog.w("Failed to generate file key=%s on device", reportKey);
+            return false;
+        }
+        return true;
+    }
+
+    void processDeviceReport(
+            ITestDevice device, File testDir, String method, String reportKey)
+            throws DeviceNotAvailableException, IOException {
+        try {
+            setUp(device);
+            while (!runDeviceTest(device, testDir, method, reportKey, 0, 0)) {
+                if (!recover(device, mMetrics.get(reportKey))) {
+                    return;
+                }
+            }
+            process(device, mMetrics.get(reportKey));
+        } finally {
+            cleanup(device, mMetrics.get(reportKey));
+        }
+    }
+
+}
diff --git a/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/SupportedBitstreamsProcessor.java b/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/SupportedBitstreamsProcessor.java
new file mode 100644
index 0000000..8a6c74e
--- /dev/null
+++ b/hostsidetests/media/bitstreams/src/android/media/cts/bitstreams/SupportedBitstreamsProcessor.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 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 android.media.cts.bitstreams;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Runs a test on target device to query support for bitstreams listed in device side
+ * dynamic configuration file.
+ */
+public class SupportedBitstreamsProcessor extends ReportProcessor {
+
+    private final String mPrefix;
+    private final boolean mDebugTargetDevice;
+    private final Set<String> mSupportedBitstreams = new LinkedHashSet<>();
+
+    public SupportedBitstreamsProcessor() {
+        this("",false);
+    }
+
+    /**
+     * @param prefix only bitstreams whose relative path starts with {@code prefix}
+     * would be processed
+     * @param debugTargetDevice whether to pause {@code device} for debugging
+     */
+    public SupportedBitstreamsProcessor(String prefix, boolean debugTargetDevice) {
+        mPrefix = prefix;
+        mDebugTargetDevice = debugTargetDevice;
+    }
+
+    /**
+     * @return paths of supported devices on device
+     */
+    public Set<String> getSupportedBitstreams() {
+        return mSupportedBitstreams;
+    }
+
+    @Override
+    Map<String, String> getArgs() {
+        Map<String, String> args = new HashMap<>();
+        args.put(MediaBitstreams.OPT_BITSTREAMS_PREFIX, mPrefix);
+        args.put(MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, Boolean.toString(mDebugTargetDevice));
+        return args;
+    }
+
+    @Override
+    void process(ITestDevice device, String reportPath) throws DeviceNotAvailableException {
+        mSupportedBitstreams.clear();
+        for (String path: getReportLines(device, reportPath)) {
+            if (path.isEmpty()) {
+                continue;
+            }
+            mSupportedBitstreams.add(path);
+        }
+    }
+
+}