Merge "Fix CameraManagerTest logic." into marshmallow-cts-dev
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index d8a96eb..f4d7825 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -339,8 +339,7 @@
     <string name="hifi_ultrasound_test_default_false_string">false</string>
     <string name="hifi_ultrasound_test_mic_no_support">
         Device does not support near-ultrasound recording.\n
-        All new phones and tablets MUST support near-ultrasound recording.\n
-        Report FAIL if this is a new device, report PASS if this is an updating device.\n</string>
+        Please report PASS.\n</string>
     <string name="hifi_ultrasound_test_spkr_no_support">
         Device does not support near-ultrasound playback.\n
         If this is your reference device, please use a different reference device.\n</string>
@@ -360,8 +359,7 @@
         If this is your reference device, please use a different reference device.\n</string>
     <string name="hifi_ultrasound_speaker_test_spkr_no_support">
         Device does not support near-ultrasound playback.\n
-        All new phones and tablets MUST support near-ultrasound playback.\n
-        Report FAIL if this is a new device, report PASS if this is an updating device.\n</string>
+        Please report PASS.\n</string>
     <string name="hifi_ultrasound_speaker_test_test_side">
         Please wait for the result on the reference device then report here.</string>
     <string name="hifi_ultrasound_speaker_test_reference_side">
@@ -1732,7 +1730,7 @@
     <string name="device_owner_wifi_lockdown_info">
             Please enter the SSID and auth method of an available WiFi Access Point and press the button to create a
             WiFi configuration. This configuration can be seen on Settings &gt; WiFi. The test cases
-            are going use this config. Please go through test cases in order (from top to bottom).
+            are going to use this config. Please go through test cases in order (from top to bottom).
     </string>
     <string name="switch_wifi_lockdown_off_button">WiFi config lockdown off</string>
     <string name="switch_wifi_lockdown_on_button">WiFi config lockdown on</string>
@@ -1854,6 +1852,18 @@
     <string name="device_owner_user_restriction_set">Set restriction</string>
     <string name="device_owner_settings_go">Go</string>
 
+    <string name="device_owner_disallow_config_vpn">Disallow configuring VPN</string>
+    <string name="device_owner_disallow_config_vpn_info">
+        Please press the Set VPN restriction button to set the VPN restriction.
+        Then press Go to open the VPN page.\n\n
+        Confirm that:\n
+        - You cannot add a new VPN network.\n
+        - You cannot edit, add or remove any existing VPNs.\n
+        \n
+        Use the Back button to return to this page.
+    </string>
+    <string name="device_owner_user_vpn_restriction_set">Set VPN restriction</string>
+
     <!-- Strings for JobScheduler Tests -->
     <string name="js_test_description">This test is mostly automated, but requires some user interaction. You can pass this test once the list items below are checked.</string>
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodFlowTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodFlowTestActivity.java
index b129665..a5022a3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodFlowTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/ByodFlowTestActivity.java
@@ -347,6 +347,9 @@
         adapter.add(mKeyguardDisabledFeaturesTest);
         adapter.add(mAuthenticationBoundKeyTest);
 
