Snap for 7110675 from 5d40d747e919bc803e77544c6d9e5879f4fe0ae8 to sdk-release

Change-Id: Ibdf9d271dabae3de3b11fc69598017cd071e1d24
diff --git a/Android.bp b/Android.bp
index dd0e560..c3f7c44 100644
--- a/Android.bp
+++ b/Android.bp
@@ -15,10 +15,15 @@
 //
 
 python_defaults {
-    name: "ota_from_raw_image_defaults",
+    name: "gki_python_defaults",
     libs: [
         "releasetools_ota_from_target_files",
     ],
+    version: {
+        py3: {
+            embedded_launcher: true,
+        },
+    },
     target: {
         darwin: {
             // required module "brillo_update_payload" is disabled on darwin
@@ -29,34 +34,21 @@
 
 python_binary_host {
     name: "ota_from_raw_image",
-    defaults: ["ota_from_raw_image_defaults"],
+    defaults: ["gki_python_defaults"],
     srcs: ["ota_from_raw_image.py"],
     required: [
         "brillo_update_payload",
     ],
-    version: {
-        py3: {
-            embedded_launcher: true,
-        },
-    },
 }
 
-python_test_host {
-    name: "ota_from_raw_image_test",
-    defaults: ["ota_from_raw_image_defaults"],
-    test_suites: ["general-tests"],
-    srcs: [
-        "ota_from_raw_image.py",
-        "ota_from_raw_image_test.py",
+python_binary_host {
+    name: "extract_img_from_apex",
+    defaults: ["gki_python_defaults"],
+    srcs: ["extract_img_from_apex.py"],
+    required: [
+        "debugfs",
+        "delta_generator",
     ],
-    version: {
-        py3: {
-            // When using embedded launcher, atest will try (but may fail) to load libc++.so from
-            // host, because the test executable won't be able to find the needed libs via its
-            // runpath.
-            embedded_launcher: false,
-        },
-    },
 }
 
 apex_key {
@@ -65,23 +57,25 @@
     private_key: "com.android.gki.pem",
 }
 
-sh_binary {
+// Use cc_prebuilt_binary because sh_binary does not support product_specific.
+// TODO(b/169954965): Change to sh_binary when product_specific is supported.
+cc_prebuilt_binary {
     name: "com.android.gki.preinstall",
-    src: "preinstall.sh",
-}
-
-sh_binary {
-    name: "com.android.gki.postinstall",
-    src: "postinstall.sh",
+    product_specific: true,
+    srcs: ["preinstall.sh"],
+    apex_available: ["com.android.gki.*"],
+    strip: {
+        none: true,
+    },
 }
 
 // Common defaults for all GKI APEXes.
 apex_defaults {
     name: "com.android.gki_defaults",
+    product_specific: true,
     binaries: [
         "update_engine_stable_client",
         "com.android.gki.preinstall",
-        "com.android.gki.postinstall",
     ],
     file_contexts: ":com.android.gki-file_contexts",
     // Key to sign apex_payload.img
@@ -108,3 +102,51 @@
         "-Werror",
     ],
 }
+
+// Build GKI APEX 5.4-android12-0 from $(PRODUCT_OUT)/boot.img.
+// Also generate test packages.
+gki_apex {
+    name: "com.android.gki.kmi_5_4_android12_0",
+    installable: true,
+    kmi_version: "5.4-android12-0",
+    product_out_path: "boot.img",
+    gen_test: true,
+}
+
+// Build GKI APEX 5.10-android12-0 from $(PRODUCT_OUT)/boot.img.
+// Also generate test packages.
+gki_apex {
+    name: "com.android.gki.kmi_5_10_android12_0",
+    installable: true,
+    kmi_version: "5.10-android12-0",
+    product_out_path: "boot.img",
+    gen_test: true,
+}
+
+// Build GKI APEX 5.4-android12-0 from $(PRODUCT_OUT)/boot-5.4.img
+gki_apex {
+    name: "com.android.gki.kmi_5_4_android12_0_boot-5.4",
+    installable: false,
+    kmi_version: "5.4-android12-0",
+    product_out_path: "boot-5.4.img",
+}
+
+// Build GKI APEX 5.4-android12-0 from $(PRODUCT_OUT)/boot-5.10.img
+gki_apex {
+    name: "com.android.gki.kmi_5_10_android12_0_boot-5.10",
+    installable: false,
+    kmi_version: "5.10-android12-0",
+    product_out_path: "boot-5.10.img",
+}
+
+// List of all test APEXes for GkiInstallTest. Append "_test_high" and "_test_low" for each
+// gki_apex with gen_test:true.
+filegroup {
+    name: "gki_install_test_files",
+    srcs: [
+        ":com.android.gki.kmi_5_4_android12_0_test_high",
+        ":com.android.gki.kmi_5_4_android12_0_test_low",
+        ":com.android.gki.kmi_5_10_android12_0_test_high",
+        ":com.android.gki.kmi_5_10_android12_0_test_low",
+    ],
+}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1479c90..e3e017d 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,8 +1,14 @@
 {
   "presubmit": [
     {
-      "name": "ota_from_raw_image_test",
+      "name": "libkver_test"
+    },
+    {
+      "name": "libkver_test",
       "host": true
+    },
+    {
+      "name": "GkiInstallTest"
     }
   ]
 }
diff --git a/build/Android.bp b/build/Android.bp
new file mode 100644
index 0000000..a991224
--- /dev/null
+++ b/build/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2020 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.
+
+bootstrap_go_package {
+    name: "gki-soong-rules",
+    pkgPath: "android/soong/gki",
+    pluginFor: ["soong_build"],
+    deps: [
+        "blueprint",
+        "blueprint-proptools",
+        "soong",
+        "soong-android",
+        "soong-apex",
+        "soong-phony",
+    ],
+    srcs: [
+        "gki.go",
+        "kmi.go",
+        "prebuilt.go",
+        "properties.go",
+        "raw_img_ota.go",
+    ],
+    testSrcs: [
+        "kmi_test.go",
+    ],
+}
diff --git a/build/gki.go b/build/gki.go
new file mode 100644
index 0000000..208aaf8
--- /dev/null
+++ b/build/gki.go
@@ -0,0 +1,311 @@
+// Copyright (C) 2020 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 gki
+
+import (
+	"path/filepath"
+	"strings"
+
+	"android/soong/android"
+	"android/soong/apex"
+	"android/soong/etc"
+	"android/soong/genrule"
+
+	"github.com/google/blueprint/proptools"
+)
+
+type gkiApexProperties struct {
+	// Path relative to $(PRODUCT_OUT) that points to the boot image. This is
+	// passed to the generated makefile_goal.
+	// Exactly one of [factory, product_out_path] must be set.
+	Product_out_path *string
+
+	// Declared KMI version of the boot image. Example: "5.4-android12-0"
+	Kmi_version *string
+
+	// The certificate to sign the OTA payload.
+	// The name of a certificate in the default certificate directory, blank to
+	// use the default product certificate,
+	// or an android_app_certificate module name in the form ":module".
+	Ota_payload_certificate *string
+
+	// Whether test APEXes are generated. Test APEXes are named with
+	// ${name}_test_high and ${name}_test_low, respectively.
+	Gen_test *bool
+
+	// Whether this APEX is installable to one of the partitions. Default:
+	// see apex.installable.
+	Installable *bool
+
+	// Whether modules should be enabled according to board variables.
+	ModulesEnabled bool `blueprint:"mutated"`
+	// APEX package name that will be declared in the APEX manifest.
+	// e.g. com.android.gki.kmi_5_4_android12_0
+	ApexName *string `blueprint:"mutated"`
+}
+
+type gkiApex struct {
+	android.ModuleBase
+	properties gkiApexProperties
+}
+
+func init() {
+	android.RegisterModuleType("gki_apex", gkiApexFactory)
+}
+
+// Declare a GKI APEX. Generate a set of modules to define an apex with name
+// "com.android.gki" + sanitized(kmi_version).
+func gkiApexFactory() android.Module {
+	g := &gkiApex{}
+	g.AddProperties(&g.properties)
+	android.InitAndroidModule(g)
+	android.AddLoadHook(g, func(ctx android.LoadHookContext) { gkiApexMutator(ctx, g) })
+	return g
+}
+
+func gkiApexMutator(mctx android.LoadHookContext, g *gkiApex) {
+	g.validateAndSetMutableProperties(mctx)
+	g.createModulesRealApexes(mctx)
+}
+
+func (g *gkiApex) validateAndSetMutableProperties(mctx android.LoadHookContext) {
+	// Parse kmi_version property to find APEX name.
+	apexName, err := kmiVersionToApexName(proptools.String(g.properties.Kmi_version))
+	if err != nil {
+		mctx.PropertyErrorf("kmi_version", err.Error())
+		return
+	}
+
+	// Set mutable properties.
+	g.properties.ModulesEnabled = g.bootImgHasRules(mctx) && g.boardDefinesKmiVersion(mctx)
+	g.properties.ApexName = proptools.StringPtr(apexName)
+}
+
+func testApexBundleFactory() android.Module {
+	return apex.ApexBundleFactory(true /* testApex */, false /* art */)
+}
+
+// Create modules for a real APEX package that contains an OTA payload.
+func (g *gkiApex) createModulesRealApexes(mctx android.LoadHookContext) {
+	// Import $(PRODUCT_OUT)/boot.img to Soong
+	bootImage := g.moduleName() + "_bootimage"
+	mctx.CreateModule(android.MakefileGoalFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(bootImage),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &makefileGoalProperties{
+		Product_out_path: g.properties.Product_out_path,
+	})
+	// boot.img -> kernel_release.txt
+	mctx.CreateModule(genrule.GenRuleFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(g.kernelReleaseFileName()),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &genRuleProperties{
+		Defaults: []string{"extract_kernel_release_defaults"},
+		Srcs:     []string{":" + bootImage},
+	})
+	// boot.img -> payload.bin and payload_properties.txt
+	otaPayloadGen := g.moduleName() + "_ota_payload_gen"
+	mctx.CreateModule(rawImageOtaFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(otaPayloadGen),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &rawImageOtaProperties{
+		Certificate: g.properties.Ota_payload_certificate,
+		Image_goals: []string{"boot:" + bootImage},
+	})
+	// copy payload.bin to <apex>/etc/ota
+	mctx.CreateModule(etc.PrebuiltEtcFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(g.otaPayloadName()),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &prebuiltEtcProperties{
+		Src:                   proptools.StringPtr(":" + otaPayloadGen + "{" + payloadTag + "}"),
+		Filename_from_src:     proptools.BoolPtr(true),
+		Relative_install_path: proptools.StringPtr("ota"),
+		Installable:           proptools.BoolPtr(false),
+	})
+	// copy payload_properties.txt to <apex>/etc/ota
+	mctx.CreateModule(etc.PrebuiltEtcFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(g.otaPropertiesName()),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &prebuiltEtcProperties{
+		Src:                   proptools.StringPtr(":" + otaPayloadGen + "{" + payloadPropertiesTag + "}"),
+		Filename_from_src:     proptools.BoolPtr(true),
+		Relative_install_path: proptools.StringPtr("ota"),
+		Installable:           proptools.BoolPtr(false),
+	})
+	// Create the APEX module with name g.moduleName(). Use factory APEX version.
+	g.createModulesRealApex(mctx, g.moduleName(), false, "")
+
+	// Create test APEX modules if gen_test. Test packages are not installable.
+	// Use hard-coded APEX version.
+	if proptools.Bool(g.properties.Gen_test) {
+		g.createModulesRealApex(mctx, g.moduleName()+"_test_high", true, "1000000000")
+		g.createModulesRealApex(mctx, g.moduleName()+"_test_low", true, "1")
+	}
+}
+
+func (g *gkiApex) createModulesRealApex(mctx android.LoadHookContext,
+	moduleName string,
+	isTestApex bool,
+	overrideApexVersion string) {
+	// Check kmi_version property against kernel_release.txt, then
+	// kernel_release.txt -> apex_manifest.json.
+	apexManifest := moduleName + "_apex_manifest"
+	mctx.CreateModule(genrule.GenRuleFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(apexManifest),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &genRuleProperties{
+		Tools: []string{"build_gki_apex_manifest"},
+		Out:   []string{"apex_manifest.json"},
+		Srcs:  []string{":" + g.kernelReleaseFileName()},
+		Cmd:   proptools.StringPtr(g.createApexManifestCmd(overrideApexVersion)),
+	})
+
+	// The APEX module.
+
+	// For test APEXes, if module is not enabled because KMI version is not
+	// compatible with the device, create a stub module that produces an empty
+	// file. This is so that the module name can be used in tests.
+	if isTestApex && !g.properties.ModulesEnabled {
+		mctx.CreateModule(genrule.GenRuleFactory, &moduleCommonProperties{
+			Name: proptools.StringPtr(moduleName),
+		}, &genRuleProperties{
+			Out: []string{moduleName + ".apex"},
+			Cmd: proptools.StringPtr(`touch $(out)`),
+		})
+		return
+	}
+
+	// For test APEXes, if module is enabled, build an apex_test with installable: false.
+	// For installed APEXes, build apex, respecting installable and enabled.
+	apexFactory := apex.BundleFactory
+	overrideInstallable := g.properties.Installable
+	if isTestApex {
+		apexFactory = testApexBundleFactory
+		overrideInstallable = proptools.BoolPtr(false)
+	}
+
+	mctx.CreateModule(apexFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(moduleName),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &apexProperties{
+		Apex_name: g.properties.ApexName,
+		Manifest:  proptools.StringPtr(":" + apexManifest),
+		Defaults:  []string{"com.android.gki_defaults"},
+		// A real GKI APEX cannot be preinstalled to the device.
+		// It can only be provided as an update.
+		Installable: overrideInstallable,
+		Prebuilts: []string{
+			g.otaPayloadName(),
+			g.otaPropertiesName(),
+		},
+	})
+}
+
+// Original module name as specified by the "name" property.
+// This is also the APEX module name, i.e. the file name of the APEX file.
+// This is also the prefix of names of all generated modules that the phony module depends on.
+// e.g. com.android.gki.kmi_5_4_android12_0_boot
+func (g *gkiApex) moduleName() string {
+	return g.BaseModuleName()
+}
+
+// The appeared name of this gkiApex object. Exposed to Soong to avoid conflicting with
+// the generated APEX module with name moduleName().
+// e.g. com.android.gki.kmi_5_4_android12_0_boot_all
+func (g *gkiApex) Name() string {
+	return g.moduleName() + "_all"
+}
+
+// Names for intermediate modules.
+func (g *gkiApex) kernelReleaseFileName() string {
+	return g.moduleName() + "_bootimage_kernel_release_file"
+}
+
+func (g *gkiApex) otaPayloadName() string {
+	return g.moduleName() + "_ota_payload"
+}
+
+func (g *gkiApex) otaPropertiesName() string {
+	return g.moduleName() + "_ota_payload_properties"
+}
+
+// If the boot image pointed at product_out_path has no rule to be generated, do not generate any
+// build rules for this gki_apex module. For example, if this gki_apex module is:
+//     { name: "foo", product_out_path: "boot-bar.img" }
+// But there is no rule to generate boot-bar.img, then
+// - `m foo` fails with `unknown target 'foo'`
+// - checkbuild is still successful. The module foo doesn't even exist, so there
+//   is no dependency on boot-bar.img
+//
+// There is a rule to generate "boot-foo.img" if "kernel-foo" is in BOARD_KERNEL_BINARIES.
+// As a special case, there is a rule to generate "boot.img" if BOARD_KERNEL_BINARIES is empty,
+// or "kernel" is in BOARD_KERNEL_BINARIES.
+func (g *gkiApex) bootImgHasRules(mctx android.EarlyModuleContext) bool {
+	kernelNames := mctx.DeviceConfig().BoardKernelBinaries()
+	if len(kernelNames) == 0 {
+		return proptools.String(g.properties.Product_out_path) == "boot.img"
+	}
+	for _, kernelName := range kernelNames {
+		validBootImagePath := strings.Replace(kernelName, "kernel", "boot", -1) + ".img"
+		if proptools.String(g.properties.Product_out_path) == validBootImagePath {
+			return true
+		}
+	}
+	return false
+}
+
+// Only generate if this module's kmi_version property is in BOARD_KERNEL_MODULE_INTERFACE_VERSIONS.
+// Otherwise, this board does not support GKI APEXes, so no modules are generated at all.
+// This function also avoids building invalid modules in checkbuild. For example, if these
+// gki_apex modules are defined:
+//   gki_apex { name: "boot-kmi-1", kmi_version: "1", product_out_path: "boot.img" }
+//   gki_apex { name: "boot-kmi-2", kmi_version: "2", product_out_path: "boot.img" }
+// But a given device's $PRODUCT_OUT/boot.img can only support at most one KMI version.
+// Disable some modules accordingly to make sure checkbuild still works.
+func boardDefinesKmiVersion(mctx android.EarlyModuleContext, kmiVersion string) bool {
+	kmiVersions := mctx.DeviceConfig().BoardKernelModuleInterfaceVersions()
+	return android.InList(kmiVersion, kmiVersions)
+}
+
+func (g *gkiApex) boardDefinesKmiVersion(mctx android.EarlyModuleContext) bool {
+	return boardDefinesKmiVersion(mctx, proptools.String(g.properties.Kmi_version))
+}
+
+// Transform kernel release file in $(in) to KMI version + sublevel.
+// e.g. 5.4.42-android12-0 => name: "com.android.gki.kmi_5_4_android12_0", version: "300000000"
+// Finally, write APEX manifest JSON to $(out).
+func (g *gkiApex) createApexManifestCmd(apexVersion string) string {
+	ret := `$(location build_gki_apex_manifest) ` +
+		`--kmi_version "` + proptools.String(g.properties.Kmi_version) + `" ` +
+		`--apex_manifest $(out) --kernel_release_file $(in)`
+	// Override version field if set.
+	if apexVersion != "" {
+		ret += ` --apex_version ` + apexVersion
+	}
+	return ret
+}
+
+func (g *gkiApex) DepsMutator(ctx android.BottomUpMutatorContext) {
+}
+
+func (g *gkiApex) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+}
+
+// OTA payload binary is signed with default_system_dev_certificate, which is equivalent to
+// DefaultAppCertificate().
+func getDefaultCertificate(ctx android.EarlyModuleContext) string {
+	pem, _ := ctx.Config().DefaultAppCertificate(ctx)
+	return strings.TrimSuffix(pem.String(), filepath.Ext(pem.String()))
+}
diff --git a/build/kmi.go b/build/kmi.go
new file mode 100644
index 0000000..27b00bf
--- /dev/null
+++ b/build/kmi.go
@@ -0,0 +1,45 @@
+// Copyright (C) 2020 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.
+
+// Minimum support of KMI version in Go. Keep in sync with libkver.
+
+package gki
+
+import (
+	"fmt"
+	"regexp"
+)
+
+var digits = "([0-9]+)"
+var reKmi = regexp.MustCompile("^([0-9]+)[.]([0-9]+)-(android[0-9]+)-([0-9]+)$")
+
+// Input is a valid KMI version, e.g. 5.4-android12-0.
+// Return a sanitized string to be used as a suffix of APEX package name
+// com.android.gki.kmi_5_4_android12_0
+// Keep in sync with libkver.
+func kmiVersionToApexName(s string) (string, error) {
+	matches := reKmi.FindAllStringSubmatch(s, 4)
+
+	if matches == nil {
+		return "", fmt.Errorf("Poorly formed KMI version: %q must match regex %q", s, reKmi)
+	}
+
+	version := matches[0][1]
+	patchLevel := matches[0][2]
+	androidRelease := matches[0][3]
+	kmiGeneration := matches[0][4]
+
+	return fmt.Sprintf("com.android.gki.kmi_%s_%s_%s_%s",
+		version, patchLevel, androidRelease, kmiGeneration), nil
+}
diff --git a/build/kmi_test.go b/build/kmi_test.go
new file mode 100644
index 0000000..de7642b
--- /dev/null
+++ b/build/kmi_test.go
@@ -0,0 +1,48 @@
+// Copyright (C) 2020 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.
+
+// Minimum support of KMI version in Go. Keep in sync with libkver.
+
+package gki
+
+import (
+	"testing"
+)
+
+func expectValid(t *testing.T, kmi string, expectedApexName string) {
+	t.Helper()
+	got, e := kmiVersionToApexName(kmi)
+	if e != nil {
+		t.Errorf("Expected no error when parsing %q, got %q", kmi, e)
+	}
+	if got != expectedApexName {
+		t.Errorf("Expected kmiVersionToApexName(%q) == %q, got %q", kmi, expectedApexName, got)
+	}
+}
+
+func expectInvalid(t *testing.T, kmi string) {
+	t.Helper()
+	got, e := kmiVersionToApexName(kmi)
+	if e == nil {
+		t.Errorf("Expected error when parsing %q, got no error with result %q", kmi, got)
+	}
+}
+
+func TestParse(t *testing.T) {
+	expectInvalid(t, "")
+	expectInvalid(t, "foobar")
+	expectInvalid(t, "1")
+	expectValid(t, "5.4-android12-0", "com.android.gki.kmi_5_4_android12_0")
+	expectValid(t, "5.4-android12-42", "com.android.gki.kmi_5_4_android12_42")
+}
diff --git a/build/prebuilt.go b/build/prebuilt.go
new file mode 100644
index 0000000..da8a123
--- /dev/null
+++ b/build/prebuilt.go
@@ -0,0 +1,125 @@
+// Copyright (C) 2020 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 gki
+
+import (
+	"fmt"
+	"io"
+
+	"android/soong/android"
+	"android/soong/apex"
+	"github.com/google/blueprint/proptools"
+)
+
+var (
+	prebuiltApexTag = dependencyTag{name: "prebuilt_apex"}
+)
+
+type prebuiltGkiApexProperties struct {
+	apex.PrebuiltProperties
+
+	// Declared KMI version of the boot image. Example: "5.4-android12-0"
+	Kmi_version *string
+}
+
+type prebuiltGkiApex struct {
+	android.ModuleBase
+	properties prebuiltGkiApexProperties
+
+	extractedBootImage android.WritablePath
+}
+
+func init() {
+	android.RegisterModuleType("prebuilt_gki_apex", prebuiltGkiApexFactory)
+}
+
+// Declare a prebuilt GKI APEX. When installed, the boot image is extracted from
+// the module.
+func prebuiltGkiApexFactory() android.Module {
+	g := &prebuiltGkiApex{}
+	g.AddProperties(&g.properties)
+	android.InitAndroidModule(g)
+	android.AddLoadHook(g, func(ctx android.LoadHookContext) { prebuiltGkiApexMutator(ctx, g) })
+	return g
+}
+func prebuiltGkiApexMutator(mctx android.LoadHookContext, g *prebuiltGkiApex) {
+	// Whether modules should be enabled according to board variables.
+	enabled := boardDefinesKmiVersion(mctx, proptools.String(g.properties.Kmi_version))
+	if !enabled {
+		g.Disable()
+	}
+
+	// The prebuilt_apex module.
+	mctx.CreateModule(apex.PrebuiltFactory, &moduleCommonProperties{
+		Name:             proptools.StringPtr(g.BaseModuleName()),
+		Enabled:          proptools.BoolPtr(enabled),
+		Product_specific: proptools.BoolPtr(true),
+	}, &g.properties.PrebuiltProperties)
+}
+
+// The appeared name of this prebuiltGkiApex object. Exposed to Soong to avoid conflicting with
+// the generated prebuilt_apex module with name BaseModuleName().
+func (g *prebuiltGkiApex) Name() string {
+	return g.BaseModuleName() + "_boot_img"
+}
+
+func (g *prebuiltGkiApex) DepsMutator(ctx android.BottomUpMutatorContext) {
+	ctx.AddDependency(ctx.Module(), prebuiltApexTag, g.BaseModuleName())
+}
+
+func (g *prebuiltGkiApex) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	var apexFile android.OptionalPath
+	ctx.VisitDirectDepsWithTag(prebuiltApexTag, func(m android.Module) {
+		if prebuiltApex, ok := m.(*apex.Prebuilt); ok {
+			srcFiles, err := prebuiltApex.OutputFiles("")
+			if err != nil {
+				ctx.ModuleErrorf("Cannot get output files from %q: %s", ctx.OtherModuleName(m), err)
+			} else if len(srcFiles) != 1 {
+				ctx.ModuleErrorf("%q generated %d files", ctx.OtherModuleName(m), len(srcFiles))
+			} else {
+				apexFile = android.OptionalPathForPath(srcFiles[0])
+			}
+		} else {
+			ctx.ModuleErrorf("%q is not a prebuilt_apex", ctx.OtherModuleName(m))
+		}
+	})
+	if !apexFile.Valid() {
+		ctx.ModuleErrorf("Can't determine the prebuilt APEX file")
+		return
+	}
+
+	genDir := android.PathForModuleOut(ctx, "extracted")
+	g.extractedBootImage = genDir.Join(ctx, "boot.img")
+
+	rule := android.NewRuleBuilder(pctx, ctx)
+	rule.Command().
+		ImplicitOutput(g.extractedBootImage).
+		BuiltTool("extract_img_from_apex").
+		Flag("--tool").BuiltTool("debugfs").
+		Flag("--tool").BuiltTool("delta_generator").
+		Input(apexFile.Path()).
+		Text(genDir.String())
+	rule.Build("extractImgFromApex", "Extract boot image from prebuilt GKI APEX")
+
+	ctx.Phony(g.BaseModuleName(), g.extractedBootImage)
+}
+
+func (g *prebuiltGkiApex) AndroidMk() android.AndroidMkData {
+	return android.AndroidMkData{
+		Custom: func(w io.Writer, name, prefix, moduleDir string, data android.AndroidMkData) {
+			fmt.Fprintf(w, "ALL_MODULES.%s.EXTRACTED_BOOT_IMAGE := %s\n", name, g.extractedBootImage)
+		},
+	}
+}
diff --git a/build/properties.go b/build/properties.go
new file mode 100644
index 0000000..af68446
--- /dev/null
+++ b/build/properties.go
@@ -0,0 +1,50 @@
+// Copyright (C) 2020 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 gki
+
+type moduleCommonProperties struct {
+	Name             *string
+	Enabled          *bool
+	Required         []string
+	Product_specific *bool
+}
+
+type makefileGoalProperties struct {
+	Product_out_path *string
+}
+
+type genRuleProperties struct {
+	Cmd      *string
+	Defaults []string
+	Out      []string
+	Srcs     []string
+	Tools    []string
+}
+
+type prebuiltEtcProperties struct {
+	Src                   *string
+	Filename_from_src     *bool
+	Relative_install_path *string
+	Installable           *bool
+}
+
+type apexProperties struct {
+	Apex_name   *string
+	Manifest    *string
+	Defaults    []string
+	Installable *bool
+	Prebuilts   []string
+	Overrides   []string
+}
diff --git a/build/raw_img_ota.go b/build/raw_img_ota.go
new file mode 100644
index 0000000..2f29e2e
--- /dev/null
+++ b/build/raw_img_ota.go
@@ -0,0 +1,250 @@
+// Copyright (C) 2020 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.
+
+// A special genrule that creates OTA payload and payload_properties from a raw
+// image. This rule is created so that the two outputs, payload and
+// payload_properties, can be distinguished with tags.
+
+package gki
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"android/soong/android"
+	"android/soong/java"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+type dependencyTag struct {
+	blueprint.BaseDependencyTag
+	name string
+}
+
+// {"foo": "fooVal", "bar": "barVal"} -> ["${foo}", "${bar}"]
+func keysToVars(deps map[string]string) []string {
+	var ret []string
+	for dep := range deps {
+		ret = append(ret, fmt.Sprintf("${%s}", dep))
+	}
+	sort.Strings(ret)
+	return ret
+}
+
+var (
+	certificateTag = dependencyTag{name: "certificate"}
+	rawImageTag    = dependencyTag{name: "raw_image"}
+
+	pctx = android.NewPackageContext("android/gki")
+
+	otaFromRawImageDeps = map[string]string{
+		"ota_from_raw_image": "ota_from_raw_image",
+
+		// Needed by ota_from_target_files
+		"brillo_update_payload": "brillo_update_payload",
+
+		// Needed by brillo_update_payload
+		"delta_generator": "delta_generator",
+		// b/171581299: shflags isn't built to the path where HostBinToolVariable
+		// points to without explicitly declaring it, even if it is stated as
+		// required by brillo_update_payload.
+		"shflags": "lib/shflags/shflags",
+
+		// Needed by GetBootImageTimestamp
+		"lz4":            "lz4",
+		"toybox":         "toybox",
+		"unpack_bootimg": "unpack_bootimg",
+	}
+
+	otaFromRawImageVarDeps = keysToVars(otaFromRawImageDeps)
+
+	otaFromRawImageRule = pctx.AndroidStaticRule("ota_from_raw_image", blueprint.RuleParams{
+		Command: `${ota_from_raw_image} --tools ` + strings.Join(otaFromRawImageVarDeps, " ") +
+			` ${kwargs} --out ${outDir} -- ${inputArg}`,
+		CommandDeps: otaFromRawImageVarDeps,
+		Description: "ota_from_raw_image ${outDir}",
+	}, "kwargs", "outDir", "inputArg")
+
+	// Tags to OutputFiles
+	payloadTag           = "payload"
+	payloadPropertiesTag = "properties"
+)
+
+func init() {
+	for dep := range otaFromRawImageDeps {
+		pctx.HostBinToolVariable(dep, otaFromRawImageDeps[dep])
+	}
+	// Intentionally not register this module so that it can only be constructed by gki_apex.
+}
+
+type rawImageOtaProperties struct {
+	// The name of a certificate in the default certificate directory, blank to use the default product certificate,
+	// or an android_app_certificate module name in the form ":module".
+	Certificate *string
+
+	// A set of images and their related modules. Must be in this form
+	// IMAGE_NAME:MODULE, where IMAGE_NAME is an image name like "boot", and
+	// MODULE is the name of a makefile_goal.
+	Image_goals []string
+}
+
+type rawImageOta struct {
+	android.ModuleBase
+	properties rawImageOtaProperties
+
+	pem android.Path
+	key android.Path
+
+	outPayload    android.WritablePath
+	outProperties android.WritablePath
+}
+
+// Declare a rule that generates a signed OTA payload from a raw image. This
+// includes payload.bin and payload_properties.txt.
+func rawImageOtaFactory() android.Module {
+	r := &rawImageOta{}
+	r.AddProperties(&r.properties)
+	android.InitAndroidModule(r)
+	return r
+}
+
+func (r *rawImageOta) OutputFiles(tag string) (android.Paths, error) {
+	switch tag {
+	case "":
+		return android.Paths{r.outPayload, r.outProperties}, nil
+	case payloadTag:
+		return android.Paths{r.outPayload}, nil
+	case payloadPropertiesTag:
+		return android.Paths{r.outProperties}, nil
+	default:
+		return nil, fmt.Errorf("unsupported module reference tag %q", tag)
+	}
+}
+
+var _ android.OutputFileProducer = (*rawImageOta)(nil)
+
+func (r *rawImageOta) getCertString(ctx android.BaseModuleContext) string {
+	moduleName := ctx.ModuleName()
+	certificate, overridden := ctx.DeviceConfig().OverrideCertificateFor(moduleName)
+	if overridden {
+		return ":" + certificate
+	}
+	return proptools.String(r.properties.Certificate)
+}
+
+// Returns module->image_name mapping, e.g. "bootimage_soong"->"boot"
+func (r *rawImageOta) goalToImage(ctx android.EarlyModuleContext) map[string]string {
+	ret := map[string]string{}
+	for _, imageGoal := range r.properties.Image_goals {
+		lst := strings.Split(imageGoal, ":")
+		if len(lst) != 2 {
+			ctx.PropertyErrorf("image_goals", "Must be in the form IMAGE_NAME:MODULE")
+			return map[string]string{}
+		}
+		ret[lst[1]] = lst[0]
+	}
+	return ret
+}
+
+func (r *rawImageOta) DepsMutator(ctx android.BottomUpMutatorContext) {
+	// Add dependency to modules in image_goals
+	for module, _ := range r.goalToImage(ctx) {
+		ctx.AddVariationDependencies(nil, rawImageTag, module)
+	}
+	// Add dependency to certificate module, if any.
+	cert := android.SrcIsModule(r.getCertString(ctx))
+	if cert != "" {
+		ctx.AddVariationDependencies(nil, certificateTag, cert)
+	}
+}
+
+func (r *rawImageOta) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	inputArg := []string{}
+	kwargs := []string{}
+	implicits := android.Paths{}
+
+	// Handle image_goals
+	goalToImage := r.goalToImage(ctx)
+	ctx.VisitDirectDepsWithTag(rawImageTag, func(module android.Module) {
+		depName := ctx.OtherModuleName(module)
+		imgPath := android.OutputFileForModule(ctx, module, "")
+		if imgPath != nil {
+			implicits = append(implicits, imgPath)
+			inputArg = append(inputArg, goalToImage[depName]+":"+imgPath.String())
+		} else {
+			ctx.ModuleErrorf("image dependency %q does not generate any output", depName)
+		}
+	})
+
+	// Handle certificate
+	ctx.VisitDirectDepsWithTag(certificateTag, func(module android.Module) {
+		depName := ctx.OtherModuleName(module)
+		if cert, ok := module.(*java.AndroidAppCertificate); ok {
+			r.pem = cert.Certificate.Pem
+			r.key = cert.Certificate.Key
+		} else {
+			ctx.ModuleErrorf("certificate dependency %q must be an android_app_certificate module", depName)
+		}
+	})
+	r.setCertificateAndPrivateKey(ctx)
+	keyName, keyError := removeCertExt(r.pem)
+	if keyError != nil {
+		ctx.ModuleErrorf("Cannot get certificate to sign the OTA payload binary: " + keyError.Error())
+	}
+	implicits = append(implicits, r.pem, r.key)
+	kwargs = append(kwargs, "--key "+proptools.String(keyName))
+
+	// Set outputs
+	outDir := android.PathForModuleGen(ctx, "payload_files")
+	r.outPayload = outDir.Join(ctx, "payload.bin")
+	r.outProperties = outDir.Join(ctx, "payload_properties.txt")
+
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        otaFromRawImageRule,
+		Description: "Generate OTA from raw image",
+		Implicits:   implicits,
+		Outputs:     android.WritablePaths{r.outPayload, r.outProperties},
+		Args: map[string]string{
+			"kwargs":   strings.Join(kwargs, " "),
+			"outDir":   outDir.String(),
+			"inputArg": strings.Join(inputArg, " "),
+		},
+	})
+}
+
+func (r *rawImageOta) setCertificateAndPrivateKey(ctx android.ModuleContext) {
+	if r.pem == nil {
+		cert := proptools.String(r.properties.Certificate)
+		if cert == "" {
+			pem, key := ctx.Config().DefaultAppCertificate(ctx)
+			r.pem = pem
+			r.key = key
+		} else {
+			defaultDir := ctx.Config().DefaultAppCertificateDir(ctx)
+			r.pem = defaultDir.Join(ctx, cert+".x509.pem")
+			r.key = defaultDir.Join(ctx, cert+".pk8")
+		}
+	}
+}
+
+func removeCertExt(path android.Path) (*string, error) {
+	s := path.String()
+	if strings.HasSuffix(s, ".x509.pem") {
+		return proptools.StringPtr(strings.TrimSuffix(s, ".x509.pem")), nil
+	}
+	return nil, fmt.Errorf("Path %q does not end with .x509.pem", s)
+}
diff --git a/build_gki_apex_manifest.cpp b/build_gki_apex_manifest.cpp
index 659afcc..796f1ad 100644
--- a/build_gki_apex_manifest.cpp
+++ b/build_gki_apex_manifest.cpp
@@ -30,16 +30,11 @@
 
 namespace {
 
-int CheckKmi(const KernelRelease& kernel_release, const std::string& kmi_version) {
-  const auto& actual_kmi_version = kernel_release.kmi_version().string();
+int CheckKmi(const KernelRelease& kernel_release, const KmiVersion& kmi_version) {
+  const auto& actual_kmi_version = kernel_release.kmi_version();
   if (actual_kmi_version != kmi_version) {
-    LOG(ERROR) << "KMI version does not match. Actual: " << actual_kmi_version
-               << ", expected: " << kmi_version;
-    return EX_SOFTWARE;
-  }
-  if (kernel_release.sub_level() == GetFactoryApexVersion()) {
-    LOG(ERROR) << "Kernel release is " << kernel_release.string() << ". Sub-level "
-               << GetFactoryApexVersion() << " is reserved for factory GKI APEX.";
+    LOG(ERROR) << "KMI version does not match. Actual: " << actual_kmi_version.string()
+               << ", expected: " << kmi_version.string();
     return EX_SOFTWARE;
   }
   return EX_OK;
@@ -51,7 +46,6 @@
   root["name"] = apex_name;
   root["version"] = apex_version;
   root["preInstallHook"] = "bin/com.android.gki.preinstall";
-  root["postInstallHook"] = "bin/com.android.gki.postinstall";
   std::string json_string = Json::StyledWriter().write(root);
   if (!android::base::WriteStringToFile(json_string, out_file)) {
     PLOG(ERROR) << "Cannot write to " << out_file;
@@ -70,6 +64,8 @@
             "--factory must be set.");
 DEFINE_string(kmi_version, "", "Declared KMI version for this APEX.");
 DEFINE_string(apex_manifest, "", "Output APEX manifest JSON file.");
+DEFINE_uint64(apex_version, GetFactoryApexVersion(),
+              "Override APEX version in APEX manifest. Use factory APEX version if unspecified.");
 
 int main(int argc, char** argv) {
   gflags::ParseCommandLineFlags(&argc, &argv, true);
@@ -89,12 +85,7 @@
     return EX_SOFTWARE;
   }
 
-  std::string apex_name;
-  uint64_t apex_version;
-  if (FLAGS_factory) {
-    apex_name = GetApexName(*kmi_version);
-    apex_version = GetFactoryApexVersion();
-  } else {
+  if (!FLAGS_kernel_release_file.empty()) {
     std::string kernel_release_string;
     if (!android::base::ReadFileToString(FLAGS_kernel_release_file, &kernel_release_string)) {
       PLOG(ERROR) << "Cannot read " << FLAGS_kernel_release_file;
@@ -105,13 +96,13 @@
       LOG(ERROR) << kernel_release_string << " is not a valid GKI kernel release string";
       return EX_SOFTWARE;
     }
-    int res = CheckKmi(*kernel_release, FLAGS_kmi_version);
+    int res = CheckKmi(*kernel_release, *kmi_version);
     if (res != EX_OK) return res;
-
-    apex_name = GetApexName(kernel_release->kmi_version());
-    apex_version = GetApexVersion(*kernel_release);
   }
 
+  std::string apex_name = GetApexName(*kmi_version);
+  uint64_t apex_version = FLAGS_apex_version;
+
   if (FLAGS_apex_manifest.empty()) {
     LOG(WARNING) << "Skip writing APEX manifest because --apex_manifest is not set.";
   } else {
diff --git a/download_boot_prebuilt.py b/download_boot_prebuilt.py
new file mode 100755
index 0000000..bd665d7
--- /dev/null
+++ b/download_boot_prebuilt.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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
+import collections
+import functools
+import glob
+import json
+import logging
+import os
+import pathlib
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+import tempfile
+import urllib.request
+
+from concurrent import futures
+from pathlib import Path
+
+BASE_URL = "https://ci.android.com/builds/submitted/{build_id}/{target}/latest/raw"
+SUPPORTED_ARCHS = ["arm64"]
+VARIANTS = ["userdebug"]
+BOOT_PREBUILT_REL_DIR = "packages/modules/BootPrebuilt"
+ANDROID_BUILD_TOP = os.environ["ANDROID_BUILD_TOP"]
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+def parse_args():
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      "build_id",
+      type=int,
+      help="the build id to download the build for, e.g. 6148204")
+  parser.add_argument(
+      "--bug",
+      type=str,
+      default=None,
+      help="optional bug number for git commit.")
+
+  return parser.parse_args()
+
+
+def download_file(url, dest_filename):
+  logger.info("Downloading %s -> %s", url, dest_filename)
+  urllib.request.urlretrieve(url, dest_filename)
+
+
+def get_artifact_download_spec(build_id, device, variant, dest_dir):
+  target = "{}-{}".format(device, variant)
+  url_base = BASE_URL.format(build_id=build_id, target=target)
+  filename = "{}-img-{}.zip".format(device, build_id)
+  url = os.path.join(url_base, filename)
+  dest_filename = os.path.join(dest_dir, filename)
+  return url, dest_filename
+
+
+def update_prebuilt(build_id, boot_prebuilt, ver, arch, variant, bug):
+  device = "aosp_" + arch
+  arch_dir = os.path.join(boot_prebuilt, ver, arch)
+  variant_dir = os.path.join(arch_dir, variant)
+  boot_img_name = "boot-{}.img".format(ver)
+  stored_img_name = "boot-{}.img".format(variant)
+  try:
+    subprocess.check_call(["repo", "start", "boot-prebuilt-{}".format(build_id)], cwd=arch_dir)
+
+    os.makedirs(variant_dir)
+    url, dest_filename = get_artifact_download_spec(build_id, device, variant, variant_dir)
+    download_file(url, dest_filename)
+    args = ["unzip", "-d", variant_dir, dest_filename, boot_img_name]
+    logger.info("Calling: %s", " ".join(args))
+    subprocess.check_call(args)
+    shutil.move(os.path.join(variant_dir, boot_img_name), os.path.join(arch_dir, stored_img_name))
+
+  finally:
+    shutil.rmtree(variant_dir)
+
+    message = """Update prebuilts to {build_id}.
+
+Test: Treehugger
+Bug: {bug}
+""".format(build_id=build_id, bug=bug or "N/A")
+
+    logger.info("Creating commit for %s", arch_dir)
+    subprocess.check_call(["git", "add", "."], cwd=arch_dir)
+    subprocess.check_call(["git", "commit", "-m", message], cwd=arch_dir)
+
+
+def main():
+  args = parse_args()
+  with futures.ThreadPoolExecutor(max_workers=10) as pool:
+    fs = []
+    boot_prebuilt = os.path.join(ANDROID_BUILD_TOP, BOOT_PREBUILT_REL_DIR)
+    for ver in os.listdir(boot_prebuilt):
+      if not re.match(r'\d+[.]\d+', ver):
+        continue
+      for arch in os.listdir(os.path.join(boot_prebuilt, ver)):
+        if arch not in SUPPORTED_ARCHS:
+          continue
+        for variant in VARIANTS:
+          fs.append((ver, arch, pool.submit(update_prebuilt, args.build_id, boot_prebuilt, ver,
+                                            arch, variant, args.bug)))
+
+    futures.wait([f for ver, arch, f in fs])
+    success_dirs = []
+    logger.info("===============")
+    logger.info("Summary:")
+    for ver, arch, future in fs:
+      if future.exception():
+        logger.error("%s/%s: %s", ver, arch, future.exception())
+      else:
+        logger.info("%s/%s: Updated.", ver, arch)
+        success_dirs.append(os.path.join(BOOT_PREBUILT_REL_DIR, ver, arch))
+
+    if success_dirs:
+      args = ["repo", "upload", "--verify", "--hashtag-branch", "--br",
+              "boot-prebuilt-{}".format(args.build_id)] + success_dirs
+      logger.info(" ".join(args))
+      subprocess.check_call(args, cwd=ANDROID_BUILD_TOP)
+
+
+if __name__ == "__main__":
+  sys.exit(main())
diff --git a/extract_img_from_apex.py b/extract_img_from_apex.py
new file mode 100644
index 0000000..12eb987
--- /dev/null
+++ b/extract_img_from_apex.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2020 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.
+
+"""
+Extract boot.img from a GKI APEX.
+
+Usage: extract_img_from_apex [--tool host_tool [--tool ...]] \
+           input_gki.apex out_dir
+"""
+
+import logging
+import os
+import sys
+
+import common
+
+APEX_PAYLOAD_FILE = "apex_payload.img"
+APEX_PAYLOAD_BIN_PATH = "/etc/ota/payload.bin"
+
+logger = logging.getLogger(__name__)
+OPTIONS = common.OPTIONS
+
+
+def ExtractImagesFromApex(apex, out_dir):
+  if not os.path.isdir(out_dir):
+    os.makedirs(out_dir)
+  apex_dir = common.UnzipTemp(apex, [APEX_PAYLOAD_FILE])
+  apex_payload = os.path.join(apex_dir, APEX_PAYLOAD_FILE)
+
+  payload_bin = common.MakeTempFile("payload_", ".bin")
+  debugfs_command = ['debugfs', '-R', "dump {} {}".format(
+      APEX_PAYLOAD_BIN_PATH, payload_bin), apex_payload]
+
+  common.RunAndCheckOutput(debugfs_command)
+  assert os.path.getsize(payload_bin) > 0, payload_bin + " is an empty file!"
+
+  boot_img = os.path.join(out_dir, 'boot.img')
+  with open(boot_img, 'w') as _:
+    pass
+
+  delta_generator_command = [
+      'delta_generator',
+      '--is_partial_update=true',
+      '--in_file=' + payload_bin,
+      '--partition_names=boot',
+      '--new_partitions=' + boot_img
+  ]
+  common.RunAndCheckOutput(delta_generator_command)
+
+
+def main(argv):
+  def option_handler(o, a):
+    if o == "--tool":
+      name = os.path.basename(a)
+      common.SetHostToolLocation(name, a)
+    else:
+      return False
+    return True
+
+  args = common.ParseOptions(
+      argv, __doc__,
+      extra_long_opts=["tool="],
+      extra_option_handler=option_handler)
+
+  if len(args) != 2:
+    common.Usage(__doc__)
+    sys.exit(1)
+
+  common.InitLogging()
+
+  ExtractImagesFromApex(args[0], args[1])
+  logger.info("done.")
+
+
+if __name__ == '__main__':
+  try:
+    common.CloseInheritedPipes()
+    main(sys.argv[1:])
+  except common.ExternalError:
+    logger.exception("\n   ERROR:\n")
+    sys.exit(1)
+  finally:
+    common.Cleanup()
diff --git a/libkver/TEST_MAPPING b/libkver/TEST_MAPPING
deleted file mode 100644
index b3fe1e6..0000000
--- a/libkver/TEST_MAPPING
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "libkver_test"
-    },
-    {
-      "name": "libkver_test",
-      "host": true
-    }
-  ]
-}
diff --git a/libkver/include/kver/kmi_version.h b/libkver/include/kver/kmi_version.h
index 71e0e8c..7066f10 100644
--- a/libkver/include/kver/kmi_version.h
+++ b/libkver/include/kver/kmi_version.h
@@ -55,4 +55,12 @@
   uint64_t gen_ = 0;
 };
 
