Make the Android incremental installer script support incrementally installing native libraries.

--
MOS_MIGRATED_REVID=99257598
diff --git a/tools/android/incremental_install.py b/tools/android/incremental_install.py
index ad2f904..9862f7a 100644
--- a/tools/android/incremental_install.py
+++ b/tools/android/incremental_install.py
@@ -33,6 +33,7 @@
 gflags.DEFINE_string("split_main_apk", None, "The main APK for split install")
 gflags.DEFINE_multistring("split_apk", [], "Split APKs to install")
 gflags.DEFINE_string("dexmanifest", None, "The .dex manifest")
+gflags.DEFINE_multistring("native_lib", None, "Native libraries to install")
 gflags.DEFINE_string("resource_apk", None, "The resource .apk")
 gflags.DEFINE_string("apk", None, "The app .apk. If not specified, "
                      "do incremental deployment")
@@ -171,6 +172,11 @@
           "non-incremental 'mobile-install' must precede incremental "
           "installs." % package)
 
+  def GetAbi(self):
+    """Returns the ABI the device supports."""
+    _, stdout, _, _ = self._Shell("getprop ro.product.cpu.abi")
+    return stdout
+
   def Push(self, local, remote):
     """Invoke 'adb push' in parallel."""
     return self._ExecParallel(["push", local, remote])
@@ -436,6 +442,90 @@
   adb.PushString(new_checksum, device_checksum_file).result()
 
 