+        /* If there is an application that handles ACTION_IMAGE_CAPTURE, test that it handles it
+         * well.
+         */
         if (canResolveIntent(ByodHelperActivity.getCaptureImageIntent())) {
             // Capture image intent can be resolved in primary profile, so test.
             mCrossProfileImageCaptureSupportTest = new DialogTestListItem(this,
@@ -362,6 +365,9 @@
                     .show();
         }
 
+        /* If there is an application that handles ACTION_VIDEO_CAPTURE, test that it handles it
+         * well.
+         */
         if (canResolveIntent(ByodHelperActivity.getCaptureVideoIntent())) {
             // Capture video intent can be resolved in primary profile, so test.
             mCrossProfileVideoCaptureSupportTest = new DialogTestListItem(this,
@@ -409,7 +415,9 @@
             adapter.add(mDisableNfcBeamTest);
         }
 
-        /* TODO: reinstate when bug b/20131958 is fixed
+        /* If there is an application that handles RECORD_SOUND_ACTION, test that it handles it
+         * well.
+         */
         if (canResolveIntent(ByodHelperActivity.getCaptureAudioIntent())) {
             // Capture audio intent can be resolved in primary profile, so test.
             mCrossProfileAudioCaptureSupportTest = new DialogTestListItem(this,
@@ -424,7 +432,6 @@
                     R.string.provisioning_byod_no_audio_capture_resolver, Toast.LENGTH_SHORT)
                     .show();
         }
-        */
     }
 
     // Return whether the intent can be resolved in the current profile
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
index a6a5e5a..79b9933 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
@@ -75,6 +75,9 @@
             PermissionLockdownTestActivity.class.getName();
     private static final String DISALLOW_CONFIG_BT_ID = "DISALLOW_CONFIG_BT";
     private static final String DISALLOW_CONFIG_WIFI_ID = "DISALLOW_CONFIG_WIFI";
+    private static final String DISALLOW_CONFIG_VPN_ID = "DISALLOW_CONFIG_VPN";
+    //TODO(rgl): This symbol should be available in android.provider.settings
+    private static final String ACTION_VPN_SETTINGS = "android.net.vpn.SETTINGS";
     private static final String REMOVE_DEVICE_OWNER_TEST_ID = "REMOVE_DEVICE_OWNER";
 
     @Override
@@ -166,6 +169,19 @@
                                     new Intent(Settings.ACTION_WIFI_SETTINGS))}));
         }
 