+inline bool operator==(const KmiVersion& left, const KmiVersion& right) {
+  return left.tuple() == right.tuple();
+}
+
+inline bool operator!=(const KmiVersion& left, const KmiVersion& right) {
+  return left.tuple() != right.tuple();
+}
+
 }  // namespace android::kver
diff --git a/libkver/include/kver/utils.h b/libkver/include/kver/utils.h
index 2ae3c19..26e0707 100644
--- a/libkver/include/kver/utils.h
+++ b/libkver/include/kver/utils.h
@@ -35,10 +35,7 @@
 // e.g. "com.android.gki.kmi_5_4_android12_0"
 std::string GetApexName(const KmiVersion& kmi_version);
 
-// Defines how kernel release is mapped to APEX versions.
-uint64_t GetApexVersion(const KernelRelease& kernel_release);
-
-// Return the APEX version for a factory GKI APEX.
+// Return the APEX version for a GKI APEX built from source.
 uint64_t GetFactoryApexVersion();
 
 }  // namespace android::kver
diff --git a/libkver/utils.cpp b/libkver/utils.cpp
index 0c6beb8..a1d2c13 100644
--- a/libkver/utils.cpp
+++ b/libkver/utils.cpp
@@ -78,14 +78,8 @@
                                      kmi_version.android_release(), kmi_version.generation());
 }
 
