bisect-kit: migrate android bisector to use codechange module

After this CL, android localbuild bisector can handle following issues:
 - add and remove repo projects
 - git history incomplete due to <project clone-depth="1">
 - manifest snapshot racing

The setup step of android checkout is changed as well. Now you have to
make a repo mirror and sync the tree from the mirror. The exact steps
are:
 1. cd $ANDROID_REPO_MIRROR_DIR
    repo init ...<original flags>... --mirror
    repo sync -c
 2. cd $ANDROID_ROOT
    repo init ...<original flags>... --reference=$ANDROID_REPO_MIRROR_DIR
    repo sync -c
 3. specify --android_repo_mirror_dir $ANDROID_REPO_MIRROR_DIR when you
    use bisect_android_repo.py and switch_arc_localbuild.py

BUG=None
TEST=unit test and following commands

$ ./bisect_android_repo.py init \
    --old 4851505 --new 4852106 --dut $DUT \
    --android_root $ANDROID_ROOT \
    --android_repo_mirror_dir $ANDROID_REPO_MIRROR_DIR
$ ./bisect_android_repo.py view
$ ./switch_arc_localbuild.py \
    --android_root $ANDROID_ROOT \
    --android_repo_mirror_dir $ANDROID_REPO_MIRROR_DIR \
    $DUT 4851505~4852106/1

Change-Id: I2708c119e328ec294a02a45bb3a7710ef1a603c5
Reviewed-on: https://chromium-review.googlesource.com/1126182
Commit-Ready: Kuang-che Wu <kcwu@chromium.org>
Tested-by: Kuang-che Wu <kcwu@chromium.org>
Reviewed-by: Chung-yih Wang <cywang@chromium.org>
Reviewed-by: Chi-Ngai Wan <cnwan@google.com>
diff --git a/bisect_kit/android_util.py b/bisect_kit/android_util.py
index e42f5a2..524a4d9 100644
--- a/bisect_kit/android_util.py
+++ b/bisect_kit/android_util.py
@@ -16,8 +16,11 @@
 import json
 import logging
 import os
+import tempfile
 
 from bisect_kit import cli
+from bisect_kit import codechange
+from bisect_kit import git_util
 from bisect_kit import repo_util
 from bisect_kit import util
 
@@ -161,30 +164,90 @@
   return manifest_name
 
 
-class AndroidManifestManager(repo_util.ManifestManager):
-  """Manifest operations for Android repo"""
+def lookup_build_timestamp(flavor, build_id):
+  """Lookup timestamp of Android prebuilt.
+
+  Args:
+    flavor: Android build flavor
+    build_id: Android build id
+
+  Returns:
+    timestamp
+  """
+  tmp_fn = tempfile.mktemp()
+  try:
+    fetch_artifact(flavor, build_id, 'BUILD_INFO', tmp_fn)
+    data = json.load(open(tmp_fn))
+    return int(data['sync_start_time'])
+  finally:
+    if os.path.exists(tmp_fn):
+      os.unlink(tmp_fn)
+
+
+class AndroidSpecManager(codechange.SpecManager):
+  """Repo manifest related operations.
+
+  This class fetches and enumerates android manifest files, parses them,
+  and sync to disk state according to them.
+  """
 
   def __init__(self, config):
     self.config = config
-    self.flavor = config['flavor']
+    self.manifest_dir = os.path.join(self.config['android_root'], '.repo',
+                                     'manifests')
+
+  def collect_float_spec(self, old, new):
+    result = []
+    path = 'default.xml'
+
+    commits = []
+    old_timestamp = lookup_build_timestamp(self.config['flavor'], old)
+    new_timestamp = lookup_build_timestamp(self.config['flavor'], new)
+    for timestamp, git_rev in git_util.get_history(self.manifest_dir, path):
+      if timestamp < old_timestamp:
+        commits = []
+      commits.append((timestamp, git_rev))
+      if timestamp > new_timestamp:
+        break
+
+    for timestamp, git_rev in commits:
+      result.append(
+          codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
+    return result
+
+  def collect_fixed_spec(self, old, new):
+    result = []
+    revlist = get_build_ids_between(self.config['branch'], int(old), int(new))
+    for rev in revlist:
+      manifest_name = fetch_manifest(self.config['android_root'],
+                                     self.config['flavor'], rev)
+      path = os.path.join(self.manifest_dir, manifest_name)
+      timestamp = lookup_build_timestamp(self.config['flavor'], rev)
+      result.append(
+          codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
+    return result
+
+  def _load_manifest_content(self, spec):
+    if spec.spec_type == codechange.SPEC_FIXED:
+      manifest_name = fetch_manifest(self.config['branch'],
+                                     self.config['flavor'], spec.name)
+      return open(os.path.join(self.manifest_dir, manifest_name)).read()
+    else:
+      return git_util.get_file_from_revision(self.manifest_dir, spec.name,
+                                             spec.path)
+
+  def parse_spec(self, spec):
+    logging.debug('parse_spec %s', spec.name)
+    manifest_content = self._load_manifest_content(spec)
+    manifest_url = repo_util.get_manifest_url(self.config['android_root'])
+    spec.entries = repo_util.parse_manifest(manifest_content, manifest_url)
+    if spec.spec_type == codechange.SPEC_FIXED:
+      assert spec.is_static()
 
   def sync_disk_state(self, rev):
-    manifest_name = self.fetch_manifest(rev)
+    manifest_name = fetch_manifest(self.config['android_root'],
+                                   self.config['flavor'], rev)
     repo_util.sync(
         self.config['android_root'],
         manifest_name=manifest_name,
         current_branch=True)
-
-  def fetch_git_repos(self, rev):
-    # TODO(kcwu): fetch git history but don't switch current disk state.
-    self.sync_disk_state(rev)
-
-  def enumerate_manifest(self, old, new):
-    revlist = get_build_ids_between(self.config['branch'], int(old), int(new))
-
-    assert revlist[0] == old
-    assert revlist[-1] == new
-    return revlist
-
-  def fetch_manifest(self, rev):
-    return fetch_manifest(self.config['android_root'], self.flavor, rev)