+        // DISALLOW_CONFIG_VPN
+        adapter.add(createInteractiveTestItem(this, DISALLOW_CONFIG_VPN_ID,
+                R.string.device_owner_disallow_config_vpn,
+                R.string.device_owner_disallow_config_vpn_info,
+                new ButtonInfo[] {
+                        new ButtonInfo(
+                                R.string.device_owner_user_vpn_restriction_set,
+                                createSetUserRestrictionIntent(
+                                        UserManager.DISALLOW_CONFIG_VPN)),
+                        new ButtonInfo(
+                                R.string.device_owner_settings_go,
+                                new Intent(ACTION_VPN_SETTINGS))}));
+
         // DISALLOW_CONFIG_BLUETOOTH
         if (packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {
             adapter.add(createInteractiveTestItem(this, DISALLOW_CONFIG_BT_ID,
@@ -326,6 +342,7 @@
             dpm.setKeyguardDisabled(admin, false);
             dpm.clearUserRestriction(admin, UserManager.DISALLOW_CONFIG_BLUETOOTH);
             dpm.clearUserRestriction(admin, UserManager.DISALLOW_CONFIG_WIFI);
+            dpm.clearUserRestriction(admin, UserManager.DISALLOW_CONFIG_VPN);
             dpm.clearDeviceOwnerApp(getPackageName());
         }
     }
diff --git a/libs/deviceutil/src/android/cts/util/MediaUtils.java b/libs/deviceutil/src/android/cts/util/MediaUtils.java
index b3ebbad..0a454b6 100755
--- a/libs/deviceutil/src/android/cts/util/MediaUtils.java
+++ b/libs/deviceutil/src/android/cts/util/MediaUtils.java
@@ -160,6 +160,14 @@
         return null;
     }
 
+    public static boolean canEncode(MediaFormat format) {
+        if (sMCL.findEncoderForFormat(format) == null) {
+            Log.i(TAG, "no encoder for " + format);
+            return false;
+        }
+        return true;
+    }
+
     public static boolean canDecode(MediaFormat format) {
         if (sMCL.findDecoderForFormat(format) == null) {
             Log.i(TAG, "no decoder for " + format);
@@ -407,6 +415,10 @@
         return canDecode(format);
     }
 
+    public static boolean checkEncoderForFormat(MediaFormat format) {
+        return check(canEncode(format), "no encoder for " + format);
+    }
+
     public static boolean checkDecoderForFormat(MediaFormat format) {
         return check(canDecode(format), "no decoder for " + format);
     }
diff --git a/tests/tests/content/src/android/content/cts/ContextWrapperTest.java b/tests/tests/content/src/android/content/cts/ContextWrapperTest.java
index 672d3ed..9d9e1663 100644
--- a/tests/tests/content/src/android/content/cts/ContextWrapperTest.java
+++ b/tests/tests/content/src/android/content/cts/ContextWrapperTest.java
@@ -82,7 +82,7 @@
     public static final String PERMISSION_GRANTED = "android.content.cts.permission.TEST_GRANTED";
     public static final String PERMISSION_DENIED = "android.content.cts.permission.TEST_DENIED";
 
-    private static final int BROADCAST_TIMEOUT = 10000;
+    private static final int BROADCAST_TIMEOUT = 15000;
 
     private Context mContext;
 
@@ -216,13 +216,13 @@
 
         // Test unwanted intent(action = MOCK_ACTION2)
         broadcastReceiver.reset();
-        waitForFilteredIntent(mContextWrapper, broadcastReceiver, MOCK_ACTION2);
+        waitForFilteredIntent(mContextWrapper, MOCK_ACTION2);
         assertFalse(broadcastReceiver.hadReceivedBroadCast1());
         assertFalse(broadcastReceiver.hadReceivedBroadCast2());
 
         // Send wanted intent(action = MOCK_ACTION1)
         broadcastReceiver.reset();
-        waitForFilteredIntent(mContextWrapper, broadcastReceiver, MOCK_ACTION1);
+        waitForFilteredIntent(mContextWrapper, MOCK_ACTION1);
         assertTrue(broadcastReceiver.hadReceivedBroadCast1());
         assertFalse(broadcastReceiver.hadReceivedBroadCast2());
 
@@ -235,13 +235,13 @@
 
         // Test unwanted intent(action = MOCK_ACTION2)
         broadcastReceiver2.reset();
-        waitForFilteredIntent(mContextWrapper, broadcastReceiver2, MOCK_ACTION2);
+        waitForFilteredIntent(mContextWrapper, MOCK_ACTION2);
         assertFalse(broadcastReceiver2.hadReceivedBroadCast1());
         assertFalse(broadcastReceiver2.hadReceivedBroadCast2());
 
         // Send wanted intent(action = MOCK_ACTION1), but the receiver is unregistered.
         broadcastReceiver2.reset();
-        waitForFilteredIntent(mContextWrapper, broadcastReceiver2, MOCK_ACTION1);
+        waitForFilteredIntent(mContextWrapper, MOCK_ACTION1);
         assertFalse(broadcastReceiver2.hadReceivedBroadCast1());
         assertFalse(broadcastReceiver2.hadReceivedBroadCast2());
     }
@@ -256,13 +256,13 @@
 
         // Test unwanted intent(action = MOCK_ACTION2)
         broadcastReceiver.reset();
-        waitForFilteredIntent(mContextWrapper, broadcastReceiver, MOCK_ACTION2);
+        waitForFilteredIntent(mContextWrapper, MOCK_ACTION2);
         assertFalse(broadcastReceiver.hadReceivedBroadCast1());
         assertFalse(broadcastReceiver.hadReceivedBroadCast2());
 
         // Send wanted intent(action = MOCK_ACTION1)
         broadcastReceiver.reset();
-        waitForFilteredIntent(mContextWrapper, broadcastReceiver, MOCK_ACTION1);
+        waitForFilteredIntent(mContextWrapper, MOCK_ACTION1);
         assertTrue(broadcastReceiver.hadReceivedBroadCast1());
         assertFalse(broadcastReceiver.hadReceivedBroadCast2());
 
@@ -792,8 +792,8 @@
         waitForCondition(con);
     }
 
