Add function to extract timestamp from boot image

Also add toybox to otatools list.

Bug: 169169031
Test: build OTA
Change-Id: Ib14dbf46a8385ccf2a9c5a9c6f4e7fa9399cf0ba
diff --git a/core/Makefile b/core/Makefile
index 26f11a4..18d901c 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -3887,6 +3887,7 @@
   signapk \
   simg2img \
   sload_f2fs \
+  toybox \
   tune2fs \
   unpack_bootimg \
   update_host_simulator \
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index 45e0514..e1543e7 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -125,6 +125,9 @@
     required: [
         "brillo_update_payload",
         "checkvintf",
+        "lz4",
+        "toybox",
+        "unpack_bootimg"
     ],
     target: {
         darwin: {
diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py
index f5dfbec..f256900 100644
--- a/tools/releasetools/common.py
+++ b/tools/releasetools/common.py
@@ -814,6 +814,15 @@
     props._LoadBuildProp(data)
     return props
 
+  @staticmethod
+  def FromBuildPropFile(name, build_prop_file):
+    """Constructs an instance from a build prop file."""
+
+    props = PartitionBuildProps("unknown", name)
+    with open(build_prop_file) as f:
+      props._LoadBuildProp(f.read())
+    return props
+
   def _LoadBuildProp(self, data):
     for line in data.split('\n'):
       line = line.strip()
diff --git a/tools/releasetools/ota_utils.py b/tools/releasetools/ota_utils.py
index f0e4fcf..cb0f6e6 100644
--- a/tools/releasetools/ota_utils.py
+++ b/tools/releasetools/ota_utils.py
@@ -14,14 +14,17 @@
 
 import copy
 import itertools
+import logging
 import os
 import zipfile
 
 import ota_metadata_pb2
 from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
                     ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
-                    SignFile, PARTITIONS_WITH_CARE_MAP, PartitionBuildProps)
+                    SignFile, PARTITIONS_WITH_CARE_MAP, PartitionBuildProps,
+                    MakeTempDir, RunAndCheckOutput, ExternalError)
 
+logger = logging.getLogger(__name__)
 
 OPTIONS.no_signing = False
 OPTIONS.force_non_ab = False
@@ -38,6 +41,9 @@
 METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb'
 UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
 
+# See sysprop.mk. If file is moved, add new search paths here; don't remove
+# existing search paths.
+RAMDISK_BUILD_PROP_REL_PATHS = ['system/etc/ramdisk/build.prop']
 
 def FinalizeMetadata(metadata, input_file, output_file, needed_property_files):
   """Finalizes the metadata and signs an A/B OTA package.
@@ -561,3 +567,55 @@
 
   SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
            whole_file=True)
+
+
+def GetBootImageTimestamp(boot_img):
+  """
+  Get timestamp from ramdisk within the boot image
+
+  Args:
+    boot_img: the boot image file. Ramdisk must be compressed with lz4 format.
+
+  Return:
+    An integer that corresponds to the timestamp of the boot image, or None
+    if file has unknown format. Raise exception if an unexpected error has
+    occurred.
+  """
+
+  tmp_dir = MakeTempDir('boot_', suffix='.img')
+  try:
+    RunAndCheckOutput(['unpack_bootimg', '--boot_img', boot_img, '--out', tmp_dir])
+    ramdisk = os.path.join(tmp_dir, 'ramdisk')
+    if not os.path.isfile(ramdisk):
+      logger.warning('Unable to get boot image timestamp: no ramdisk in boot')
+      return None
+    uncompressed_ramdisk = os.path.join(tmp_dir, 'uncompressed_ramdisk')
+    RunAndCheckOutput(['lz4', '-d', ramdisk, uncompressed_ramdisk])
+
+    abs_uncompressed_ramdisk = os.path.abspath(uncompressed_ramdisk)
+    extracted_ramdisk = MakeTempDir('extracted_ramdisk')
+    # Use "toybox cpio" instead of "cpio" because the latter invokes cpio from
+    # the host environment.
+    RunAndCheckOutput(['toybox', 'cpio', '-F', abs_uncompressed_ramdisk, '-i'],
+               cwd=extracted_ramdisk)
+
+    prop_file = None
+    for search_path in RAMDISK_BUILD_PROP_REL_PATHS:
+      prop_file = os.path.join(extracted_ramdisk, search_path)
+      if os.path.isfile(prop_file):
+        break
+      logger.warning('Unable to get boot image timestamp: no %s in ramdisk', search_path)
+
+    if not prop_file:
+      return None
+
+    props = PartitionBuildProps.FromBuildPropFile('boot', prop_file)
+    timestamp = props.GetProp('ro.bootimage.build.date.utc')
+    if timestamp:
+      return int(timestamp)
+    logger.warning('Unable to get boot image timestamp: ro.bootimage.build.date.utc is undefined')
+    return None
+
+  except ExternalError as e:
+    logger.warning('Unable to get boot image timestamp: %s', e)
+    return None