Revert "Remove iorap framework codes"

Revert "Remove iorap daemon codes"

Revert "Remove configs relevant to iorap"

Revert submission 16528474-remove-iorap

Reason for revert: build break
Reverted Changes:
I464c9e9c4:Remove scripts related to iorap
I0b8b1b064:Remove iorap daemon codes
I848f65908:Remove iorap framework codes
I294f37265:Remove configs relevant to iorap

Change-Id: Idbd2f34e952325d9fee00ce3b269293b848cf545
diff --git a/startop/iorap/Android.bp b/startop/iorap/Android.bp
new file mode 100644
index 0000000..4fdf34c
--- /dev/null
+++ b/startop/iorap/Android.bp
@@ -0,0 +1,44 @@
+// Copyright (C) 2018 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+  name: "services.startop.iorap-javasources",
+  srcs: ["src/**/*.java"],
+  path: "src",
+  visibility: ["//visibility:private"],
+}
+
+filegroup {
+  name: "services.startop.iorap-sources",
+  srcs: [
+    ":services.startop.iorap-javasources",
+    ":iorap-aidl",
+  ],
+  visibility: ["//frameworks/base/services:__subpackages__"],
+}
+
+java_library_static {
+  name: "services.startop.iorap",
+  srcs: [":services.startop.iorap-sources"],
+  libs: ["services.core"],
+}
diff --git a/startop/iorap/TEST_MAPPING b/startop/iorap/TEST_MAPPING
new file mode 100644
index 0000000..8c9d4df
--- /dev/null
+++ b/startop/iorap/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "libiorap-java-tests"
+    }
+  ],
+  "imports": [
+    {
+      "path": "system/iorap"
+    }
+  ]
+}
diff --git a/startop/iorap/functional_tests/Android.bp b/startop/iorap/functional_tests/Android.bp
new file mode 100644
index 0000000..43c6155
--- /dev/null
+++ b/startop/iorap/functional_tests/Android.bp
@@ -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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "iorap-functional-tests",
+    srcs: ["src/**/*.java"],
+    data: [":iorap-functional-test-apps"],
+    static_libs: [
+        // Non-test dependencies
+        // library under test
+        "services.startop.iorap",
+        // Test Dependencies
+        // test android dependencies
+        "platform-test-annotations",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "androidx.test.uiautomator_uiautomator",
+        // test framework dependencies
+        "truth-prebuilt",
+    ],
+    dxflags: ["--multi-dex"],
+    test_suites: ["device-tests"],
+    compile_multilib: "both",
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    certificate: "platform",
+    platform_apis: true,
+}
diff --git a/startop/iorap/functional_tests/AndroidManifest.xml b/startop/iorap/functional_tests/AndroidManifest.xml
new file mode 100644
index 0000000..6bddc4a3
--- /dev/null
+++ b/startop/iorap/functional_tests/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+<!--suppress AndroidUnknownAttribute -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.startop.iorap.tests"
+    android:sharedUserId="com.google.android.startop.iorap.tests.functional"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <!--suppress AndroidDomInspection -->
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.google.android.startop.iorap.tests" />
+
+      <!--
+       'debuggable=true' is required to properly load mockito jvmti dependencies,
+         otherwise it gives the following error at runtime:
+
+       Openjdkjvmti plugin was loaded on a non-debuggable Runtime.
+       Plugin was loaded too late to change runtime state to DEBUGGABLE. -->
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/startop/iorap/functional_tests/AndroidTest.xml b/startop/iorap/functional_tests/AndroidTest.xml
new file mode 100644
index 0000000..31d4f6c
--- /dev/null
+++ b/startop/iorap/functional_tests/AndroidTest.xml
@@ -0,0 +1,70 @@
+<?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="Runs iorap-functional-tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="iorap-functional-tests.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer
+        class="com.android.tradefed.targetprep.DeviceSetup">
+
+        <!-- iorapd does not pick up the above changes until we restart it -->
+        <option name="run-command" value="stop iorapd" />
+
+        <!-- Clean up the existing iorap database. -->
+        <option name="run-command" value="rm -r /data/misc/iorapd/*" />
+        <option name="run-command" value="sleep 1" />
+
+        <!-- Set system properties to enable perfetto tracing, readahead and detailed logging. -->
+        <option name="run-command" value="setprop iorapd.perfetto.enable true" />
+        <option name="run-command" value="setprop iorapd.readahead.enable true" />
+        <option name="run-command" value="setprop iorapd.log.verbose true" />
+
+        <option name="run-command" value="start iorapd" />
+
+        <!-- give it some time to restart the service; otherwise the first unit test might fail -->
+        <option name="run-command" value="sleep 1" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="abort-on-push-failure" value="true" />
+        <option name="push-file"
+          key="iorap_test_app_v1.apk"
+          value="/data/misc/iorapd/iorap_test_app_v1.apk" />
+        <option name="push-file"
+          key="iorap_test_app_v2.apk"
+          value="/data/misc/iorapd/iorap_test_app_v2.apk" />
+        <option name="push-file"
+          key="iorap_test_app_v3.apk"
+          value="/data/misc/iorapd/iorap_test_app_v3.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.google.android.startop.iorap.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <!-- test-timeout unit is ms, value = 30 min -->
+        <option name="test-timeout" value="1800000" />
+    </test>
+
+</configuration>
+
diff --git a/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
new file mode 100644
index 0000000..5352be6
--- /dev/null
+++ b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
@@ -0,0 +1,416 @@
+/*
+ * 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.google.android.startop.iorapd;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.Date;
+import java.util.function.BooleanSupplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.List;
+import java.text.SimpleDateFormat;
+
+/**
+ * Test for the work flow of iorap.
+ *
+ * <p> This test tests the function of iorap from:
+ * perfetto collection -> compilation ->  prefetching -> version update -> perfetto collection.
+ */
+@RunWith(AndroidJUnit4.class)
+public class IorapWorkFlowTest {
+  private static final String TAG = "IorapWorkFlowTest";
+
+  private static final String TEST_APP_VERSION_ONE_PATH = "/data/misc/iorapd/iorap_test_app_v1.apk";
+  private static final String TEST_APP_VERSION_TWO_PATH = "/data/misc/iorapd/iorap_test_app_v2.apk";
+  private static final String TEST_APP_VERSION_THREE_PATH = "/data/misc/iorapd/iorap_test_app_v3.apk";
+
+  private static final String DB_PATH = "/data/misc/iorapd/sqlite.db";
+  private static final Duration TIMEOUT = Duration.ofSeconds(300L);
+
+  private UiDevice mDevice;
+
+  @Before
+  public void setUp() throws Exception {
+    // Initialize UiDevice instance
+    mDevice = UiDevice.getInstance(getInstrumentation());
+
+    // Start from the home screen
+    mDevice.pressHome();
+
+    // Wait for launcher
+    final String launcherPackage = mDevice.getLauncherPackageName();
+    assertThat(launcherPackage, notNullValue());
+    mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIMEOUT.getSeconds());
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    String packageName = "com.example.ioraptestapp";
+    uninstallApk(packageName);
+  }
+
+  @Test (timeout = 300000)
+  public void testNormalWorkFlow() throws Exception {
+    assertThat(mDevice, notNullValue());
+
+    // Install test app version one
+    installApk(TEST_APP_VERSION_ONE_PATH);
+    String packageName = "com.example.ioraptestapp";
+    String activityName = "com.example.ioraptestapp.MainActivity";
+
+    // Perfetto trace collection phase.
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/1L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/1L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/1L));
+
+    // Trigger maintenance service for compilation.
+    TimeUnit.SECONDS.sleep(5L);
+    assertTrue(compile(packageName, activityName, /*version=*/1L));
+
+    // Run app with prefetching
+    assertTrue(startAppWithCompiledTrace(
+        packageName, activityName, /*version=*/1L));
+  }
+
+  @Test (timeout = 300000)
+  public void testUpdateApp() throws Exception {
+    assertThat(mDevice, notNullValue());
+
+    // Install test app version two,
+    String packageName = "com.example.ioraptestapp";
+    String activityName = "com.example.ioraptestapp.MainActivity";
+    installApk(TEST_APP_VERSION_TWO_PATH);
+
+    // Perfetto trace collection phase.
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/2L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/2L));
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/2L));
+
+    // Trigger maintenance service for compilation.
+    TimeUnit.SECONDS.sleep(5L);
+    assertTrue(compile(packageName, activityName, /*version=*/2L));
+
+    // Run app with prefetching
+    assertTrue(startAppWithCompiledTrace(
+        packageName, activityName, /*version=*/2L));
+
+    // Update test app to version 3
+    installApk(TEST_APP_VERSION_THREE_PATH);
+
+    // Rerun app, should do pefetto tracing.
+    assertTrue(startAppForPerfettoTrace(
+        packageName, activityName, /*version=*/3L));
+  }
+
+  private static void installApk(String apkPath) throws Exception {
+    // Disable the selinux to allow pm install apk in the dir.
+    executeShellCommand("setenforce 0");
+    executeShellCommand("pm install -r -d " + apkPath);
+    executeShellCommand("setenforce 1");
+
+  }
+
+  private static void uninstallApk(String apkPath) throws Exception {
+    executeShellCommand("pm uninstall " + apkPath);
+  }
+
+  /**
+   * Starts the testing app to collect the perfetto trace.
+   *
+   * @param expectPerfettoTraceCount is the expected count of perfetto traces.
+   */
+  private boolean startAppForPerfettoTrace(
+      String packageName, String activityName, long version)
+      throws Exception {
+    LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
+    return waitForPerfettoTraceSavedFromLogcat(
+        packageName, activityName, version, timestamp);
+  }
+
+  private boolean startAppWithCompiledTrace(
+      String packageName, String activityName, long version)
+      throws Exception {
+    LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
+    return waitForPrefetchingFromLogcat(
+        packageName, activityName, version, timestamp);
+  }
+
+  private LogcatTimestamp runAppOnce(String packageName, String activityName) throws Exception {
+    // Close the specified app if it's open
+    closeApp(packageName);
+    LogcatTimestamp timestamp = new LogcatTimestamp();
+    // Launch the specified app
+    startApp(packageName, activityName);
+    // Wait for the app to appear
+    mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), TIMEOUT.getSeconds());
+    return timestamp;
+  }
+
+  // Invokes the maintenance to compile the perfetto traces to compiled trace.
+  private boolean compile(
+      String packageName, String activityName, long version) throws Exception {
+    // The job id (283673059) is defined in class IorapForwardingService.
+    executeShellCommandViaTmpFile("cmd jobscheduler run -f android 283673059");
+    return waitForFileExistence(getCompiledTracePath(packageName, activityName, version));
+  }
+
+  private String getCompiledTracePath(
+      String packageName, String activityName, long version) {
+    return String.format(
+        "/data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
+        packageName, version, activityName);
+  }
+
+  /**
+   * Starts the testing app.
+   */
+  private void startApp(String packageName, String activityName) throws Exception {
+    executeShellCommandViaTmpFile(
+        String.format("am start %s/%s", packageName, activityName));
+  }
+
+  /**
+   * Closes the testing app.
+   * <p> Keep trying to kill the process of the app until no process of the app package
+   * appears.</p>
+   */
+  private void closeApp(String packageName) throws Exception {
+    while (true) {
+      String pid = executeShellCommand("pidof " + packageName);
+      if (pid.isEmpty()) {
+        Log.i(TAG, "Closed app " + packageName);
+        return;
+      }
+      executeShellCommand("kill -9 " + pid);
+      TimeUnit.SECONDS.sleep(1L);
+    }
+  }
+
+  /** Waits for a file to appear. */
+  private boolean waitForFileExistence(String fileName) throws Exception {
+    return retryWithTimeout(TIMEOUT, () -> {
+      try {
+        String fileExists = executeShellCommandViaTmpFile(
+            String.format("test -f %s; echo $?", fileName));
+        Log.i(TAG, fileName + " existence is " +  fileExists);
+        return fileExists.trim().equals("0");
+      } catch (Exception e) {
+        Log.i(TAG, e.getMessage());
+        return false;
+      }
+    });
+  }
+
+  /** Waits for the perfetto trace saved message from logcat. */
+  private boolean waitForPerfettoTraceSavedFromLogcat(
+      String packageName, String activityName, long version, LogcatTimestamp timestamp)
+      throws Exception {
+    Pattern p = Pattern.compile(".*"
+        + getPerfettoTraceSavedIndicator(packageName, activityName, version)
+        + "(.*[.]perfetto_trace[.]pb)\n.*", Pattern.DOTALL);
+
+    return retryWithTimeout(TIMEOUT, () -> {
+      try {
+        String log = timestamp.getLogcatAfter();
+        Matcher m = p.matcher(log);
+        Log.d(TAG, "Tries to find perfetto trace...");
+        if (!m.matches()) {
+          Log.i(TAG, "Cannot find perfetto trace saved in log.");
+          return false;
+        }
+        String filePath = m.group(1);
+        Log.i(TAG, "Perfetto trace is saved to " + filePath);
+        return true;
+      } catch(Exception e) {
+        Log.e(TAG, e.getMessage());
+        return false;
+      }
+   });
+  }
+
+  private String getPerfettoTraceSavedIndicator(
+      String packageName, String activityName, long version) {
+    return String.format(
+        "Perfetto TraceBuffer saved to file: /data/misc/iorapd/%s/%d/%s/raw_traces/",
+        packageName, version, activityName);
+  }
+
+  /**
+   * Waits for the prefetching log in the logcat.
+   *
+   * <p> When prefetching works, the perfetto traces should not be collected. </p>
+   */
+  private boolean waitForPrefetchingFromLogcat(
+      String packageName, String activityName, long version, LogcatTimestamp timestamp)
+      throws Exception {
+    Pattern p = Pattern.compile(
+    ".*" + getReadaheadIndicator(packageName, activityName, version) +
+    ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+        + ".*Total Entries=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+        + ".*Total Bytes=(\\d+) \\(good: (\\d+[.]?\\d*)%\\).*",
+    Pattern.DOTALL);
+
+    return retryWithTimeout(TIMEOUT, () -> {
+      try {
+        String log = timestamp.getLogcatAfter();
+        Matcher m = p.matcher(log);
+        if (!m.matches()) {
+          Log.i(TAG, "Cannot find readahead log.");
+          return false;
+        }
+
+        int totalFilePath = Integer.parseInt(m.group(1));
+        float totalFilePathGoodRate = Float.parseFloat(m.group(2)) / 100;
+        int totalEntries = Integer.parseInt(m.group(3));
+        float totalEntriesGoodRate = Float.parseFloat(m.group(4)) / 100;
+        int totalBytes = Integer.parseInt(m.group(5));
+        float totalBytesGoodRate = Float.parseFloat(m.group(6)) / 100;
+
+        Log.i(TAG, String.format(
+            "totalFilePath: %d (good %.2f) totalEntries: %d (good %.2f) totalBytes: %d (good %.2f)",
+            totalFilePath, totalFilePathGoodRate, totalEntries, totalEntriesGoodRate, totalBytes,
+            totalBytesGoodRate));
+
+        return totalFilePath > 0 &&
+          totalEntries > 0 &&
+          totalBytes > 0 &&
+          totalFilePathGoodRate > 0.5 &&
+          totalEntriesGoodRate > 0.5 &&
+          totalBytesGoodRate > 0.5;
+      } catch(Exception e) {
+        return false;
+      }
+   });
+  }
+
+  private static String getReadaheadIndicator(
+      String packageName, String activityName, long version) {
+    return String.format(
+        "Description = /data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
+        packageName, version, activityName);
+  }
+
+  /** Retry until timeout. */
+  private boolean retryWithTimeout(Duration timeout, BooleanSupplier supplier) throws Exception {
+    long totalSleepTimeSeconds = 0L;
+    long sleepIntervalSeconds = 2L;
+    while (true) {
+      if (supplier.getAsBoolean()) {
+        return true;
+      }
+      TimeUnit.SECONDS.sleep(sleepIntervalSeconds);
+      totalSleepTimeSeconds += sleepIntervalSeconds;
+      if (totalSleepTimeSeconds > timeout.getSeconds()) {
+        return false;
+      }
+    }
+  }
+
+  /**
+   * Executes command in adb shell via a tmp file.
+   *
+   * <p> This should be run as root.</p>
+   */
+  private static String executeShellCommandViaTmpFile(String cmd) throws Exception {
+    Log.i(TAG, "Execute via tmp file: " + cmd);
+    Path tmp = null;
+    try {
+      tmp = Files.createTempFile(/*prefix=*/null, /*suffix=*/".sh");
+      Files.write(tmp, cmd.getBytes(StandardCharsets.UTF_8));
+      tmp.toFile().setExecutable(true);
+      return UiDevice.getInstance(
+          InstrumentationRegistry.getInstrumentation()).
+          executeShellCommand(tmp.toString());
+    } finally {
+      if (tmp != null) {
+        Files.delete(tmp);
+      }
+    }
+  }
+
+  /**
+   * Executes command in adb shell.
+   *
+   * <p> This should be run as root.</p>
+   */
+  private static String executeShellCommand(String cmd) throws Exception {
+    Log.i(TAG, "Execute: " + cmd);
+    return UiDevice.getInstance(
+        InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
+  }
+
+  static class LogcatTimestamp {
+    private String epochTime;
+
+    public LogcatTimestamp() throws Exception{
+      long currentTimeMillis = System.currentTimeMillis();
+      epochTime = String.format(
+          "%d.%03d", currentTimeMillis/1000, currentTimeMillis%1000);
+      Log.i(TAG, "Current logcat timestamp is " + epochTime);
+    }
+
+    // For example, 1585264100.000
+    public String getEpochTime() {
+      return epochTime;
+    }
+
+    // Gets the logcat after this epoch time.
+    public String getLogcatAfter() throws Exception {
+      return executeShellCommandViaTmpFile(
+          "logcat -v epoch -t '" + epochTime + "'");
+    }
+  }
+}
+
diff --git a/startop/iorap/src/com/google/android/startop/iorap/ActivityHintEvent.java b/startop/iorap/src/com/google/android/startop/iorap/ActivityHintEvent.java
new file mode 100644
index 0000000..1d38f4c
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/ActivityHintEvent.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Provide a hint to iorapd that an activity has transitioned state.<br /><br />
+ *
+ * Knowledge of when an activity starts/stops can be used by iorapd to increase system
+ * performance (e.g. by launching perfetto tracing to record an io profile, or by
+ * playing back an ioprofile via readahead) over the long run.<br /><br />
+ *
+ * /@see com.google.android.startop.iorap.IIorap#onActivityHintEvent<br /><br />
+ *
+ * Once an activity hint is in {@link #TYPE_STARTED} it must transition to another type.
+ * All other states could be terminal, see below: <br /><br />
+ *
+ * <pre>
+ *
+ *          ┌──────────────────────────────────────┐
+ *          │                                      ▼
+ *        ┌─────────┐     ╔════════════════╗     ╔═══════════╗
+ *    ──▶ │ STARTED │ ──▶ ║   COMPLETED    ║ ──▶ ║ CANCELLED ║
+ *        └─────────┘     ╚════════════════╝     ╚═══════════╝
+ *                          │
+ *                          │
+ *                          ▼
+ *                        ╔════════════════╗
+ *                        ║ POST_COMPLETED ║
+ *                        ╚════════════════╝
+ *
+ * </pre> <!-- system/iorap/docs/binder/ActivityHint.dot -->
+ *
+ * @hide
+ */
+public class ActivityHintEvent implements Parcelable {
+
+    public static final int TYPE_STARTED = 0;
+    public static final int TYPE_CANCELLED = 1;
+    public static final int TYPE_COMPLETED = 2;
+    public static final int TYPE_POST_COMPLETED = 3;
+    private static final int TYPE_MAX = TYPE_POST_COMPLETED;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_STARTED,
+            TYPE_CANCELLED,
+            TYPE_COMPLETED,
+            TYPE_POST_COMPLETED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+    public final ActivityInfo activityInfo;
+
+    public ActivityHintEvent(@Type int type, ActivityInfo activityInfo) {
+        this.type = type;
+        this.activityInfo = activityInfo;
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+        Objects.requireNonNull(activityInfo, "activityInfo");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{type: %d, activityInfo: %s}", type, activityInfo);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof ActivityHintEvent) {
+            return equals((ActivityHintEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(ActivityHintEvent other) {
+        return type == other.type &&
+                Objects.equals(activityInfo, other.activityInfo);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        activityInfo.writeToParcel(out, flags);
+    }
+
+    private ActivityHintEvent(Parcel in) {
+        this.type = in.readInt();
+        this.activityInfo = ActivityInfo.CREATOR.createFromParcel(in);
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<ActivityHintEvent> CREATOR
+            = new Parcelable.Creator<ActivityHintEvent>() {
+        public ActivityHintEvent createFromParcel(Parcel in) {
+            return new ActivityHintEvent(in);
+        }
+
+        public ActivityHintEvent[] newArray(int size) {
+            return new ActivityHintEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/ActivityInfo.java b/startop/iorap/src/com/google/android/startop/iorap/ActivityInfo.java
new file mode 100644
index 0000000..f47a42c
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/ActivityInfo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import java.util.Objects;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * Provide minimal information for launched activities to iorap.<br /><br />
+ *
+ * This uniquely identifies a system-wide activity by providing the {@link #packageName} and
+ * {@link #activityName}.
+ *
+ * @see ActivityHintEvent
+ * @see AppIntentEvent
+ *
+ * @hide
+ */
+public class ActivityInfo implements Parcelable {
+
+    /** The name of the package, for example {@code com.android.calculator}. */
+    public final String packageName;
+    /** The name of the activity, for example {@code .activities.activity.MainActivity} */
+    public final String activityName;
+
+    public ActivityInfo(String packageName, String activityName) {
+        this.packageName = packageName;
+        this.activityName = activityName;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        Objects.requireNonNull(packageName, "packageName");
+        Objects.requireNonNull(activityName, "activityName");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{packageName: %s, activityName: %s}", packageName, activityName);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof ActivityInfo) {
+            return equals((ActivityInfo) other);
+        }
+        return false;
+    }
+
+    private boolean equals(ActivityInfo other) {
+        return Objects.equals(packageName, other.packageName) &&
+                Objects.equals(activityName, other.activityName);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(packageName);
+        out.writeString(activityName);
+    }
+
+    private ActivityInfo(Parcel in) {
+        packageName = in.readString();
+        activityName = in.readString();
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<ActivityInfo> CREATOR
+            = new Parcelable.Creator<ActivityInfo>() {
+        public ActivityInfo createFromParcel(Parcel in) {
+            return new ActivityInfo(in);
+        }
+
+        public ActivityInfo[] newArray(int size) {
+            return new ActivityInfo[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/AppIntentEvent.java b/startop/iorap/src/com/google/android/startop/iorap/AppIntentEvent.java
new file mode 100644
index 0000000..1cd37b5
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/AppIntentEvent.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Notifications for iorapd specifying when a system-wide intent defaults change.<br /><br />
+ *
+ * Intent defaults provide a mechanism for an app to register itself as an automatic handler.
+ * For example the camera app might be registered as the default handler for
+ * {@link android.provider.MediaStore#INTENT_ACTION_STILL_IMAGE_CAMERA} intent. Subsequently,
+ * if an arbitrary other app requests for a still image camera photo to be taken, the system
+ * will launch the respective default camera app to be launched to handle that request.<br /><br />
+ *
+ * In some cases iorapd might need to know default intents, e.g. for boot-time pinning of
+ * applications that resolve from the default intent. If the application would now be resolved
+ * differently, iorapd would unpin the old application and pin the new application.<br /><br />
+ *
+ * @hide
+ */
+public class AppIntentEvent implements Parcelable {
+
+    /** @see android.content.Intent#CATEGORY_DEFAULT */
+    public static final int TYPE_DEFAULT_INTENT_CHANGED = 0;
+    private static final int TYPE_MAX = 0;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_DEFAULT_INTENT_CHANGED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+
+    public final ActivityInfo oldActivityInfo;
+    public final ActivityInfo newActivityInfo;
+
+    // TODO: Probably need the corresponding action here as well.
+
+    public static AppIntentEvent createDefaultIntentChanged(ActivityInfo oldActivityInfo,
+            ActivityInfo newActivityInfo) {
+        return new AppIntentEvent(TYPE_DEFAULT_INTENT_CHANGED, oldActivityInfo,
+                newActivityInfo);
+    }
+
+    private AppIntentEvent(@Type int type, ActivityInfo oldActivityInfo,
+            ActivityInfo newActivityInfo) {
+        this.type = type;
+        this.oldActivityInfo = oldActivityInfo;
+        this.newActivityInfo = newActivityInfo;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+        Objects.requireNonNull(oldActivityInfo, "oldActivityInfo");
+        Objects.requireNonNull(oldActivityInfo, "newActivityInfo");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{oldActivityInfo: %s, newActivityInfo: %s}", oldActivityInfo,
+                newActivityInfo);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof AppIntentEvent) {
+            return equals((AppIntentEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(AppIntentEvent other) {
+        return type == other.type &&
+                Objects.equals(oldActivityInfo, other.oldActivityInfo) &&
+                Objects.equals(newActivityInfo, other.newActivityInfo);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        oldActivityInfo.writeToParcel(out, flags);
+        newActivityInfo.writeToParcel(out, flags);
+    }
+
+    private AppIntentEvent(Parcel in) {
+        this.type = in.readInt();
+        this.oldActivityInfo = ActivityInfo.CREATOR.createFromParcel(in);
+        this.newActivityInfo = ActivityInfo.CREATOR.createFromParcel(in);
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<AppIntentEvent> CREATOR
+            = new Parcelable.Creator<AppIntentEvent>() {
+        public AppIntentEvent createFromParcel(Parcel in) {
+            return new AppIntentEvent(in);
+        }
+
+        public AppIntentEvent[] newArray(int size) {
+            return new AppIntentEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java b/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java
new file mode 100644
index 0000000..8263e0a
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.wm.ActivityMetricsLaunchObserver;
+import com.android.server.wm.ActivityMetricsLaunchObserver.ActivityRecordProto;
+import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Provide a hint to iorapd that an app launch sequence has transitioned state.<br /><br />
+ *
+ * Knowledge of when an activity starts/stops can be used by iorapd to increase system
+ * performance (e.g. by launching perfetto tracing to record an io profile, or by
+ * playing back an ioprofile via readahead) over the long run.<br /><br />
+ *
+ * /@see com.google.android.startop.iorap.IIorap#onAppLaunchEvent <br /><br />
+ * @see com.android.server.wm.ActivityMetricsLaunchObserver
+ *      ActivityMetricsLaunchObserver for the possible event states.
+ * @hide
+ */
+public abstract class AppLaunchEvent implements Parcelable {
+    @LongDef
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SequenceId {}
+
+    public final @SequenceId
+    long sequenceId;
+
+    protected AppLaunchEvent(@SequenceId long sequenceId) {
+        this.sequenceId = sequenceId;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof AppLaunchEvent) {
+            return equals((AppLaunchEvent) other);
+        }
+        return false;
+    }
+
+    protected boolean equals(AppLaunchEvent other) {
+        return sequenceId == other.sequenceId;
+    }
+
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() +
+                "{" + "sequenceId=" + Long.toString(sequenceId) +
+                toStringBody() + "}";
+    }
+
+    protected String toStringBody() { return ""; };
+
+    // List of possible variants:
+
+    public static final class IntentStarted extends AppLaunchEvent {
+        @NonNull
+        public final Intent intent;
+        public final long timestampNs;
+
+        public IntentStarted(@SequenceId long sequenceId,
+                             Intent intent,
+                             long timestampNs) {
+            super(sequenceId);
+            this.intent = intent;
+            this.timestampNs = timestampNs;
+
+            Objects.requireNonNull(intent, "intent");
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof IntentStarted) {
+                return intent.equals(((IntentStarted)other).intent) &&
+                       timestampNs == ((IntentStarted)other).timestampNs &&
+                       super.equals(other);
+            }
+            return false;
+        }
+
+        @Override
+        protected String toStringBody() {
+            return ", intent=" + intent.toString() +
+                   " , timestampNs=" + Long.toString(timestampNs);
+        }
+
+
+        @Override
+        protected void writeToParcelImpl(Parcel p, int flags) {
+            super.writeToParcelImpl(p, flags);
+            IntentProtoParcelable.write(p, intent, flags);
+            p.writeLong(timestampNs);
+        }
+
+        IntentStarted(Parcel p) {
+            super(p);
+            intent = IntentProtoParcelable.create(p);
+            timestampNs = p.readLong();
+        }
+    }
+
+    public static final class IntentFailed extends AppLaunchEvent {
+        public IntentFailed(@SequenceId long sequenceId) {
+            super(sequenceId);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof IntentFailed) {
+                return super.equals(other);
+            }
+            return false;
+        }
+
+        IntentFailed(Parcel p) {
+            super(p);
+        }
+    }
+
+    public static abstract class BaseWithActivityRecordData extends AppLaunchEvent {
+        public final @NonNull
+        @ActivityRecordProto byte[] activityRecordSnapshot;
+
+        protected BaseWithActivityRecordData(@SequenceId long sequenceId,
+                @NonNull @ActivityRecordProto byte[] snapshot) {
+            super(sequenceId);
+            activityRecordSnapshot = snapshot;
+
+            Objects.requireNonNull(snapshot, "snapshot");
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof BaseWithActivityRecordData) {
+                return (Arrays.equals(activityRecordSnapshot,
+                      ((BaseWithActivityRecordData)other).activityRecordSnapshot)) &&
+                        super.equals(other);
+            }
+            return false;
+        }
+
+        @Override
+        protected String toStringBody() {
+            return ", " + new String(activityRecordSnapshot);
+        }
+
+        @Override
+        protected void writeToParcelImpl(Parcel p, int flags) {
+           super.writeToParcelImpl(p, flags);
+           ActivityRecordProtoParcelable.write(p, activityRecordSnapshot, flags);
+        }
+
+        BaseWithActivityRecordData(Parcel p) {
+            super(p);
+            activityRecordSnapshot = ActivityRecordProtoParcelable.create(p);
+        }
+    }
+
+    public static final class ActivityLaunched extends BaseWithActivityRecordData {
+        public final @ActivityMetricsLaunchObserver.Temperature
+        int temperature;
+
+        public ActivityLaunched(@SequenceId long sequenceId,
+                @NonNull @ActivityRecordProto byte[] snapshot, 
+                @ActivityMetricsLaunchObserver.Temperature int temperature) {
+            super(sequenceId, snapshot);
+            this.temperature = temperature;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof ActivityLaunched) {
+                return temperature == ((ActivityLaunched)other).temperature &&
+                        super.equals(other);
+            }
+            return false;
+        }
+
+        @Override
+        protected String toStringBody() {
+            return super.toStringBody() + ", temperature=" + Integer.toString(temperature);
+        }
+
+        @Override
+        protected void writeToParcelImpl(Parcel p, int flags) {
+           super.writeToParcelImpl(p, flags);
+           p.writeInt(temperature);
+        }
+
+        ActivityLaunched(Parcel p) {
+            super(p);
+            temperature = p.readInt();
+        }
+    }
+
+    public static final class ActivityLaunchFinished extends BaseWithActivityRecordData {
+        public final long timestampNs;
+
+        public ActivityLaunchFinished(@SequenceId long sequenceId,
+                @NonNull @ActivityRecordProto byte[] snapshot,
+                long timestampNs) {
+            super(sequenceId, snapshot);
+            this.timestampNs = timestampNs;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof ActivityLaunchFinished) {
+                return timestampNs == ((ActivityLaunchFinished)other).timestampNs &&
+                       super.equals(other);
+            }
+            return false;
+        }
+
+        @Override
+        protected String toStringBody() {
+            return super.toStringBody() + ", timestampNs=" + Long.toString(timestampNs);
+        }
+
+        @Override
+        protected void writeToParcelImpl(Parcel p, int flags) {
+           super.writeToParcelImpl(p, flags);
+           p.writeLong(timestampNs);
+        }
+
+        ActivityLaunchFinished(Parcel p) {
+            super(p);
+            timestampNs = p.readLong();
+        }
+    }
+
+     public static class ActivityLaunchCancelled extends AppLaunchEvent {
+        public final @Nullable @ActivityRecordProto byte[] activityRecordSnapshot;
+
+        public ActivityLaunchCancelled(@SequenceId long sequenceId,
+                @Nullable @ActivityRecordProto byte[] snapshot) {
+            super(sequenceId);
+            activityRecordSnapshot = snapshot;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof ActivityLaunchCancelled) {
+                return Arrays.equals(activityRecordSnapshot,
+                    ((ActivityLaunchCancelled)other).activityRecordSnapshot) &&
+                        super.equals(other);
+            }
+            return false;
+        }
+
+        @Override
+        protected String toStringBody() {
+            return super.toStringBody() + ", " + new String(activityRecordSnapshot);
+        }
+
+        @Override
+        protected void writeToParcelImpl(Parcel p, int flags) {
+           super.writeToParcelImpl(p, flags);
+           if (activityRecordSnapshot != null) {
+               p.writeBoolean(true);
+               ActivityRecordProtoParcelable.write(p, activityRecordSnapshot, flags);
+           } else {
+               p.writeBoolean(false);
+           }
+        }
+
+        ActivityLaunchCancelled(Parcel p) {
+            super(p);
+            if (p.readBoolean()) {
+                activityRecordSnapshot = ActivityRecordProtoParcelable.create(p);
+            } else {
+                activityRecordSnapshot = null;
+            }
+        }
+    }
+
+    public static final class ReportFullyDrawn extends BaseWithActivityRecordData {
+        public final long timestampNs;
+
+        public ReportFullyDrawn(@SequenceId long sequenceId,
+                @NonNull @ActivityRecordProto byte[] snapshot,
+                long timestampNs) {
+            super(sequenceId, snapshot);
+            this.timestampNs = timestampNs;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof ReportFullyDrawn) {
+                return timestampNs == ((ReportFullyDrawn)other).timestampNs &&
+                        super.equals(other);
+            }
+            return false;
+        }
+
+        @Override
+        protected String toStringBody() {
+            return super.toStringBody() + ", timestampNs=" + Long.toString(timestampNs);
+        }
+
+        @Override
+        protected void writeToParcelImpl(Parcel p, int flags) {
+           super.writeToParcelImpl(p, flags);
+           p.writeLong(timestampNs);
+        }
+
+        ReportFullyDrawn(Parcel p) {
+            super(p);
+            timestampNs = p.readLong();
+        }
+    }
+
+    @Override
+    public @ContentsFlags int describeContents() { return 0; }
+
+    @Override
+    public void writeToParcel(Parcel p, @WriteFlags int flags) {
+        p.writeInt(getTypeIndex());
+
+        writeToParcelImpl(p, flags);
+    }
+
+
+    public static Creator<AppLaunchEvent> CREATOR =
+            new Creator<AppLaunchEvent>() {
+        @Override
+        public AppLaunchEvent createFromParcel(Parcel source) {
+            int typeIndex = source.readInt();
+
+            Class<?> kls = getClassFromTypeIndex(typeIndex);
+            if (kls == null) {
+                throw new IllegalArgumentException("Invalid type index: " + typeIndex);
+            }
+
+            try {
+                return (AppLaunchEvent) kls.getConstructor(Parcel.class).newInstance(source);
+            } catch (InstantiationException e) {
+                throw new AssertionError(e);
+            } catch (IllegalAccessException e) {
+                throw new AssertionError(e);
+            } catch (InvocationTargetException e) {
+                throw new AssertionError(e);
+            } catch (NoSuchMethodException e) {
+                throw new AssertionError(e);
+            }
+        }
+
+        @Override
+        public AppLaunchEvent[] newArray(int size) {
+            return new AppLaunchEvent[0];
+        }
+    };
+
+    protected void writeToParcelImpl(Parcel p, int flags) {
+        p.writeLong(sequenceId);
+    }
+
+    protected AppLaunchEvent(Parcel p) {
+        sequenceId = p.readLong();
+    }
+
+    private int getTypeIndex() {
+        for (int i = 0; i < sTypes.length; ++i) {
+            if (sTypes[i].equals(this.getClass())) {
+                return i;
+            }
+        }
+        throw new AssertionError("sTypes did not include this type: " + this.getClass());
+    }
+
+    private static @Nullable Class<?> getClassFromTypeIndex(int typeIndex) {
+        if (typeIndex >= 0 && typeIndex < sTypes.length) {
+            return sTypes[typeIndex];
+        }
+        return null;
+    }
+
+    // Index position matters: It is used to encode the specific type in parceling.
+    // Keep up-to-date with C++ side.
+    private static Class<?>[] sTypes = new Class[] {
+            IntentStarted.class,
+            IntentFailed.class,
+            ActivityLaunched.class,
+            ActivityLaunchFinished.class,
+            ActivityLaunchCancelled.class,
+            ReportFullyDrawn.class,
+    };
+
+    public static class ActivityRecordProtoParcelable {
+        public static void write(Parcel p, @ActivityRecordProto byte[] activityRecordSnapshot,
+                int flags) {
+            p.writeByteArray(activityRecordSnapshot);
+        }
+
+        public static @ActivityRecordProto byte[] create(Parcel p) {
+            byte[] data = p.createByteArray();
+
+            return data;
+        }
+    }
+
+    public static class IntentProtoParcelable {
+        private static final int INTENT_PROTO_CHUNK_SIZE = 1024;
+
+        public static void write(Parcel p, @NonNull Intent intent, int flags) {
+            // There does not appear to be a way to 'reset' a ProtoOutputBuffer stream,
+            // so create a new one every time.
+            final ProtoOutputStream protoOutputStream =
+                    new ProtoOutputStream(INTENT_PROTO_CHUNK_SIZE);
+            // Write this data out as the top-most IntentProto (i.e. it is not a sub-object).
+            intent.dumpDebug(protoOutputStream);
+            final byte[] bytes = protoOutputStream.getBytes();
+
+            p.writeByteArray(bytes);
+        }
+
+        // TODO: Should be mockable for testing?
+        // We cannot deserialize in the platform because we don't have a 'readFromProto'
+        // code.
+        public static @NonNull Intent create(Parcel p) {
+            // This will "read" the correct amount of data, but then we discard it.
+            byte[] data = p.createByteArray();
+
+            // Never called by real code in a platform, this binder API is implemented only in C++.
+            return new Intent("<cannot deserialize IntentProto>");
+        }
+    }
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/CheckHelpers.java b/startop/iorap/src/com/google/android/startop/iorap/CheckHelpers.java
new file mode 100644
index 0000000..34aedd7
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/CheckHelpers.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+/**
+ * Convenience short-hand to throw {@link IllegalAccessException} when the arguments
+ * are out-of-range.
+ */
+public class CheckHelpers {
+    /** @throws IllegalAccessException if {@param type} is not in {@code [0..maxValue]} */
+    public static void checkTypeInRange(int type, int maxValue) {
+        if (type < 0) {
+            throw new IllegalArgumentException(
+                    String.format("type must be non-negative (value=%d)", type));
+        }
+        if (type > maxValue) {
+            throw new IllegalArgumentException(
+                    String.format("type out of range (value=%d, max=%d)", type, maxValue));
+        }
+    }
+
+    /** @throws IllegalAccessException if {@param state} is not in {@code [0..maxValue]} */
+    public static void checkStateInRange(int state, int maxValue) {
+        if (state < 0) {
+            throw new IllegalArgumentException(
+                    String.format("state must be non-negative (value=%d)", state));
+        }
+        if (state > maxValue) {
+            throw new IllegalArgumentException(
+                    String.format("state out of range (value=%d, max=%d)", state, maxValue));
+        }
+    }
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java b/startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java
new file mode 100644
index 0000000..72c5eaa
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java
@@ -0,0 +1,114 @@
+/*
+ * 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.google.android.startop.iorap;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Notifications for iorapd specifying when a package is updated by dexopt service.<br /><br />
+ *
+ * @hide
+ */
+public class DexOptEvent implements Parcelable {
+    public static final int TYPE_PACKAGE_UPDATE = 0;
+    private static final int TYPE_MAX = 0;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_PACKAGE_UPDATE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+    public final String packageName;
+
+    @NonNull
+    public static DexOptEvent createPackageUpdate(String packageName) {
+        return new DexOptEvent(TYPE_PACKAGE_UPDATE, packageName);
+    }
+
+    private DexOptEvent(@Type int type, String packageName) {
+        this.type = type;
+        this.packageName = packageName;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+        Objects.requireNonNull(packageName, "packageName");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{DexOptEvent: packageName: %s}", packageName);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof DexOptEvent) {
+            return equals((DexOptEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(DexOptEvent other) {
+        return packageName.equals(other.packageName);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        out.writeString(packageName);
+    }
+
+    private DexOptEvent(Parcel in) {
+        this.type = in.readInt();
+        this.packageName = in.readString();
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<DexOptEvent> CREATOR
+            = new Parcelable.Creator<DexOptEvent>() {
+        public DexOptEvent createFromParcel(Parcel in) {
+            return new DexOptEvent(in);
+        }
+
+        public DexOptEvent[] newArray(int size) {
+            return new DexOptEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java b/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java
new file mode 100644
index 0000000..dcaff26
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2019 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.google.android.startop.iorap;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.util.Log;
+
+import com.android.server.wm.ActivityMetricsLaunchObserver;
+
+import java.io.StringWriter;
+import java.io.PrintWriter;
+
+/**
+ * A validator to check the correctness of event sequence during app startup.
+ *
+ * <p> A valid state transition of event sequence is shown as the following:
+ *
+ * <pre>
+ *
+ *                                +--------------------+
+ *                                |                    |
+ *                                |        INIT        |
+ *                                |                    |
+ *                                +--------------------+
+ *                                          |
+ *                                          |
+ *                                          ↓
+ *                                +--------------------+
+ *                                |                    |
+ *            +-------------------|   INTENT_STARTED   | ←--------------------------------+
+ *            |                   |                    |                                  |
+ *            |                   +--------------------+                                  |
+ *            |                             |                                             |
+ *            |                             |                                             |
+ *            ↓                             ↓                                             |
+ * +--------------------+         +--------------------+                                  |
+ * |                    |         |                    |                                  |
+ * |   INTENT_FAILED    |         | ACTIVITY_LAUNCHED  |------------------+               |
+ * |                    |         |                    |                  |               |
+ * +--------------------+         +--------------------+                  |               |
+ *            |                              |                            |               |
+ *            |                              ↓                            ↓               |
+ *            |                   +--------------------+       +--------------------+     |
+ *            |                   |                    |       |                    |     |
+ *            +------------------ |  ACTIVITY_FINISHED |       | ACTIVITY_CANCELLED |     |
+ *            |                   |                    |       |                    |     |
+ *            |                   +--------------------+       +--------------------+     |
+ *            |                              |                            |               |
+ *            |                              |                            |               |
+ *            |                              ↓                            |               |
+ *            |                   +--------------------+                  |               |
+ *            |                   |                    |                  |               |
+ *            |                   | REPORT_FULLY_DRAWN |                  |               |
+ *            |                   |                    |                  |               |
+ *            |                   +--------------------+                  |               |
+ *            |                              |                            |               |
+ *            |                              |                            |               |
+ *            |                              ↓                            |               |
+ *            |                   +--------------------+                  |               |
+ *            |                   |                    |                  |               |
+ *            +-----------------→ |        END         |←-----------------+               |
+ *                                |                    |                                  |
+ *                                +--------------------+                                  |
+ *                                           |                                            |
+ *                                           |                                            |
+ *                                           |                                            |
+ *                                           +---------------------------------------------
+ *
+ * <p> END is not a real state in implementation. All states that points to END directly
+ * could transition to INTENT_STARTED.
+ *
+ * <p> If any bad transition happened, the state becomse UNKNOWN. The UNKNOWN state
+ * could be accumulated, because during the UNKNOWN state more IntentStarted may
+ * be triggered. To recover from UNKNOWN to INIT, all the accumualted IntentStarted
+ * should termniate.
+ *
+ * <p> During UNKNOWN state, each IntentStarted increases the accumulation, and any of
+ * IntentFailed, ActivityLaunchCancelled and ActivityFinished decreases the accumulation.
+ * ReportFullyDrawn doesn't impact the accumulation.
+ */
+public class EventSequenceValidator implements ActivityMetricsLaunchObserver {
+  static final String TAG = "EventSequenceValidator";
+  /** $> adb shell 'setprop log.tag.EventSequenceValidator VERBOSE' */
+  public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+  private State state = State.INIT;
+  private long accIntentStartedEvents = 0;
+
+  @Override
+  public void onIntentStarted(@NonNull Intent intent, long timestampNs) {
+    if (state == State.UNKNOWN) {
+      logWarningWithStackTrace("IntentStarted during UNKNOWN. " + intent);
+      incAccIntentStartedEvents();
+      return;
+    }
+
+    if (state != State.INIT &&
+        state != State.INTENT_FAILED &&
+        state != State.ACTIVITY_CANCELLED &&
+        state != State.ACTIVITY_FINISHED &&
+        state != State.REPORT_FULLY_DRAWN) {
+      logWarningWithStackTrace(
+          String.format("Cannot transition from %s to %s", state, State.INTENT_STARTED));
+      incAccIntentStartedEvents();
+      incAccIntentStartedEvents();
+      return;
+    }
+
+    Log.i(TAG, String.format("Transition from %s to %s", state, State.INTENT_STARTED));
+    state = State.INTENT_STARTED;
+  }
+
+  @Override
+  public void onIntentFailed() {
+    if (state == State.UNKNOWN) {
+      logWarningWithStackTrace("onIntentFailed during UNKNOWN.");
+      decAccIntentStartedEvents();
+      return;
+    }
+    if (state != State.INTENT_STARTED) {
+      logWarningWithStackTrace(
+          String.format("Cannot transition from %s to %s", state, State.INTENT_FAILED));
+      incAccIntentStartedEvents();
+      return;
+    }
+
+    Log.i(TAG, String.format("Transition from %s to %s", state, State.INTENT_FAILED));
+    state = State.INTENT_FAILED;
+  }
+
+  @Override
+  public void onActivityLaunched(@NonNull @ActivityRecordProto byte[] activity,
+      @Temperature int temperature) {
+    if (state == State.UNKNOWN) {
+      logWarningWithStackTrace("onActivityLaunched during UNKNOWN.");
+      return;
+    }
+    if (state != State.INTENT_STARTED) {
+      logWarningWithStackTrace(
+          String.format("Cannot transition from %s to %s", state, State.ACTIVITY_LAUNCHED));
+      incAccIntentStartedEvents();
+      return;
+    }
+
+    Log.i(TAG, String.format("Transition from %s to %s", state, State.ACTIVITY_LAUNCHED));
+    state = State.ACTIVITY_LAUNCHED;
+  }
+
+  @Override
+  public void onActivityLaunchCancelled(@Nullable @ActivityRecordProto byte[] activity) {
+    if (state == State.UNKNOWN) {
+      logWarningWithStackTrace("onActivityLaunchCancelled during UNKNOWN.");
+      decAccIntentStartedEvents();
+      return;
+    }
+    if (state != State.ACTIVITY_LAUNCHED) {
+      logWarningWithStackTrace(
+          String.format("Cannot transition from %s to %s", state, State.ACTIVITY_CANCELLED));
+      incAccIntentStartedEvents();
+      return;
+    }
+
+    Log.i(TAG, String.format("Transition from %s to %s", state, State.ACTIVITY_CANCELLED));
+    state = State.ACTIVITY_CANCELLED;
+  }
+
+  @Override
+  public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity,
+      long timestampNs) {
+    if (state == State.UNKNOWN) {
+      logWarningWithStackTrace("onActivityLaunchFinished during UNKNOWN.");
+      decAccIntentStartedEvents();
+      return;
+    }
+
+    if (state != State.ACTIVITY_LAUNCHED) {
+      logWarningWithStackTrace(
+          String.format("Cannot transition from %s to %s", state, State.ACTIVITY_FINISHED));
+      incAccIntentStartedEvents();
+      return;
+    }
+
+    Log.i(TAG, String.format("Transition from %s to %s", state, State.ACTIVITY_FINISHED));
+    state = State.ACTIVITY_FINISHED;
+  }
+
+  @Override
+  public void onReportFullyDrawn(@NonNull @ActivityRecordProto byte[] activity,
+      long timestampNs) {
+    if (state == State.UNKNOWN) {
+      logWarningWithStackTrace("onReportFullyDrawn during UNKNOWN.");
+      return;
+    }
+    if (state == State.INIT) {
+      return;
+    }
+
+    if (state != State.ACTIVITY_FINISHED) {
+      logWarningWithStackTrace(
+          String.format("Cannot transition from %s to %s", state, State.REPORT_FULLY_DRAWN));
+      return;
+    }
+
+    Log.i(TAG, String.format("Transition from %s to %s", state, State.REPORT_FULLY_DRAWN));
+    state = State.REPORT_FULLY_DRAWN;
+  }
+
+  enum State {
+    INIT,
+    INTENT_STARTED,
+    INTENT_FAILED,
+    ACTIVITY_LAUNCHED,
+    ACTIVITY_CANCELLED,
+    ACTIVITY_FINISHED,
+    REPORT_FULLY_DRAWN,
+    UNKNOWN,
+  }
+
+  private void incAccIntentStartedEvents() {
+    if (accIntentStartedEvents < 0) {
+      throw new AssertionError("The number of unknowns cannot be negative");
+    }
+    if (accIntentStartedEvents == 0) {
+      state = State.UNKNOWN;
+    }
+    ++accIntentStartedEvents;
+    Log.i(TAG,
+        String.format("inc AccIntentStartedEvents to %d", accIntentStartedEvents));
+  }
+
+  private void decAccIntentStartedEvents() {
+    if (accIntentStartedEvents <= 0) {
+      throw new AssertionError("The number of unknowns cannot be negative");
+    }
+    if(accIntentStartedEvents == 1) {
+      state = State.INIT;
+    }
+    --accIntentStartedEvents;
+    Log.i(TAG,
+        String.format("dec AccIntentStartedEvents to %d", accIntentStartedEvents));
+  }
+
+  private void logWarningWithStackTrace(String log) {
+    if (DEBUG) {
+      StringWriter sw = new StringWriter();
+      PrintWriter pw = new PrintWriter(sw);
+      new Throwable("EventSequenceValidator#getStackTrace").printStackTrace(pw);
+      Log.wtf(TAG, String.format("%s\n%s", log, sw));
+    }
+  }
+}
+
diff --git a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java
new file mode 100644
index 0000000..77046f2
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java
@@ -0,0 +1,806 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+// TODO: rename to com.android.server.startop.iorap
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.IoThread;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.pm.BackgroundDexOptService;
+import com.android.server.pm.PackageManagerService;
+import com.android.server.wm.ActivityMetricsLaunchObserver;
+import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+
+/**
+ * System-server-local proxy into the {@code IIorap} native service.
+ */
+public class IorapForwardingService extends SystemService {
+
+    public static final String TAG = "IorapForwardingService";
+    /** $> adb shell 'setprop log.tag.IorapForwardingService VERBOSE' */
+    public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    /** $> adb shell 'setprop ro.iorapd.enable true' */
+    private static boolean IS_ENABLED = SystemProperties.getBoolean("ro.iorapd.enable", true);
+    /** $> adb shell 'setprop iorapd.forwarding_service.wtf_crash true' */
+    private static boolean WTF_CRASH = SystemProperties.getBoolean(
+            "iorapd.forwarding_service.wtf_crash", false);
+    private static final Duration TIMEOUT = Duration.ofSeconds(600L);
+
+    // "Unique" job ID from the service name. Also equal to 283673059.
+    public static final int JOB_ID_IORAPD = encodeEnglishAlphabetStringIntoInt("iorapd");
+    // Run every 24 hours.
+    public static final long JOB_INTERVAL_MS = TimeUnit.HOURS.toMillis(24);
+
+    private IIorap mIorapRemote;
+    private final Object mLock = new Object();
+    /** Handle onBinderDeath by periodically trying to reconnect. */
+    private final Handler mHandler =
+            new BinderConnectionHandler(IoThread.getHandler().getLooper());
+
+    private volatile IorapdJobService mJobService;  // Write-once (null -> non-null forever).
+    private volatile static IorapForwardingService sSelfService;  // Write once (null -> non-null).
+
+
+    /**
+     * Atomics set to true if the JobScheduler requests an abort.
+     */
+    private final AtomicBoolean mAbortIdleCompilation = new AtomicBoolean(false);
+
+    /**
+     * Initializes the system service.
+     * <p>
+     * Subclasses must define a single argument constructor that accepts the context
+     * and passes it to super.
+     * </p>
+     *
+     * @param context The system server context.
+     */
+    public IorapForwardingService(Context context) {
+        super(context);
+
+        if (DEBUG) {
+            Log.v(TAG, "IorapForwardingService (Context=" + context.toString() + ")");
+        }
+
+        if (sSelfService != null) {
+            throw new AssertionError("only one service instance allowed");
+        }
+        sSelfService = this;
+    }
+
+    //<editor-fold desc="Providers">
+    /*
+     * Providers for external dependencies:
+     * - These are marked as protected to allow tests to inject different values via mocks.
+     */
+
+    @VisibleForTesting
+    protected ActivityMetricsLaunchObserverRegistry provideLaunchObserverRegistry() {
+        ActivityTaskManagerInternal amtInternal =
+                LocalServices.getService(ActivityTaskManagerInternal.class);
+        ActivityMetricsLaunchObserverRegistry launchObserverRegistry =
+                amtInternal.getLaunchObserverRegistry();
+        return launchObserverRegistry;
+    }
+
+    @VisibleForTesting
+    protected IIorap provideIorapRemote() {
+        IIorap iorap;
+        try {
+            iorap = IIorap.Stub.asInterface(ServiceManager.getServiceOrThrow("iorapd"));
+        } catch (ServiceManager.ServiceNotFoundException e) {
+            Log.w(TAG, e.getMessage());
+            return null;
+        }
+
+        try {
+            iorap.asBinder().linkToDeath(provideDeathRecipient(), /*flags*/0);
+        } catch (RemoteException e) {
+            handleRemoteError(e);
+            return null;
+        }
+
+        return iorap;
+    }
+
+    @VisibleForTesting
+    protected DeathRecipient provideDeathRecipient() {
+        return new DeathRecipient() {
+            @Override
+            public void binderDied() {
+                Log.w(TAG, "iorapd has died");
+                retryConnectToRemoteAndConfigure(/*attempts*/0);
+
+                if (mJobService != null) {
+                    mJobService.onIorapdDisconnected();
+                }
+            }
+        };
+    }
+
+    @VisibleForTesting
+    protected boolean isIorapEnabled() {
+        // These two mendel flags should match those in iorapd native process
+        // system/iorapd/src/common/property.h
+        boolean isTracingEnabled =
+            getMendelFlag("iorap_perfetto_enable", "iorapd.perfetto.enable", false);
+        boolean isReadAheadEnabled =
+            getMendelFlag("iorap_readahead_enable", "iorapd.readahead.enable", false);
+        // Same as the property in iorapd.rc -- disabling this will mean the 'iorapd' binder process
+        // never comes up, so all binder connections will fail indefinitely.
+        return IS_ENABLED && (isTracingEnabled || isReadAheadEnabled);
+    }
+
+    private boolean getMendelFlag(String mendelFlag, String sysProperty, boolean defaultValue) {
+        // TODO(yawanng) use DeviceConfig to get mendel property.
+        // DeviceConfig doesn't work and the reason is not clear.
+        // Provider service is already up before IORapForwardService.
+        String mendelProperty = "persist.device_config."
+            + DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT
+            + "."
+            + mendelFlag;
+        return SystemProperties.getBoolean(mendelProperty,
+            SystemProperties.getBoolean(sysProperty, defaultValue));
+    }
+
+    //</editor-fold>
+
+    @Override
+    public void onStart() {
+        if (DEBUG) {
+            Log.v(TAG, "onStart");
+        }
+
+        retryConnectToRemoteAndConfigure(/*attempts*/0);
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == PHASE_BOOT_COMPLETED) {
+            if (DEBUG) {
+                Log.v(TAG, "onBootPhase(PHASE_BOOT_COMPLETED)");
+            }
+
+            if (isIorapEnabled()) {
+                // Set up a recurring background job. This has to be done in a later phase since it
+                // has a dependency the job scheduler.
+                //
+                // Doing this too early can result in a ServiceNotFoundException for 'jobservice'
+                // or a null reference for #getSystemService(JobScheduler.class)
+                mJobService = new IorapdJobService(getContext());
+            }
+        }
+    }
+
+    private class BinderConnectionHandler extends Handler {
+        public BinderConnectionHandler(android.os.Looper looper) {
+            super(looper);
+        }
+
+        public static final int MESSAGE_BINDER_CONNECT = 0;
+
+        private int mAttempts = 0;
+
+        @Override
+        public void handleMessage(android.os.Message message) {
+           switch (message.what) {
+               case MESSAGE_BINDER_CONNECT:
+                   if (!retryConnectToRemoteAndConfigure(mAttempts)) {
+                       mAttempts++;
+                   } else {
+                       mAttempts = 0;
+                   }
+                   break;
+               default:
+                   throw new AssertionError("Unknown message: " + message.toString());
+           }
+        }
+    }
+
+    /**
+     * Handle iorapd shutdowns and crashes, by attempting to reconnect
+     * until the service is reached again.
+     *
+     * <p>The first connection attempt is synchronous,
+     * subsequent attempts are done by posting delayed tasks to the IoThread.</p>
+     *
+     * @return true if connection succeeded now, or false if it failed now [and needs to requeue].
+     */
+    private boolean retryConnectToRemoteAndConfigure(int attempts) {
+        final int sleepTime = 1000;  // ms
+
+        if (DEBUG) {
+            Log.v(TAG, "retryConnectToRemoteAndConfigure - attempt #" + attempts);
+        }
+
+        if (connectToRemoteAndConfigure()) {
+            return true;
+        }
+
+        // Either 'iorapd' is stuck in a crash loop (ouch!!) or we manually
+        // called 'adb shell stop iorapd' , which means this would loop until it comes back
+        // up.
+        //
+        // TODO: it would be good to get nodified of 'adb shell stop iorapd' to avoid
+        // printing this warning.
+        if (DEBUG) {
+            Log.v(TAG, "Failed to connect to iorapd, is it down? Delay for " + sleepTime);
+        }
+
+        // Use a handler instead of Thread#sleep to avoid backing up the binder thread
+        // when this is called from the death recipient callback.
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(BinderConnectionHandler.MESSAGE_BINDER_CONNECT),
+                sleepTime);
+
+        return false;
+
+        // Log.e(TAG, "Can't connect to iorapd - giving up after " + attempts + " attempts");
+    }
+
+    private boolean connectToRemoteAndConfigure() {
+        synchronized (mLock) {
+            // Synchronize against any concurrent calls to this via the DeathRecipient.
+            return connectToRemoteAndConfigureLocked();
+        }
+    }
+
+    private boolean connectToRemoteAndConfigureLocked() {
+        if (!isIorapEnabled()) {
+            if (DEBUG) {
+                Log.v(TAG, "connectToRemoteAndConfigure - iorapd is disabled, skip rest of work");
+            }
+            // When we see that iorapd is disabled (when system server comes up),
+            // it stays disabled permanently until the next system server reset.
+
+            // TODO: consider listening to property changes as a callback, then we can
+            // be more dynamic about handling enable/disable.
+            return true;
+        }
+
+        // Connect to the native binder service.
+        mIorapRemote = provideIorapRemote();
+        if (mIorapRemote == null) {
+            if (DEBUG) {
+                Log.e(TAG, "connectToRemoteAndConfigure - null iorap remote. check for Log.wtf?");
+            }
+            return false;
+        }
+        invokeRemote(mIorapRemote,
+            (IIorap remote) -> remote.setTaskListener(new RemoteTaskListener()) );
+        registerInProcessListenersLocked();
+
+        Log.i(TAG, "Connected to iorapd native service.");
+
+        return true;
+    }
+
+    private final AppLaunchObserver mAppLaunchObserver = new AppLaunchObserver();
+    private final EventSequenceValidator mEventSequenceValidator = new EventSequenceValidator();
+    private final DexOptPackagesUpdated mDexOptPackagesUpdated = new DexOptPackagesUpdated();
+    private boolean mRegisteredListeners = false;
+
+    private void registerInProcessListenersLocked() {
+        if (mRegisteredListeners) {
+            // Listeners are registered only once (idempotent operation).
+            //
+            // Today listeners are tolerant of the remote side going away
+            // by handling remote errors.
+            //
+            // We could try to 'unregister' the listener when we get a binder disconnect,
+            // but we'd still have to handle the case of encountering synchronous errors so
+            // it really wouldn't be a win (other than having less log spew).
+            return;
+        }
+
+        // Listen to App Launch Sequence events from ActivityTaskManager,
+        // and forward them to the native binder service.
+        ActivityMetricsLaunchObserverRegistry launchObserverRegistry =
+                provideLaunchObserverRegistry();
+        launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver);
+        launchObserverRegistry.registerLaunchObserver(mEventSequenceValidator);
+
+        BackgroundDexOptService.getService().addPackagesUpdatedListener(
+                mDexOptPackagesUpdated);
+
+
+        mRegisteredListeners = true;
+    }
+
+    private class DexOptPackagesUpdated implements BackgroundDexOptService.PackagesUpdatedListener {
+        @Override
+        public void onPackagesUpdated(ArraySet<String> updatedPackages) {
+            String[] updated = updatedPackages.toArray(new String[0]);
+            for (String packageName : updated) {
+                Log.d(TAG, "onPackagesUpdated: " + packageName);
+                invokeRemote(mIorapRemote,
+                    (IIorap remote) ->
+                        remote.onDexOptEvent(RequestId.nextValueForSequence(),
+                                DexOptEvent.createPackageUpdate(packageName))
+                );
+            }
+        }
+    }
+
+    private class AppLaunchObserver implements ActivityMetricsLaunchObserver {
+        // We add a synthetic sequence ID here to make it easier to differentiate new
+        // launch sequences on the native side.
+        private @AppLaunchEvent.SequenceId long mSequenceId = -1;
+
+        // All callbacks occur on the same background thread. Don't synchronize explicitly.
+
+        @Override
+        public void onIntentStarted(@NonNull Intent intent, long timestampNs) {
+            // #onIntentStarted [is the only transition that] initiates a new launch sequence.
+            ++mSequenceId;
+
+            if (DEBUG) {
+                Log.v(TAG, String.format("AppLaunchObserver#onIntentStarted(%d, %s, %d)",
+                        mSequenceId, intent, timestampNs));
+            }
+
+            invokeRemote(mIorapRemote,
+                (IIorap remote) ->
+                    remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+                        new AppLaunchEvent.IntentStarted(mSequenceId, intent, timestampNs))
+            );
+        }
+
+        @Override
+        public void onIntentFailed() {
+            if (DEBUG) {
+                Log.v(TAG, String.format("AppLaunchObserver#onIntentFailed(%d)", mSequenceId));
+            }
+
+            invokeRemote(mIorapRemote,
+                (IIorap remote) ->
+                    remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+                        new AppLaunchEvent.IntentFailed(mSequenceId))
+            );
+        }
+
+        @Override
+        public void onActivityLaunched(@NonNull @ActivityRecordProto byte[] activity,
+                @Temperature int temperature) {
+            if (DEBUG) {
+                Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunched(%d, %s, %d)",
+                        mSequenceId, activity, temperature));
+            }
+
+            invokeRemote(mIorapRemote,
+                (IIorap remote) ->
+                    remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+                            new AppLaunchEvent.ActivityLaunched(mSequenceId, activity, temperature))
+            );
+        }
+
+        @Override
+        public void onActivityLaunchCancelled(@Nullable @ActivityRecordProto byte[] activity) {
+            if (DEBUG) {
+                Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchCancelled(%d, %s)",
+                        mSequenceId, activity));
+            }
+
+            invokeRemote(mIorapRemote,
+                (IIorap remote) ->
+                    remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+                            new AppLaunchEvent.ActivityLaunchCancelled(mSequenceId,
+                                    activity)));
+        }
+
+        @Override
+        public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity,
+            long timestampNs) {
+            if (DEBUG) {
+                Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchFinished(%d, %s, %d)",
+                        mSequenceId, activity, timestampNs));
+            }
+
+            invokeRemote(mIorapRemote,
+                (IIorap remote) ->
+                    remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+                        new AppLaunchEvent.ActivityLaunchFinished(mSequenceId,
+                            activity,
+                            timestampNs))
+            );
+        }
+
+        @Override
+        public void onReportFullyDrawn(@NonNull @ActivityRecordProto byte[] activity,
+            long timestampNs) {
+            if (DEBUG) {
+                Log.v(TAG, String.format("AppLaunchObserver#onReportFullyDrawn(%d, %s, %d)",
+                        mSequenceId, activity, timestampNs));
+            }
+
+            invokeRemote(mIorapRemote,
+                (IIorap remote) ->
+                    remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+                        new AppLaunchEvent.ReportFullyDrawn(mSequenceId, activity, timestampNs))
+            );
+        }
+    }
+
+    /**
+     * Debugging:
+     *
+     * $> adb shell dumpsys jobscheduler
+     *
+     * Search for 'IorapdJobServiceProxy'.
+     *
+     *   JOB #1000/283673059: 6e54ed android/com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy
+     *   ^    ^                      ^
+     *   (uid, job id)               ComponentName(package/class)
+     *
+     * Forcing the job to be run, ignoring constraints:
+     *
+     * $> adb shell cmd jobscheduler run -f android 283673059
+     *                                      ^        ^
+     *                                      package  job_id
+     *
+     * ------------------------------------------------------------
+     *
+     * This class is instantiated newly by the JobService every time
+     * it wants to run a new job.
+     *
+     * We need to forward invocations to the current running instance of
+     * IorapForwardingService#IorapdJobService.
+     *
+     * Visibility: Must be accessible from android.app.AppComponentFactory
+     */
+    public static class IorapdJobServiceProxy extends JobService {
+
+        public IorapdJobServiceProxy() {
+            getActualIorapdJobService().bindProxy(this);
+        }
+
+
+        @NonNull
+        private IorapdJobService getActualIorapdJobService() {
+            // Can't ever be null, because the guarantee is that the
+            // IorapForwardingService is always running.
+            // We are in the same process as Job Service.
+            return sSelfService.mJobService;
+        }
+
+        // Called by system to start the job.
+        @Override
+        public boolean onStartJob(JobParameters params) {
+            return getActualIorapdJobService().onStartJob(params);
+        }
+
+        // Called by system to prematurely stop the job.
+        @Override
+        public boolean onStopJob(JobParameters params) {
+            return getActualIorapdJobService().onStopJob(params);
+        }
+    }
+
+    private class IorapdJobService extends JobService {
+        private final ComponentName IORAPD_COMPONENT_NAME;
+
+        private final Object mLock = new Object();
+        // Jobs currently running remotely on iorapd.
+        // They were started by the JobScheduler and need to be finished.
+        private final HashMap<RequestId, JobParameters> mRunningJobs = new HashMap<>();
+
+        private final JobInfo IORAPD_JOB_INFO;
+
+        private volatile IorapdJobServiceProxy mProxy;
+
+        public void bindProxy(IorapdJobServiceProxy proxy) {
+            mProxy = proxy;
+        }
+
+        // Create a new job service which immediately schedules a 24-hour idle maintenance mode
+        // background job to execute.
+        public IorapdJobService(Context context) {
+            if (DEBUG) {
+                Log.v(TAG, "IorapdJobService (Context=" + context.toString() + ")");
+            }
+
+            // Schedule the proxy class to be instantiated by the JobScheduler
+            // when it is time to invoke background jobs for IorapForwardingService.
+
+
+            // This also needs a BIND_JOB_SERVICE permission in
+            // frameworks/base/core/res/AndroidManifest.xml
+            IORAPD_COMPONENT_NAME = new ComponentName(context, IorapdJobServiceProxy.class);
+
+            JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_IORAPD, IORAPD_COMPONENT_NAME);
+            builder.setPeriodic(JOB_INTERVAL_MS);
+
+            builder.setRequiresCharging(true);
+            builder.setRequiresDeviceIdle(true);
+
+            builder.setRequiresStorageNotLow(true);
+
+            IORAPD_JOB_INFO = builder.build();
+
+            JobScheduler js = context.getSystemService(JobScheduler.class);
+            js.schedule(IORAPD_JOB_INFO);
+            Log.d(TAG,
+                    "BgJob Scheduled (jobId=" + JOB_ID_IORAPD
+                            + ", interval: " + JOB_INTERVAL_MS + "ms)");
+        }
+
+        // Called by system to start the job.
+        @Override
+        public boolean onStartJob(JobParameters params) {
+            // Tell iorapd to start a background job.
+            Log.d(TAG, "Starting background job: " + params.toString());
+
+            mAbortIdleCompilation.set(false);
+            // PackageManagerService starts before IORap service.
+            PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
+            List<String> pkgs = pm.getAllPackages();
+            runIdleCompilationAsync(params, pkgs);
+            return true;
+        }
+
+        private void runIdleCompilationAsync(final JobParameters params, final List<String> pkgs) {
+            new Thread("IORap_IdleCompilation") {
+                @Override
+                public void run() {
+                    for (int i = 0; i < pkgs.size(); i++) {
+                        String pkg = pkgs.get(i);
+                        if (mAbortIdleCompilation.get()) {
+                            Log.i(TAG, "The idle compilation is aborted");
+                            return;
+                        }
+
+                        // We wait until that job's sequence ID returns to us with 'Completed',
+                        RequestId request;
+                        synchronized (mLock) {
+                            // TODO: would be cleaner if we got the request from the
+                            // 'invokeRemote' function. Better yet, consider
+                            // a Pair<RequestId, Future<TaskResult>> or similar.
+                            request = RequestId.nextValueForSequence();
+                            mRunningJobs.put(request, params);
+                        }
+
+                        Log.i(TAG, String.format("IORap compile package: %s, [%d/%d]",
+                              pkg, i + 1, pkgs.size()));
+                        boolean shouldUpdateVersions = (i == 0);
+                        if (!invokeRemote(mIorapRemote, (IIorap remote) ->
+                                remote.onJobScheduledEvent(request,
+                                        JobScheduledEvent.createIdleMaintenance(
+                                                JobScheduledEvent.TYPE_START_JOB,
+                                                params,
+                                                pkg,
+                                                shouldUpdateVersions)))) {
+                            synchronized (mLock) {
+                                mRunningJobs.remove(request); // Avoid memory leaks.
+                            }
+                        }
+
+                        // Wait until the job is complete and removed from the running jobs.
+                        retryWithTimeout(TIMEOUT, () -> {
+                            synchronized (mLock) {
+                                return !mRunningJobs.containsKey(request);
+                            }
+                        });
+                    }
+
+                    // Finish the job after all packages are compiled.
+                    if (mProxy != null) {
+                        mProxy.jobFinished(params, /*reschedule*/false);
+                    }
+                }
+          }.start();
+        }
+
+        /** Retry until timeout. */
+        private boolean retryWithTimeout(final Duration timeout, BooleanSupplier supplier) {
+            long totalSleepTimeMs = 0L;
+            long sleepIntervalMs = 10L;
+            while (true) {
+                if (supplier.getAsBoolean()) {
+                    return true;
+                }
+                try {
+                    TimeUnit.MILLISECONDS.sleep(sleepIntervalMs);
+                } catch (InterruptedException e) {
+                    Log.e(TAG, e.getMessage());
+                    return false;
+                }
+
+                totalSleepTimeMs += sleepIntervalMs;
+                if (totalSleepTimeMs > timeout.toMillis()) {
+                    return false;
+                }
+            }
+        }
+
+        // Called by system to prematurely stop the job.
+        @Override
+        public boolean onStopJob(JobParameters params) {
+            // As this is unexpected behavior, print a warning.
+            Log.w(TAG, "onStopJob(params=" + params.toString() + ")");
+            mAbortIdleCompilation.set(true);
+
+            // Yes, retry the job at a later time no matter what.
+            return true;
+        }
+
+        // Listen to *all* task completes for all requests.
+        // The majority of these might be unrelated to background jobs.
+        public void onIorapdTaskCompleted(RequestId requestId) {
+            JobParameters jobParameters;
+            synchronized (mLock) {
+                jobParameters = mRunningJobs.remove(requestId);
+            }
+
+            // Typical case: This was a task callback unrelated to our jobs.
+            if (jobParameters == null) {
+                return;
+            }
+
+            if (DEBUG) {
+                Log.v(TAG,
+                        String.format("IorapdJobService#onIorapdTaskCompleted(%s), found params=%s",
+                                requestId, jobParameters));
+            }
+
+            Log.d(TAG, "Finished background job: " + jobParameters.toString());
+        }
+
+        public void onIorapdDisconnected() {
+            synchronized (mLock) {
+                mRunningJobs.clear();
+            }
+
+            if (DEBUG) {
+                Log.v(TAG, String.format("IorapdJobService#onIorapdDisconnected"));
+            }
+
+            // TODO: should we try to resubmit all incomplete jobs after it's reconnected?
+        }
+    }
+
+    private class RemoteTaskListener extends ITaskListener.Stub {
+        @Override
+        public void onProgress(RequestId requestId, TaskResult result) throws RemoteException {
+            if (DEBUG) {
+                Log.v(TAG,
+                        String.format("RemoteTaskListener#onProgress(%s, %s)", requestId, result));
+            }
+
+            // TODO: implement rest.
+        }
+
+        @Override
+        public void onComplete(RequestId requestId, TaskResult result) throws RemoteException {
+            if (DEBUG) {
+                Log.v(TAG,
+                        String.format("RemoteTaskListener#onComplete(%s, %s)", requestId, result));
+            }
+
+            if (mJobService != null) {
+                mJobService.onIorapdTaskCompleted(requestId);
+            }
+
+            // TODO: implement rest.
+        }
+    }
+
+    /** Allow passing lambdas to #invokeRemote */
+    private interface RemoteRunnable {
+        // TODO: run(RequestId) ?
+        void run(IIorap iorap) throws RemoteException;
+    }
+
+    // Always pass in the iorap directly here to avoid data race.
+    private static boolean invokeRemote(IIorap iorap, RemoteRunnable r) {
+       if (iorap == null) {
+         Log.w(TAG, "IIorap went to null in this thread, drop invokeRemote.");
+         return false;
+       }
+       try {
+           r.run(iorap);
+           return true;
+       } catch (RemoteException e) {
+           // This could be a logic error (remote side returning error), which we need to fix.
+           //
+           // This could also be a DeadObjectException in which case its probably just iorapd
+           // being manually restarted.
+           //
+           // Don't make any assumption, since DeadObjectException could also mean iorapd crashed
+           // unexpectedly.
+           //
+           // DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath.
+           handleRemoteError(e);
+           return false;
+       }
+    }
+
+    private static void handleRemoteError(Throwable t) {
+        if (WTF_CRASH) {
+            // In development modes, we just want to crash.
+            throw new AssertionError("unexpected remote error", t);
+        } else {
+            // Log to wtf which gets sent to dropbox, and in system_server this does not crash.
+            Log.wtf(TAG, t);
+        }
+    }
+
+    // Encode A-Z bitstring into bits. Every character is bits.
+    // Characters outside of the range [a,z] are considered out of range.
+    //
+    // The least significant bits hold the last character.
+    // First 2 bits are left as 0.
+    private static int encodeEnglishAlphabetStringIntoInt(String name) {
+        int value = 0;
+
+        final int CHARS_PER_INT = 6;
+        final int BITS_PER_CHAR = 5;
+        // Note: 2 top bits are unused, this also means our values are non-negative.
+        final char CHAR_LOWER = 'a';
+        final char CHAR_UPPER = 'z';
+
+        if (name.length() > CHARS_PER_INT) {
+            throw new IllegalArgumentException(
+                    "String too long. Cannot encode more than 6 chars: " + name);
+        }
+
+        for (int i = 0; i < name.length(); ++i) {
+           char c = name.charAt(i);
+
+           if (c < CHAR_LOWER || c > CHAR_UPPER) {
+               throw new IllegalArgumentException("String has out-of-range [a-z] chars: " + name);
+           }
+
+           // Avoid sign extension during promotion.
+           int cur_value = (c & 0xFFFF) - (CHAR_LOWER & 0xFFFF);
+           if (cur_value >= (1 << BITS_PER_CHAR)) {
+               throw new AssertionError("wtf? i=" + i + ", name=" + name);
+           }
+
+           value = value << BITS_PER_CHAR;
+           value = value | cur_value;
+        }
+
+        return value;
+    }
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java
new file mode 100644
index 0000000..b91dd71
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.app.job.JobParameters;
+import android.annotation.NonNull;
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Forward JobService events to iorapd. <br /><br />
+ *
+ * iorapd sometimes need to use background jobs. Forwarding these events to iorapd
+ * notifies iorapd when it is an opportune time to execute these background jobs.
+ *
+ * @hide
+ */
+public class JobScheduledEvent implements Parcelable {
+
+    /** JobService#onJobStarted */
+    public static final int TYPE_START_JOB = 0;
+    /** JobService#onJobStopped */
+    public static final int TYPE_STOP_JOB = 1;
+    private static final int TYPE_MAX = 1;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_START_JOB,
+            TYPE_STOP_JOB,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+
+    /** @see JobParameters#getJobId() */
+    public final int jobId;
+
+    public final String packageName;
+
+    public final boolean shouldUpdateVersions;
+
+    /** Device is 'idle' and it's charging (plugged in). */
+    public static final int SORT_IDLE_MAINTENANCE = 0;
+    private static final int SORT_MAX = 0;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "SORT_" }, value = {
+            SORT_IDLE_MAINTENANCE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Sort {}
+
+    /**
+     * Roughly corresponds to the {@code extras} fields in a JobParameters.
+     */
+    @Sort public final int sort;
+
+    /**
+     * Creates a {@link #SORT_IDLE_MAINTENANCE} event from the type and job parameters.
+     *
+     * Only the job ID is retained from {@code jobParams}, all other param info is dropped.
+     */
+    @NonNull
+    public static JobScheduledEvent createIdleMaintenance(
+        @Type int type, JobParameters jobParams, String packageName, boolean shouldUpdateVersions) {
+        return new JobScheduledEvent(
+            type, jobParams.getJobId(), SORT_IDLE_MAINTENANCE, packageName, shouldUpdateVersions);
+    }
+
+    private JobScheduledEvent(@Type int type,
+                              int jobId,
+                              @Sort int sort,
+                              String packageName,
+                              boolean shouldUpdateVersions) {
+        this.type = type;
+        this.jobId = jobId;
+        this.sort = sort;
+        this.packageName = packageName;
+        this.shouldUpdateVersions = shouldUpdateVersions;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+        // No check for 'jobId': any int is valid.
+        CheckHelpers.checkTypeInRange(sort, SORT_MAX);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof JobScheduledEvent) {
+            return equals((JobScheduledEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(JobScheduledEvent other) {
+        return type == other.type &&
+                jobId == other.jobId &&
+                sort == other.sort &&
+                packageName.equals(other.packageName) &&
+                shouldUpdateVersions == other.shouldUpdateVersions;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+            "{type: %d, jobId: %d, sort: %d, packageName: %s, shouldUpdateVersions %b}",
+            type, jobId, sort, packageName, shouldUpdateVersions);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        out.writeInt(jobId);
+        out.writeInt(sort);
+        out.writeString(packageName);
+        out.writeBoolean(shouldUpdateVersions);
+
+        // We do not parcel the entire JobParameters here because there is no C++ equivalent
+        // of that class [which the iorapd side of the binder interface requires].
+    }
+
+    private JobScheduledEvent(Parcel in) {
+        this.type = in.readInt();
+        this.jobId = in.readInt();
+        this.sort = in.readInt();
+        this.packageName = in.readString();
+        this.shouldUpdateVersions = in.readBoolean();
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<JobScheduledEvent> CREATOR
+            = new Parcelable.Creator<JobScheduledEvent>() {
+        public JobScheduledEvent createFromParcel(Parcel in) {
+            return new JobScheduledEvent(in);
+        }
+
+        public JobScheduledEvent[] newArray(int size) {
+            return new JobScheduledEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/PackageEvent.java b/startop/iorap/src/com/google/android/startop/iorap/PackageEvent.java
new file mode 100644
index 0000000..aa4eea7
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/PackageEvent.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.net.Uri;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Forward package manager events to iorapd. <br /><br />
+ *
+ * Knowing when packages are modified by the system are a useful tidbit to help with performance:
+ * for example when a package is replaced, it could be a hint used to invalidate any collected
+ * io profiles used for prefetching or pinning.
+ *
+ * @hide
+ */
+public class PackageEvent implements Parcelable {
+
+    /** @see android.content.Intent#ACTION_PACKAGE_REPLACED */
+    public static final int TYPE_REPLACED = 0;
+    private static final int TYPE_MAX = 0;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_REPLACED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+
+    /** The path that a package is installed in, for example {@code /data/app/.../base.apk}. */
+    public final Uri packageUri;
+    /** The name of the package, for example {@code com.android.calculator}. */
+    public final String packageName;
+
+    @NonNull
+    public static PackageEvent createReplaced(Uri packageUri, String packageName) {
+        return new PackageEvent(TYPE_REPLACED, packageUri, packageName);
+    }
+
+    private PackageEvent(@Type int type, Uri packageUri, String packageName) {
+        this.type = type;
+        this.packageUri = packageUri;
+        this.packageName = packageName;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+        Objects.requireNonNull(packageUri, "packageUri");
+        Objects.requireNonNull(packageName, "packageName");
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof PackageEvent) {
+            return equals((PackageEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(PackageEvent other) {
+        return type == other.type &&
+                Objects.equals(packageUri, other.packageUri) &&
+                Objects.equals(packageName, other.packageName);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{packageUri: %s, packageName: %s}", packageUri, packageName);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        packageUri.writeToParcel(out, flags);
+        out.writeString(packageName);
+    }
+
+    private PackageEvent(Parcel in) {
+        this.type = in.readInt();
+        this.packageUri = Uri.CREATOR.createFromParcel(in);
+        this.packageName = in.readString();
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<PackageEvent> CREATOR
+            = new Parcelable.Creator<PackageEvent>() {
+        public PackageEvent createFromParcel(Parcel in) {
+            return new PackageEvent(in);
+        }
+
+        public PackageEvent[] newArray(int size) {
+            return new PackageEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/RequestId.java b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java
new file mode 100644
index 0000000..503e1c6
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.NonNull;
+
+/**
+ * Uniquely identify an {@link com.google.android.startop.iorap.IIorap} method invocation,
+ * used for asynchronous callbacks by the server. <br /><br />
+ *
+ * As all system server binder calls must be {@code oneway}, this means all invocations
+ * into {@link com.google.android.startop.iorap.IIorap} are non-blocking. The request ID
+ * exists to associate all calls with their respective callbacks in
+ * {@link com.google.android.startop.iorap.ITaskListener}.
+ *
+ * @see com.google.android.startop.iorap.IIorap
+ *
+ * @hide
+ */
+public class RequestId implements Parcelable {
+
+    public final long requestId;
+
+    private static Object mLock = new Object();
+    private static long mNextRequestId = 0;
+
+    /**
+     * Create a monotonically increasing request ID.<br /><br />
+     *
+     * It is invalid to re-use the same request ID for multiple method calls on
+     * {@link com.google.android.startop.iorap.IIorap}; a new request ID must be created
+     * each time.
+     */
+    @NonNull public static RequestId nextValueForSequence() {
+        long currentRequestId;
+        synchronized (mLock) {
+            currentRequestId = mNextRequestId;
+            ++mNextRequestId;
+        }
+        return new RequestId(currentRequestId);
+    }
+
+    private RequestId(long requestId) {
+        this.requestId = requestId;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        if (requestId < 0) {
+            throw new IllegalArgumentException("request id must be non-negative");
+        }
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{requestId: %d}", requestId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Long.hashCode(requestId);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof RequestId) {
+            return equals((RequestId) other);
+        }
+        return false;
+    }
+
+    private boolean equals(RequestId other) {
+        return requestId == other.requestId;
+    }
+
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(requestId);
+    }
+
+    private RequestId(Parcel in) {
+        requestId = in.readLong();
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<RequestId> CREATOR
+            = new Parcelable.Creator<RequestId>() {
+        public RequestId createFromParcel(Parcel in) {
+            return new RequestId(in);
+        }
+
+        public RequestId[] newArray(int size) {
+            return new RequestId[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/SystemServiceEvent.java b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceEvent.java
new file mode 100644
index 0000000..75d47f9
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceEvent.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Forward system service events to iorapd.
+ *
+ * @see com.android.server.SystemService
+ *
+ * @hide
+ */
+public class SystemServiceEvent implements Parcelable {
+
+    /** @see com.android.server.SystemService#onBootPhase */
+    public static final int TYPE_BOOT_PHASE = 0;
+    /** @see com.android.server.SystemService#onStart */
+    public static final int TYPE_START = 1;
+    private static final int TYPE_MAX = TYPE_START;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_BOOT_PHASE,
+            TYPE_START,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+
+    // TODO: do we want to pass the exact build phase enum?
+
+    public SystemServiceEvent(@Type int type) {
+        this.type = type;
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{type: %d}", type);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof SystemServiceEvent) {
+            return equals((SystemServiceEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(SystemServiceEvent other) {
+        return type == other.type;
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+    }
+
+    private SystemServiceEvent(Parcel in) {
+        this.type = in.readInt();
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<SystemServiceEvent> CREATOR
+            = new Parcelable.Creator<SystemServiceEvent>() {
+        public SystemServiceEvent createFromParcel(Parcel in) {
+            return new SystemServiceEvent(in);
+        }
+
+        public SystemServiceEvent[] newArray(int size) {
+            return new SystemServiceEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/SystemServiceUserEvent.java b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceUserEvent.java
new file mode 100644
index 0000000..2e7bafe
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceUserEvent.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Forward user events to iorapd.<br /><br />
+ *
+ * Knowledge of the logged-in user is reserved to be used to set-up appropriate policies
+ * by iorapd (e.g. to handle user default pinned applications changing).
+ *
+ * @see com.android.server.SystemService
+ *
+ * @hide
+ */
+public class SystemServiceUserEvent implements Parcelable {
+
+    /** @see com.android.server.SystemService#onUserStarting */
+    public static final int TYPE_START_USER = 0;
+    /** @see com.android.server.SystemService#onUserUnlocking */
+    public static final int TYPE_UNLOCK_USER = 1;
+    /** @see com.android.server.SystemService#onUserSwitching*/
+    public static final int TYPE_SWITCH_USER = 2;
+    /** @see com.android.server.SystemService#onUserStopping */
+    public static final int TYPE_STOP_USER = 3;
+    /** @see com.android.server.SystemService#onUserStopped */
+    public static final int TYPE_CLEANUP_USER = 4;
+    private static final int TYPE_MAX = TYPE_CLEANUP_USER;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+            TYPE_START_USER,
+            TYPE_UNLOCK_USER,
+            TYPE_SWITCH_USER,
+            TYPE_STOP_USER,
+            TYPE_CLEANUP_USER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    @Type public final int type;
+    public final int userHandle;
+
+    public SystemServiceUserEvent(@Type int type, int userHandle) {
+        this.type = type;
+        this.userHandle = userHandle;
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+        if (userHandle < 0) {
+            throw new IllegalArgumentException("userHandle must be non-negative");
+        }
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{type: %d, userHandle: %d}", type, userHandle);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof SystemServiceUserEvent) {
+            return equals((SystemServiceUserEvent) other);
+        }
+        return false;
+    }
+
+    private boolean equals(SystemServiceUserEvent other) {
+        return type == other.type &&
+                userHandle == other.userHandle;
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        out.writeInt(userHandle);
+    }
+
+    private SystemServiceUserEvent(Parcel in) {
+        this.type = in.readInt();
+        this.userHandle = in.readInt();
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<SystemServiceUserEvent> CREATOR
+            = new Parcelable.Creator<SystemServiceUserEvent>() {
+        public SystemServiceUserEvent createFromParcel(Parcel in) {
+            return new SystemServiceUserEvent(in);
+        }
+
+        public SystemServiceUserEvent[] newArray(int size) {
+            return new SystemServiceUserEvent[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/TaskResult.java b/startop/iorap/src/com/google/android/startop/iorap/TaskResult.java
new file mode 100644
index 0000000..b5fd6d8
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/TaskResult.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Result data accompanying a request for {@link com.google.android.startop.iorap.ITaskListener}
+ * callbacks.<br /><br />
+ *
+ * Following {@link com.google.android.startop.iorap.IIorap} method invocation,
+ * iorapd will issue in-order callbacks for that corresponding {@link RequestId}.<br /><br />
+ *
+ * State transitions are as follows: <br /><br />
+ *
+ * <pre>
+ *          ┌─────────────────────────────┐
+ *          │                             ▼
+ *        ┌───────┐     ┌─────────┐     ╔═══════════╗
+ *    ──▶ │ BEGAN │ ──▶ │ ONGOING │ ──▶ ║ COMPLETED ║
+ *        └───────┘     └─────────┘     ╚═══════════╝
+ *          │             │
+ *          │             │
+ *          ▼             │
+ *        ╔═══════╗       │
+ *    ──▶ ║ ERROR ║ ◀─────┘
+ *        ╚═══════╝
+ *
+ * </pre> <!-- system/iorap/docs/binder/TaskResult.dot -->
+ *
+ * @hide
+ */
+public class TaskResult implements Parcelable {
+
+    public static final int STATE_BEGAN = 0;
+    public static final int STATE_ONGOING = 1;
+    public static final int STATE_COMPLETED = 2;
+    public static final int STATE_ERROR = 3;
+    private static final int STATE_MAX = STATE_ERROR;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "STATE_" }, value = {
+            STATE_BEGAN,
+            STATE_ONGOING,
+            STATE_COMPLETED,
+            STATE_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface State {}
+
+    @State public final int state;
+
+    @Override
+    public String toString() {
+        return String.format("{state: %d}", state);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other instanceof TaskResult) {
+            return equals((TaskResult) other);
+        }
+        return false;
+    }
+
+    private boolean equals(TaskResult other) {
+        return state == other.state;
+    }
+
+    public TaskResult(@State int state) {
+        this.state = state;
+
+        checkConstructorArguments();
+    }
+
+    private void checkConstructorArguments() {
+        CheckHelpers.checkStateInRange(state, STATE_MAX);
+    }
+
+    //<editor-fold desc="Binder boilerplate">
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(state);
+    }
+
+    private TaskResult(Parcel in) {
+        state = in.readInt();
+
+        checkConstructorArguments();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<TaskResult> CREATOR
+            = new Parcelable.Creator<TaskResult>() {
+        public TaskResult createFromParcel(Parcel in) {
+            return new TaskResult(in);
+        }
+
+        public TaskResult[] newArray(int size) {
+            return new TaskResult[size];
+        }
+    };
+    //</editor-fold>
+}
diff --git a/startop/iorap/stress/Android.bp b/startop/iorap/stress/Android.bp
new file mode 100644
index 0000000..6e8725d
--- /dev/null
+++ b/startop/iorap/stress/Android.bp
@@ -0,0 +1,42 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+cc_binary {
+  name: "iorap.stress.memory",
+  srcs: ["main_memory.cc"],
+
+  cflags: [
+      "-Wall",
+      "-Wextra",
+      "-Werror",
+      "-Wno-unused-parameter"
+  ],
+
+  shared_libs: [
+      "libbase"
+  ],
+
+  host_supported: true,
+}
diff --git a/startop/iorap/stress/main_memory.cc b/startop/iorap/stress/main_memory.cc
new file mode 100644
index 0000000..1f26861
--- /dev/null
+++ b/startop/iorap/stress/main_memory.cc
@@ -0,0 +1,126 @@
+//
+// 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.
+//
+
+#include <chrono>
+#include <fstream>
+#include <iostream>
+#include <random>
+#include <string>
+
+#include <string.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+
+#include <android-base/parseint.h>
+
+static constexpr size_t kBytesPerMb = 1048576;
+const size_t kMemoryAllocationSize = 2 * 1024 * kBytesPerMb;
+
+#define USE_MLOCKALL 0
+
+std::string GetProcessStatus(const char* key) {
+  // Build search pattern of key and separator.
+  std::string pattern(key);
+  pattern.push_back(':');
+
+  // Search for status lines starting with pattern.
+  std::ifstream fs("/proc/self/status");
+  std::string line;
+  while (std::getline(fs, line)) {
+    if (strncmp(pattern.c_str(), line.c_str(), pattern.size()) == 0) {
+      // Skip whitespace in matching line (if any).
+      size_t pos = line.find_first_not_of(" \t", pattern.size());
+      if (pos == std::string::npos) {
+        break;
+      }
+      return std::string(line, pos);
+    }
+  }
+  return "<unknown>";
+}
+
+int main(int argc, char** argv) {
+  size_t allocationSize = 0;
+  if (argc >= 2) {
+    if (!android::base::ParseUint(argv[1], /*out*/&allocationSize)) {
+      std::cerr << "Failed to parse the allocation size (must be 0,MAX_SIZE_T)" << std::endl;
+      return 1;
+    }
+  } else {
+    allocationSize = kMemoryAllocationSize;
+  }
+
+  void* mem = malloc(allocationSize);
+  if (mem == nullptr) {
+    std::cerr << "Malloc failed" << std::endl;
+    return 1;
+  }
+
+  volatile int* imem = static_cast<int *>(mem);  // don't optimize out memory usage
+
+  size_t imemCount = allocationSize / sizeof(int);
+
+  std::cout << "Allocated " << allocationSize << " bytes" << std::endl;
+
+  auto seed = std::chrono::high_resolution_clock::now().time_since_epoch().count();
+  std::mt19937 mt_rand(seed);
+
+  size_t randPrintCount = 10;
+
+  // Write random numbers:
+  // * Ensures each page is resident
+  // * Avoids zeroed out pages (zRAM)
+  // * Avoids same-page merging
+  for (size_t i = 0; i < imemCount; ++i) {
+    imem[i] = mt_rand();
+
+    if (i < randPrintCount) {
+      std::cout << "Generated random value: " << imem[i] << std::endl;
+    }
+  }
+
+#if USE_MLOCKALL
+  /*
+   * Lock all pages from the address space of this process.
+   */
+  if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
+    std::cerr << "Mlockall failed" << std::endl;
+    return 1;
+  }
+#else
+  // Use mlock because of the predictable VmLck size.
+  // Using mlockall tends to bring in anywhere from 2-2.5GB depending on the device.
+  if (mlock(mem, allocationSize) != 0) {
+    std::cerr << "Mlock failed" << std::endl;
+    return 1;
+  }
+#endif
+
+  // Validate memory is actually resident and locked with:
+  // $> cat /proc/$(pidof iorap.stress.memory)/status | grep VmLck
+  std::cout << "Locked memory (VmLck) = " << GetProcessStatus("VmLck") << std::endl;
+
+  std::cout << "Press any key to terminate" << std::endl;
+  int any_input;
+  std::cin >> any_input;
+
+  std::cout << "Terminating..." << std::endl;
+
+  munlockall();
+  free(mem);
+
+  return 0;
+}
diff --git a/startop/iorap/tests/Android.bp b/startop/iorap/tests/Android.bp
new file mode 100644
index 0000000..ad3d001
--- /dev/null
+++ b/startop/iorap/tests/Android.bp
@@ -0,0 +1,72 @@
+// Copyright (C) 2018 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.
+
+// TODO: once b/80095087 is fixed, rewrite this back to android_test
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library {
+    name: "libiorap-java-test-lib",
+    srcs: ["src/**/*.kt"],
+    static_libs: [
+        // Non-test dependencies
+        // library under test
+        "services.startop.iorap",
+        // need the system_server code to be on the classpath,
+        "services.core",
+        // Test Dependencies
+        // test android dependencies
+        "platform-test-annotations",
+        "androidx.test.rules",
+        // test framework dependencies
+        "mockito-target-inline-minus-junit4",
+        // "mockito-target-minus-junit4",
+        // Mockito also requires JNI (see Android.mk)
+        // and android:debuggable=true (see AndroidManifest.xml)
+        "truth-prebuilt",
+    ],
+    // sdk_version: "current",
+    // certificate: "platform",
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    // test_suites: ["device-tests"],
+}
+
+android_test {
+    name: "libiorap-java-tests",
+    dxflags: ["--multi-dex"],
+    test_suites: ["device-tests"],
+    static_libs: ["libiorap-java-test-lib"],
+    compile_multilib: "both",
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+        "libmultiplejvmtiagentsinterferenceagent",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.runner",
+    ],
+    // Use private APIs
+    certificate: "platform",
+    platform_apis: true,
+}
diff --git a/startop/iorap/tests/AndroidManifest.xml b/startop/iorap/tests/AndroidManifest.xml
new file mode 100644
index 0000000..b967e72
--- /dev/null
+++ b/startop/iorap/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<!--suppress AndroidUnknownAttribute -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.startop.iorap.tests"
+    android:sharedUserId="com.google.android.startop.iorap.tests"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <!--suppress AndroidDomInspection -->
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.google.android.startop.iorap.tests" />
+
+      <!--
+       'debuggable=true' is required to properly load mockito jvmti dependencies,
+         otherwise it gives the following error at runtime:
+
+       Openjdkjvmti plugin was loaded on a non-debuggable Runtime.
+       Plugin was loaded too late to change runtime state to DEBUGGABLE. -->
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/startop/iorap/tests/AndroidTest.xml b/startop/iorap/tests/AndroidTest.xml
new file mode 100644
index 0000000..6102c44
--- /dev/null
+++ b/startop/iorap/tests/AndroidTest.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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="Runs libiorap-java-tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="libiorap-java-tests.apk" />
+    </target_preparer>
+
+    <!--
+      Our IIorapIntegrationTest.kt requires setlinux to be disabled:
+      it connects to the iorapd binder service but this requires selinux permissions:
+
+      avc:  denied  { find } for service=iorapd pid=2738 uid=10050
+        scontext=u:r:platform_app:s0:c512,c768 tcontext=u:object_r:iorapd_service:s0
+        tclass=service_manager permissive=0
+    -->
+    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer">
+    </target_preparer>
+
+    <!-- do not use DeviceSetup#set-property because it reboots the device b/136200738.
+         furthermore the changes in /data/local.prop don't actually seem to get picked up.
+    -->
+    <target_preparer
+        class="com.android.tradefed.targetprep.DeviceSetup">
+        <!-- we need this magic flag, otherwise it always reboots and breaks the selinux -->
+        <option name="force-skip-system-props" value="true" />
+
+        <!-- Crash instead of using Log.wtf within the system_server iorap code. -->
+        <option name="run-command" value="setprop iorapd.forwarding_service.wtf_crash true" />
+        <!-- IIorapd has fake behavior: it doesn't do anything but reply with 'DONE' status -->
+        <option name="run-command" value="setprop iorapd.binder.fake true" />
+
+        <!-- iorapd does not pick up the above changes until we restart it -->
+        <option name="run-command" value="stop iorapd" />
+        <option name="run-command" value="start iorapd" />
+        <!-- give it some time to restart the service; otherwise the first unit test might fail -->
+        <option name="run-command" value="sleep 1" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.google.android.startop.iorap.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+
+    <!-- using DeviceSetup again does not work. we simply leave the device in a semi-bad
+         state. there is no way to clean this up as far as I know.
+         -->
+
+</configuration>
+
diff --git a/startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt b/startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt
new file mode 100644
index 0000000..51e407d
--- /dev/null
+++ b/startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2019 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.google.android.startop.iorap
+
+import android.content.Intent;
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.test.filters.SmallTest
+import com.google.android.startop.iorap.AppLaunchEvent;
+import com.google.android.startop.iorap.AppLaunchEvent.ActivityLaunched
+import com.google.android.startop.iorap.AppLaunchEvent.ActivityLaunchCancelled
+import com.google.android.startop.iorap.AppLaunchEvent.ActivityLaunchFinished
+import com.google.android.startop.iorap.AppLaunchEvent.IntentStarted;
+import com.google.android.startop.iorap.AppLaunchEvent.IntentFailed;
+import com.google.android.startop.iorap.AppLaunchEvent.ReportFullyDrawn
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+
+/**
+ * Basic unit tests to test all of the [AppLaunchEvent]s in [com.google.android.startop.iorap].
+ */
+@SmallTest
+class AppLaunchEventTest {
+  /**
+   * Test for IntentStarted.
+   */
+  @Test
+  fun testIntentStarted() {
+    var intent = Intent()
+    val valid = IntentStarted(/* sequenceId= */2L, intent, /* timestampNs= */ 1L)
+    val copy = IntentStarted(/* sequenceId= */2L, intent, /* timestampNs= */ 1L)
+    val noneCopy1 = IntentStarted(/* sequenceId= */1L, intent, /* timestampNs= */ 1L)
+    val noneCopy2 = IntentStarted(/* sequenceId= */2L, intent, /* timestampNs= */ 2L)
+    val noneCopy3 = IntentStarted(/* sequenceId= */2L, Intent(), /* timestampNs= */ 1L)
+
+    // equals(Object other)
+    assertThat(valid).isEqualTo(copy)
+    assertThat(valid).isNotEqualTo(noneCopy1)
+    assertThat(valid).isNotEqualTo(noneCopy2)
+    assertThat(valid).isNotEqualTo(noneCopy3)
+
+    // test toString()
+    val result = valid.toString()
+    assertThat(result).isEqualTo("IntentStarted{sequenceId=2, intent=Intent {  } , timestampNs=1}")
+  }
+
+  /**
+   * Test for IntentFailed.
+   */
+  @Test
+  fun testIntentFailed() {
+    val valid = IntentFailed(/* sequenceId= */2L)
+    val copy = IntentFailed(/* sequenceId= */2L)
+    val noneCopy = IntentFailed(/* sequenceId= */1L)
+
+    // equals(Object other)
+    assertThat(valid).isEqualTo(copy)
+    assertThat(valid).isNotEqualTo(noneCopy)
+
+    // test toString()
+    val result = valid.toString()
+    assertThat(result).isEqualTo("IntentFailed{sequenceId=2}")
+  }
+
+  /**
+   * Test for ActivityLaunched.
+   */
+  @Test
+  fun testActivityLaunched() {
+    //var activityRecord =
+    val valid = ActivityLaunched(/* sequenceId= */2L, "test".toByteArray(),
+      /* temperature= */ 0)
+    val copy = ActivityLaunched(/* sequenceId= */2L, "test".toByteArray(),
+      /* temperature= */ 0)
+    val noneCopy1 = ActivityLaunched(/* sequenceId= */1L, "test".toByteArray(),
+      /* temperature= */ 0)
+    val noneCopy2 = ActivityLaunched(/* sequenceId= */1L, "test".toByteArray(),
+      /* temperature= */ 1)
+    val noneCopy3 = ActivityLaunched(/* sequenceId= */1L, "test1".toByteArray(),
+      /* temperature= */ 0)
+
+    // equals(Object other)
+    assertThat(valid).isEqualTo(copy)
+    assertThat(valid).isNotEqualTo(noneCopy1)
+    assertThat(valid).isNotEqualTo(noneCopy2)
+    assertThat(valid).isNotEqualTo(noneCopy3)
+
+    // test toString()
+    val result = valid.toString()
+    assertThat(result).isEqualTo("ActivityLaunched{sequenceId=2, test, temperature=0}")
+  }
+
+
+  /**
+   * Test for ActivityLaunchFinished.
+   */
+  @Test
+  fun testActivityLaunchFinished() {
+    val valid = ActivityLaunchFinished(/* sequenceId= */2L, "test".toByteArray(),
+      /* timestampNs= */ 1L)
+    val copy = ActivityLaunchFinished(/* sequenceId= */2L, "test".toByteArray(),
+      /* timestampNs= */ 1L)
+    val noneCopy1 = ActivityLaunchFinished(/* sequenceId= */1L, "test".toByteArray(),
+      /* timestampNs= */ 1L)
+    val noneCopy2 = ActivityLaunchFinished(/* sequenceId= */1L, "test".toByteArray(),
+      /* timestampNs= */ 2L)
+    val noneCopy3 = ActivityLaunchFinished(/* sequenceId= */2L, "test1".toByteArray(),
+      /* timestampNs= */ 1L)
+
+    // equals(Object other)
+    assertThat(valid).isEqualTo(copy)
+    assertThat(valid).isNotEqualTo(noneCopy1)
+    assertThat(valid).isNotEqualTo(noneCopy2)
+    assertThat(valid).isNotEqualTo(noneCopy3)
+
+    // test toString()
+    val result = valid.toString()
+    assertThat(result).isEqualTo("ActivityLaunchFinished{sequenceId=2, test, timestampNs=1}")
+  }
+
+  /**
+   * Test for ActivityLaunchCancelled.
+   */
+  @Test
+  fun testActivityLaunchCancelled() {
+    val valid = ActivityLaunchCancelled(/* sequenceId= */2L, "test".toByteArray())
+    val copy = ActivityLaunchCancelled(/* sequenceId= */2L, "test".toByteArray())
+    val noneCopy1 = ActivityLaunchCancelled(/* sequenceId= */1L, "test".toByteArray())
+    val noneCopy2 = ActivityLaunchCancelled(/* sequenceId= */2L, "test1".toByteArray())
+
+    // equals(Object other)
+    assertThat(valid).isEqualTo(copy)
+    assertThat(valid).isNotEqualTo(noneCopy1)
+    assertThat(valid).isNotEqualTo(noneCopy2)
+
+    // test toString()
+    val result = valid.toString()
+    assertThat(result).isEqualTo("ActivityLaunchCancelled{sequenceId=2, test}")
+  }
+
+  /**
+   * Test for ReportFullyDrawn.
+   */
+  @Test
+  fun testReportFullyDrawn() {
+    val valid = ReportFullyDrawn(/* sequenceId= */2L, "test".toByteArray(), /* timestampNs= */ 1L)
+    val copy = ReportFullyDrawn(/* sequenceId= */2L, "test".toByteArray(), /* timestampNs= */ 1L)
+    val noneCopy1 = ReportFullyDrawn(/* sequenceId= */1L, "test".toByteArray(),
+      /* timestampNs= */ 1L)
+    val noneCopy2 = ReportFullyDrawn(/* sequenceId= */1L, "test".toByteArray(),
+      /* timestampNs= */ 1L)
+    val noneCopy3 = ReportFullyDrawn(/* sequenceId= */2L, "test1".toByteArray(),
+      /* timestampNs= */ 1L)
+
+    // equals(Object other)
+    assertThat(valid).isEqualTo(copy)
+    assertThat(valid).isNotEqualTo(noneCopy1)
+    assertThat(valid).isNotEqualTo(noneCopy2)
+    assertThat(valid).isNotEqualTo(noneCopy3)
+
+    // test toString()
+    val result = valid.toString()
+    assertThat(result).isEqualTo("ReportFullyDrawn{sequenceId=2, test, timestampNs=1}")
+  }
+}
diff --git a/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt b/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt
new file mode 100644
index 0000000..18c2491
--- /dev/null
+++ b/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap
+
+import android.net.Uri
+import android.os.ServiceManager
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.MediumTest
+import org.junit.Test
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.timeout
+
+// @Ignore("Test is disabled until iorapd is added to init and there's selinux policies for it")
+@MediumTest
+@FlakyTest(bugId = 149098310) // Failing on cuttlefish with SecurityException.
+class IIorapIntegrationTest {
+    /**
+     * @throws ServiceManager.ServiceNotFoundException if iorapd service could not be found
+     */
+    private val iorapService: IIorap by lazy {
+        // TODO: connect to 'iorapd.stub' which doesn't actually do any work other than reply.
+        IIorap.Stub.asInterface(ServiceManager.getServiceOrThrow("iorapd"))
+
+        // Use 'adb shell setenforce 0' otherwise this whole test fails,
+        // because the servicemanager is not allowed to hand out the binder token for iorapd.
+
+        // TODO: implement the selinux policies for iorapd.
+    }
+
+    // A dummy binder stub implementation is required to use with mockito#spy.
+    // Mockito overrides the methods at runtime and tracks how methods were invoked.
+    open class DummyTaskListener : ITaskListener.Stub() {
+        // Note: make parameters nullable to avoid the kotlin IllegalStateExceptions
+        // from using the mockito matchers (eq, argThat, etc).
+        override fun onProgress(requestId: RequestId?, result: TaskResult?) {
+        }
+
+        override fun onComplete(requestId: RequestId?, result: TaskResult?) {
+        }
+    }
+
+    private fun testAnyMethod(func: (RequestId) -> Unit) {
+        val taskListener = spy(DummyTaskListener())!!
+
+        // FIXME: b/149098310
+        return
+
+        try {
+            iorapService.setTaskListener(taskListener)
+            // Note: Binder guarantees total order for oneway messages sent to the same binder
+            // interface, so we don't need any additional blocking here before sending later calls.
+
+            // Every new method call should have a unique request id.
+            val requestId = RequestId.nextValueForSequence()!!
+
+            // Apply the specific function under test.
+            func(requestId)
+
+            // Typical mockito behavior is to allow any-order callbacks, but we want to test order.
+            val inOrder = inOrder(taskListener)
+
+            // The "stub" behavior of iorapd is that every request immediately gets a response of
+            //   BEGAN,ONGOING,COMPLETED
+            inOrder.verify(taskListener, timeout(100))
+                .onProgress(eq(requestId), argThat { it!!.state == TaskResult.STATE_BEGAN })
+            inOrder.verify(taskListener, timeout(100))
+                .onProgress(eq(requestId), argThat { it!!.state == TaskResult.STATE_ONGOING })
+            inOrder.verify(taskListener, timeout(100))
+                .onComplete(eq(requestId), argThat { it!!.state == TaskResult.STATE_COMPLETED })
+            inOrder.verifyNoMoreInteractions()
+        } finally {
+            // iorapService.setTaskListener(null)
+            // FIXME: null is broken, C++ side sees a non-null object.
+        }
+    }
+
+    @Test
+    fun testOnPackageEvent() {
+        // FIXME (b/137134253): implement PackageEvent parsing on the C++ side.
+        // This is currently (silently: b/137135024) failing because IIorap is 'oneway' and the
+        // C++ PackageEvent un-parceling fails since its not implemented fully.
+        /*
+        testAnyMethod { requestId : RequestId ->
+            iorapService.onPackageEvent(requestId,
+                    PackageEvent.createReplaced(
+                            Uri.parse("https://www.google.com"), "com.fake.package"))
+        }
+        */
+    }
+
+    @Test
+    fun testOnAppIntentEvent() {
+        testAnyMethod { requestId: RequestId ->
+            iorapService.onAppIntentEvent(requestId, AppIntentEvent.createDefaultIntentChanged(
+                    ActivityInfo("dont care", "dont care"),
+                    ActivityInfo("dont care 2", "dont care 2")))
+        }
+    }
+
+    @Test
+    fun testOnAppLaunchEvent() {
+        testAnyMethod { requestId : RequestId ->
+            iorapService.onAppLaunchEvent(requestId, AppLaunchEvent.IntentFailed(/*sequenceId*/123))
+        }
+    }
+
+    @Test
+    fun testOnSystemServiceEvent() {
+        testAnyMethod { requestId: RequestId ->
+            iorapService.onSystemServiceEvent(requestId,
+                    SystemServiceEvent(SystemServiceEvent.TYPE_START))
+        }
+    }
+
+    @Test
+    fun testOnSystemServiceUserEvent() {
+        testAnyMethod { requestId: RequestId ->
+            iorapService.onSystemServiceUserEvent(requestId,
+                    SystemServiceUserEvent(SystemServiceUserEvent.TYPE_START_USER, 0))
+        }
+    }
+}
diff --git a/startop/iorap/tests/src/com/google/android/startop/iorap/ParcelablesTest.kt b/startop/iorap/tests/src/com/google/android/startop/iorap/ParcelablesTest.kt
new file mode 100644
index 0000000..150577a
--- /dev/null
+++ b/startop/iorap/tests/src/com/google/android/startop/iorap/ParcelablesTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2018 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.google.android.startop.iorap
+
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import com.google.common.truth.Truth.assertThat
+import org.junit.runners.Parameterized
+
+/**
+ * Basic unit tests to ensure that all of the [Parcelable]s in [com.google.android.startop.iorap]
+ * have a valid-conforming interface implementation.
+ */
+@SmallTest
+@RunWith(Parameterized::class)
+class ParcelablesTest<T : Parcelable>(private val inputData: InputData<T>) {
+    companion object {
+        private val initialRequestId = RequestId.nextValueForSequence()!!
+
+        @JvmStatic
+        @Parameterized.Parameters
+        fun data() = listOf(
+                InputData(
+                        newActivityInfo(),
+                        newActivityInfo(),
+                        ActivityInfo("some package", "some other activity")),
+                InputData(
+                        ActivityHintEvent(ActivityHintEvent.TYPE_COMPLETED, newActivityInfo()),
+                        ActivityHintEvent(ActivityHintEvent.TYPE_COMPLETED, newActivityInfo()),
+                        ActivityHintEvent(ActivityHintEvent.TYPE_POST_COMPLETED,
+                                newActivityInfo())),
+                InputData(
+                        AppIntentEvent.createDefaultIntentChanged(newActivityInfo(),
+                                newActivityInfoOther()),
+                        AppIntentEvent.createDefaultIntentChanged(newActivityInfo(),
+                                newActivityInfoOther()),
+                        AppIntentEvent.createDefaultIntentChanged(newActivityInfoOther(),
+                                newActivityInfo())),
+                InputData(
+                        PackageEvent.createReplaced(newUri(), "some package"),
+                        PackageEvent.createReplaced(newUri(), "some package"),
+                        PackageEvent.createReplaced(newUri(), "some other package")
+                ),
+                InputData(initialRequestId, cloneRequestId(initialRequestId),
+                        RequestId.nextValueForSequence()),
+                InputData(
+                        SystemServiceEvent(SystemServiceEvent.TYPE_BOOT_PHASE),
+                        SystemServiceEvent(SystemServiceEvent.TYPE_BOOT_PHASE),
+                        SystemServiceEvent(SystemServiceEvent.TYPE_START)),
+                InputData(
+                        SystemServiceUserEvent(SystemServiceUserEvent.TYPE_START_USER, 12345),
+                        SystemServiceUserEvent(SystemServiceUserEvent.TYPE_START_USER, 12345),
+                        SystemServiceUserEvent(SystemServiceUserEvent.TYPE_CLEANUP_USER, 12345)),
+                InputData(
+                        TaskResult(TaskResult.STATE_COMPLETED),
+                        TaskResult(TaskResult.STATE_COMPLETED),
+                        TaskResult(TaskResult.STATE_ONGOING))
+        )
+
+        private fun newActivityInfo(): ActivityInfo {
+            return ActivityInfo("some package", "some activity")
+        }
+
+        private fun newActivityInfoOther(): ActivityInfo {
+            return ActivityInfo("some package 2", "some activity 2")
+        }
+
+        private fun newUri(): Uri {
+            return Uri.parse("https://www.google.com")
+        }
+
+        private fun cloneRequestId(requestId: RequestId): RequestId {
+            val constructor = requestId::class.java.declaredConstructors[0]
+            constructor.isAccessible = true
+            return constructor.newInstance(requestId.requestId) as RequestId
+        }
+    }
+
+    /**
+     * Test for [Object.equals] implementation.
+     */
+    @Test
+    fun testEquality() {
+        assertThat(inputData.valid).isEqualTo(inputData.valid)
+        assertThat(inputData.valid).isEqualTo(inputData.validCopy)
+        assertThat(inputData.valid).isNotEqualTo(inputData.validOther)
+    }
+
+    /**
+     * Test for [Parcelable] implementation.
+     */
+    @Test
+    fun testParcelRoundTrip() {
+        // calling writeToParcel and then T::CREATOR.createFromParcel would return the same data.
+        val assertParcels = { it: T, data: InputData<T> ->
+            val parcel = Parcel.obtain()
+            it.writeToParcel(parcel, 0)
+            parcel.setDataPosition(0) // future reads will see all previous writes.
+            assertThat(it).isEqualTo(data.createFromParcel(parcel))
+            parcel.recycle()
+        }
+
+        assertParcels(inputData.valid, inputData)
+        assertParcels(inputData.validCopy, inputData)
+        assertParcels(inputData.validOther, inputData)
+    }
+
+    data class InputData<T : Parcelable>(val valid: T, val validCopy: T, val validOther: T) {
+        val kls = valid.javaClass
+        init {
+            assertThat(valid).isNotSameInstanceAs(validCopy)
+            // Don't use isInstanceOf because of phantom warnings in intellij about Class!
+            assertThat(validCopy.javaClass).isEqualTo(valid.javaClass)
+            assertThat(validOther.javaClass).isEqualTo(valid.javaClass)
+        }
+
+        fun createFromParcel(parcel: Parcel): T {
+            val field = kls.getDeclaredField("CREATOR")
+            val creator = field.get(null) as Parcelable.Creator<T>
+
+            return creator.createFromParcel(parcel)
+        }
+    }
+}