-    private void waitForFilteredIntent(ContextWrapper contextWrapper,
-            final FilteredReceiver receiver, final String action) throws InterruptedException {
+    private void waitForFilteredIntent(ContextWrapper contextWrapper, final String action)
+            throws InterruptedException {
         contextWrapper.sendOrderedBroadcast(new Intent(action), null);
 
         synchronized (mLockObj) {
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
index 6d62067..28693c6 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
@@ -178,9 +178,10 @@
 
             if (activeArraySize.getWidth() >= FULLHD.getWidth() &&
                     activeArraySize.getHeight() >= FULLHD.getHeight()) {
-                assertArrayContains(String.format(
+                assertArrayContainsAnyOf(String.format(
                         "Required FULLHD size not found for format %x for: ID %s",
-                        ImageFormat.JPEG, mIds[counter]), jpegSizes, FULLHD);
+                        ImageFormat.JPEG, mIds[counter]), jpegSizes,
+                        new Size[] {FULLHD, FULLHD_ALT});
             }
 
             if (activeArraySize.getWidth() >= HD.getWidth() &&
diff --git a/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java b/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java
index 40934f5..7c4d1fb 100644
--- a/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java
+++ b/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java
@@ -16,6 +16,7 @@
 
 package android.media.cts;
 
+import android.cts.util.MediaUtils;
 import android.graphics.ImageFormat;
 import android.media.Image;
 import android.media.MediaCodec;
@@ -284,6 +285,10 @@
 
         @Override
         public void run() {
+            if (mTest.shouldSkip()) {
+                return;
+            }
+
             InputSurface inputSurface = null;
             try {
                 if (!mUsePersistentInput) {
@@ -339,6 +344,22 @@
         mAllowBT709 = allowBT709;
     }
 
+    private boolean shouldSkip() {
+        if (!MediaUtils.hasEncoder(mMimeType)) {
+            return true;
+        }
+
+        MediaFormat format = MediaFormat.createVideoFormat(mMimeType, mWidth, mHeight);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
+        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
+        if (!MediaUtils.checkEncoderForFormat(format)) {
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Tests encoding and subsequently decoding video from frames generated into a buffer.
      * <p>
@@ -348,6 +369,10 @@
      * See http://b.android.com/37769 for a discussion of input format pitfalls.
      */
     private void encodeDecodeVideoFromBuffer(boolean toSurface) throws Exception {
+        if (shouldSkip()) {
+            return;
+        }
+
         MediaCodec encoder = null;
         MediaCodec decoder = null;
 
diff --git a/tools/cts-media/get_achievable_rates.py b/tools/cts-media/get_achievable_rates.py
new file mode 100755
index 0000000..81412da
--- /dev/null
+++ b/tools/cts-media/get_achievable_rates.py
@@ -0,0 +1,396 @@
+#!/usr/bin/python
+# Copyright (C) 2015 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.
+#
+
+import argparse, math, re, sys
+import xml.etree.ElementTree as ET
+from collections import defaultdict, namedtuple
+import itertools
+
+
+def createLookup(values, key):
+  """Creates a lookup table for a collection of values based on keys.
+
+  Arguments:
+    values: a collection of arbitrary values. Must be iterable.
+    key: a function of one argument that returns the key for a value.
+
+  Returns:
+    A dict mapping keys (as generated by the key argument) to lists of
+    values. All values in the lists have the same key, and are in the order
+    they appeared in the collection.
+  """
+  lookup = defaultdict(list)
+  for v in values:
+    lookup[key(v)].append(v)
+  return lookup
+
+
+def _intify(value):
+  """Returns a value converted to int if possible, else the original value."""
+  try:
+    return int(value)
+  except ValueError:
+    return value
+
+
+class Size(namedtuple('Size', ['width', 'height'])):
+  """A namedtuple with width and height fields."""
+  def __str__(self):
+    return '%dx%d' % (self.width, self.height)
+
+
+class _VideoResultBase(object):
+  """Helper methods for results. Not for use by applications.
+
+  Attributes:
+    codec: The name of the codec (string) or None
+    size: Size representing the video size or None
+    mime: The mime-type of the codec (string) or None
+    rates: The measured achievable frame rates
+    is_decoder: True iff codec is a decoder.
+  """
+
+  def __init__(self, is_decoder):
+    self.codec = None
+    self.mime = None
+    self.size = None
+    self._rates_from_failure = []
+    self._rates_from_message = []
+    self.is_decoder = is_decoder
+
+  def _inited(self):
+    """Returns true iff codec, mime and size was set."""
+    return None not in (self.codec, self.mime, self.size)
+
+  def __len__(self):
+    # don't report any result if codec name, mime type and size is unclear
+    if not self._inited():
+      return 0
+    return len(self.rates)
+
+  @property
+  def rates(self):
+    return self._rates_from_failure or self._rates_from_message
+
+  def _parseDict(self, value):
+    """Parses a MediaFormat from its string representation sans brackets."""
+    return dict((k, _intify(v))
+                for k, v in re.findall(r'([^ =]+)=([^ [=]+(?:|\[[^\]]+\]))(?:, |$)', value))
+
+  def _cleanFormat(self, format):
+    """Removes internal fields from a parsed MediaFormat."""
+    format.pop('what', None)
+    format.pop('image-data', None)
+
+  MESSAGE_PATTERN = r'(?P<key>\w+)=(?P<value>\{[^}]*\}|[^ ,{}]+)'
+
+  def _parsePartialResult(self, message_match):
+    """Parses a partial test result conforming to the message pattern.
+
+    Returns:
+      A tuple of string key and int, string or dict value, where dict has
+      string keys mapping to int or string values.
+    """
+    key, value = message_match.group('key', 'value')
+    if value.startswith('{'):
+      value = self._parseDict(value[1:-1])
+      if key.endswith('Format'):
+        self._cleanFormat(value)
+    else:
+      value = _intify(value)
+    return key, value
+
+  def _parseValuesFromBracket(self, line):
+    """Returns the values enclosed in brackets without the brackets.
+
+    Parses a line matching the pattern "<tag>: [<values>]" and returns <values>.
+
+    Raises:
+      ValueError: if the line does not match the pattern.
+    """
+    try:
+      return re.match(r'^[^:]+: *\[(?P<values>.*)\]\.$', line).group('values')
+    except AttributeError:
+      raise ValueError('line does not match "tag: [value]": %s' % line)
+
+  def _parseRawData(self, line):
+    """Parses the raw data line for video performance tests.
+
+    Yields:
+      Dict objects corresponding to parsed results, mapping string keys to
+      int, string or dict values.
+    """
+    try:
+      values = self._parseValuesFromBracket(line)
+      result = {}
+      for m in re.finditer(self.MESSAGE_PATTERN + r'(?P<sep>,? +|$)', values):
+        key, value = self._parsePartialResult(m)
+        result[key] = value
+        if m.group('sep') != ' ':
+          yield result
+          result = {}
+    except ValueError:
+      print >> sys.stderr, 'could not parse line %s' % repr(line)
+
+  def _tryParseMeasuredFrameRate(self, line):
+    """Parses a line starting with 'Measured frame rate:'."""
+    if line.startswith('Measured frame rate: '):
+      try:
+        values = self._parseValuesFromBracket(line)
+        values = re.split(r' *, *', values)
+        self._rates_from_failure = list(map(float, values))
+      except ValueError:
+        print >> sys.stderr, 'could not parse line %s' % repr(line)
+
+  def parse(self, test):
+    """Parses the ValueArray and FailedScene lines of a test result.
+
+    Arguments:
+      test: An ElementTree <Test> element.
+    """
+    failure = test.find('FailedScene')
+    if failure is not None:
+      trace = failure.find('StackTrace')
+      if trace is not None:
+        for line in re.split(r'[\r\n]+', trace.text):
+          self._parseFailureLine(line)
+    details = test.find('Details')
+    if details is not None:
+      for array in details.iter('ValueArray'):
+        message = array.get('message')
+        self._parseMessage(message, array)
+
+  def _parseFailureLine(self, line):
+    raise NotImplementedError
+
+  def _parseMessage(self, message, array):
+    raise NotImplementedError
+
+  def getData(self):
+    """Gets the parsed test result data.
+
+    Yields:
+       Result objects containing at least codec, size, mime and rates attributes."""
+    yield self
+
+
+class VideoEncoderDecoderTestResult(_VideoResultBase):
+  """Represents a result from a VideoEncoderDecoderTest performance case."""
+
+  def __init__(self, unused_m):
+    super(VideoEncoderDecoderTestResult, self).__init__(is_decoder=False)
+
+  # If a VideoEncoderDecoderTest succeeds, it provides the results in the
+  # message of a ValueArray. If fails, it provides the results in the failure
+  # using raw data. (For now it also includes some data in the ValueArrays even
+  # if it fails, which we ignore.)
+
+  def _parseFailureLine(self, line):
+    """Handles parsing a line from the failure log."""
+    self._tryParseMeasuredFrameRate(line)
+
+  def _parseMessage(self, message, array):
+    """Handles parsing a message from ValueArrays."""
+    if message.startswith('codec='):
+      result = dict(self._parsePartialResult(m)
+                  for m in re.finditer(self.MESSAGE_PATTERN + '(?: |$)', message))
+      if 'EncInputFormat' in result:
+        self.codec = result['codec']
+        fmt = result['EncInputFormat']
+        self.size = Size(fmt['width'], fmt['height'])
+        self.mime = result['EncOutputFormat']['mime']
+        self._rates_from_message.append(1000000./result['min'])
+
+
+class VideoDecoderPerfTestResult(_VideoResultBase):
+  """Represents a result from a VideoDecoderPerfTest performance case."""
+
+  # If a VideoDecoderPerfTest succeeds, it provides the results in the message
+  # of a ValueArray. If fails, it provides the results in the failure only
+  # using raw data.
+
+  def __init__(self, unused_m):
+    super(VideoDecoderPerfTestResult, self).__init__(is_decoder=True)
+
+  def _parseFailureLine(self, line):
+    """Handles parsing a line from the failure log."""
+    self._tryParseMeasuredFrameRate(line)
+    # if the test failed, we can only get the codec/size/mime from the raw data.
+    if line.startswith('Raw data: '):
+      for result in self._parseRawData(line):
+        fmt = result['DecOutputFormat']
+        self.size = Size(fmt['width'], fmt['height'])
+        self.codec = result['codec']
+        self.mime = result['mime']
+
+  def _parseMessage(self, message, array):
+    """Handles parsing a message from ValueArrays."""
+    if message.startswith('codec='):
+      result = dict(self._parsePartialResult(m)
+                  for m in re.finditer(self.MESSAGE_PATTERN + '(?: |$)', message))
+      if result.get('decodeto') == 'surface':
+        self.codec = result['codec']
+        fmt = result['DecOutputFormat']
+        self.size = Size(fmt['width'], fmt['height'])
+        self.mime = result['mime']
+        self._rates_from_message.append(1000000. / result['min'])
+
+
+class Results(object):
+  """Container that keeps all test results."""
+  def __init__(self):
+      self._results = [] # namedtuples
+      self._device = None
+
+  VIDEO_ENCODER_DECODER_TEST_REGEX = re.compile(
+      'test(.*)(\d{4})x(\d{4})(Goog|Other)$')
+
+  VIDEO_DECODER_PERF_TEST_REGEX = re.compile(
+      'test(VP[89]|H26[34]|MPEG4|HEVC)(\d+)x(\d+)(.*)$')
+
+  TestCaseSpec = namedtuple('TestCaseSpec', 'package path class_ regex result_class')
+
+  def _getTestCases(self):
+    return [
+      self.TestCaseSpec(package='CtsDeviceVideoPerf',
+                   path='TestSuite/TestSuite/TestSuite/TestSuite/TestCase',
+                   class_='VideoEncoderDecoderTest',
+                   regex=self.VIDEO_ENCODER_DECODER_TEST_REGEX,
+                   result_class=VideoEncoderDecoderTestResult),
+      self.TestCaseSpec(package='CtsMediaTestCases',
+                   path='TestSuite/TestSuite/TestSuite/TestCase',
+                   class_='VideoDecoderPerfTest',
+                   regex=self.VIDEO_DECODER_PERF_TEST_REGEX,
+                   result_class=VideoDecoderPerfTestResult)
+    ]
+
+  def _verifyDeviceInfo(self, device):
+    assert self._device in (None, device), "expected %s device" % self._device
+    self._device = device
+
+  def importXml(self, xml):
+    self._verifyDeviceInfo(xml.find('DeviceInfo/BuildInfo').get('buildName'))
+
+    packages = createLookup(self._getTestCases(), lambda tc: tc.package)
+
+    for pkg in xml.iter('TestPackage'):
+      tests_in_package = packages.get(pkg.get('name'))
+      if not tests_in_package:
+        continue
+      paths = createLookup(tests_in_package, lambda tc: tc.path)
+      for path, tests_in_path in paths.items():
+        classes = createLookup(tests_in_path, lambda tc: tc.class_)
+        for tc in pkg.iterfind(path):
+          tests_in_class = classes.get(tc.get('name'))
+          if not tests_in_class:
+            continue
+          for test in tc.iter('Test'):
+            for tc in tests_in_class:
+              m = tc.regex.match(test.get('name'))
+              if m:
+                result = tc.result_class(m)
+                result.parse(test)
+                self._results.append(result)
+
+  def importFile(self, path):
+    print >> sys.stderr, 'Importing "%s"...' % path
+    try:
+      return self.importXml(ET.parse(path))
+    except ET.ParseError:
+      raise ValueError('not a valid XML file')
+
+  def getData(self):
+    for result in self._results:
+      for data in result.getData():
+        yield data
+
+  def dumpXml(self, results):
+    yield '<?xml version="1.0" encoding="utf-8" ?>'
+    yield '<!-- Copyright 2015 The Android Open Source Project'
+    yield ''
+    yield '     Licensed under the Apache License, Version 2.0 (the "License");'
+    yield '     you may not use this file except in compliance with the License.'
+    yield '     You may obtain a copy of the License at'
+    yield ''
+    yield '          http://www.apache.org/licenses/LICENSE-2.0'
+    yield ''
+    yield '     Unless required by applicable law or agreed to in writing, software'
+    yield '     distributed under the License is distributed on an "AS IS" BASIS,'
+    yield '     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.'
+    yield '     See the License for the specific language governing permissions and'
+    yield '     limitations under the License.'
+    yield '-->'
+    yield ''
+    yield '<MediaCodecs>'
+    last_section = None
+    Comp = namedtuple('Comp', 'is_decoder google mime name')
+    by_comp = createLookup(results,
+                           lambda e: Comp(is_decoder=e.is_decoder, google='.google.' in e.codec, mime=e.mime, name=e.codec))
+    for comp in sorted(by_comp):
+      section = 'Decoders' if comp.is_decoder else 'Encoders'
+      if section != last_section:
+        if last_section:
+          yield '    </%s>' % last_section
+        yield '    <%s>' % section
+        last_section = section
+      yield '        <MediaCodec name="%s" type="%s" update="true">' % (comp.name, comp.mime)
+      by_size = createLookup(by_comp[comp], lambda e: e.size)
+      for size in sorted(by_size):
+        values = list(itertools.chain(*(e.rates for e in by_size[size])))
+        min_, max_ = min(values), max(values)
+        med_ = int(math.sqrt(min_ * max_))
+        yield '            <Limit name="measured-frame-rate-%s" range="%d-%d" />' % (size, med_, med_)
+      yield '        </MediaCodec>'
+    if last_section:
+      yield '    </%s>' % last_section
+    yield '</MediaCodecs>'
+
+
+class Main(object):
+  """Executor of this utility."""
+
+  def __init__(self):
+    self._result = Results()
+
+    self._parser = argparse.ArgumentParser('get_achievable_framerates')
+    self._parser.add_argument('result_xml', nargs='+')
+
+  def _parseArgs(self):
+    self._args = self._parser.parse_args()
+
+  def _importXml(self, xml):
+    self._result.importFile(xml)
+
+  def _report(self):
+    for line in self._result.dumpXml(r for r in self._result.getData() if r):
+      print line
+
+  def run(self):
+    self._parseArgs()
+    try:
+      for xml in self._args.result_xml:
+        try:
+          self._importXml(xml)
+        except (ValueError, IOError, AssertionError) as e:
+          print >> sys.stderr, e
+          raise KeyboardInterrupt
+      self._report()
+    except KeyboardInterrupt:
+      print >> sys.stderr, 'Interrupted.'
+
+if __name__ == '__main__':
+  Main().run()
+