-uint64_t GetApexVersion(const KernelRelease& kernel_release) {
-  // TODO(b/168255100): define APEX version
-  return kernel_release.sub_level();
-}
-
 uint64_t GetFactoryApexVersion() {
-  // TODO(b/168255100): define APEX version
-  return 1;
+  return 300000000;
 }
 
 }  // namespace android::kver
diff --git a/ota_from_raw_image.py b/ota_from_raw_image.py
index c6b4ea0..acd0697 100644
--- a/ota_from_raw_image.py
+++ b/ota_from_raw_image.py
@@ -40,9 +40,6 @@
   parser.add_argument("--key", type=str,
                       help="Key to use to sign the package. If unspecified, script does not sign "
                            "the package and payload_properties.txt is not generated.")
-  parser.add_argument("--kernel-release-file", type=str,
-                      help="If boot is in input, a file containing the kernel release of the boot "
-                           "image. Create the file with `extract_kernel --output-release`.")
   parser.add_argument("--out", type=str, required=True,
                       help="Required output directory to payload.bin and payload_properties.txt")
   parser.add_argument("input", metavar="NAME:IMAGE", nargs="+",
@@ -58,32 +55,13 @@
     return
   for path in args.tools:
     name = os.path.basename(path)
-    common.SetHostToolLocation(name, path)
+    # Use absolute path because GetBootImageTimestamp changes cwd when running some tools.
+    common.SetHostToolLocation(name, os.path.abspath(path))
     # brillo_update_payload is a shell script that depends on this environment variable.
     if name == "delta_generator":
       os.environ["GENERATOR"] = path
 
 
-def _GetKernelRelease(line):
-  """
-  Get GKI kernel release string from the given line.
-  """
-  PATTERN = r"^(\d+[.]\d+[.]\d+-android\d+-\d+).*$"
-  mo = re.match(PATTERN, line)
-  assert mo, "Kernel release '{}' does not match regex r'{}'".format(line, PATTERN)
-  return mo.group(1)
-
-
-def _GetKernelReleaseFromFile(filename):
-  """
-  Get GKI kernel release string from the given text file.
-  """
-  assert filename, "--kernel-release-file must be specified if boot is in input"
-  with open(filename) as f:
-    line = f.read().strip()
-    return _GetKernelRelease(line)
-
-
 def CreateOtaFromRawImages(args):
   _PrepareEnvironment(args)
 
@@ -98,8 +76,10 @@
       zip.write(img_path, arcname=os.path.join("IMAGES", name + ".img"))
       names.append(name)
       if name == "boot":
+        timestamp = common.GetBootImageTimestamp(img_path)
+        assert timestamp is not None, "Cannot extract timestamp from boot image"
         payload_additional_args += ["--partition_timestamps",
-                                    "boot:" + _GetKernelReleaseFromFile(args.kernel_release_file)]
+                                    "boot:" + str(timestamp)]
 
     zip.writestr("META/ab_partitions.txt", "\n".join(names) + "\n")
     zip.writestr("META/dynamic_partitions_info.txt", """
diff --git a/ota_from_raw_image_test.py b/ota_from_raw_image_test.py
deleted file mode 100644
index 0151547..0000000
--- a/ota_from_raw_image_test.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright (C) 2020 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 unittest
-from ota_from_raw_image import _GetKernelRelease
-
-
-class OtaFromRawImageTest(unittest.TestCase):
-  def test_get_kernel_release_trivial(self):
-    self.assertEqual("5.4.42-android12-15", _GetKernelRelease("5.4.42-android12-15"))
-
-  def test_get_kernel_release_suffix(self):
-    self.assertEqual("5.4.42-android12-15", _GetKernelRelease("5.4.42-android12-15-something"))
-
-  def test_get_kernel_release_invalid(self):
-    with self.assertRaises(AssertionError):
-      _GetKernelRelease("5.4-android12-15")
-
-
-if __name__ == '__main__':
-  # atest needs a verbosity level of >= 2 to correctly parse the result.
-  unittest.main(verbosity=2)
diff --git a/postinstall.sh b/postinstall.sh
deleted file mode 100644
index d29e65b..0000000
--- a/postinstall.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/system/bin/sh
-
-log -p i -t gki "GKI APEX postinstall hook starting."
diff --git a/sysprop/api/gkiprops-current.txt b/sysprop/api/gkiprops-current.txt
index 38edfea..e69de29 100644
--- a/sysprop/api/gkiprops-current.txt
+++ b/sysprop/api/gkiprops-current.txt
@@ -1,13 +0,0 @@
-props {
-  module: "android.sysprop.GkiProperties"
-  prop {
-    api_name: "prevent_downgrade_spl"
-    scope: Internal
-    prop_name: "ro.build.ab_update.gki.prevent_downgrade_spl"
-  }
-  prop {
-    api_name: "prevent_downgrade_version"
-    scope: Internal
-    prop_name: "ro.build.ab_update.gki.prevent_downgrade_version"
-  }
-}
diff --git a/test/Android.bp b/test/Android.bp
new file mode 100644
index 0000000..528a3eb
--- /dev/null
+++ b/test/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2020 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.
+
+// A list of names of test APEXes for GkiInstallTest to instantiate test cases.
+genrule {
+    name: "gki_install_test_file_list",
+    srcs: [":gki_install_test_files"],
+    out: ["gki_install_test_file_list.txt"],
+    cmd: "for file in $(in); do basename $${file} >> $(out); done",
+}
+
+java_test_host {
+    name: "GkiInstallTest",
+    srcs: [
+        "src/**/*.java",
+        ":gki_install_test_file_list",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+    libs: [
+        "compatibility-tradefed",
+        "tradefed",
+    ],
+    static_libs: [
+        "cts-host-utils",
+        "hamcrest-library",
+    ],
+    data: [":gki_install_test_files"],
+    java_resources: [":gki_install_test_file_list"],
+}
+
+// Prebuilts
+
+prebuilt_gki_apex {
+    name: "com.android.gki.kmi_5_4_android12_0_test_prebuilt",
+    installable: false,
+    kmi_version: "5.4-android12-0",
+    src: "com.android.gki.kmi_5_4_android12_0_test_prebuilt.apex",
+}
diff --git a/test/AndroidTest.xml b/test/AndroidTest.xml
new file mode 100644
index 0000000..5eceeb2
--- /dev/null
+++ b/test/AndroidTest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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="GKI Install test">
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="jar" value="GkiInstallTest.jar" />
+    </test>
+</configuration>
diff --git a/test/com.android.gki.kmi_5_4_android12_0_test_prebuilt.apex b/test/com.android.gki.kmi_5_4_android12_0_test_prebuilt.apex
new file mode 100644
index 0000000..1c01dd9
--- /dev/null
+++ b/test/com.android.gki.kmi_5_4_android12_0_test_prebuilt.apex
Binary files differ
diff --git a/test/src/com/android/gki/tests/GkiInstallTest.java b/test/src/com/android/gki/tests/GkiInstallTest.java
new file mode 100644
index 0000000..9abc5f9
--- /dev/null
+++ b/test/src/com/android/gki/tests/GkiInstallTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2020 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 com.android.gki.tests;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.everyItem;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isIn;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.io.FileMatchers.aFileWithSize;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeThat;
+import static org.junit.Assert.fail;
+
+import static java.util.stream.Collectors.toList;
+
+import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
+import android.cts.host.utils.DeviceJUnit4Parameterized;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.ITestDevice.ApexInfo;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Scanner;
+import java.util.Set;
+
+@RunWith(DeviceJUnit4Parameterized.class)
+@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
+public class GkiInstallTest extends BaseHostJUnit4Test {
+
+    // Keep in sync with gki.go.
+    private static final String HIGH_SUFFIX = "_test_high.apex";
+    private static final String LOW_SUFFIX = "_test_low.apex";
+    private static final long TEST_HIGH_VERSION = 1000000000L;
+
+    // Timeout between device online for adb commands and boot completed flag is set.
+    private static final long DEVICE_AVAIL_TIMEOUT_MS = 180000; // 3mins
+    // Timeout for `adb install`.
+    private static final long INSTALL_TIMEOUT_MS = 600000; // 10mins
+
+    @Parameter
+    public String mFileName;
+
+    private String mPackageName;
+    private File mApexFile;
+    private boolean mExpectInstallSuccess;
+    private final Set<String> mOverlayfs = new HashSet();
+
+    @Parameters(name = "{0}")
+    public static Iterable<String> getTestFileNames() {
+        try (Scanner scanner = new Scanner(
+                GkiInstallTest.class.getClassLoader().getResourceAsStream(
+                        "gki_install_test_file_list.txt"))) {
+            List<String> list = new ArrayList<>();
+            scanner.forEachRemaining(list::add);
+            return list;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        inferPackageName();
+        skipTestIfPackageNotInstalled();
+        findTestApexFile();
+        prepareOverlayfs();
+    }
+
+    /** Set mPackageName and mExpectInstallSuccess according to mFileName. */
+    private void inferPackageName() throws Exception {
+        if (mFileName.endsWith(HIGH_SUFFIX)) {
+            mPackageName = mFileName.substring(0, mFileName.length() - HIGH_SUFFIX.length());
+            mExpectInstallSuccess = true;
+        } else if (mFileName.endsWith(LOW_SUFFIX)) {
+            mPackageName = mFileName.substring(0, mFileName.length() - LOW_SUFFIX.length());
+            mExpectInstallSuccess = false;
+        } else {
+            fail("Unrecognized test data file: " + mFileName);
+        }
+    }
+
+    /** Skip the test if mPackageName is not installed on the device. */
+    private void skipTestIfPackageNotInstalled() throws Exception {
+        CLog.i("Wait for device to be available for %d ms...", DEVICE_AVAIL_TIMEOUT_MS);
+        getDevice().waitForDeviceAvailable(DEVICE_AVAIL_TIMEOUT_MS);
+        CLog.i("Device is available after %d ms", DEVICE_AVAIL_TIMEOUT_MS);
+
+        // Skip if the device does not support this APEX package.
+        CLog.i("Checking if %s is installed on the device.", mPackageName);
+        ApexInfo oldApexInfo = getApexInfo(getDevice(), mPackageName);
+        assumeThat(oldApexInfo, is(notNullValue()));
+        assumeThat(oldApexInfo.name, is(mPackageName));
+    }
+
+    /** Find the corresponding APEX test file with mFileName. */
+    private void findTestApexFile() throws Exception {
+        // Find the APEX file.
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
+        mApexFile = buildHelper.getTestFile(mFileName);
+
+        // There may be empty .apex files in the directory for disabled APEXes. But if the device
+        // is known to install the package, the test must be built with non-empty APEXes for this
+        // particular package.
+        assertThat("Test is not built properly. It does not contain a non-empty " + mFileName,
+                mApexFile, is(aFileWithSize(greaterThan(0L))));
+    }
+
+    /**
+     * Record what partitions have overlayfs set up. Then, tear down overlayfs because it may
+     * make OTA fail.
+     *
+     * Usually, the test does not require root to run, but if the device has overlayfs set up,
+     * the test assumes that the device has root functionality, and attempts to tear down
+     * overlayfs before the test starts.
+     * Note that this function immediately reboots after enabling adb root to ensure the test runs
+     * with the same permission before it is called.
+     */
+    private void prepareOverlayfs() throws Exception {
+        mOverlayfs.addAll(getOverlayfsState(getDevice()));
+
+        if (!mOverlayfs.isEmpty()) {
+            getDevice().enableAdbRoot();
+            getDevice().executeAdbCommand("enable-verity");
+            rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS);
+        }
+    }
+
+    @Test
+    public void testInstallAndReboot() throws Exception {
+        CLog.i("Installing %s with %d ms timeout", mApexFile, INSTALL_TIMEOUT_MS);
+        String result = getDevice().installPackage(mApexFile, false,
+                "--staged-ready-timeout", String.valueOf(INSTALL_TIMEOUT_MS));
+        if (!mExpectInstallSuccess) {
+            assertNotNull("Should not be able to install downgrade package", result);
+            assertThat(result, containsString("Downgrade of APEX package " + mPackageName +
+                    " is not allowed."));
+            return;
+        }
+
+        assertNull("Installation failed with " + result, result);
+        rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS);
+
+        ApexInfo newApexInfo = getApexInfo(getDevice(), mPackageName);
+        assertNotNull(newApexInfo);
+        assertThat(newApexInfo.versionCode, is(TEST_HIGH_VERSION));
+    }
+
+    /**
+     * Restore overlayfs on partitions.
+     *
+     * Usually, tearDown() does not require root to run, but if the device had overlayfs set up
+     * before the test has started,
+     * the test assumes that the device has root functionality, and attempts to re-set up
+     * overlayfs after the test ends.
+     * Note that tearDown() immediately reboots after enabling adb root to ensure the test ends up
+     * with the same permission before the test has started.
+     */
+    @After
+    public void tearDown() throws Exception {
+        // Restore overlayfs for partitions that the test knows of.
+        CLog.i("Test ends, now restoring overlayfs partitions %s.", mOverlayfs);
+        if (mOverlayfs.contains("system")) {
+            getDevice().enableAdbRoot();
+            getDevice().remountSystemWritable();
+        }
+        if (mOverlayfs.contains("vendor")) {
+            getDevice().enableAdbRoot();
+            getDevice().remountVendorWritable();
+        }
+        CLog.i("Restoring overlayfs partition ends, now rebooting.");
+
+        // Reboot device no matter what to avoid interference.
+        rebootUntilAvailable(getDevice(), DEVICE_AVAIL_TIMEOUT_MS);
+
+        // remount*Writable should have enabled overlayfs for all necessary partitions. If not,
+        // throw an error.
+        Set<String> newOverlayfsState = getOverlayfsState(getDevice());
+        assertThat("Some partitions did not restore overlayfs properly. Before test: " + mOverlayfs
+                        + ", after test: " + newOverlayfsState, mOverlayfs,
+                everyItem(isIn(newOverlayfsState)));
+        CLog.i("All overlayfs states are restored.");
+    }
+
+    /**
+     * @param device the device under test
+     * @param packageName the package name to look for
+     * @return The {@link ApexInfo} of the APEX named {@code packageName} on the
+     * {@code device}, or {@code null} if the device does not have the APEX installed.
+     * @throws Exception an error has occurred.
+     */
+    private static ApexInfo getApexInfo(ITestDevice device, String packageName)
+            throws Exception {
+        assertNotNull(packageName);
+        List<ApexInfo> list = device.getActiveApexes().stream().filter(
+                apexInfo -> packageName.equals(apexInfo.name)).collect(toList());
+        if (list.isEmpty()) return null;
+        assertThat(list.size(), is(1));
+        return list.get(0);
+    }
+
+    /**
+     * Similar to device.reboot(), but with a timeout on waitForDeviceAvailable. Note that
+     * the timeout does not include the rebootUntilOnline() call.
+     *
+     * @param device    the device under test
+     * @param timeoutMs timeout for waitForDeviceAvailable() call
+     * @throws Exception an error has occurred.
+     */
+    private static void rebootUntilAvailable(ITestDevice device, long timeoutMs)
+            throws Exception {
+        CLog.i("Reboot and waiting for device to be online");
+        device.rebootUntilOnline();
+        CLog.i("Device online, wait for device to be available for %d ms...", timeoutMs);
+        device.waitForDeviceAvailable(timeoutMs);
+        CLog.i("Device is available after %d ms", timeoutMs);
+    }
+    
+    /**
+     * Get all partitions that have overlayfs setup. Parse /proc/mounts and if it finds lines like:
+     * {@code overlayfs /vendor ...}, then put {@code vendor} in the returned set.
+     * @param device the device under test
+     * @return a list of partitions like {@code system}, {@code vendor} that has overlayfs set up
+     * @throws Exception an error has occurred.
+     */
+    private static Set<String> getOverlayfsState(ITestDevice device) throws Exception {
+        Set<String> ret = new HashSet();
+        File mounts = device.pullFile("/proc/mounts");
+        try (Scanner scanner = new Scanner(mounts)) {
+            while (scanner.hasNextLine()) {
+                String line = scanner.nextLine();
+                String[] tokens = line.split("\\s");
+                if (tokens.length < 2) continue;
+                if (!"overlay".equals(tokens[0])) continue;
+                Path path = Paths.get(tokens[1]);
+                if (path.getNameCount() == 0) continue;
+                ret.add(path.getName(0).toString());
+            }
+        }
+        CLog.i("Device has overlayfs set up on partitions %s", ret);
+        return ret;
+    }
+}