+def ConvertNativeLibs(args):
+  """Converts the --native_libs command line argument to an arch -> libs map."""
+  native_libs = {}
+  if args is not None:
+    for native_lib in args:
+      abi, path = native_lib.split(":")
+      if abi not in native_libs:
+        native_libs[abi] = set()
+
+      native_libs[abi].add(path)
+
+  return native_libs
+
+
+def UploadNativeLibs(adb, native_lib_args, app_dir):
+  """Uploads native libraries to the device."""
+
+  native_libs = ConvertNativeLibs(native_lib_args)
+  libs = set()
+  if native_libs:
+    abi = adb.GetAbi()
+    if abi not in native_libs:
+      logging.warn("No native libs for device ABI '%s'. Available ABIs: %s",
+                   abi, ", ".join(native_libs))
+    else:
+      libs = native_libs[abi]
+
+  basename_to_path = {}
+  install_checksums = {}
+  for lib in sorted(libs):
+    install_checksums[os.path.basename(lib)] = Checksum(lib)
+    basename_to_path[os.path.basename(lib)] = lib
+
+  device_manifest = adb.Pull("%s/native/native_manifest" % app_dir)
+  device_checksums = {}
+  if device_manifest:
+    for name, checksum in [l.split(" ") for l in device_manifest.split("\n")]:
+      device_checksums[name] = checksum
+
+  libs_to_delete = set(device_checksums) - set(install_checksums)
+  libs_to_upload = set(install_checksums) - set(device_checksums)
+  common_libs = set(install_checksums).intersection(set(device_checksums))
+  libs_to_upload.update([l for l in common_libs
+                         if install_checksums[l] != device_checksums[l]])
+
+  libs_to_push = [(basename_to_path[lib], "%s/native/%s" % (app_dir, lib))
+                  for lib in libs_to_upload]
+
+  if not libs_to_delete and not libs_to_push and device_manifest is not None:
+    logging.info("Native libs up-to-date")
+    return
+
+  num_files = len(libs_to_delete) + len(libs_to_push)
+  logging.info("Updating %d native lib%s...",
+               num_files, "s" if num_files != 1 else "")
+
+  adb.Delete("%s/native/native_manifest" % app_dir)
+
+  if libs_to_delete:
+    adb.DeleteMultiple([
+        "%s/native/%s" % (app_dir, lib) for lib in libs_to_delete])
+
+  upload_walltime_start = time.time()
+  fs = [adb.Push(local, remote) for local, remote in libs_to_push]
+  done, not_done = futures.wait(fs, return_when=futures.FIRST_EXCEPTION)
+  upload_walltime = time.time() - upload_walltime_start
+  logging.debug("Native library upload walltime: %s seconds", upload_walltime)
+
+  # If there is anything in not_done, then some adb call failed and we
+  # can cancel the rest.
+  if not_done:
+    for f in not_done:
+      f.cancel()
+
+  # If any adb call resulted in an exception, re-raise it.
+  for f in done:
+    f.result()
+
+  install_manifest = [
+      name + " " + checksum for name, checksum in install_checksums.iteritems()]
+  adb.PushString("\n".join(install_manifest),
+                 "%s/native/native_manifest" % app_dir).result()
+
+
 def VerifyInstallTimestamp(adb, app_package):
   """Verifies that the app is unchanged since the last mobile-install."""
   expected_timestamp = adb.Pull("%s/%s/install_timestamp" % (
@@ -454,7 +544,8 @@
 
 def IncrementalInstall(adb_path, execroot, stub_datafile, output_marker,
                        adb_jobs, start_app, dexmanifest=None, apk=None,
-                       resource_apk=None, split_main_apk=None, split_apks=None,
+                       native_libs=None, resource_apk=None,
+                       split_main_apk=None, split_apks=None,
                        user_home_dir=None):
   """Performs an incremental install.
 
@@ -467,6 +558,7 @@
     start_app: If True, starts the app after updating.
     dexmanifest: Path to the .dex manifest file.
     apk: Path to the .apk file. May be None to perform an incremental install.
+    native_libs: Native libraries to install.
     resource_apk: Path to the apk containing the app's resources.
     split_main_apk: the split main .apk if split installation is desired.
     split_apks: the list of split .apks to be installed.
@@ -480,6 +572,8 @@
     if split_main_apk:
       adb.InstallMultiple(os.path.join(execroot, split_main_apk))
       for split_apk in split_apks:
+        # TODO(build-team): This always reinstalls everything, which defeats the
+        # purpose of this whole system.
         adb.InstallMultiple(os.path.join(execroot, split_apk), app_package)
     else:
       if not apk:
@@ -492,6 +586,7 @@
       # then UploadResources is called. We could instead enqueue everything
       # onto the threadpool so that uploading resources happens sooner.
       UploadResources(adb, os.path.join(execroot, resource_apk), app_dir)
+      UploadNativeLibs(adb, native_libs, app_dir)
       if apk:
         apk_path = os.path.join(execroot, apk)
         adb.Install(apk_path)
@@ -542,6 +637,7 @@
       stub_datafile=FLAGS.stub_datafile,
       output_marker=FLAGS.output_marker,
       start_app=FLAGS.start_app,
+      native_libs=FLAGS.native_lib,
       split_main_apk=FLAGS.split_main_apk,
       split_apks=FLAGS.split_apk,
       dexmanifest=FLAGS.dexmanifest,
diff --git a/tools/android/incremental_install_test.py b/tools/android/incremental_install_test.py
index b8ea64f..a040f74 100644
--- a/tools/android/incremental_install_test.py
+++ b/tools/android/incremental_install_test.py
@@ -78,6 +78,8 @@
       elif shell_cmdln.startswith("rm"):
         file_path = shell_cmdln.split()[2]
         self.files.pop(file_path, None)
+      elif shell_cmdln.startswith("getprop ro.product.cpu.abi"):
+        return self._CreatePopenMock(0, "armeabi-v7a", "")
       else:
         raise Exception("Unknown shell command line: %s" % shell_cmdln)
     # Return a mock subprocess.Popen object
@@ -151,11 +153,13 @@
   def _PutDeviceFile(self, f, content):
     self._mock_adb.files[self._GetDeviceAppPath(f)] = content
 
-  def _CallIncrementalInstall(self, incremental, start_app=False):
+  def _CallIncrementalInstall(self, incremental, native_libs=None,
+                              start_app=False):
     if incremental:
       apk = None
     else:
       apk = self._APK
+
     incremental_install.IncrementalInstall(
         adb_path=self._ADB_PATH,
         execroot=self._EXEC_ROOT,
@@ -163,13 +167,13 @@
         dexmanifest=self._DEXMANIFEST,
         apk=apk,
         resource_apk=self._RESOURCE_APK,
+        native_libs=native_libs,
         output_marker=self._OUTPUT_MARKER,
         adb_jobs=1,
         start_app=start_app,
         user_home_dir="/home/root")
 
   def testUploadToPristineDevice(self):
-
     self._CreateZip()
 
     with open("dex1", "w") as f:
@@ -190,8 +194,35 @@
     self.assertEquals("content3", self._GetDeviceFile("dex/ip3"))
     self.assertEquals("resource apk", self._GetDeviceFile("resources.ap_"))
 
-  def testUploadWithOneChangedFile(self):
+  def testUploadNativeLibs(self):
+    self._CreateZip()
+    with open("liba.so", "w") as f:
+      f.write("liba_1")
+    with open("libb.so", "w") as f:
+      f.write("libb_1")
 
+    native_libs = ["armeabi-v7a:liba.so", "armeabi-v7a:libb.so"]
+    self._CallIncrementalInstall(incremental=False, native_libs=native_libs)
+    self.assertEquals("liba_1", self._GetDeviceFile("native/liba.so"))
+    self.assertEquals("libb_1", self._GetDeviceFile("native/libb.so"))
+
+    # Change a library
+    with open("libb.so", "w") as f:
+      f.write("libb_2")
+    self._CallIncrementalInstall(incremental=True, native_libs=native_libs)
+    self.assertEquals("libb_2", self._GetDeviceFile("native/libb.so"))
+
+    # Delete a library
+    self._CallIncrementalInstall(
+        incremental=True, native_libs=["armeabi-v7a:liba.so"])
+    self.assertFalse(
+        self._GetDeviceAppPath("native/libb.so") in self._mock_adb.files)
+
+    # Add the deleted library back
+    self._CallIncrementalInstall(incremental=True, native_libs=native_libs)
+    self.assertEquals("libb_2", self._GetDeviceFile("native/libb.so"))
+
+  def testUploadWithOneChangedFile(self):
     # Existing manifest from a previous install.
     self._CreateRemoteManifest(
         "zip1 zp1 ip1 0",