DO NOT MERGE - Merge Android 10 into master

Bug: 139893257
Change-Id: Iade42e1a78a3f2fdd0fe86e274028138114ba6aa
diff --git a/Android.bp b/Android.bp
index 0ae5f8a..e947946 100644
--- a/Android.bp
+++ b/Android.bp
@@ -48,6 +48,18 @@
         "src",
     ],
 
+    /*
+    TODO: Header refactoring cleanup:
+
+    Option 1): Move src/$component/file_name.h to src/$component/include/$component/file_name.h
+    Option 2): Symlink src/$component/include/$component to src/$component
+
+    Set export_include_dirs to '$component/include' for that component.
+
+    Also delete the 'include' directory unless we have code other non-iorap
+    targets are allowed to reference.
+    */
+
     clang: true,
     shared_libs: ["libbase"],
 }
@@ -57,31 +69,29 @@
 
     static_libs: [
         "libiorap-binder",
+        "libplatformprotos",  // android framework C++ protos.
     ],
     shared_libs: [
-        "libiorap",
         "libbinder",
         "libutils",
         "libcutils", // tracing.
 
+        "libfruit",  // dependency injection.
         // TODO: remove these annoying dependencies by hiding them in the main library code.
-    ],
-}
 
-cc_library_shared {
-    name: "libiorap",
-    defaults: [
-        "iorap-default-flags",
-    ],
-    srcs: [
+        // dependency for libplatformprotos
+        // "libprotobuf-cpp-lite",
+
+        // libplatformprotos has an indirect dependency on full, causing compilation/linking
+        // errors if we use lite
+        "libprotobuf-cpp-full",
     ],
 
-    //export_include_dirs: ["include"],
-    //local_include_dirs: ["src/native"],
+    // srcs: [":libprotobuf-internal-protos"],
+    // commented out because it causes compilation errors
+    // TODO: can we use the lite library somehow?
 
-    static_libs: [
-        "libprotobuf-cpp-lite",
-    ],
+    header_libs: ["librxcpp"],
 }
 
 cc_library_static {
@@ -102,6 +112,34 @@
         include_dirs: ["frameworks/native/aidl/binder"],
         export_aidl_headers: true,
     },
+    static_libs: [
+       "libplatformprotos",  // android framework C++ protos.
+    ],
+}
+
+cc_defaults {
+    name: "libiorap-manager-default-dependencies",
+    static_libs: [
+        "libiorap-perfetto",
+    ],
+    defaults: [
+        "libiorap-perfetto-default-dependencies",
+    ],
+    // Users of 'libiorap-manager' also need to include these defaults to avoid
+    // linking errors.
+}
+
+cc_library_static {
+    name: "libiorap-manager",
+    defaults: [
+        "iorap-default-flags",
+        "iorap-default-dependencies",
+        "libiorap-manager-default-dependencies",
+    ],
+
+    srcs: [
+        "src/manager/**/*.cc",
+    ],
 }
 
 cc_binary {
@@ -109,18 +147,61 @@
     defaults: [
         "iorap-default-flags",
         "iorap-default-dependencies",
+        "libiorap-manager-default-dependencies",
     ],
     srcs: [
         "src/iorapd/main.cc",
     ],
+    static_libs: [
+        "libiorap-manager",
+    ],
     init_rc: [
         "iorapd.rc",
     ],
 }
 
+cc_library_static {
+    name: "libiorap-inode2filename",
+    defaults: [
+        "iorap-default-flags",
+        "iorap-default-dependencies",
+    ],
+
+    srcs: [
+        "src/inode2filename/**/*.cc",
+    ],
+}
+
+cc_binary {
+  name: "iorap.inode2filename",
+  defaults: [
+      "iorap-default-flags",
+      "iorap-default-dependencies",
+  ],
+  srcs: [
+      "src/inode2filename/**/*.cc",
+  ],
+  // Easier debugging. TODO: make a separate debug config.
+  // XX: Using -O0 seems to completely hide some errors.
+  cflags: ["-O2", "-UNDEBUG", "-DIORAP_INODE2FILENAME_MAIN=1"],
+  sanitize: {
+    undefined: true,
+    all_undefined: true,
+    // Pretty print when ubsan detects a problem.
+    // Otherwise it just calls abort().
+
+/*
+    diag: {
+      undefined: true,
+    },
+    */ // don't use the diag when you want it to crash.
+  },
+}
+
 cc_test {
     name: "iorapd-tests",
     test_suites: ["device-tests"],
+    gtest: false,  // we use gtest *and* gmock.
     defaults: [
         "iorap-default-flags",
         "iorap-default-dependencies",
@@ -128,4 +209,88 @@
     srcs: [
         "tests/src/**/*.cc",
     ],
+    cflags: ["-O2", "-UNDEBUG"],
+
+    // TODO:  we should probably have per-component tests.
+    static_libs: ["libgmock_main", "libgmock", "libgtest", "libiorap-inode2filename"],
+}
+
+filegroup {
+  name: "libiorap-perfetto-protos",
+  srcs: [
+  ],
+}
+
+// Static libraries cannot export their dependencies,
+// the current convention is to use an extra 'defaults' rule for statics
+// to bring in all the dependencies.
+cc_defaults {
+    name: "libiorap-perfetto-default-dependencies",
+
+    // Some of the libperfetto header typedefs leak out into iorap.
+    // Avoids compilation #include errors.
+    // TODO: clean this up, the headers should not leak out (maybe all we need is a PerfettoConsumer
+    // forward declaration?).
+    include_dirs: ["external/perfetto/include"],
+    // Various perfetto protos are used directly by iorap.
+    //
+    // Furthermore, we need this regardless to avoid linking errors when linking
+    // libiorap-perfetto.a into the main cc_binary rule.
+    static_libs: [
+        "perfetto_trace_protos",
+    ],
+
+    shared_libs: [
+        // Not part of true dependencies: Users of 'libiorap-perfetto' do not link against
+        // libperfetto.
+        // We only put this to avoid linking errors when building iorapd.
+        // TODO: can we split iorapd into libiorapd-main that doesn't link against libperfetto?
+        // only the last cc_binary should need the full transitive closure of the dependency graph.
+        "libperfetto",
+    ]
+}
+
+cc_library_static {
+    name: "libiorap-perfetto",
+    defaults: [
+        "iorap-default-flags",
+        "iorap-default-dependencies",
+        "libiorap-perfetto-default-dependencies",
+    ],
+
+    srcs: [
+        "src/perfetto/**/*.cc",
+    ],
+}
+
+cc_binary {
+  name: "iorap.cmd.perfetto",
+  defaults: [
+      "iorap-default-flags",
+      "iorap-default-dependencies",
+  ],
+  shared_libs: ["libperfetto"],
+  include_dirs: ["external/perfetto/include"],
+  srcs: [
+      "src/perfetto/**/*.cc",
+  ],
+  // Easier debugging. TODO: make a separate debug config.
+  // XX: Using -O0 seems to completely hide some errors.
+  cflags: ["-O2", "-UNDEBUG", "-DIORAP_PERFETTO_MAIN=1"],
+  sanitize: {
+    undefined: true,
+    all_undefined: true,
+    // Pretty print when ubsan detects a problem.
+    // Otherwise it just calls abort().
+
+/*
+    diag: {
+      undefined: true,
+    },
+    */ // don't use the diag when you want it to crash.
+  },
+
+  static_libs: [
+    "perfetto_trace_protos",
+  ],
 }
diff --git a/binder/com/google/android/startop/iorap/AppLaunchEvent.aidl b/binder/com/google/android/startop/iorap/AppLaunchEvent.aidl
new file mode 100644
index 0000000..ea3f9f1
--- /dev/null
+++ b/binder/com/google/android/startop/iorap/AppLaunchEvent.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** @hide */
+parcelable AppLaunchEvent cpp_header "binder/app_launch_event.h";
diff --git a/binder/com/google/android/startop/iorap/IIorap.aidl b/binder/com/google/android/startop/iorap/IIorap.aidl
index 2b58905..fe91d15 100644
--- a/binder/com/google/android/startop/iorap/IIorap.aidl
+++ b/binder/com/google/android/startop/iorap/IIorap.aidl
@@ -19,6 +19,7 @@
 import com.google.android.startop.iorap.ITaskListener;
 
 import com.google.android.startop.iorap.PackageEvent;
+import com.google.android.startop.iorap.AppLaunchEvent;
 import com.google.android.startop.iorap.AppIntentEvent;
 import com.google.android.startop.iorap.SystemServiceEvent;
 import com.google.android.startop.iorap.SystemServiceUserEvent;
@@ -107,6 +108,7 @@
     // in frameworks/base/startop/src/com/google/android/startop/iorap/${Type}Event.java
 
     // void onActivityHintEvent(in RequestId request, in ActivityHintEvent event);
+    void onAppLaunchEvent(in RequestId request, in AppLaunchEvent event);
     void onPackageEvent(in RequestId request, in PackageEvent event);
     void onAppIntentEvent(in RequestId request, in AppIntentEvent event);
     void onSystemServiceEvent(in RequestId request, in SystemServiceEvent event);
diff --git a/include/binder/app_launch_event.h b/include/binder/app_launch_event.h
new file mode 100644
index 0000000..784db16
--- /dev/null
+++ b/include/binder/app_launch_event.h
@@ -0,0 +1,400 @@
+/*
+ * 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.
+ */
+
+#ifndef IORAP_BINDER_APP_LAUNCH_EVENT_H_
+#define IORAP_BINDER_APP_LAUNCH_EVENT_H_
+
+#include "binder/common.h"
+#include "common/introspection.h"
+#include "common/expected.h"
+
+#include <binder/Parcel.h>
+#include <binder/Parcelable.h>
+#include <frameworks/base/core/proto/android/content/intent.pb.h>  // IntentProto
+#include <frameworks/base/core/proto/android/server/activitymanagerservice.pb.h>  // ActivityRecord
+
+namespace iorap {
+namespace binder {
+
+// These protos are part of the iorapd binder ABI, alias them for easier usage.
+using IntentProto = ::android::content::IntentProto;
+using ActivityRecordProto = ::com::android::server::am::ActivityRecordProto;
+
+struct AppLaunchEvent : public ::android::Parcelable {
+  // Index position matters: Keep up-to-date with AppLaunchEvent.java sTypes field.
+  enum class Type : int32_t {
+    kUninitialized = -1,
+    kIntentStarted = 0,
+    kIntentFailed = 1,
+    kActivityLaunched = 2,
+    kActivityLaunchFinished = 3,
+    kActivityLaunchCancelled = 4,
+  };
+
+  enum class Temperature : int32_t {
+    kUninitialized = -1,
+    kCold = 1,
+    kWarm = 2,
+    kHot = 3,
+  };
+
+  Type type{Type::kUninitialized};
+  int64_t sequence_id{-1};
+  // kIntentStarted only.
+  std::unique_ptr<IntentProto> intent_proto;
+  // kActivityLaunched only.
+  Temperature temperature{Temperature::kUninitialized};
+  // kActivityLaunch*. Can be null in kActivityLaunchCancelled.
+  std::unique_ptr<ActivityRecordProto> activity_record_proto;
+
+  AppLaunchEvent() = default;
+  AppLaunchEvent(Type type,
+                 int64_t sequence_id,
+                 std::unique_ptr<IntentProto> intent_proto = nullptr,
+                 Temperature temperature = Temperature::kUninitialized,
+                 std::unique_ptr<ActivityRecordProto> activity_record_proto = nullptr)
+    : type(type),
+      sequence_id(sequence_id),
+      intent_proto(std::move(intent_proto)),
+      temperature(temperature),
+      activity_record_proto(std::move(activity_record_proto)) {
+  }
+
+  ::android::status_t readFromParcel(const android::Parcel* parcel) override {
+
+#   define PARCEL_READ_OR_RETURN(function, ...) \
+    if (::android::status_t res = function(__VA_ARGS__); res != ::android::NO_ERROR) { \
+      LOG(ERROR) << "AppLaunchEvent::readFromParcel failed"; \
+      return res; \
+    }
+
+    int32_t type_int;
+    PARCEL_READ_OR_RETURN(parcel->readInt32, &type_int);
+    type = static_cast<Type>(type_int);
+
+    LOG(VERBOSE) << "AppLaunchEvent::readFromParcel (type=" << type_int << ")";
+
+    PARCEL_READ_OR_RETURN(parcel->readInt64, &sequence_id);
+
+    switch (type) {
+      case Type::kIntentStarted:
+        PARCEL_READ_OR_RETURN(readIntent, parcel);
+        break;
+      case Type::kIntentFailed:
+        // No extra arguments.
+        break;
+      case Type::kActivityLaunched: {
+        PARCEL_READ_OR_RETURN(readActivityRecordProto, parcel);
+        int32_t temperature_int;
+        PARCEL_READ_OR_RETURN(parcel->readInt32, &temperature_int);
+        temperature = static_cast<Temperature>(temperature_int);
+        break;
+      }
+      case Type::kActivityLaunchFinished:
+        PARCEL_READ_OR_RETURN(readActivityRecordProto, parcel);
+        break;
+      case Type::kActivityLaunchCancelled:
+        PARCEL_READ_OR_RETURN(readActivityRecordProtoNullable, parcel);
+        break;
+      default:
+        return android::BAD_VALUE;
+    }
+#   undef PARCEL_READ_OR_RETURN
+
+    return ::android::NO_ERROR;
+
+    // TODO: std::variant + protobuf implementation in AutoParcelable.
+  }
+
+#define PARCEL_WRITE_OR_RETURN(function, ...) \
+  if (::android::status_t res = function(__VA_ARGS__); res != ::android::NO_ERROR) { \
+    return res; \
+  }
+
+  ::android::status_t writeToParcel(android::Parcel* parcel) const override {
+    PARCEL_WRITE_OR_RETURN(parcel->writeInt32, static_cast<int32_t>(type));
+    PARCEL_WRITE_OR_RETURN(parcel->writeInt64, sequence_id);
+
+    switch (type) {
+      case Type::kIntentStarted:
+        PARCEL_WRITE_OR_RETURN(writeIntent, parcel);
+        break;
+      case Type::kIntentFailed:
+        // No extra arguments.
+        break;
+      case Type::kActivityLaunched:
+        PARCEL_WRITE_OR_RETURN(writeActivityRecordProto, parcel);
+        PARCEL_WRITE_OR_RETURN(parcel->writeInt32, static_cast<int32_t>(temperature));
+        break;
+      case Type::kActivityLaunchFinished:
+        PARCEL_WRITE_OR_RETURN(writeActivityRecordProto, parcel);
+        break;
+      case Type::kActivityLaunchCancelled:
+        PARCEL_WRITE_OR_RETURN(writeActivityRecordProtoNullable, parcel);
+        break;
+      default:
+        DCHECK(false) << "attempted to write an uninitialized AppLaunchEvent to Parcel";
+        return android::BAD_VALUE;
+    }
+
+#undef PARCEL_WRITE_OR_RETURN
+
+    return android::NO_ERROR;
+  }
+
+ private:
+  // Using 'unique_ptr' here because protobufs don't have a move constructor. Is there
+  // a better way that is cheap to pass them around?
+  template <typename T>
+  static expected<std::unique_ptr<T>, ::android::status_t>
+  ReadProto(const android::Parcel* parcel) {
+    DCHECK(parcel != nullptr);
+
+    ::android::status_t res;
+
+    std::vector<uint8_t> byte_vector;
+    if ((res = parcel->readByteVector(/*out*/&byte_vector)) != ::android::NO_ERROR) {
+      return unexpected(res);
+    }
+    // TODO: we may want to do this without an extra copy, by parsing
+    // the protobuf directly out of the parcel.
+
+    const uint8_t* data = byte_vector.data();
+    const size_t size = byte_vector.size();
+
+    std::unique_ptr<T> proto_ptr{new T{}};
+
+    if (!proto_ptr) {
+      return unexpected(::android::NO_MEMORY);
+    }
+
+    if (!proto_ptr->ParseFromArray(data, size)) {
+      return unexpected(::android::BAD_VALUE);
+    }
+
+    return proto_ptr;
+  }
+
+  template <typename T>
+  static expected<std::unique_ptr<T>, ::android::status_t>
+  ReadNullableProto(const android::Parcel* parcel) {
+    DCHECK(parcel != nullptr);
+
+    bool value;
+
+    ::android::status_t res;
+    res = parcel->readBool(/*out*/&value);
+
+    if (res != ::android::NO_ERROR) {
+      return unexpected(res);
+    }
+
+    if (!value) {
+      return std::unique_ptr<T>{nullptr};
+    }
+
+    return ReadProto<T>(parcel);
+  }
+
+  template <typename T>
+  static ::android::status_t
+  WriteProto(android::Parcel* parcel, const std::unique_ptr<T>& proto) {
+    DCHECK(parcel != nullptr);
+    DCHECK(proto != nullptr);
+
+    std::vector<uint8_t> byte_vector;
+    {
+      const int serialized_size = proto->ByteSize();
+      byte_vector.resize(serialized_size);
+      if (!proto->SerializeToArray(byte_vector.data(), serialized_size)) {
+        return ::android::BAD_VALUE;
+      }
+    }
+
+    ::android::status_t res;
+    if ((res = parcel->writeByteVector(/*in*/byte_vector)) != ::android::NO_ERROR) {
+      return res;
+    }
+
+    return ::android::NO_ERROR;
+  }
+
+  template <typename T>
+  static ::android::status_t
+  WriteNullableProto(android::Parcel* parcel, const std::unique_ptr<T>& maybe_proto) {
+    bool value = (maybe_proto != nullptr);
+
+    ::android::status_t res;
+    res = parcel->writeBool(value);
+
+    if (res != ::android::NO_ERROR) {
+      return res;
+    }
+
+    if (!value) {
+      return ::android::NO_ERROR;
+    }
+
+    return WriteProto<T>(parcel, maybe_proto);
+  }
+
+  android::status_t readIntent(const android::Parcel* parcel) {
+    expected<std::unique_ptr<IntentProto>, ::android::status_t> maybe_intent =
+        ReadProto<IntentProto>(parcel);
+
+    if (maybe_intent) {
+      intent_proto = std::move(maybe_intent.value());
+      return ::android::NO_ERROR;
+    } else {
+      return maybe_intent.error();
+    }
+  }
+
+  android::status_t readActivityRecordProto(const android::Parcel* parcel) {
+    expected<std::unique_ptr<ActivityRecordProto>, ::android::status_t> maybe_record =
+        ReadProto<ActivityRecordProto>(parcel);
+
+    if (maybe_record) {
+      activity_record_proto = std::move(maybe_record.value());
+      return ::android::NO_ERROR;
+    } else {
+      return maybe_record.error();
+    }
+  }
+
+  android::status_t readActivityRecordProtoNullable(const android::Parcel* parcel) {
+    expected<std::unique_ptr<ActivityRecordProto>, ::android::status_t> maybe_record =
+        ReadNullableProto<ActivityRecordProto>(parcel);
+
+    if (maybe_record) {
+      activity_record_proto = std::move(maybe_record.value());
+      return ::android::NO_ERROR;
+    } else {
+      return maybe_record.error();
+    }
+  }
+
+  android::status_t writeIntent(android::Parcel* parcel) const {
+    return WriteProto<IntentProto>(parcel, intent_proto);
+  }
+
+  android::status_t writeActivityRecordProto(android::Parcel* parcel) const {
+    return WriteProto<ActivityRecordProto>(parcel, activity_record_proto);
+  }
+
+  android::status_t writeActivityRecordProtoNullable(android::Parcel* parcel) const {
+    return WriteNullableProto<ActivityRecordProto>(parcel, activity_record_proto);
+  }
+};
+
+inline std::ostream& operator<<(std::ostream& os, const AppLaunchEvent::Type& type) {
+  switch (type) {
+    case AppLaunchEvent::Type::kUninitialized:
+      os << "kUninitialized";
+      break;
+    case AppLaunchEvent::Type::kIntentStarted:
+      os << "kIntentStarted";
+      break;
+    case AppLaunchEvent::Type::kIntentFailed:
+      os << "kIntentFailed";
+      break;
+    case AppLaunchEvent::Type::kActivityLaunched:
+      os << "kActivityLaunched";
+      break;
+    case AppLaunchEvent::Type::kActivityLaunchCancelled:
+      os << "kActivityLaunchCancelled";
+      break;
+    case AppLaunchEvent::Type::kActivityLaunchFinished:
+      os << "kActivityLaunchFinished";
+      break;
+    default:
+      os << "(unknown)";
+  }
+  return os;
+}
+
+inline std::ostream& operator<<(std::ostream& os, const AppLaunchEvent::Temperature& type) {
+  switch (type) {
+    case AppLaunchEvent::Temperature::kUninitialized:
+      os << "kUninitialized";
+      break;
+    case AppLaunchEvent::Temperature::kCold:
+      os << "kCold";
+      break;
+    case AppLaunchEvent::Temperature::kWarm:
+      os << "kWarm";
+      break;
+    case AppLaunchEvent::Temperature::kHot:
+      os << "kHot";
+      break;
+    default:
+      os << "(unknown)";
+  }
+  return os;
+}
+
+inline std::ostream& operator<<(std::ostream& os, const AppLaunchEvent& e) {
+  os << "AppLaunchEvent{";
+  os << "type=" << e.type << ",";
+  os << "sequence_id=" << e.sequence_id << ",";
+
+  os << "intent_proto=";
+  if (e.intent_proto == nullptr) {
+    os << "(nullptr)";
+  } else {
+    os << "(action=" << e.intent_proto->action() << ",";
+    os << "component=";
+    if (e.intent_proto->has_component()) {
+      // $package/$class_name
+      os << e.intent_proto->component().package_name() << "/"
+         << e.intent_proto->component().class_name();
+    } else {
+      os << "(no component)";
+    }
+    os << ")";
+  }
+  os << ",";
+
+  os << "temperature=" << e.temperature << ",";
+  os << ",";
+
+  os << "activity_record_proto=";
+  if (e.activity_record_proto == nullptr) {
+    os << "(nullptr)";
+  } else {
+    // title or component name.
+    os << "'" << e.activity_record_proto->identifier().title() << "'";
+  }
+  os << "}";
+
+  return os;
+}
+
+/*
+IORAP_INTROSPECT_ADAPT_STRUCT(AppLaunchEvent,
+                              type,
+                              sequence_id,
+                              intent_proto,
+                              temperature,
+                              activity_record_proto);
+*/
+
+}  // namespace binder
+}  // namespace iorap
+
+IORAP_JAVA_NAMESPACE_BINDER_TYPEDEF(AppLaunchEvent)
+
+#endif  // IORAP_BINDER_APP_LAUNCH_EVENT_H_
diff --git a/src/binder/iiorap_def.h b/src/binder/iiorap_def.h
index b88f873..0c1eac8 100644
--- a/src/binder/iiorap_def.h
+++ b/src/binder/iiorap_def.h
@@ -27,6 +27,8 @@
 FN_BEGIN(::com::google::android::startup::iorap::,IIorap)                                          \
 /* name              <see IORAP_BINDER_PARAM_JOIN> */                                              \
 FN(setTaskListener, (const ::android::sp<::com::google::android::startop::iorap::,ITaskListener,>&,listener)) /*NOLINT*/ \
+FN(onAppLaunchEvent,(const ::com::google::android::startop::iorap::,RequestId,&,request),          \
+                    (const ::com::google::android::startop::iorap::,AppLaunchEvent,&,event))       \
 FN(onPackageEvent,  (const ::com::google::android::startop::iorap::,RequestId,&,request),          \
                     (const ::com::google::android::startop::iorap::,PackageEvent,&,event))         \
 FN(onAppIntentEvent,(const ::com::google::android::startop::iorap::,RequestId,&,request),          \
diff --git a/src/binder/iiorap_impl.cc b/src/binder/iiorap_impl.cc
index 52ef9fa..a6f6409 100644
--- a/src/binder/iiorap_impl.cc
+++ b/src/binder/iiorap_impl.cc
@@ -17,8 +17,10 @@
 #include "binder/iiorap_impl.h"
 #include "binder/iiorap_def.h"
 #include "common/macros.h"
+#include "manager/event_manager.h"
 
 #include <android-base/logging.h>
+#include <android-base/properties.h>
 #include <binder/BinderService.h>
 #include <binder/IPCThreadState.h>
 #include <include/binder/request_id.h>
@@ -57,6 +59,25 @@
 #undef IIORAP_IMPL_ARG_NAMES
 #undef IIORAP_IMPL_ARGS
 
+namespace {
+
+struct ServiceParams {
+  bool fake_{false};
+  std::shared_ptr<manager::EventManager> event_manager_;
+};
+
+static std::atomic<bool> s_service_started_{false};
+static std::atomic<bool> s_service_params_ready_{false};
+
+// TODO: BinderService constructs IIorapImpl,
+// but how do I get a pointer to it afterwards?
+//
+// This is a workaround for that, by using a global.
+static ServiceParams s_service_params_;
+static std::atomic<ServiceParams*> s_service_params_atomic_;
+
+}  // namespace anonymous
+
 class IIorapImpl::Impl {
  public:
   void SetTaskListener(const ::android::sp<ITaskListener>& listener) {
@@ -90,28 +111,88 @@
     }
   }
 
+  bool OnAppLaunchEvent(const RequestId& request_id,
+                        const AppLaunchEvent& event) {
+    if (MaybeHandleFakeBehavior(request_id)) {
+      return true;
+    }
+
+    return service_params_.event_manager_->OnAppLaunchEvent(request_id, event);
+  }
+
+  void HandleFakeBehavior(const RequestId& request_id) {
+    DCHECK(service_params_.fake_);
+
+    // Send these dummy callbacks for testing only.
+    ReplyWithResult(request_id, TaskResult::State::kBegan);
+    ReplyWithResult(request_id, TaskResult::State::kOngoing);
+    ReplyWithResult(request_id, TaskResult::State::kCompleted);
+  }
+
+  // TODO: Subclass IIorap with a separate fake implementation.
+  bool MaybeHandleFakeBehavior(const RequestId& request_id) {
+    if (service_params_.fake_) {
+      HandleFakeBehavior(request_id);
+      return true;
+    }
+
+    return false;
+  }
+
   ::android::sp<ITaskListener> listener_;
+
+  Impl(ServiceParams p) : service_params_{std::move(p)} {
+    CHECK(service_params_.event_manager_ != nullptr);
+  }
+
+  ServiceParams service_params_;
 };
 
 using Impl = IIorapImpl::Impl;
 
-IIorapImpl::IIorapImpl() : impl_(new Impl()) {}
+IIorapImpl::IIorapImpl() {
+  // Acquire edge of synchronizes-with IIorapImpl::Start().
+  CHECK(s_service_params_ready_.load());
+  // Do not turn this into a DCHECK, the above atomic load
+  // must happen-before the read of s_service_params_ready_.
+  impl_.reset(new Impl(std::move(s_service_params_)));
+}
 
 namespace {
   static bool started_ = false;
 }
-bool IIorapImpl::Start() {
-  if (started_) {
+bool IIorapImpl::Start(std::shared_ptr<manager::EventManager> event_manager) {
+  if (s_service_started_.load()) {  // Acquire-edge (see bottom of function).
+    // Note: Not meant to be idempotent. Two threads could race, and the second
+    // one would likely fail the publish.
+
     LOG(ERROR) << "service was already started";
     return false;  // Already started
   }
 
+  CHECK(event_manager != nullptr);
+
+  {
+    // This block of code needs to happen-before IIorapImpl::IIorapImpl.
+
+    // TODO: There should be a simpler way of passing down
+    // this data which doesn't involve globals and memory synchronization.
+    ServiceParams* p = &s_service_params_;
+    // TODO: move all property reads to a dedicated Config class.
+    p->fake_ = ::android::base::GetBoolProperty("iorapd.binder.fake", /*default*/false);
+    p->event_manager_ = std::move(event_manager);
+
+    // Release edge of synchronizes-with IIorapImpl::IIorapImpl.
+    s_service_params_ready_.store(true);
+  }
+
   ::android::IPCThreadState::self()->disableBackgroundScheduling(/*disable*/true);
   ::android::status_t ret = android::BinderService<IIorapImpl>::publish();
   if (ret != android::OK) {
     LOG(ERROR) << "BinderService::publish failed with error code: " << ret;
     return false;
   }
+
   android::sp<android::ProcessState> ps = android::ProcessState::self();
   // Reduce thread consumption by only using 1 thread.
   // We should also be able to leverage this by avoiding locks, etc.
@@ -119,45 +200,72 @@
   ps->startThreadPool();
   ps->giveThreadPoolName();
 
-  started_ = true;
+  // Release edge synchronizes-with the top of this function.
+  s_service_started_.store(true);
 
   return true;
 }
 
 namespace {
-template <typename ... Args>
-void SendArgs(const char* function_name,
-              Impl* self,
-              const RequestId& request_id,
-              Args&&... /*rest*/) {
-  // TODO: verbose, not INFO
-  LOG(VERBOSE) << "IIorap::" << function_name << " (request_id = " << request_id.request_id << ")";
-  // TODO: implementation.
 
-  // Send these dummy callbacks for testing only.
-  // TODO: these should only be sent back when the client connects in a special 'test' mode.
-  self->ReplyWithResult(request_id, TaskResult::State::kBegan);
-  self->ReplyWithResult(request_id, TaskResult::State::kOngoing);
-  self->ReplyWithResult(request_id, TaskResult::State::kCompleted);
+#define MAYBE_HAVE_FAKE_BEHAVIOR(self, request_id) \
+  if (self->MaybeHandleFakeBehavior(request_id)) { return ::android::binder::Status::ok(); }
+
+template <typename ... Args>
+Status SendArgs(const char* function_name,
+                Impl* self,
+                const RequestId& request_id,
+                Args&&... /*rest*/) {
+  LOG(VERBOSE) << "IIorap::" << function_name << " (request_id = " << request_id.request_id << ")";
+
+  MAYBE_HAVE_FAKE_BEHAVIOR(self, request_id);
+
+  // TODO: implementation.
+  LOG(ERROR) << "IIorap::" << function_name << " -- not implemented for real code";
+  return Status::fromStatusT(::android::INVALID_OPERATION);
 }
 
 template <typename ... Args>
-void SendArgs(const char* /*function_name*/, Impl* self, Args&&... rest) {
-  // TODO: may want an assert here for readability.
+Status SendArgs(const char* function_name, Impl* self, Args&&... rest) {
+  DCHECK_EQ(std::string(function_name), "setTaskListener");
   LOG(VERBOSE) << "IIorap::setTaskListener";
   self->SetTaskListener(std::forward<Args&&>(rest)...);
+
+  return Status::ok();
+}
+
+template <typename ... Args>
+Status SendArgs(const char* function_name,
+                Impl* self,
+                const RequestId& request_id,
+                const AppLaunchEvent& app_launch_event) {
+  DCHECK_EQ(std::string(function_name), "onAppLaunchEvent");
+  LOG(VERBOSE) << "IIorap::onAppLaunchEvent";
+
+  MAYBE_HAVE_FAKE_BEHAVIOR(self, request_id);
+
+  if (self->OnAppLaunchEvent(request_id, app_launch_event)) {
+    return Status::ok();
+  } else {
+    // TODO: I suppose this should write out an exception back,
+    // like a service-specific error or something.
+    //
+    // It depends on whether or not we even have any synchronous
+    // errors.
+    //
+    // Most of the work here is done async, so it should handle
+    // async callbacks.
+    return Status::fromStatusT(::android::BAD_VALUE);
+  }
 }
 
 template <typename ... Args>
 Status Send(const char* function_name, Args&&... args) {
   LOG(VERBOSE) << "IIorap::Send(" << function_name << ")";
 
-  SendArgs(function_name, std::forward<Args>(args)...);
+  return SendArgs(function_name, std::forward<Args>(args)...);
+}
+}  // namespace <anonymous>
 
-  // Note: The exact return code doesn't matter: all the AIDL methods are oneway.
-  return Status::ok();
-}
-}
-
-}
-}
+}  // namespace binder
+}  // namespace iorap
diff --git a/src/binder/iiorap_impl.h b/src/binder/iiorap_impl.h
index 753967d..811497f 100644
--- a/src/binder/iiorap_impl.h
+++ b/src/binder/iiorap_impl.h
@@ -22,11 +22,17 @@
 
 #include "com/google/android/startop/iorap/BnIorap.h"
 
+#include <memory>
+
 namespace android {
 template <typename Service>
 class BinderService;
 }
 
+namespace iorap::manager {
+  class EventManager;
+};
+
 namespace iorap {
 namespace binder {
 
@@ -36,7 +42,7 @@
 // See also IIorap.aidl.
 class IIorapImpl : public ::com::google::android::startop::iorap::BnIorap {
 public:
-  static bool Start();
+  static bool Start(std::shared_ptr<iorap::manager::EventManager> event_manager);
   static constexpr const char* getServiceName() { return "iorapd"; };
 
 // Join all parameter declarations by splitting each parameter with a comma.
diff --git a/src/common/debug.h b/src/common/debug.h
new file mode 100644
index 0000000..bc21c74
--- /dev/null
+++ b/src/common/debug.h
@@ -0,0 +1,98 @@
+// 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.
+
+#include <ostream>
+
+namespace iorap {
+
+// kIsDebugBuild is special.
+// It gets to be in the 'iorap' namespace
+// so that different modules don't need to qualify it.
+#ifndef NDEBUG
+static constexpr bool kIsDebugBuild = true;
+#else
+static constexpr bool kIsDebugBuild = false;
+#endif
+
+namespace common {
+
+// TODO: move below code to helpers.
+template <typename T, bool>
+struct base_if_condition {};
+
+template <typename T>
+struct base_if_condition<T, true> : public T {};
+
+template <typename T>
+using base_if_debug = base_if_condition<T, kIsDebugBuild>;
+
+namespace detail {
+// "if constexpr" doesn't allow us to exclude fields from a struct/class,
+// and also "if constexpr" doesn't allow us to reference a field that does not
+// exist.
+// so we must move everything into a separate base class.
+template <bool kIsDebug = kIsDebugBuild>
+struct DebugCounterBase {
+  constexpr size_t value() const {
+    return counter;
+  }
+
+  constexpr void set_value(size_t value) {
+    counter = value;
+  }
+
+  size_t counter{1};  // Don't start with 0.
+};
+
+template <>
+struct DebugCounterBase<false /*kIsDebug*/> {
+  constexpr size_t value() const {
+    return 0;
+  }
+
+  constexpr void set_value(size_t value) {
+  }
+};
+}  // namespace detail
+
+// This counter does absolutely nothing, the code compiles to no-ops
+// when debugging is disabled.
+struct DebugCounter : detail::DebugCounterBase<> {
+  constexpr DebugCounter& operator++() {
+    set_value(value() + 1);
+    return *this;
+  }
+
+  constexpr DebugCounter operator++(int) {
+    DebugCounter now = *this;
+    set_value(value() + 1);
+    return now;
+  }
+
+  constexpr operator size_t() const {
+    return value();
+  }
+
+  friend std::ostream& operator<<(std::ostream& os, DebugCounter counter);
+};
+
+inline std::ostream& operator<<(std::ostream& os, DebugCounter counter) {
+  os << counter.value();
+  return os;
+}
+
+// TODO: refactor DebugCounter and base traits into their own files?
+
+}  // namespace common
+}  // namespace iorap
diff --git a/src/common/expected.h b/src/common/expected.h
new file mode 100644
index 0000000..3d8eef7
--- /dev/null
+++ b/src/common/expected.h
@@ -0,0 +1,404 @@
+// 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.
+
+#ifndef IORAP_SRC_COMMON_EXPECTED_H_
+#define IORAP_SRC_COMMON_EXPECTED_H_
+
+#include <type_traits>
+#include <utility>
+
+#include <android-base/logging.h>  // CHECK/DCHECK.
+
+// Ignore the tautological-undefined-compare warning.
+// We obviously want to do this to protect against undefined behavior
+// that sets a reference to a null value.
+#define DCHECK_UB_NOT_NULL(x) \
+  DCHECK(reinterpret_cast<volatile decltype(x)>(x) != nullptr)
+
+/**
+ * Result<Value, Error>-like interface.
+ *
+ * Subset of the experimental standard C++ proposal (p0323r3)
+ *
+ * Example:
+ *
+ *   expected<std::string, status_t> x = function_which_might_fail();
+ *   if (x) {
+ *     std::string str = x.value();
+ *   } else {
+ *     status_t err = x.error();
+ *   }
+ */
+
+namespace iorap {
+namespace detail {
+  // Use perfect forwarding for expected_data constructors with overloading.
+  struct expected_tag{};
+  struct expected_tag_right : public expected_tag {
+    static constexpr bool is_right_v = true;
+  };
+  struct expected_tag_error : public expected_tag {
+    static constexpr bool is_right_v = false;
+  };
+
+  template <typename T, typename E, bool DefineDestructor>
+  struct expected_data;
+
+  // This doesn't always work because this code could be instantiated with a non-trivial T/E,
+  // and then the union becomes invalid.
+  template <typename T, typename E>
+  struct expected_data<T, E, /*DefineDestructor*/true> {
+    // Mark everything 'constexpr' to keep the code the same as the other partial specialization.
+
+    template <typename U>
+    constexpr expected_data(U&& either, expected_tag_right)
+        : right_{std::forward<U>(either)}, is_right_{true} {}
+
+    template <typename U>
+    constexpr expected_data(U&& either, expected_tag_error)
+        : error_{std::forward<U>(either)}, is_right_{false} {}
+
+    constexpr bool has_value() const {
+      return is_right_;
+    }
+
+    constexpr const T& value() const {
+      return right_;
+    }
+
+    constexpr T& value() {
+      return right_;
+    }
+
+    constexpr const E& error() const {
+      return error_;
+    }
+
+    constexpr E& error() {
+      return error_;
+    }
+
+    // Using an "anonymous union" here allows non-trivial types to be stored.
+    union {
+      T right_;
+      E error_;
+    };
+
+    bool is_right_;
+
+    // Below code differs slightly by handling non-trivial constructors/destructors.
+    bool moved_from_{false};
+
+    // Note: Destructors cannot be templated, so it is illegal to use SFINAE to try to
+    // conditionalize this destructor somehow.
+    ~expected_data() {
+      if (moved_from_) { return; }
+      if (is_right_) {
+        right_.~T();
+      } else {
+        error_.~E();
+      }
+    }
+
+    expected_data(expected_data&& other)
+        noexcept(
+            noexcept(T(std::move(other.right_))) &&
+            noexcept(E(std::move(other.error_)))
+        ) {
+      DCHECK_UB_NOT_NULL(&other) << __PRETTY_FUNCTION__;
+      DCHECK_EQ(other.moved_from_, false) << __PRETTY_FUNCTION__;
+      if (other.is_right_) {
+        new (&right_) T(std::move(other.right_));
+      } else {
+        new (&error_) E(std::move(other.error_));
+      }
+      other.moved_from_ = true;
+      is_right_ = other.is_right_;
+    }
+
+    expected_data(const expected_data& other) {
+      DCHECK_UB_NOT_NULL(&other) << __PRETTY_FUNCTION__;
+      DCHECK_EQ(other.moved_from_, false) << __PRETTY_FUNCTION__;
+      if (other.is_right_) {
+        new (&right_) T(other.right_);
+      } else {
+        new (&error_) E(other.error_);
+      }
+      is_right_ = other.is_right_;
+    }
+
+    expected_data& operator=(const expected_data& other) {
+      DCHECK_UB_NOT_NULL(&other) << __PRETTY_FUNCTION__;
+      DCHECK_EQ(other.moved_from_, false) << __PRETTY_FUNCTION__;
+
+      if (this == &other) {
+        return *this;
+      }
+
+      if (other.is_right_) {
+        if (!is_right_) {
+          error_.~E();
+          new (&right_) T(other.right_);
+        } else {
+          right_ = other.right_;
+        }
+      } else {
+        if (is_right_) {
+          right_.~T();
+          new (&error_) E(other.error_);
+        } else {
+          error_ = other.error_;
+        }
+      }
+      is_right_ = other.is_right_;
+
+      return *this;
+    }
+
+    expected_data& operator=(expected_data&& other) {
+      DCHECK_UB_NOT_NULL(&other) << __PRETTY_FUNCTION__;
+      DCHECK_EQ(other.moved_from_, false) << __PRETTY_FUNCTION__;
+
+      if (this == &other) {
+        return *this;
+      }
+
+      if (other.is_right_) {
+        if (!is_right_) {
+          error_.~E();
+          new (&right_) T(std::move(other.right_));
+        } else {
+          right_ = std::move(other.right_);
+        }
+      } else {
+        if (is_right_) {
+          right_.~T();
+          new (&error_) E(std::move(other.error_));
+        } else {
+          error_ = std::move(other.error_);
+        }
+      }
+
+      other.moved_from_ = true;
+      is_right_ = other.is_right_;
+
+      return *this;
+    }
+  };
+
+  // Trivial-destructor copy of the above struct.
+  //
+  // A separate copy is required because otherwise compilation fails with an error about
+  // the union having an implicitly deleted constructor.
+  //
+  // Having this implementation gives us the property that
+  //
+  //     (is_trivially_destructible<T> && is_trivially_destructible<E>
+  //       ==> is_trivially_destructible<expected<T, E>>)
+  template <typename T, typename E>
+  struct expected_data<T, E, /*DefineDestructor*/false> {
+    template <typename U>
+    constexpr expected_data(U&& either, expected_tag_right)
+        : right_{std::forward<U>(either)}, is_right_{true} {}
+
+    template <typename U>
+    constexpr expected_data(U&& either, expected_tag_error)
+        : error_{std::forward<U>(either)}, is_right_{false} {}
+
+    constexpr bool has_value() const {
+      return is_right_;
+    }
+
+    constexpr const T& value() const {
+      return right_;
+    }
+
+    constexpr T& value() {
+      return right_;
+    }
+
+    constexpr const E& error() const {
+      return error_;
+    }
+
+    constexpr E& error() {
+      return error_;
+    }
+
+    // Using an "anonymous union" here allows non-trivial types to be stored.
+    union {
+      T right_;
+      E error_;
+    };
+
+    bool is_right_;
+
+    ~expected_data() = default;
+  };
+
+  // Select between trivial and non-trivial implementations. Trivial implementations
+  // are more optimized and constexpr-compatible.
+  template <typename T, typename E>
+  using expected_pick_data_t =
+      expected_data<T, E,
+        !(std::is_trivially_destructible_v<T> && std::is_trivially_destructible_v<E>) >;
+}  // namespace detail
+
+template <typename E>
+struct unexpected;
+
+// Subset of std::experimental::expected proposal (p0323r3).
+template <typename T, typename E>
+struct expected {
+  // Never-empty: expected<T,E> values have either 'T' or 'E' in them.
+  template <typename U = T, typename _ = std::enable_if_t<std::is_default_constructible_v<U>>>
+  constexpr expected() noexcept(noexcept(T{})) : expected(T{}) {}
+
+  constexpr expected(const T& value) : data_{value, detail::expected_tag_right{}} {}
+  constexpr expected(T&& value) : data_{std::move(value), detail::expected_tag_right{}} {}
+  constexpr expected(const E& error) : data_{error, detail::expected_tag_error{}} {}
+  constexpr expected(E&& error) : data_{std::move(error), detail::expected_tag_error{}} {}
+
+  template <typename G = E>
+  constexpr expected(unexpected<G> const& u) : expected{u.value()} {}
+
+  template <typename G = E>
+  constexpr expected(unexpected<G>&& u) : expected{std::move(u.value())} {}
+
+  explicit constexpr operator bool() const {
+    return has_value();
+  }
+
+  constexpr bool has_value() const {
+    return data_.has_value();
+  }
+
+  constexpr const T& operator*() const {
+    return data_.value();
+  }
+
+  constexpr T& operator*() {
+    return data_.value();
+  }
+
+  // TODO: arrow operator?
+
+  constexpr T& value() & {
+    CHECK(has_value());
+    return data_.value();
+  }
+
+  constexpr const T& value() const & {
+    CHECK(has_value());
+    return data_.value();
+  }
+
+  constexpr T&& value() && {
+    CHECK(has_value());
+    return std::move(data_.value());
+  }
+
+  constexpr const T& value() const && {
+    CHECK(has_value());
+    return std::move(data_.value());
+  }
+
+  constexpr E& error() {
+    DCHECK(!has_value());
+    return data_.error();
+  }
+
+  constexpr const E& error() const {
+    DCHECK(!has_value());
+    return data_.error();
+  }
+
+  // TODO: other functions such as operator=, unexpected, etc.
+ private:
+  detail::expected_pick_data_t<T, E> data_;
+};
+
+// TODO: move to tests file
+namespace {
+  struct TestType {
+    TestType() {}
+    ~TestType() {}
+  };
+  struct TestType2 : TestType {};
+
+  static_assert(std::is_trivially_destructible_v<expected<int, /*error*/double> >);
+  static_assert(!std::is_trivially_destructible_v<expected<TestType, /*error*/double> >);
+  static_assert(!std::is_trivially_destructible_v<expected<int, /*error*/TestType> >);
+  static_assert(!std::is_trivially_destructible_v<expected<TestType, /*error*/TestType2> >);
+
+  // Ensure expected is constexpr-compatible.
+  struct TestCase {
+    static constexpr auto t1 = expected<int, double>{};
+  };
+}  // namespace <anonymous>
+
+template <typename E>
+struct unexpected {
+  unexpected() = delete;
+  constexpr explicit unexpected(const E& error) : error_{error} {}
+  constexpr explicit unexpected(E&& error) : error_{std::move(error)} {}
+  constexpr const E& value() const& { return error_; }
+  constexpr E& value() & { return error_; }
+  constexpr E&& value() && { return std::move(error_); }
+  constexpr E const&& value() const&& { return std::move(error_); }
+ private:
+  E error_;
+};
+
+template <class E>
+constexpr bool operator==(const unexpected<E>& x, const unexpected<E>& y) {
+  return x.value() == y.value();
+}
+
+template <class E>
+constexpr bool operator!=(const unexpected<E>& x, const unexpected<E>& y) {
+  return !(x == y);
+}
+
+// TODO: move below codes to separate utils file
+//
+// future C++20 implementation of std::identity
+struct identity {
+  template <typename U>
+  constexpr auto operator()(U&& v) const noexcept {
+    return std::forward<U>(v);
+  }
+};
+
+// Given a lambda [...](auto&& var) {...}
+//   apply std::forward to 'var' to achieve perfect forwarding.
+//
+// Note that this doesn't work when var is a template type, i.e.
+//   template <typename T>
+//   void func(T&& tvar) {...}
+//
+// It would be invalid to use this macro with 'tvar' in that context.
+#define IORAP_FORWARD_LAMBDA(var) std::forward<decltype(var)>(var)
+
+// Borrowed non-null pointer, i.e. we do not own the lifetime.
+//
+// Function calls: This pointer is not used past the call.
+// Struct fields: This pointer is not used past the lifetime of the struct.
+template <class T, class = std::enable_if_t<std::is_pointer<T>::value>>
+using borrowed = T _Nonnull;
+// TODO: need a DCHECK or high warning levels, since null is technically well-defined.
+
+}  // namespace iorap
+
+#endif  // IORAP_SRC_COMMON_EXPECTED_H_
diff --git a/src/inode2filename/inode.cc b/src/inode2filename/inode.cc
new file mode 100644
index 0000000..cdb2325
--- /dev/null
+++ b/src/inode2filename/inode.cc
@@ -0,0 +1,81 @@
+// 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.
+
+#include "inode2filename/inode.h"
+
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+
+#include <string>
+#include <vector>
+
+#include <sys/sysmacros.h>
+
+using android::base::ParseUint;
+
+namespace iorap::inode2filename {
+
+// TODO: refactor to return expected<Inode, string>
+bool Inode::Parse(const std::string& str, Inode* out, std::string* error_msg) {
+  DCHECK(out != nullptr);
+  DCHECK(error_msg != nullptr);
+
+  // Major:minor:inode OR dev_t@inode
+  std::vector<std::string> lst_pair = android::base::Split(str, "@");
+  if (lst_pair.size() == 2) {
+    size_t dev_whole = 0;
+    if (!ParseUint(lst_pair[0], &dev_whole)) {
+      *error_msg = "Failed to parse the whole device id as uint.";
+      return false;
+    }
+
+    dev_t dev_w = static_cast<dev_t>(dev_whole);
+    out->device_major = major(dev_w);
+    out->device_minor = minor(dev_w);
+
+    if (!ParseUint(lst_pair[1], &out->inode)) {
+      *error_msg = "Failed to parse inode as uint.";
+      return false;
+    }
+
+    return true;
+  }
+
+  std::vector<std::string> lst = android::base::Split(str, ":");
+
+  if (lst.size() != 3) {
+    *error_msg = "Too few : separated items";
+    return false;
+  }
+
+  if (!ParseUint(lst[0], &out->device_major)) {
+    *error_msg = "Failed to parse 0th element as a uint";
+    return false;
+  }
+
+  if (!ParseUint(lst[1], &out->device_minor)) {
+    *error_msg = "Failed to parse 1st element as a uint";
+    return false;
+  }
+
+  if (!ParseUint(lst[2], &out->inode)) {
+    *error_msg = "Failed to parse 2nd element as a uint";
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace iorap::inode2filename
diff --git a/src/inode2filename/inode.h b/src/inode2filename/inode.h
new file mode 100644
index 0000000..2dd5611
--- /dev/null
+++ b/src/inode2filename/inode.h
@@ -0,0 +1,65 @@
+// 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.
+
+#ifndef IORAP_SRC_INODE2FILENAME_INODE_H_
+#define IORAP_SRC_INODE2FILENAME_INODE_H_
+
+#include <functional>
+#include <ostream>
+#include <string>
+
+#include <stddef.h>
+
+namespace iorap::inode2filename {
+
+struct Inode {
+  size_t device_major;  // dev_t = makedev(major, minor)
+  size_t device_minor;
+  size_t inode;         // ino_t = inode
+
+  static bool Parse(const std::string& str, /*out*/Inode* out, /*out*/std::string* error_msg);
+
+  bool operator==(const Inode& rhs) const {
+    return device_major == rhs.device_major &&
+        device_minor == rhs.device_minor &&
+        inode == rhs.inode;
+  }
+
+  bool operator!=(const Inode& rhs) const {
+    return !(*this == rhs);
+  }
+};
+
+inline std::ostream& operator<<(std::ostream& os, const Inode& inode) {
+  os << inode.device_major << ":" << inode.device_minor << ":" << inode.inode;
+  return os;
+}
+
+}  // namespace iorap::inode2filename
+
+namespace std {
+  template <>
+  struct hash<iorap::inode2filename::Inode> {
+      using argument_type = iorap::inode2filename::Inode;
+      using result_type = size_t;
+      result_type operator()(argument_type const& s) const noexcept {
+        // Hash the inode by using only the inode#. Ignore devices, we are extremely unlikely
+        // to ever collide on the devices.
+        result_type const h1 = std::hash<size_t>{}(s.inode);
+        return h1;
+      }
+  };
+}  // namespace std
+
+#endif  // IORAP_SRC_INODE2FILENAME_INODE_H_
diff --git a/src/inode2filename/main.cc b/src/inode2filename/main.cc
new file mode 100644
index 0000000..2da364c
--- /dev/null
+++ b/src/inode2filename/main.cc
@@ -0,0 +1,152 @@
+// 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.
+
+#include "common/debug.h"
+#include "common/expected.h"
+#include "inode2filename/search_directories.h"
+
+using namespace iorap::inode2filename;  // NOLINT
+
+#if defined(IORAP_INODE2FILENAME_MAIN)
+
+void Usage(char** argv) {
+  std::cerr << "Usage: " << argv[0] << " <options> <<inode_syntax>> [inode_syntax1 inode_syntax2 ...]" << std::endl;
+  std::cerr << "" << std::endl;
+  std::cerr << "  Block until all inodes have been read in, then begin searching for filenames for those inodes." << std::endl;
+  std::cerr << "  Results are written immediately as they are available, and once all inodes are found, " << std::endl;
+  std::cerr << "  the program will terminate." << std::endl;
+  std::cerr << "" << std::endl;
+  std::cerr << "    Inode syntax:     ('dev_t@inode' | 'major:minor:inode')" << std::endl;
+  std::cerr << "    --help,-h         Print this Usage." << std::endl;
+  std::cerr << "    --root,-r         Add root directory (default '.'). Repeatable." << std::endl;
+  std::cerr << "    --verbose,-v      Set verbosity (default off)." << std::endl;
+  std::cerr << "    --wait,-w         Wait for key stroke before continuing (default off)." << std::endl;
+  exit(1);
+}
+
+static fruit::Component<SearchDirectories> GetSearchDirectoriesComponent() {
+    return fruit::createComponent().bind<SystemCall, SystemCallImpl>();
+}
+
+int main(int argc, char** argv) {
+  android::base::InitLogging(argv);
+  android::base::SetLogger(android::base::StderrLogger);
+
+  bool wait_for_keystroke = false;
+  bool enable_verbose = false;
+  std::vector<std::string> root_directories;
+  std::vector<Inode> inode_list;
+
+  if (argc == 1) {
+    Usage(argv);
+  }
+
+  for (int arg = 1; arg < argc; ++arg) {
+    std::string argstr = argv[arg];
+    bool has_arg_next = (arg+1)<argc;
+    std::string arg_next = has_arg_next ? argv[arg+1] : "";
+
+    if (argstr == "--help" || argstr == "-h") {
+      Usage(argv);
+    } else if (argstr == "--root" || argstr == "-r") {
+      if (!has_arg_next) {
+        std::cerr << "Missing --root <value>" << std::endl;
+        return 1;
+      }
+      root_directories.push_back(arg_next);
+      ++arg;
+    } else if (argstr == "--verbose" || argstr == "-v") {
+      enable_verbose = true;
+    } else if (argstr == "--wait" || argstr == "-w") {
+      wait_for_keystroke = true;
+    } else {
+      Inode maybe_inode{};
+
+      std::string error_msg;
+      if (Inode::Parse(argstr, /*out*/&maybe_inode, /*out*/&error_msg)) {
+        inode_list.push_back(maybe_inode);
+      } else {
+        if (argstr.size() >= 1) {
+          if (argstr[0] == '-') {
+            std::cerr << "Unrecognized flag: " << argstr << std::endl;
+            return 1;
+          }
+        }
+
+        std::cerr << "Failed to parse inode (" << argstr << ") because: " << error_msg << std::endl;
+        return 1;
+      }
+    }
+  }
+
+  if (root_directories.size() == 0) {
+    root_directories.push_back(".");
+  }
+
+  if (inode_list.size() == 0) {
+    DCHECK_EQ(true, false);
+    std::cerr << "Provide at least one inode." << std::endl;
+    return 1;
+  }
+
+  if (enable_verbose) {
+    android::base::SetMinimumLogSeverity(android::base::VERBOSE);
+
+    LOG(VERBOSE) << "Verbose check";
+    LOG(VERBOSE) << "Debug check: " << ::iorap::kIsDebugBuild;
+
+    for (auto& inode_num : inode_list) {
+      LOG(VERBOSE) << "Searching for inode " << inode_num;
+    }
+  }
+
+  // Useful to attach a debugger...
+  // 1) $> inode2filename -w <args>
+  // 2) $> gdbclient <pid>
+  if (wait_for_keystroke) {
+    LOG(INFO) << "Self pid: " << getpid();
+    LOG(INFO) << "Press any key to continue...";
+    std::cin >> wait_for_keystroke;
+  }
+
+  fruit::Injector<SearchDirectories> injector(GetSearchDirectoriesComponent);
+  SearchDirectories* search_directories = injector.get<SearchDirectories*>();
+
+  auto/*observable[2]*/ [inode_results, connectable] =
+      search_directories->FindFilenamesFromInodesPair(
+          std::move(root_directories),
+          std::move(inode_list),
+          SearchMode::kInProcessDirect);
+
+  int return_code = 1;
+  inode_results.subscribe([&return_code](const InodeResult& result) {
+    if (result) {
+      LOG(DEBUG) << "Inode match: " << result.inode << ", " << result.data.value();
+      std::cout << "Inode match: " << result.inode << ", " << result.data.value() << std::endl;
+      return_code = 0;
+    } else {
+      LOG(WARNING) << "Failed to match inode: " << result.inode;
+    }
+  });
+
+  // Normally #subscribe would start emitting items immediately, but this does nothing yet
+  // because one of the nodes in the flow graph was published. Published streams make the entire
+  // downstream inert until #connect is called.
+  connectable->connect();
+
+  // 0 -> found at least a single match, 1 -> could not find any matches.
+  return return_code;
+}
+
+#endif
diff --git a/src/inode2filename/search_directories.cc b/src/inode2filename/search_directories.cc
new file mode 100644
index 0000000..2ec09d9
--- /dev/null
+++ b/src/inode2filename/search_directories.cc
@@ -0,0 +1,915 @@
+// 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.
+
+#include "common/debug.h"
+#include "inode2filename/search_directories.h"
+#include "inode2filename/system_call.h"
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/scopeguard.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+
+#include "rxcpp/rx.hpp"
+
+#include <iostream>
+#include <stdio.h>
+#include <fstream>
+#include <vector>
+#include <optional>
+
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <sys/types.h>
+
+#ifdef __ANDROID__
+#include <sys/sysmacros.h>
+#endif
+
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <dirent.h>
+
+#include <unordered_map>
+
+namespace rx = rxcpp;
+using android::base::unique_fd;  // NOLINT
+using android::base::StringPrintf;  // NOLINT
+
+namespace iorap::inode2filename {
+
+// A multimap of 'ino_t -> List[Inode]' (where the value Inodes have the same ino_t as the key).
+//
+// A flat list of Inodes is turned into the above map, then keys can be removed one at a time
+// until the InodeSet eventually becomes empty.
+struct InodeSet {
+  struct ValueRange {
+    auto/*Iterable<Inode>*/ begin() {
+      return begin_;
+    }
+
+    auto/*Iterable<Inode>*/ end() {
+      return end_;
+    }
+
+    bool empty() const {
+      return begin_ == end_;
+    }
+
+    explicit operator bool() const {
+      return !empty();
+    }
+
+    std::unordered_multimap<ino_t, Inode>::iterator begin_, end_;
+
+    friend std::ostream& operator<<(std::ostream& os, const ValueRange& s);
+  };
+
+  // Create an observable that emits the remaining inodes in the map.
+  //
+  // Mutation functions must not be called until this observable
+  // has been finished emitting all values (e.g. with on_completed) since that
+  // would cause the underlying iterators to go into an undefined state.
+  auto/*observable<Inode>*/ IterateValues() const {
+    return rxcpp::observable<>::iterate(set_).map(  // XX: should we use identity_immediate here?
+        [](const std::pair<const ino_t, Inode>& pair) {
+          return pair.second;
+        }
+    );
+    // TODO: this would be more efficient as a range-v3 view.
+  }
+
+  constexpr bool Empty() const {
+    return set_.empty();
+  }
+
+  static InodeSet OfList(const std::vector<Inode>& list) {
+    InodeSet new_inode_set;
+    std::unordered_multimap<ino_t, Inode>* map = &new_inode_set.set_;
+
+    for (const Inode& inode : list) {
+      map->insert({inode.inode, inode});
+    }
+
+    return new_inode_set;
+  }
+
+  // Return an optional list of 'Inode' structs whose 'inode' field matches the 'inode' parameter.
+  // Returns an empty range if there was nothing found.
+  ValueRange FindInodeList(ino_t inode) {
+    auto range = set_.equal_range(inode);
+    return ValueRange{range.first, range.second};
+  }
+
+  // Match all fields of an Inode against a 'struct stat' stat_buf.
+  //
+  // The returned Inode (if any) is removed from the InodeSet; it will not be returned by
+  // FindInodeList in future calls.
+  std::optional<Inode> FindAndRemoveInodeInList(ValueRange inode_list,
+                                                const struct stat& stat_buf) {
+    LOG(VERBOSE) << "FindAndRemoveInodeInList " << inode_list << ", "
+                 << "stat_buf{st_dev=" << stat_buf.st_dev << ",st_ino=" << stat_buf.st_ino << "}";
+
+    auto /*iterator*/ found = std::find_if(inode_list.begin(),
+                                           inode_list.end(),
+                                           [&](const std::pair<ino_t, Inode>& pair) {
+      const Inode& inode = pair.second;
+      if (inode.inode != stat_buf.st_ino) {
+        return false;
+      }
+
+      dev_t inode_dev =
+          makedev(static_cast<int>(inode.device_major), static_cast<int>(inode.device_minor));
+
+      // Inodes could be the same across different devices.
+      // Also match the device id.
+      if (inode_dev != stat_buf.st_dev) {
+        LOG(VERBOSE) << "InodeSet:FindAndRemoveInodeInList matched ino: " << inode.inode
+                     << " but not device"
+                     << ", expected dev: " << stat_buf.st_dev
+                     << ", actual dev: " << inode_dev;
+        return false;
+      }
+      return true;
+    });
+
+    if (found != inode_list.end()) {
+      const Inode& inode = found->second;
+      LOG(VERBOSE) << "InodeSet:FindAndRemoveInodeInList *success* inode+device " << inode;
+      DCHECK(found->second.inode == stat_buf.st_ino);
+      // Erase the inode from the list. This is important.
+      set_.erase(found);
+      return inode;
+    }
+
+    return std::nullopt;
+  }
+
+  // TODO: equality and string operators for testing/logging.
+ private:
+  // Explanation: readdir returns a 'file' -> 'ino_t inode' mapping.
+  //
+  // However inodes can be reused on different partitions (but they have a different device number).
+  // To handle this edge case, and to avoid calling stat whenever the inode definitely doesn't match
+  // store the inodes into a single-key,multi-value container.
+  //
+  // This enables fast scanning of readdir results by matching just the 'inode' portion,
+  // then calling stat only when the inode portion definitely matches to confirm the device.
+
+  // There are no single-key multi-value containers in standard C++, so pretend
+  // we have one by writing this simple facade around an unordered set.
+  //
+  // We expect that the vector size is usually size=1 (or 2 or 3) since the # of devices
+  // is fixed by however many partitions there are on the system, AND the same inode #
+  // would have to be reused across a different file.
+  std::unordered_multimap<ino_t, Inode> set_;  // TODO: Rename to map_.
+
+  friend std::ostream& operator<<(std::ostream& os, const InodeSet& s);
+};
+
+std::ostream& operator<<(std::ostream& os, const InodeSet& s) {
+  os << "InodeSet{";
+  for (const auto& kv : s.set_) {
+    // e.g. "123=>(1:2:123)" ... its expected for the 'ino_t' portion to be repeated.
+    os << "" << kv.first << "=>(" << kv.second << "),";
+  }
+  os << "}";
+  return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const InodeSet::ValueRange& v) {
+  // Don't want to make a const and non const version of ValueRange.
+  InodeSet::ValueRange& s = const_cast<InodeSet::ValueRange&>(v);
+
+  os << "InodeSet::ValueRange{";
+  for (const auto& kv : s) {
+    // e.g. "123=>(1:2:123)" ... its expected for the 'ino_t' portion to be repeated.
+    os << "" << kv.first << "=>(" << kv.second << "),";
+  }
+  os << "}";
+  return os;
+}
+
+void search_for_inodes_in(std::vector<Inode>& inode_list, const std::string& dirpath);
+
+enum DirectoryEntryErrorCode {
+  kInvalid,    // not a real error code. to detect bad initialization.
+  kOpenDir,    // opendir failed.
+  kReadDir,    // readdir failed.
+  kDtUnknown,  // d_type was DT_UNKNOWN error.
+};
+
+struct DirectoryEntryError {
+  DirectoryEntryErrorCode code;
+  int err_no;
+  std::string filename;
+};
+
+std::ostream& operator<<(std::ostream& os, const DirectoryEntryError& e) {
+  os << "DirectoryEntryError{"
+     << static_cast<int>(e.code) << "," << e.err_no << "," << e.filename << "}";
+  return os;
+  // TODO: pretty-print code and err-no
+}
+
+static common::DebugCounter gDebugDirectoryEntryCounter{};
+static constexpr bool kDebugDirectoryEntry = false;
+
+#define DIRECTORY_ENTRY_MOVE_DCHECK() \
+    DCHECK_EQ(other.moved_from_, false) << __PRETTY_FUNCTION__ << "CNT:" << other.debug_counter_;
+#define DIRECTORY_ENTRY_TRACE_CTOR() \
+    if (kDebugDirectoryEntry) LOG(VERBOSE) << __PRETTY_FUNCTION__ << "@CNT:" << debug_counter_
+
+struct DirectoryEntry {
+  using ResultT = iorap::expected<DirectoryEntry, DirectoryEntryError>;
+  using ObservableT = rx::observable<ResultT>;
+
+  static constexpr ino_t kInvalidIno = std::numeric_limits<ino_t>::max();
+  static constexpr auto kInvalidFileName = "";
+
+  // Path to file, the prefix is one of the root directories.
+  std::string filename{kInvalidFileName};
+  // Inode number of the file. Not unique across different devices.
+  ino_t d_ino{kInvalidIno};
+  // File type (DT_LNK, DT_REG, DT_DIR, or DT_UNKNOWN)
+  unsigned char d_type{DT_UNKNOWN};  // Note: not seen outside of sentinel roots.
+  // TODO: Consider invariant checks for valid combinations of above fields?
+
+  // Debug-only flags.
+  bool moved_from_{false};
+  size_t debug_counter_{0};
+
+ private:
+  // TODO: remove default constructor?
+  //
+  // SEEMS TO BE USED by std::vector etc. FIX DAT.
+  DirectoryEntry() noexcept {
+    debug_counter_ = gDebugDirectoryEntryCounter++;
+    DIRECTORY_ENTRY_TRACE_CTOR();
+  }
+ public:
+  DirectoryEntry(std::string filename, ino_t d_ino, unsigned char d_type) noexcept
+    : filename{std::move(filename)},
+      d_ino{d_ino},
+      d_type{d_type} {
+    debug_counter_ = gDebugDirectoryEntryCounter++;
+    DIRECTORY_ENTRY_TRACE_CTOR();
+  }
+
+  DirectoryEntry(const DirectoryEntry& other) noexcept {
+    // Do not use member-initialization syntax so that this DCHECK can execute first.
+    DIRECTORY_ENTRY_MOVE_DCHECK();
+
+    filename = other.filename;
+    d_ino = other.d_ino;
+    d_type = other.d_type;
+    children_paths_ = other.children_paths_;
+    children_initialized_ = other.children_initialized_;
+    debug_counter_ = other.debug_counter_;
+    DIRECTORY_ENTRY_TRACE_CTOR();
+  }
+
+  DirectoryEntry& operator=(const DirectoryEntry& other) noexcept {
+    if (this == &other) {
+      return *this;
+    }
+
+    DIRECTORY_ENTRY_MOVE_DCHECK();
+
+    filename = other.filename;
+    d_ino = other.d_ino;
+    d_type = other.d_type;
+    children_paths_ = other.children_paths_;
+    children_initialized_ = other.children_initialized_;
+    debug_counter_ = other.debug_counter_;
+    DIRECTORY_ENTRY_TRACE_CTOR();
+
+    return *this;
+  }
+
+  DirectoryEntry& operator=(DirectoryEntry&& other) noexcept {
+    if (this == &other) {
+      return *this;
+    }
+
+    DIRECTORY_ENTRY_MOVE_DCHECK();
+
+    filename = std::move(other.filename);
+    d_ino = other.d_ino;
+    d_type = other.d_type;
+    children_paths_ = std::move(other.children_paths_);
+    children_initialized_ = other.children_initialized_;
+    debug_counter_ = other.debug_counter_;
+    DIRECTORY_ENTRY_TRACE_CTOR();
+
+    return *this;
+  }
+
+  DirectoryEntry(DirectoryEntry&& other) noexcept {
+    DIRECTORY_ENTRY_MOVE_DCHECK();
+    other.moved_from_ = true;
+
+    filename = std::move(other.filename);
+    d_ino = other.d_ino;
+    d_type = other.d_type;
+    children_paths_ = std::move(other.children_paths_);
+    children_initialized_ = other.children_initialized_;
+    debug_counter_ = other.debug_counter_;
+    DIRECTORY_ENTRY_TRACE_CTOR();
+  }
+
+  // Create a sentinel (root of roots) whose children entries are those specified by
+  // children_paths.
+  static DirectoryEntry CreateSentinel(std::vector<std::string> children_paths) {
+    DirectoryEntry e;
+    e.d_type = DT_DIR;
+    ++gDebugDirectoryEntryCounter;
+
+    for (std::string& child_path : children_paths) {
+      // TODO: Should we call Stat on the child path here to reconstitute the ino_t for a root dir?
+      // Otherwise it can look a little strange (i.e. the root dir itself will never match
+      // the searched inode).
+      //
+      // Probably not too big of a problem in practice.
+      DirectoryEntry child_entry{std::move(child_path), kInvalidIno, DT_DIR};
+      ResultT child_entry_as_result{std::move(child_entry)};
+      e.children_paths_.push_back(std::move(child_entry_as_result));
+    }
+
+    e.children_initialized_ = true;
+
+    return e;
+  }
+
+  // Return an observable which emits the direct children only.
+  // The children entries are now read from disk (with readdir) if they weren't read previously.
+  std::vector<ResultT> GetChildrenEntries(borrowed<SystemCall*> system_call) const& {
+    BuildChildrenPaths(system_call);
+    return children_paths_;
+  }
+
+  // Return an observable which emits the direct children only.
+  // The children entries are now read from disk (with readdir) if they weren't read previously.
+  // Movable overload.
+  std::vector<ResultT> GetChildrenEntries(borrowed<SystemCall*> system_call) && {
+    BuildChildrenPaths(system_call);
+    return std::move(children_paths_);
+  }
+
+  // Returns a (lazy) observable that emits every single node, in pre-order,
+  // rooted at this tree.
+  //
+  // New entries are only read from disk (with e.g. readdir) when more values are pulled
+  // from the observable. Only the direct children of any entry are read at any time.
+  //
+  // The emission can be stopped prematurely by unsubscribing from the observable.
+  // This means the maximum amount of 'redundant' IO reads is bounded by the children count
+  // of all entries emitted thus far minus entries actually emitted.
+  ObservableT GetSubTreePreOrderEntries(borrowed<SystemCall*> system_call) const;
+
+ private:
+  // Out-of-line definition to avoid circular type dependency.
+  void BuildChildrenPaths(borrowed<SystemCall*> system_call) const;
+
+  // We need to lazily initialize children_paths_ only when we try to read them.
+  //
+  // Assuming the underlying file system doesn't change (which isn't strictly true),
+  // the directory children are referentially transparent.
+  //
+  // In practice we do not need to distinguish between the file contents changing out
+  // from under us in this code, so we don't need the more strict requirements.
+  mutable std::vector<ResultT> children_paths_;
+  mutable bool children_initialized_{false};
+
+  friend std::ostream& operator<<(std::ostream& os, const DirectoryEntry& d);
+};
+
+std::ostream& operator<<(std::ostream& os, const DirectoryEntry& d) {
+  os << "DirectoryEntry{" << d.filename << ",ino:" << d.d_ino << ",type:" << d.d_type << "}";
+  return os;
+}
+
+using DirectoryEntryResult = DirectoryEntry::ResultT;
+
+// Read all directory entries and return it as a vector. This must be an eager operation,
+// as readdir is not re-entrant.
+//
+// This could be considered as a limitation from the 'observable' perspective since
+// one can end up reading unnecessary extra directory entries that are then never consumed.
+//
+// The following entries are skipped:
+//  - '.' self
+//  - ".." parent
+//
+// All DT types except the following are removed:
+//  * DT_LNK - symbolic link (empty children)
+//  * DT_REG - regular file  (empty children)
+//  * DT_DIR - directory     (has children)
+static std::vector<DirectoryEntryResult>
+    ReadDirectoryEntriesFromDirectoryPath(std::string dirpath, borrowed<SystemCall*> system_call) {
+  DIR *dirp;
+  struct dirent *dp;
+
+  LOG(VERBOSE) << "ReadDirectoryEntriesFromDirectoryPath(" << dirpath << ")";
+
+  if ((dirp = system_call->opendir(dirpath.c_str())) == nullptr) {
+    PLOG(ERROR) << "Couldn't open directory: " << dirpath;
+    return {DirectoryEntryError{kOpenDir, errno, dirpath}};
+  }
+
+  // Read all the results up front because readdir is not re-entrant.
+  std::vector<DirectoryEntryResult> results;
+
+  // Get full path + the directory entry path.
+  auto child_path = [&] { return dirpath + "/" + dp->d_name; };
+
+  do {
+    errno = 0;
+    if ((dp = system_call->readdir(dirp)) != nullptr) {
+      if (dp->d_type == DT_DIR) {
+        if (strcmp(".", dp->d_name) == 0 || strcmp("..", dp->d_name) == 0) {
+          LOG(VERBOSE) << "Skip self/parent: " << dp->d_name;
+          continue;
+        }
+
+        LOG(VERBOSE) << "Find entry " << child_path()
+                     << ", ino: " << dp->d_ino << ", type: " << dp->d_type;
+        results.push_back(DirectoryEntry{child_path(),
+                                         static_cast<ino_t>(dp->d_ino),
+                                         dp->d_type});
+      } else if (dp->d_type == DT_UNKNOWN) {
+        // This seems bad if it happens. We should probably do something about this.
+        LOG(WARNING) << "Found unknown DT entry: " << child_path();
+
+        results.push_back(DirectoryEntryError{kDtUnknown, /*errno*/0, child_path()});
+      } else if (dp->d_type == DT_LNK || dp->d_type == DT_REG) {
+        // Regular non-directory file entry.
+        results.push_back(DirectoryEntry{child_path(),
+                                         static_cast<ino_t>(dp->d_ino),
+                                         dp->d_type});
+      } else {
+        // Block device, character device, socket, etc...
+        LOG(VERBOSE) << "Skip DT entry of type: " << dp->d_type << " " << child_path();
+      }
+    } else if (errno != 0) {
+      PLOG(ERROR) << "Error reading directory entry in " << dirpath;
+
+      results.push_back(DirectoryEntryError{kReadDir, errno, dirpath});
+    }
+  } while (dp != nullptr);
+
+  if (system_call->closedir(dirp) < 0) {
+    PLOG(ERROR) << "Failed to close directory " << dirpath;
+  }
+
+  return results;
+}
+
+void DirectoryEntry::BuildChildrenPaths(borrowed<SystemCall*> system_call) const {
+  if (children_initialized_) {
+    return;
+  }
+
+  if (d_type == DT_DIR) {
+    children_paths_ = ReadDirectoryEntriesFromDirectoryPath(filename, system_call);
+    // TODO: consider using dependency injection here to substitute this function during testing?
+  }
+}
+
+struct InodeSearchParameters {
+  std::vector<Inode> inode_list;
+  std::vector<std::string> root_dirs;
+};
+
+// [IN]
+// observable: expected<Value, Error>, ...
+// [OUT]
+// observable: Value, ...
+//
+// Any encountered 'Error' items are dropped after logging.
+template <typename T>
+auto MapExpectedOrLog(T&& observable,
+                      ::android::base::LogSeverity log_level) {
+  return observable.filter([log_level](const auto& result) {
+    if (result) {
+      return true;
+    } else {
+      LOG(log_level) << result.error();
+      return false;
+    }
+  }).map([](auto&& result) {
+    return IORAP_FORWARD_LAMBDA(result).value();
+  });
+}
+
+template <typename T>
+auto MapExpectedOrLogError(T&& observable) {
+  return MapExpectedOrLog(std::forward<T>(observable), ::android::base::ERROR);
+}
+
+template <typename T>
+auto MapOptionalOrDrop(T&& observable) {
+  return observable.filter([](const auto& result) {
+    return result.has_value();
+  }).map([](auto&& result) {
+    return IORAP_FORWARD_LAMBDA(result).value();
+  });
+  // TODO: static_assert this isn't used with an unexpected.
+}
+
+template <typename T, typename F>
+auto VisitValueOrLogError(T&& expected, F&& visit_func, const char* error_prefix = "") {
+  if (!expected) {
+    LOG(ERROR) << error_prefix << " " << expected.error();
+  } else {
+    visit_func(std::forward<T>(expected).value());
+  }
+  // TODO: Could be good to make this more monadic by returning an optional.
+}
+
+template <typename TSimple, typename T, typename F>
+void TreeTraversalPreOrderObservableImpl(rx::subscriber<TSimple> dest, T&& node, F&& fn) {
+  LOG(VERBOSE) << "TreeTraversalPreOrderObservableImpl (begin) " << __PRETTY_FUNCTION__;
+
+  if (!dest.is_subscribed()) {
+    LOG(VERBOSE) << "TreeTraversalPreOrderObservableImpl (unsubscribed)";
+    return;
+  } else {
+    LOG(VERBOSE) << "TreeTraversalPreOrderObservableImpl (on_next node)";
+
+    // Copy the node here. This is less bad than it seems since we haven't yet
+    // calculated its children (except in the root), so its just doing a shallow memcpy (sizeof(T)).
+    //
+    // This assumes the children are calculated lazily, otherwise we'd need to have a separate
+    // NodeBody class which only holds the non-children elements.
+
+    TSimple copy = std::forward<T>(node);
+    dest.on_next(std::move(copy));
+
+    if (!node.has_value()) {
+      return;
+    }
+
+    // Whenever we call 'on_next' also check if we end up unsubscribing.
+    // This avoids the expensive call into the children.
+    if (!dest.is_subscribed()) {
+      LOG(VERBOSE) << "TreeTraversalPreOrderObservableImpl (post-self unsubscribe)";
+      return;
+    }
+
+    // Eagerly get the childrem, moving them instead of copying them.
+    auto&& children = fn(std::forward<T>(node));
+    for (auto&& child : children) {
+      TreeTraversalPreOrderObservableImpl(dest, IORAP_FORWARD_LAMBDA(child), fn);
+      // TODO: double check this is doing the std::move properly for rvalues.
+
+      if (!dest.is_subscribed()) {
+        LOG(VERBOSE) << "TreeTraversalPreOrderObservableImpl (unsubscribed in children)";
+        break;
+      }
+    };
+  }
+}
+
+// Creates an observable over all the nodes in the tree rooted at node.
+// fn is a function that returns the children of that node.
+//
+// The items are emitted left-to-right pre-order, and stop early if the
+// observable is unsubscribed from.
+//
+// Implementation requirement:
+//    typeof(node) -> expected<V, E> or optional<V> or similar.
+//    fn(node) -> iterable<typeof(node)>
+//
+// preorder(self):
+//   visit(self)
+//   for child in fn(self):
+//     preorder(child)
+template <typename T, typename F>
+auto/*observable<T>*/ TreeTraversalPreOrderObservable(T&& node, F&& fn) {
+  LOG(VERBOSE) << "TreeTraversalPreOrderObservable: " << __PRETTY_FUNCTION__;
+
+  using T_simple = std::decay_t<T>;
+  return rx::observable<>::create<T_simple>(
+    // Copy node to avoid lifetime issues.
+    [node=node,fn=std::forward<F>(fn)](rx::subscriber<T_simple> dest) {
+      LOG(VERBOSE) << "TreeTraversalPreOrderObservable (lambda)";
+      TreeTraversalPreOrderObservableImpl<T_simple>(dest,
+                                                    std::move(node),
+                                                    std::move(fn));
+      dest.on_completed();
+    }
+  );
+}
+
+DirectoryEntry::ObservableT
+    DirectoryEntry::GetSubTreePreOrderEntries(borrowed<SystemCall*> system_call) const {
+  return TreeTraversalPreOrderObservable(
+      DirectoryEntryResult{*this},
+      [system_call=system_call](auto/*DirectoryEntryResult*/&& result)
+          -> std::vector<DirectoryEntryResult> {
+        if (!result) {
+          LOG(VERBOSE) << "GetSubTreePreOrderEntries (no value return)";
+          // Cannot have children when it was an error.
+          return {};
+        }
+        return
+            IORAP_FORWARD_LAMBDA(result)
+            .value()
+            .GetChildrenEntries(system_call);
+      });
+}
+
+struct StatError {
+  int err_no;
+  std::string path_name;
+};
+
+std::ostream& operator<<(std::ostream& os, const StatError& e) {
+  os << "StatError{" << e.err_no << "," << e.path_name << "}";
+  return os;
+}
+
+template <typename U = void>  // suppress unused warning.
+static iorap::expected<struct stat, StatError> Stat(const std::string& path_name,
+                                                    borrowed<SystemCall*> system_call) {
+  struct stat statbuf{};
+
+  // Call stat(2) in live code. Overridden in test code.
+  if (system_call->stat(path_name.c_str(), /*out*/&statbuf) == 0) {
+    return statbuf;
+  } else {
+    return iorap::unexpected(StatError{errno, path_name});
+  }
+}
+
+using StatResult = iorap::expected<struct stat, StatError>;
+
+// An inode's corresponding filename on the system.
+struct SearchMatch {
+  Inode inode;
+  // Relative path joined with a root directory.
+  //
+  // Use absolute path root dirs to get back absolute path filenames.
+  // If relative, this is relative to the current working directory.
+  std::string filename;
+};
+
+std::ostream& operator<<(std::ostream& os, const SearchMatch& s) {
+  os << "SearchMatch{" << s.inode << ", " << s.filename << "}";
+  return os;
+}
+
+struct SearchState {
+  // Emit 'match' Inodes corresponding to the ones here.
+  InodeSet inode_set;
+
+  // An inode matching one of the ones in inode_set was discovered in the most-recently
+  // emitted SearchState.
+  //
+  // The InodeSet removes any matching 'Inode'.
+  std::optional<SearchMatch> match;
+
+  // TODO: make sure this doesn't copy [inodes], as that would be unnecessarily expensive.
+};
+
+std::ostream& operator<<(std::ostream& os, const SearchState& s) {
+  os << "SearchState{match:";
+  // Print the 'match' first. The InodeSet could be very large so it could be truncated in logs.
+  if (s.match) {
+    os << s.match.value();
+  } else {
+    os << "(none)";
+  }
+  os << ", inode_set:" << s.inode_set << "}";
+  return os;
+}
+
+// TODO: write operator<< etc.
+
+// Return a lazy observable that will search for all filenames whose inodes
+// match the inodes in inode_search_list.
+//
+// Every unmatched inode will be emitted as an unexpected at the end of the stream.
+auto/*[observable<InodeResult>, connectable]*/ SearchDirectoriesForMatchingInodes(
+    std::vector<std::string> root_dirs,
+    std::vector<Inode> inode_search_list,
+    borrowed<SystemCall*> system_call) {
+
+  // Create a (lazy) observable that will emit each DirectoryEntry that is a recursive subchild
+  // of root_dirs. Emission will be stopped when its unsubscribed from.
+  //
+  // This is done by calling readdir(3) lazily.
+  auto/*obs<DirectoryEntry>*/ find_all_subdir_entries = ([&]() {
+    DirectoryEntry sentinel = DirectoryEntry::CreateSentinel(std::move(root_dirs));
+    auto/*obs<DirectoryEntryResult*/ results = sentinel.GetSubTreePreOrderEntries(system_call);
+
+    // Drop any errors by logging them to logcat. "Unwrap" the expected into the underlying data.
+    auto/*obs<DirectoryEntry*>*/ expected_drop_errors = MapExpectedOrLogError(std::move(results));
+    return expected_drop_errors;
+  })();
+
+  // DirectoryEntry is missing the dev_t portion, so we may need to call scan(2) again
+  // to confirm the dev_t. We skip calling scan(2) when the ino_t does not match.
+  // InodeSet lets us optimally avoid calling scan(2).
+  SearchState initial;
+  initial.inode_set = InodeSet::OfList(inode_search_list);
+
+  auto/*[observable<SearchState>,Connectable]*/ search_state_results = find_all_subdir_entries.scan(
+      std::move(initial),
+      [system_call=system_call](SearchState search_state, const DirectoryEntry& dir_entry) {
+        LOG(VERBOSE) << "SearchDirectoriesForMatchingInodes#Scan "
+                     << dir_entry << ", state: " << search_state;
+
+        search_state.match = std::nullopt;
+
+        InodeSet* inodes = &search_state.inode_set;
+
+        // Find all the possible inodes across different devices.
+        InodeSet::ValueRange inode_list = inodes->FindInodeList(dir_entry.d_ino);
+
+        // This directory doesn't correspond to any inodes we are searching for.
+        if (!inode_list) {
+          return search_state;
+        }
+
+        StatResult maybe_stat = Stat(dir_entry.filename, system_call);
+        VisitValueOrLogError(maybe_stat, [&](const struct stat& stat_buf) {
+          // Try to match the specific inode. Usually this will not result in a match (nullopt).
+          std::optional<Inode> inode = inodes->FindAndRemoveInodeInList(inode_list, stat_buf);
+
+          if (inode) {
+            search_state.match = SearchMatch{inode.value(), dir_entry.filename};
+          }
+        });
+
+        return search_state;  // implicit move.
+      }
+  // Avoid exhausting a potentially 'infinite' stream of files by terminating as soon
+  // as we find every single inode we care about.
+  ).take_while([](const SearchState& state) {
+      // Also emit the last item that caused the search set to go empty.
+      bool cond = !state.inode_set.Empty() || state.match;
+
+      if (WOULD_LOG(VERBOSE)) {
+        static int kCounter = 0;
+        LOG(VERBOSE) << "SearchDirectoriesForMatchingInodes#take_while (" << kCounter++ <<
+                     ",is_empty:"
+                     << state.inode_set.Empty() << ", match:" << state.match.has_value();
+      }
+      // Minor O(1) implementation inefficiency:
+      // (Too minor to fix but it can be strange if looking at the logs or readdir traces).
+      //
+      // Note, because we return 'true' after the search set went empty,
+      // the overall stream graph still pulls from search_state_results exactly once more:
+      //
+      // This means that for cond to go to false, we would've read one extra item and then discarded
+      // it. If that item was the first child of a directory, that means we essentially did
+      // one redundant pass of doing a readdir.
+      //
+      // In other words if the search set goes to empty while the current item is a directory,
+      // it will definitely readdir on it at least once as we try to get the first child in
+      // OnTreeTraversal.
+      //
+      // This could be fixed with a 'take_until(Predicate)' operator which doesn't discard
+      // the last item when the condition becomes false. However rxcpp seems to lack this operator,
+      // whereas RxJava has it.
+
+      if (!cond) {
+        LOG(VERBOSE) << "SearchDirectoriesForMatchingInodes#take_while "
+                     << "should now terminate for " << state;
+      }
+
+      return cond;
+  }).publish();
+  // The publish here is mandatory. The stream is consumed twice (once by matched and once by
+  // unmatched streams). Without the publish, once all items from 'matched' were consumed it would
+  // start another instance of 'search_state_results' (i.e. it appears as if the search
+  // is restarted).
+  //
+  // By using 'publish', the search_state_results is effectively shared by both downstream nodes.
+  // Note that this also requires the subscriber to additionally call #connect on the above stream,
+  // otherwise no work will happen.
+
+  // Lifetime notes:
+  //
+  // The the 'SearchState' is emitted into both below streams simultaneously.
+  //    The 'unmatched_inode_values' only touches the inode_set.
+  //    The 'matched_inode_values' only touches the match.
+  // Either stream can 'std::move' from those fields because they don't move each other's fields.
+  auto/*observable<InodeResult>*/ matched_inode_values = search_state_results
+      .filter([](const SearchState& search_state) { return search_state.match.has_value(); })
+      .map([](SearchState& search_state) { return std::move(search_state.match.value()); })
+                     // observable<SearchMatch>
+      .map([](SearchMatch search_match) {
+          return InodeResult::makeSuccess(search_match.inode, std::move(search_match.filename));
+      });            // observable<InodeResult>
+
+  auto/*observable<?>*/ unmatched_inode_values = search_state_results
+      // The 'last' SearchState is the one that contains all the remaining inodes.
+      .take_last(1)  // observable<SearchState>
+      .flat_map([](const SearchState& search_state) {
+          LOG(VERBOSE) << "SearchDirectoriesForMatchingInodes#unmatched -- flat_map";
+          // Aside: Could've used a move here if the inodes weren't so lightweight already.
+          return search_state.inode_set.IterateValues(); })
+                     // observable<Inode>
+      .map([](const Inode& inode) {
+          LOG(VERBOSE) << "SearchDirectoriesForMatchingInodes#unmatched -- map";
+          return InodeResult::makeFailure(inode, InodeResult::kCouldNotFindFilename);
+      });
+                     // observable<InodeResult>
+
+  // The matched and unmatched InodeResults are emitted together.
+  //   Use merge, not concat, because we need both observables to be subscribed to simultaneously.
+
+  auto/*observable<InodeResult*/ all_inode_results =
+      matched_inode_values.merge(unmatched_inode_values);
+
+  // Now that all mid-stream observables have been connected, turn the Connectable observable
+  // into a regular observable.
+
+  // The caller has to call 'connect' on the search_state_results after subscribing
+  // and before any work can actually start.
+  return std::make_pair(all_inode_results, search_state_results);
+}
+
+
+rxcpp::observable<InodeResult> SearchDirectories::FindFilenamesFromInodes(
+    std::vector<std::string> root_directories,
+    std::vector<Inode> inode_list,
+    SearchMode mode) {
+  DCHECK(mode == SearchMode::kInProcessDirect) << " other modes not implemented yet";
+
+  auto/*observable[2]*/ [inode_results, connectable] = SearchDirectoriesForMatchingInodes(
+      std::move(root_directories),
+      std::move(inode_list),
+      system_call_);
+
+  return inode_results;
+}
+
+// I think we could avoid this with auto_connect, which rxcpp doesn't seem to have.
+//
+// I can't figure out any other way to avoid this, or at least to allow connecting
+// on the primary observable (instead of a secondary side-observable).
+//
+// If using the obvious publish+ref_count then the unmerged stream gets no items emitted into it.
+// If tried to ref_count later, everything turns into no-op.
+// If trying to call connect too early, the subscribe is missed.
+template <typename T>
+struct RxAnyConnectableFromObservable : public SearchDirectories::RxAnyConnectable {
+  virtual void connect() override {
+    observable.connect();
+  }
+
+  virtual ~RxAnyConnectableFromObservable() {}
+
+  RxAnyConnectableFromObservable(rxcpp::connectable_observable<T> observable)
+    : observable(observable) {
+  }
+
+  rxcpp::connectable_observable<T> observable;
+};
+
+// Type deduction helper.
+template <typename T>
+std::unique_ptr<SearchDirectories::RxAnyConnectable>
+    MakeRxAnyConnectableFromObservable(rxcpp::connectable_observable<T> observable) {
+  SearchDirectories::RxAnyConnectable* ptr = new RxAnyConnectableFromObservable<T>{observable};
+  return std::unique_ptr<SearchDirectories::RxAnyConnectable>{ptr};
+}
+
+std::pair<rxcpp::observable<InodeResult>, std::unique_ptr<SearchDirectories::RxAnyConnectable>>
+    SearchDirectories::FindFilenamesFromInodesPair(
+        std::vector<std::string> root_directories,
+        std::vector<Inode> inode_list,
+        SearchMode mode) {
+  DCHECK(mode == SearchMode::kInProcessDirect) << " other modes not implemented yet";
+
+  auto/*observable[2]*/ [inode_results, connectable] = SearchDirectoriesForMatchingInodes(
+      std::move(root_directories),
+      std::move(inode_list),
+      system_call_);
+
+  std::unique_ptr<SearchDirectories::RxAnyConnectable> connectable_ptr =
+    MakeRxAnyConnectableFromObservable(connectable.as_dynamic());
+
+  return {inode_results, std::move(connectable_ptr)};
+}
+
+}  // namespace iorap::inode2filename
diff --git a/src/inode2filename/search_directories.h b/src/inode2filename/search_directories.h
new file mode 100644
index 0000000..8156574
--- /dev/null
+++ b/src/inode2filename/search_directories.h
@@ -0,0 +1,147 @@
+// 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.
+
+#ifndef IORAP_SRC_INODE2FILENAME_SEARCH_DIRECTORIES_H_
+#define IORAP_SRC_INODE2FILENAME_SEARCH_DIRECTORIES_H_
+
+#include "common/expected.h"
+#include "inode2filename/inode.h"
+#include "inode2filename/system_call.h"
+
+#include <fruit/fruit.h>
+
+#include <rxcpp/rx.hpp>
+namespace iorap::inode2filename {
+
+// Tuple of (Inode -> (Filename|Errno))
+struct InodeResult {
+  // We set this error when all root directories have been searched and
+  // yet we still could not find a corresponding filename for the inode under search.
+  static constexpr int kCouldNotFindFilename = -ENOENT;
+
+  Inode inode;
+  // Value: Contains the filename (with a root directory as a prefix).
+  // Error: Contains the errno, usually -ENOENT or perhaps a security error.
+  iorap::expected<std::string /*filename*/, int /*errno*/> data;
+
+  static InodeResult makeSuccess(Inode inode, std::string filename) {
+    return InodeResult{inode, std::move(filename)};
+  }
+
+  static InodeResult makeFailure(Inode inode, int err_no) {
+    return InodeResult{inode, iorap::unexpected{err_no}};
+  }
+
+  constexpr operator bool() const {
+    return data.has_value();
+  }
+};
+
+enum class SearchMode {
+  // Test modes:
+  kInProcessDirect,  // Execute the code directly.
+  kInProcessIpc,     // Execute code via IPC layer using multiple threads.
+  // Shipping mode:
+  kOutOfProcessIpc,  // Execute code via fork+exec with IPC.
+
+  // Note: in-process system-wide stat(2)/readdir/etc is blocked by selinux.
+  // Attempting to call the test modes will fail with -EPERM.
+  //
+  // Use fork+exec mode in shipping configurations, which spawns inode2filename
+  // as a separate command.
+};
+
+struct SearchDirectories {
+  // Type-erased subset of rxcpp::connectable_observable<?>
+  struct RxAnyConnectable {
+    // Connects to the underlying observable.
+    //
+    // This kicks off the graph, streams begin emitting items.
+    // This method will block until all items have been fully emitted
+    // and processed by any subscribers.
+    virtual void connect() = 0;
+
+    virtual ~RxAnyConnectable(){}
+  };
+
+
+  // Create a cold observable of inode results (a lazy stream) corresponding
+  // to the inode search list.
+  //
+  // A depth-first search is done on each of the root directories (in order),
+  // until all inodes have been found (or until all directories have been exhausted).
+  //
+  // Some internal errors may occur during emission that aren't part of an InodeResult;
+  // these will be sent to the error logcat and dropped.
+  //
+  // Calling this function does not begin the search.
+  // The returned observable will begin the search after subscribing to it.
+  //
+  // The emitted InodeResult stream has these guarantees:
+  // - All inodes in inode_list will eventually be emitted exactly once in an InodeResult
+  // - When all inodes are found, directory traversal is halted.
+  // - The order of emission can be considered arbitrary.
+  //
+  // Lifetime rules:
+  // - The observable must be fully consumed before deleting any of the SearchDirectory's
+  //   borrowed constructor parameters (e.g. the SystemCall).
+  // - SearchDirectory itself can be deleted at any time after creating an observable.
+  rxcpp::observable<InodeResult>
+      FindFilenamesFromInodes(std::vector<std::string> root_directories,
+                              std::vector<Inode> inode_list,
+                              SearchMode mode);
+
+  // Create a cold observable of inode results (a lazy stream) corresponding
+  // to the inode search list.
+  //
+  // A depth-first search is done on each of the root directories (in order),
+  // until all inodes have been found (or until all directories have been exhausted).
+  //
+  // Some internal errors may occur during emission that aren't part of an InodeResult;
+  // these will be sent to the error logcat and dropped.
+  //
+  // Calling this function does not begin the search.
+  // The returned observable will begin the search after subscribing to it.
+  //
+  // The emitted InodeResult stream has these guarantees:
+  // - All inodes in inode_list will eventually be emitted exactly once in an InodeResult
+  // - When all inodes are found, directory traversal is halted.
+  // - The order of emission can be considered arbitrary.
+  //
+  // Lifetime rules:
+  // - The observable must be fully consumed before deleting any of the SearchDirectory's
+  //   borrowed constructor parameters (e.g. the SystemCall).
+  // - SearchDirectory itself can be deleted at any time after creating an observable.
+  std::pair<rxcpp::observable<InodeResult>, std::unique_ptr<RxAnyConnectable>>
+      FindFilenamesFromInodesPair(std::vector<std::string> root_directories,
+                                  std::vector<Inode> inode_list,
+                                  SearchMode mode);
+
+  // Any borrowed parameters here can also be borrowed by the observables returned by the above
+  // member functions.
+  //
+  // The observables must be fully consumed within the lifetime of the borrowed parameters.
+  INJECT(SearchDirectories(borrowed<SystemCall*> system_call))
+      : system_call_(system_call) {}
+
+  // TODO: is there a way to get rid of this second RxAnyConnectable parameter?
+ private:
+  // This gets passed around to lazy lambdas, so we must finish consuming any observables
+  // before the injected system call is deleted.
+  borrowed<SystemCall*> system_call_;
+};
+
+}  // namespace iorap::inode2filename
+
+#endif  // IORAP_SRC_INODE2FILENAME_SEARCH_DIRECTORIES_H_
diff --git a/src/inode2filename/system_call.h b/src/inode2filename/system_call.h
new file mode 100644
index 0000000..43c371f
--- /dev/null
+++ b/src/inode2filename/system_call.h
@@ -0,0 +1,73 @@
+// 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.
+
+#ifndef IORAP_SRC_INODE2FILENAME_SYSTEM_CALL_H_
+#define IORAP_SRC_INODE2FILENAME_SYSTEM_CALL_H_
+
+#include <fruit/fruit.h>
+
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+// Abstract out the system calls behind a virtual interface:
+// This enables us to use dependency injection to provide mock implementations
+// during tests.
+struct SystemCall {
+  // stat(2)
+  virtual int stat(const char *pathname, struct stat *statbuf) = 0;
+
+  // opendir(3)
+  virtual DIR *opendir(const char *name) = 0;
+
+  // readdir(3)
+  virtual struct dirent *readdir(DIR *dirp) = 0;
+
+  // closedir(3)
+  virtual int closedir(DIR *dirp) = 0;
+
+  virtual ~SystemCall() {}
+};
+
+// "Live" implementation that calls down to libc.
+struct SystemCallImpl : public SystemCall {
+  // Marks this constructor as the one to use for injection.
+  INJECT(SystemCallImpl()) = default;
+
+  // stat(2)
+  virtual int stat(const char *pathname, struct stat *statbuf) override {
+    return ::stat(pathname, statbuf);
+  }
+
+  // opendir(3)
+  virtual DIR *opendir(const char *name) override {
+    return ::opendir(name);
+  }
+
+  // readdir(3)
+  virtual struct dirent *readdir(DIR *dirp) override {
+    return ::readdir(dirp);
+  }
+
+  // closedir(3)
+  virtual int closedir(DIR *dirp) override {
+    return ::closedir(dirp);
+  }
+
+  virtual ~SystemCallImpl() {}
+};
+
+#endif  // IORAP_SRC_INODE2FILENAME_SYSTEM_CALL_H_
+
diff --git a/src/iorapd/main.cc b/src/iorapd/main.cc
index 074984d..ab73fe5 100644
--- a/src/iorapd/main.cc
+++ b/src/iorapd/main.cc
@@ -15,8 +15,11 @@
  */
 
 #include "binder/iiorap_impl.h"
+#include "common/debug.h"
+#include "manager/event_manager.h"
 
 #include <android-base/logging.h>
+#include <android-base/properties.h>
 #include <binder/IPCThreadState.h>
 #include <utils/Trace.h>
 
@@ -46,22 +49,24 @@
 };
 
 int main(int /*argc*/, char** argv) {
-  // Log everything!! TODO: less aggressive logging once this is closer to being shipped.
-  setenv("ANDROID_LOG_TAGS", "*:v", /*overwrite*/ 1);
+  if (android::base::GetBoolProperty("iorapd.log.verbose", iorap::kIsDebugBuild)) {
+    // Show verbose logs if the property is enabled or if we are a debug build.
+    setenv("ANDROID_LOG_TAGS", "*:v", /*overwrite*/ 1);
+  }
 
   // Logs go to system logcat.
   android::base::InitLogging(argv, StderrAndLogdLogger{android::base::SYSTEM});
 
-  // TODO: an selinux context is required, otherwise clients are rejected when trying to
-  // find this service.
-
-  // Testing workaround: use 'adb shell setenforce 0'
   {
     android::ScopedTrace trace_main{ATRACE_TAG_PACKAGE_MANAGER, "main"};
     LOG(INFO) << kServiceName << " (the prefetchening) firing up";
 
     android::ScopedTrace trace_start{ATRACE_TAG_PACKAGE_MANAGER, "IorapNativeService::start"};
-    if (!iorap::binder::IIorapImpl::Start()) {
+
+    // TODO: use fruit for this DI.
+    auto /*std::shared_ptr<EventManager>*/ event_manager =
+        iorap::manager::EventManager::Create();
+    if (!iorap::binder::IIorapImpl::Start(std::move(event_manager))) {
       LOG(ERROR) << "Unable to start IorapNativeService";
       exit(1);
     }
diff --git a/src/manager/event_manager.cc b/src/manager/event_manager.cc
new file mode 100644
index 0000000..3b75b89
--- /dev/null
+++ b/src/manager/event_manager.cc
@@ -0,0 +1,496 @@
+/*
+ * 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.
+ */
+
+#include "common/debug.h"
+#include "common/expected.h"
+#include "manager/event_manager.h"
+#include "perfetto/rx_producer.h"
+
+#include <android-base/properties.h>
+#include <rxcpp/rx.hpp>
+
+#include <atomic>
+#include <functional>
+
+using rxcpp::observe_on_one_worker;
+
+namespace iorap::manager {
+
+using binder::RequestId;
+using binder::AppLaunchEvent;
+using perfetto::PerfettoStreamCommand;
+using perfetto::PerfettoTraceProto;
+
+struct AppComponentName {
+  std::string package;
+  std::string activity_name;
+
+  static bool HasAppComponentName(const std::string& s) {
+    return s.find('/') != std::string::npos;
+  }
+
+  // "com.foo.bar/.A" -> {"com.foo.bar", ".A"}
+  static AppComponentName FromString(const std::string& s) {
+    constexpr const char delimiter = '/';
+    std::string package = s.substr(0, delimiter);
+
+    std::string activity_name = s;
+    activity_name.erase(0, s.find(delimiter) + sizeof(delimiter));
+
+    return {std::move(package), std::move(activity_name)};
+  }
+
+  // {"com.foo.bar", ".A"} -> "com.foo.bar/.A"
+  std::string ToString() const {
+    return package + "/" + activity_name;
+  }
+
+  /*
+   * '/' is encoded into %2F
+   * '%' is encoded into %25
+   *
+   * This allows the component name to be be used as a file name
+   * ('/' is illegal due to being a path separator) with minimal
+   * munging.
+   */
+
+  // "com.foo.bar%2F.A%25" -> {"com.foo.bar", ".A%"}
+  static AppComponentName FromUrlEncodedString(const std::string& s) {
+    std::string cpy = s;
+    Replace(cpy, "%2F", "/");
+    Replace(cpy, "%25", "%");
+
+    return FromString(cpy);
+  }
+
+  // {"com.foo.bar", ".A%"} -> "com.foo.bar%2F.A%25"
+  std::string ToUrlEncodedString() const {
+    std::string s = ToString();
+    Replace(s, "%", "%25");
+    Replace(s, "/", "%2F");
+    return s;
+  }
+
+ private:
+  static bool Replace(std::string& str, const std::string& from, const std::string& to) {
+    // TODO: call in a loop to replace all occurrences, not just the first one.
+    const size_t start_pos = str.find(from);
+    if (start_pos == std::string::npos) {
+      return false;
+    }
+
+    str.replace(start_pos, from.length(), to);
+
+    return true;
+}
+};
+
+std::ostream& operator<<(std::ostream& os, const AppComponentName& name) {
+  os << name.ToString();
+  return os;
+}
+
+// Main logic of the #OnAppLaunchEvent scan method.
+//
+// All functions are called from the same thread as the event manager
+// functions.
+//
+// This is a data type, it's moved (std::move) around from one iteration
+// of #scan to another.
+struct AppLaunchEventState {
+  std::optional<AppComponentName> component_name_;
+
+  bool is_tracing_{false};
+  std::optional<rxcpp::composite_subscription> rx_lifetime_;
+  std::vector<rxcpp::composite_subscription> rx_in_flight_;
+
+  borrowed<perfetto::RxProducerFactory*> perfetto_factory_;  // not null
+  borrowed<observe_on_one_worker*> thread_;  // not null
+  borrowed<observe_on_one_worker*> io_thread_;  // not null
+
+  explicit AppLaunchEventState(borrowed<perfetto::RxProducerFactory*> perfetto_factory,
+                               borrowed<observe_on_one_worker*> thread,
+                               borrowed<observe_on_one_worker*> io_thread) {
+    perfetto_factory_ = perfetto_factory;
+    DCHECK(perfetto_factory_ != nullptr);
+
+    thread_ = thread;
+    DCHECK(thread_ != nullptr);
+
+    io_thread_ = io_thread;
+    DCHECK(io_thread_ != nullptr);
+  }
+
+  // Updates the values in this struct only as a side effect.
+  //
+  // May create and fire a new rx chain on the same threads as passed
+  // in by the constructors.
+  void OnNewEvent(const AppLaunchEvent& event) {
+    LOG(VERBOSE) << "AppLaunchEventState#OnNewEvent: " << event;
+
+    using Type = AppLaunchEvent::Type;
+
+    switch (event.type) {
+      case Type::kIntentStarted: {
+        DCHECK(!IsTracing());
+        // Optimistically start tracing if we have the activity in the intent.
+        if (!event.intent_proto->has_component()) {
+          // Can't do anything if there is no component in the proto.
+          LOG(VERBOSE) << "AppLaunchEventState#OnNewEvent: no component, can't trace";
+          break;
+        }
+
+        const std::string& package_name = event.intent_proto->component().package_name();
+        const std::string& class_name = event.intent_proto->component().class_name();
+        AppComponentName component_name{package_name, class_name};
+
+        component_name_ = component_name;
+        rx_lifetime_ = StartTracing(std::move(component_name));
+
+        break;
+      }
+      case Type::kIntentFailed:
+        AbortTrace();
+        break;
+      case Type::kActivityLaunched: {
+        // Cancel tracing for warm/hot.
+        // Restart tracing if the activity was unexpected.
+
+        AppLaunchEvent::Temperature temperature = event.temperature;
+        if (temperature != AppLaunchEvent::Temperature::kCold) {
+          LOG(DEBUG) << "AppLaunchEventState#OnNewEvent aborting trace due to non-cold temperature";
+          AbortTrace();
+        } else if (!IsTracing()) {  // and the temperature is Cold.
+          // Start late trace when intent didn't have a component name
+          LOG(VERBOSE) << "AppLaunchEventState#OnNewEvent need to start new trace";
+
+          const std::string& title = event.activity_record_proto->identifier().title();
+          if (!AppComponentName::HasAppComponentName(title)) {
+            // Proto comment claim this is sometimes a window title.
+            // We need the actual 'package/component' here, so just ignore it if it's a title.
+            LOG(WARNING) << "App launched without a component name: " << event;
+            break;
+          }
+
+          AppComponentName component_name = AppComponentName::FromString(title);
+
+          component_name_ = component_name;
+          rx_lifetime_ = StartTracing(std::move(component_name));
+        } else {
+          // FIXME: match actual component name against intent component name.
+          // abort traces if they don't match.
+
+          LOG(VERBOSE) << "AppLaunchEventState#OnNewEvent already tracing";
+        }
+        break;
+      }
+      case Type::kActivityLaunchFinished:
+        // Finish tracing and collect trace buffer.
+        //
+        // TODO: this happens automatically when perfetto finishes its
+        // trace duration.
+        if (IsTracing()) {
+          MarkPendingTrace();
+        }
+        break;
+      case Type::kActivityLaunchCancelled:
+        // Abort tracing.
+        AbortTrace();
+        break;
+      default:
+        DCHECK(false) << "invalid type: " << event;  // binder layer should've rejected this.
+        LOG(ERROR) << "invalid type: " << event;  // binder layer should've rejected this.
+    }
+  }
+
+  bool IsTracing() const {
+    return is_tracing_;
+  }
+
+  rxcpp::composite_subscription StartTracing(AppComponentName component_name) {
+    DCHECK(!IsTracing());
+
+    auto /*observable<PerfettoStreamCommand>*/ perfetto_commands =
+      rxcpp::observable<>::just(PerfettoStreamCommand::kStartTracing)
+          // wait 1x
+          .concat(
+              // Pick a value longer than the perfetto config delay_ms, so that we send
+              // 'kShutdown' after tracing has already finished.
+              rxcpp::observable<>::interval(std::chrono::milliseconds(10000))
+                  .take(2)  // kStopTracing, kShutdown.
+                  .map([](int value) {
+                         // value is 1,2,3,...
+                         return static_cast<PerfettoStreamCommand>(value);  // 1,2, ...
+                       })
+          );
+
+    auto /*observable<PerfettoTraceProto>*/ trace_proto_stream =
+        perfetto_factory_->CreateTraceStream(perfetto_commands);
+    // This immediately connects to perfetto asynchronously.
+    //
+    // TODO: create a perfetto handle earlier, to minimize perfetto startup latency.
+
+    rxcpp::composite_subscription lifetime;
+
+    trace_proto_stream
+      .tap([](const PerfettoTraceProto& trace_proto) {
+             LOG(VERBOSE) << "StartTracing -- PerfettoTraceProto received (1)";
+           })
+      .observe_on(*thread_)   // All work prior to 'observe_on' is handled on thread_.
+      .subscribe_on(*thread_)   // All work prior to 'observe_on' is handled on thread_.
+      .observe_on(*io_thread_)  // Write data on an idle-class-priority thread.
+      .tap([](const PerfettoTraceProto& trace_proto) {
+             LOG(VERBOSE) << "StartTracing -- PerfettoTraceProto received (2)";
+           })
+      .as_blocking()  // TODO: remove.
+      .subscribe(/*out*/lifetime,
+        /*on_next*/[component_name]
+        (PerfettoTraceProto trace_proto) {
+          std::string file_path = "/data/misc/iorapd/";
+          file_path += component_name.ToUrlEncodedString();
+          file_path += ".perfetto_trace.pb";
+
+          // TODO: timestamp each file into a subdirectory.
+
+          if (!trace_proto.WriteFullyToFile(file_path)) {
+            LOG(ERROR) << "Failed to save TraceBuffer to " << file_path;
+          } else {
+            LOG(INFO) << "Perfetto TraceBuffer saved to file: " << file_path;
+          }
+        },
+        /*on_error*/[](rxcpp::util::error_ptr err) {
+          LOG(ERROR) << "Perfetto trace proto collection error: " << rxcpp::util::what(err);
+        });
+
+    is_tracing_ = true;
+
+    return lifetime;
+  }
+
+  void AbortTrace() {
+    LOG(VERBOSE) << "AppLaunchEventState - AbortTrace";
+    is_tracing_ = false;
+    if (rx_lifetime_) {
+      // TODO: it would be good to call perfetto Destroy.
+
+      LOG(VERBOSE) << "AppLaunchEventState - AbortTrace - Unsubscribe";
+      rx_lifetime_->unsubscribe();
+      rx_lifetime_.reset();
+    }
+  }
+
+  void MarkPendingTrace() {
+    LOG(VERBOSE) << "AppLaunchEventState - MarkPendingTrace";
+    DCHECK(is_tracing_);
+    DCHECK(rx_lifetime_.has_value());
+
+    if (rx_lifetime_) {
+      LOG(VERBOSE) << "AppLaunchEventState - MarkPendingTrace - lifetime moved";
+      // Don't unsubscribe because that would cause the perfetto TraceBuffer
+      // to get dropped on the floor.
+      //
+      // Instead, we want to let it finish and write it out to a file.
+      rx_in_flight_.push_back(*std::move(rx_lifetime_));
+      rx_lifetime_.reset();
+    } else {
+      LOG(VERBOSE) << "AppLaunchEventState - MarkPendingTrace - lifetime was empty";
+    }
+
+    // FIXME: how do we clear this vector?
+  }
+};
+
+// Convert callback pattern into reactive pattern.
+struct AppLaunchEventSubject {
+  using RefWrapper =
+    std::reference_wrapper<const AppLaunchEvent>;
+
+  AppLaunchEventSubject() {}
+
+  void Subscribe(rxcpp::subscriber<RefWrapper> subscriber) {
+    DCHECK(ready_ != true) << "Cannot Subscribe twice";
+
+    subscriber_ = std::move(subscriber);
+
+    // Release edge of synchronizes-with AcquireIsReady.
+    ready_.store(true);
+  }
+
+  void OnNext(const AppLaunchEvent& e) {
+    if (!AcquireIsReady()) {
+      return;
+    }
+
+    if (!subscriber_->is_subscribed()) {
+      return;
+    }
+
+    /*
+     * TODO: fix upstream.
+     *
+     * Rx workaround: this fails to compile when
+     * the observable is a reference type:
+     *
+     * external/Reactive-Extensions/RxCpp/Rx/v2/src/rxcpp/rx-observer.hpp:354:18: error: multiple overloads of 'on_next' instantiate to the same signature 'void (const iorap::binder::AppLaunchEvent &) const'
+     *   virtual void on_next(T&&) const {};
+     *
+     * external/Reactive-Extensions/RxCpp/Rx/v2/src/rxcpp/rx-observer.hpp:353:18: note: previous declaration is here
+     *   virtual void on_next(T&) const {};
+     *
+     * (The workaround is to use reference_wrapper instead
+     *  of const AppLaunchEvent&)
+     */
+    subscriber_->on_next(std::cref(e));
+
+  }
+
+  void OnCompleted() {
+    if (!AcquireIsReady()) {
+      return;
+    }
+
+    subscriber_->on_completed();
+  }
+
+ private:
+  bool AcquireIsReady() {
+    // Synchronizes-with the release-edge in Subscribe.
+    // This can happen much later, only once the subscription actually happens.
+
+    // However, as far as I know, 'rxcpp::subscriber' is not thread safe,
+    // (but the observable chain itself can be made thread-safe via #observe_on, etc).
+    // so we must avoid reading it until it has been fully synchronized.
+    //
+    // TODO: investigate rxcpp subscribers and see if we can get rid of this atomics,
+    // to make it simpler.
+    return ready_.load();
+  }
+
+  // TODO: also track the RequestId ?
+
+  std::atomic<bool> ready_{false};
+
+
+  std::optional<rxcpp::subscriber<RefWrapper>> subscriber_;
+};
+
+class EventManager::Impl {
+ public:
+  Impl(/*borrow*/perfetto::RxProducerFactory& perfetto_factory)
+    : perfetto_factory_(perfetto_factory),
+      worker_thread_(rxcpp::observe_on_new_thread()),
+      worker_thread2_(rxcpp::observe_on_new_thread()),
+      io_thread_(perfetto::ObserveOnNewIoThread()) {
+
+    // TODO: read all properties from one config class.
+    tracing_allowed_ = ::android::base::GetBoolProperty("iorapd.perfetto.enable", /*default*/false);
+
+    if (tracing_allowed_) {
+      rx_lifetime_ = InitializeRxGraph();
+    } else {
+      LOG(WARNING) << "Tracing disabled by iorapd.perfetto.enable=false";
+    }
+  }
+
+  bool OnAppLaunchEvent(RequestId request_id,
+                        const AppLaunchEvent& event) {
+    LOG(VERBOSE) << "EventManager::OnAppLaunchEvent("
+                 << "request_id=" << request_id.request_id << ","
+                 << event;
+
+    app_launch_event_subject_.OnNext(event);
+
+    return true;
+  }
+
+  rxcpp::composite_subscription InitializeRxGraph() {
+    LOG(VERBOSE) << "EventManager::InitializeRxGraph";
+
+    app_launch_events_ = rxcpp::observable<>::create<AppLaunchEventRefWrapper>(
+      [&](rxcpp::subscriber<AppLaunchEventRefWrapper> subscriber) {
+        app_launch_event_subject_.Subscribe(std::move(subscriber));
+      });
+
+    rxcpp::composite_subscription lifetime;
+
+    AppLaunchEventState initial_state{&perfetto_factory_, &worker_thread2_, &io_thread_};
+    app_launch_events_
+      .subscribe_on(worker_thread_)
+      .scan(std::move(initial_state),
+            [](AppLaunchEventState state, AppLaunchEventRefWrapper event) {
+              state.OnNewEvent(event.get());
+              return state;
+            })
+      .subscribe(/*out*/lifetime, [](const AppLaunchEventState& state) {
+                   // Intentionally left blank.
+                   (void)state;
+                 });
+
+    return lifetime;
+  }
+
+  perfetto::RxProducerFactory& perfetto_factory_;
+  bool tracing_allowed_{true};
+
+  using AppLaunchEventRefWrapper = AppLaunchEventSubject::RefWrapper;
+  rxcpp::observable<AppLaunchEventRefWrapper> app_launch_events_;
+  AppLaunchEventSubject app_launch_event_subject_;
+
+  rxcpp::observable<RequestId> completed_requests_;
+
+  // regular-priority thread to handle binder callbacks.
+  observe_on_one_worker worker_thread_;
+  observe_on_one_worker worker_thread2_;
+  // low priority idle-class thread for IO operations.
+  observe_on_one_worker io_thread_;
+
+  rxcpp::composite_subscription rx_lifetime_;
+
+//INTENTIONAL_COMPILER_ERROR_HERE:
+  // FIXME:
+  // ok so we want to expose a 'BlockingSubscribe' or a 'Subscribe' or some kind of function
+  // that the main thread can call. This would subscribe on all the observables we internally
+  // have here (probably on an event-manager-dedicated thread for simplicity).
+  //
+  // ideally we'd just reuse the binder thread to handle the events but I'm not super sure,
+  // maybe this already works with the identity_current_thread coordination?
+};
+using Impl = EventManager::Impl;
+
+EventManager::EventManager(perfetto::RxProducerFactory& perfetto_factory)
+    : impl_(new Impl(perfetto_factory)) {}
+
+std::shared_ptr<EventManager> EventManager::Create() {
+  static perfetto::PerfettoDependencies::Injector injector{
+    perfetto::PerfettoDependencies::CreateComponent
+  };
+  static perfetto::RxProducerFactory producer_factory{
+    /*borrow*/injector
+  };
+  return EventManager::Create(/*borrow*/producer_factory);
+}
+
+std::shared_ptr<EventManager> EventManager::Create(perfetto::RxProducerFactory& perfetto_factory) {
+  std::shared_ptr<EventManager> p{new EventManager{/*borrow*/perfetto_factory}};
+  return p;
+}
+
+bool EventManager::OnAppLaunchEvent(RequestId request_id,
+                                    const AppLaunchEvent& event) {
+  return impl_->OnAppLaunchEvent(request_id, event);
+}
+
+}  // namespace iorap::manager
diff --git a/src/manager/event_manager.h b/src/manager/event_manager.h
new file mode 100644
index 0000000..5355296
--- /dev/null
+++ b/src/manager/event_manager.h
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#ifndef IORAP_MANAGER_EVENT_MANAGER_H_
+#define IORAP_MANAGER_EVENT_MANAGER_H_
+
+#include "binder/app_launch_event.h"
+#include "binder/request_id.h"
+
+#include <memory>
+
+namespace iorap::perfetto {
+  struct RxProducerFactory;
+}  // namespace iorap::perfetto
+
+namespace iorap::manager {
+
+class EventManager {
+ public:
+  static std::shared_ptr<EventManager> Create();
+  static std::shared_ptr<EventManager> Create(
+      /*borrow*/perfetto::RxProducerFactory& perfetto_factory);
+
+  // Handles an AppLaunchEvent:
+  //
+  // * Intent starts and app launch starts are treated critically
+  //   and will be handled immediately. This means the caller
+  //   (e.g. the binder pool thread) could be starved in the name
+  //   of low latency.
+  //
+  // * Other types are handled in a separate thread.
+  bool OnAppLaunchEvent(binder::RequestId request_id,
+                        const binder::AppLaunchEvent& event);
+
+  class Impl;
+ private:
+  std::unique_ptr<Impl> impl_;
+
+  EventManager(perfetto::RxProducerFactory& perfetto_factory);
+};
+
+}  // namespace iorap::manager
+
+#endif  // IORAP_MANAGER_EVENT_MANAGER_H_
diff --git a/src/perfetto/main.cc b/src/perfetto/main.cc
new file mode 100644
index 0000000..0b1056e
--- /dev/null
+++ b/src/perfetto/main.cc
@@ -0,0 +1,244 @@
+// 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.
+
+//#undef NDEBUG // get DCHECK etc.
+
+
+#include "common/debug.h"
+#include "common/expected.h"
+#include "perfetto/rx_producer.h"
+
+#include <android-base/unique_fd.h>
+#include <android-base/parseint.h>
+#include <android-base/file.h>
+
+#include "rxcpp/rx.hpp"
+#include <iostream>
+#include <optional>
+
+#include <sched.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <syscall.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+using namespace iorap::perfetto;  // NOLINT
+
+#if defined(IORAP_PERFETTO_MAIN)
+
+void Usage(char** argv) {
+  std::cerr << "Usage: " << argv[0] << " [--config-proto=config.pb] [--duration-ms=5000] [--output-proto=output.pb]" << std::endl;
+  std::cerr << "" << std::endl;
+  std::cerr << "  Request a perfetto trace, blocking until it's complete. The resulting trace proto" << std::endl;
+  std::cerr << "  is output to stdout as text, or to --output-proto as a binary." << std::endl;
+  std::cerr << "" << std::endl;
+  std::cerr << "  Optional flags:" << std::endl;
+  std::cerr << "    --help,-h                  Print this Usage." << std::endl;
+  std::cerr << "    --output-proto $,-op $     Perfetto tracebuffer output file (default stdout)." << std::endl;
+  std::cerr << "    --config-proto $,-cp $     Path to binary protobuf config." << std::endl;
+  std::cerr << "    --duration-ms $,-dm $      How long to run trace for in milliseconds." << std::endl;
+  std::cerr << "    --simple                   Simplest possible perfetto state transitions (default off)." << std::endl;
+  std::cerr << "    --verbose,-v               Set verbosity (default off)." << std::endl;
+  std::cerr << "    --wait,-w                  Wait for key stroke before continuing (default off)." << std::endl;
+  exit(1);
+}
+
+PerfettoDependencies::Component CreateCommandLinePerfettoDependenciesComponent(
+    uint32_t duration_ms) {
+  // TODO: read from command line.
+  static const uint32_t kBufferSize = 4096;
+
+  // TODO: remove this hack.
+  static const uint32_t kTraceDurationMs = duration_ms;
+
+  // fruit: using 'bindInstance' causes a segfault every time.
+#if 0
+
+  // fruit: Can't use a stateful lambda, so use bindInstance instead of registerProvider.
+  auto config = PerfettoDependencies::CreateConfig(duration_ms,
+                                                   /*deferred_start*/true,
+                                                   kBufferSize);
+
+  .... bindInstance(config);
+#endif
+
+  return fruit::createComponent()
+    .bind<PerfettoConsumer, PerfettoConsumerImpl>()
+    .registerProvider([]() /* -> TraceConfig */ {
+        return PerfettoDependencies::CreateConfig(kTraceDurationMs,
+                                                  /*deferred_start*/true,
+                                                  kBufferSize);
+    });
+}
+
+static void CollectPerfettoTraceBufferViaAbstractions(
+    RxProducerFactory& producer_factory,
+    const std::string& arg_output_proto,
+    const int arg_duration_ms) {
+  LOG(VERBOSE) << "CollectPerfettoTraceBufferViaAbstractions";
+
+  // Don't create a subscriber to emit the PerfettoStreamCommand.
+  // RxCpp is "greedy" and consumes every possible item emitted (it doesn't support 'pull'). We want
+  // to operate on a (command,state) iteration every time, just like in a real scenario.
+  // Adding the 'interval' turns into a non-greedy version (i.e. push).
+
+  // Immediately emit 'kStartTracing', wait and emit kStopTracing, wait and emit kShutdown.
+  // In reality, there would be a delay between all these events.
+  auto /*observable<PerfettoStreamCommand>*/ commands =
+      rxcpp::observable<>::just(PerfettoStreamCommand::kStartTracing)
+          // wait 1x
+          .concat(
+              // Pick a value longer than the perfetto config delay_ms, so that we send
+              // 'kShutdown' after tracing has already finished.
+              rxcpp::observable<>::interval(std::chrono::milliseconds(arg_duration_ms * 2))
+                  .take(2)  // kStopTracing, kShutdown.
+                  .map([](int value) {
+                         // value is 1,2,3,...
+                         return static_cast<PerfettoStreamCommand>(value);  // 1,2, ...
+                       })
+          );
+
+  auto /*observable<PerfettoTraceProto>*/ trace_proto_stream =
+      producer_factory.CreateTraceStream(commands);
+
+  trace_proto_stream
+    .observe_on(ObserveOnNewIoThread())  // Write data on an idle-class-priority thread.
+    .as_blocking()  // Wait for observable to terminate with on_completed or on_error.
+    .subscribe(/*on_next*/[arg_output_proto]
+      (PerfettoTraceProto trace_proto) {
+             if (!trace_proto.WriteFullyToFile(arg_output_proto)) {
+               LOG(ERROR) << "Failed to save TraceBuffer to " << arg_output_proto;
+             } else {
+               LOG(INFO) << "TraceBuffer saved to file: " << arg_output_proto;
+               LOG(INFO);
+               LOG(INFO) << "To print this in a human readable form, execute these commands:";
+               LOG(INFO) << "$> adb pull '" << arg_output_proto << "'";
+               LOG(INFO) << "$> trace_to_text systrace <filename.pb>";
+             }
+      },
+      /*on_error*/[](rxcpp::util::error_ptr err) {
+        LOG(ERROR) << "Perfetto trace proto collection error: " << rxcpp::util::what(err);
+      });
+}
+
+namespace iorap::perfetto {
+// Reach inside rx_producer.cc
+// Not part of any headers because it's internal.
+void CollectPerfettoTraceBufferImmediately(
+    RxProducerFactory& producer_factory,
+    const std::string& arg_output_proto);
+}
+
+int main(int argc, char** argv) {
+  android::base::InitLogging(argv);
+  android::base::SetLogger(android::base::StderrLogger);
+
+  bool wait_for_keystroke = false;
+  bool enable_verbose = false;
+
+  std::string arg_output_proto;
+  std::string arg_config_proto;
+  uint32_t arg_duration_ms = 1000;
+  bool arg_simple = false;
+
+  if (argc == 1) {
+    Usage(argv);
+  }
+
+  for (int arg = 1; arg < argc; ++arg) {
+    std::string argstr = argv[arg];
+    bool has_arg_next = (arg+1)<argc;
+    std::string arg_next = has_arg_next ? argv[arg+1] : "";
+
+    if (argstr == "--help" || argstr == "-h") {
+      Usage(argv);
+    } else if (argstr == "--output-proto" || argstr == "-op") {
+      if (!has_arg_next) {
+        std::cerr << "Missing --output-proto <value>" << std::endl;
+        return 1;
+      }
+      arg_output_proto = arg_next;
+      ++arg;
+    } else if (argstr == "--config-proto" || argstr == "-cp") {
+      if (!has_arg_next) {
+        std::cerr << "Missing --config-proto <value>" << std::endl;
+        return 1;
+      }
+      arg_config_proto = arg_next;
+      LOG(WARNING) << "TODO: parse configs from a file, not implemented yet.";
+      ++arg;
+    } else if (argstr == "--duration-ms" || argstr == "-dm") {
+      if (!has_arg_next) {
+        std::cerr << "Missing --duration-ms <value>" << std::endl;
+        return 1;
+      }
+      if (!android::base::ParseUint(arg_next.c_str(), /*out*/&arg_duration_ms)) {
+        std::cerr << "Invalid --duration-ms " << arg_next << ", reason: " << strerror(errno);
+        return 1;
+      }
+      ++arg;
+    } else if (argstr == "--simple") {
+      arg_simple = true;
+    } else if (argstr == "--verbose" || argstr == "-v") {
+      enable_verbose = true;
+    } else if (argstr == "--wait" || argstr == "-w") {
+      wait_for_keystroke = true;
+    }
+  }
+
+  if (enable_verbose) {
+    android::base::SetMinimumLogSeverity(android::base::VERBOSE);
+
+    LOG(VERBOSE) << "Verbose check";
+    LOG(VERBOSE) << "Debug check: " << ::iorap::kIsDebugBuild;
+  }
+
+  // Useful to attach a debugger...
+  // 1) $> iorap-cmd-perfetto -w <args>
+  // 2) $> gdbclient <pid>
+  if (wait_for_keystroke) {
+    LOG(INFO) << "Self pid: " << getpid();
+    LOG(INFO) << "Press any key to continue...";
+    std::cin >> wait_for_keystroke;
+  }
+
+  int return_code = 0;
+  // TODO: convert #on-error into a non-0 return code.
+
+  PerfettoDependencies::Injector injector{
+      CreateCommandLinePerfettoDependenciesComponent,
+      arg_duration_ms
+  };
+  RxProducerFactory rx_producer_factory{/*borrow*/injector};
+
+  if (arg_simple) {
+    // To debug any kind of low-level perfetto issues.
+    CollectPerfettoTraceBufferImmediately(/*inout*/rx_producer_factory, arg_output_proto);
+  } else {
+    // To debug our own iorap internal abstractions.
+    CollectPerfettoTraceBufferViaAbstractions(/*inout*/rx_producer_factory,
+                                              arg_output_proto,
+                                              arg_duration_ms);
+  }
+
+  // Uncomment this if we want to leave the process around to inspect it from adb shell.
+  // sleep(100000);
+
+  // 0 -> successfully wrote the TraceProto out to file.
+  // 1 -> failed along the way (#on_error and also see the error logs).
+  return return_code;
+}
+
+#endif
diff --git a/src/perfetto/perfetto_consumer.h b/src/perfetto/perfetto_consumer.h
new file mode 100644
index 0000000..ce04ffd
--- /dev/null
+++ b/src/perfetto/perfetto_consumer.h
@@ -0,0 +1,86 @@
+// 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.
+
+#ifndef IORAP_SRC_PERFETTO_PERFETTO_CONSUMER_H_
+#define IORAP_SRC_PERFETTO_PERFETTO_CONSUMER_H_
+
+#include <fruit/fruit.h>
+#include <perfetto/public/consumer_api.h>  // libperfetto
+
+namespace iorap::perfetto {
+
+// Abstract out the Perfetto C API behind a virtual interface:
+// This enables us to use dependency injection to provide mock implementations
+// during tests.
+struct PerfettoConsumer {
+  // 1:1 aliasing of type definitions and constants in perfetto/public/consumer_api.h
+  // Refer to the documentation there.
+  using State = ::perfetto::consumer::State;
+  using Handle = ::perfetto::consumer::Handle;
+  static constexpr Handle kInvalidHandle = ::perfetto::consumer::kInvalidHandle;
+  using OnStateChangedCb = ::perfetto::consumer::OnStateChangedCb;
+  using TraceBuffer = ::perfetto::consumer::TraceBuffer;
+
+  // 1:1 forwarding of C-style functions in perfetto/public/consumer_api.h
+  // Refer to the documentation there.
+
+  virtual Handle Create(const void* config_proto,
+                        size_t config_len,
+                        OnStateChangedCb callback,
+                        void* callback_arg) = 0;
+  virtual void StartTracing(Handle) = 0;
+  virtual TraceBuffer ReadTrace(Handle) = 0;
+  virtual void Destroy(Handle) = 0;
+  virtual State PollState(Handle) = 0;
+
+  virtual ~PerfettoConsumer() {}
+};
+
+// "Live" implementation that calls down to libperfetto.
+struct PerfettoConsumerImpl : public PerfettoConsumer {
+  // Marks this constructor as the one to use for injection.
+  INJECT(PerfettoConsumerImpl()) = default;
+
+  virtual Handle Create(const void* config_proto,
+                        size_t config_len,
+                        OnStateChangedCb callback,
+                        void* callback_arg) override {
+    return ::perfetto::consumer::Create(config_proto,
+                                      config_len,
+                                      callback,
+                                      callback_arg);
+  }
+
+  virtual void StartTracing(Handle handle) override {
+    ::perfetto::consumer::StartTracing(handle);
+  }
+
+  virtual TraceBuffer ReadTrace(Handle handle) override {
+    return ::perfetto::consumer::ReadTrace(handle);
+  }
+
+  virtual void Destroy(Handle handle) override {
+    ::perfetto::consumer::Destroy(handle);
+  }
+  virtual State PollState(Handle handle) override {
+    return ::perfetto::consumer::PollState(handle);
+  }
+
+  virtual ~PerfettoConsumerImpl() {}
+};
+
+}  // namespace iorap::perfetto
+
+#endif  // IORAP_SRC_PERFETTO_PERFETTO_CONSUMER_H_
+
diff --git a/src/perfetto/rx_producer.cc b/src/perfetto/rx_producer.cc
new file mode 100644
index 0000000..14311a1
--- /dev/null
+++ b/src/perfetto/rx_producer.cc
@@ -0,0 +1,865 @@
+// 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.
+
+#include "common/debug.h"
+#include "common/expected.h"
+#include "perfetto/rx_producer.h"
+
+#include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/unique_fd.h>
+
+#include <iostream>
+
+#include <sched.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <syscall.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+// TODO: move to perfetto code
+namespace perfetto {
+namespace consumer {
+
+std::ostream& operator<<(std::ostream& os, State state) {
+  switch (state) {
+    case State::kTraceFailed:
+      os << "kTraceFailed";
+      break;
+    case State::kConnectionError:
+      os << "kConnectionError";
+      break;
+    case State::kSessionNotFound:
+      os << "kSessionNotFound";
+      break;
+    case State::kIdle:
+      os << "kIdle";
+      break;
+    case State::kConnecting:
+      os << "kConnecting";
+      break;
+    case State::kConfigured:
+      os << "kConfigured";
+      break;
+    case State::kTracing:
+      os << "kTracing";
+      break;
+    case State::kTraceEnded:
+      os << "kTraceEnded";
+      break;
+    default:
+      os << "(unknown)";  // did someone forget to update this code?
+      break;
+  }
+  return os;
+}
+
+}  // namespace consumer
+}  // namespace perfetto
+
+namespace iorap::perfetto {
+
+PerfettoDependencies::Component PerfettoDependencies::CreateComponent() {
+  // TODO: read from config.
+  static const uint32_t kTraceDurationMs =
+      ::android::base::GetUintProperty("iorapd.perfetto.trace_duration_ms", /*default*/5000U);
+
+  static const uint32_t kBufferSize =
+      ::android::base::GetUintProperty("iorapd.perfetto.buffer_size", /*default*/4096U);
+
+  return fruit::createComponent()
+    .bind<PerfettoConsumer, PerfettoConsumerImpl>()
+    .registerProvider([]() /* -> TraceConfig */ {
+        return CreateConfig(kTraceDurationMs,
+                            /*deferred_start*/true,
+                            kBufferSize);
+    });
+}
+
+::perfetto::protos::TraceConfig PerfettoDependencies::CreateConfig(uint32_t duration_ms,
+                                                                   bool deferred_start,
+                                                                   uint32_t buffer_size) {
+  ::perfetto::protos::TraceConfig trace_config;
+
+  trace_config.set_duration_ms(duration_ms);
+  trace_config.add_buffers()->set_size_kb(buffer_size);
+  trace_config.set_deferred_start(deferred_start);
+
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("linux.ftrace");
+  ds_config->mutable_ftrace_config()->add_ftrace_events(
+      "mm_filemap_add_to_page_cache");
+  ds_config->mutable_ftrace_config()->add_ftrace_events(
+      "mm_filemap_delete_from_page_cache");
+  ds_config->set_target_buffer(0);
+
+  return trace_config;
+}
+
+// RAII-style wrapper around a perfetto handle that calls Destroy
+// in a thread-safe manner.
+struct PerfettoConsumerHandle {
+ private:
+  std::shared_ptr<PerfettoConsumer> consumer_;
+  PerfettoConsumer::Handle handle_;
+
+ public:
+  // Takes over ownership of the 'handle'.
+  //
+  // Consumer must not be null.
+  PerfettoConsumerHandle(std::shared_ptr<PerfettoConsumer> consumer,
+                         PerfettoConsumer::Handle handle)
+    : consumer_{std::move(consumer)},
+      handle_{std::move(handle)} {
+    DCHECK(consumer_ != nullptr);
+  }
+
+  std::shared_ptr<PerfettoConsumer> GetConsumer() const {
+    return consumer_;
+  }
+
+  PerfettoConsumer::Handle GetHandle() const {
+    return handle_;
+  }
+
+  ~PerfettoConsumerHandle() {
+    LOG(VERBOSE) << "PerfettoConsumerHandle::Destroy(" << handle_ << ")";
+    consumer_->Destroy(handle_);
+  }
+
+  bool operator==(const PerfettoConsumerHandle& other) const {
+    return handle_ == other.handle_ && consumer_ == other.consumer_;
+  }
+
+  bool operator!=(const PerfettoConsumerHandle& other) const {
+    return !(*this == other);
+  }
+};
+
+
+// Snapshot of a single perfetto OnStateChanged callback.
+//
+// Operate on the PerfettoConsumer to further change the state.
+//
+// The Handle is kept 'valid' until all references to the PerfettoConsumerHandle
+// are dropped to 0. This ensures the Handle is not destroyed too early. All
+// direct usages of 'Handle' must be scoped by the PerfettoConsumerHandle.
+struct PerfettoStateChange {
+ public:
+  using State = ::perfetto::consumer::State;
+  using Handle = ::perfetto::consumer::Handle;
+
+  State state;                                                           // Never invalid.
+  std::shared_ptr<PerfettoConsumerHandle> perfetto_consumer_and_handle;  // Never null.
+
+  // Safety: Use only within scope of the PerfettoStateChange.
+  Handle GetHandle() const {
+    // TODO: it would be even safer to wrap all the calls to the handle inside a class,
+    // instead of exposing this raw Handle.
+    return perfetto_consumer_and_handle->GetHandle();
+  }
+
+  std::shared_ptr<PerfettoConsumer> GetConsumer() const {
+    return perfetto_consumer_and_handle->GetConsumer();
+  }
+};
+
+std::ostream& operator<<(std::ostream& os, const PerfettoStateChange& state_change) {
+  os << "PerfettoStateChange{" << state_change.state << ","
+     << state_change.GetHandle() << ","
+     << state_change.GetConsumer().get() << "}";
+  return os;
+}
+
+// Once created, this acts as a hot observable, emitting 'PerfettoStateChange' transition items.
+// Only the 'state' will vary, the handle and perfetto_consumer are always the same value.
+//
+// Clients only need to handle the success states in #on_next, all failure states will go to
+// #on_error.
+//
+// Upon reaching the appropriate terminal states, either #on_completed or #on_error is called.
+// No future callbacks will then occur, so this object should be subsequently deleted.
+//
+// The Handle is destroyed automatically after the last item is emitted, so it must only be
+// manipulated from the #on_next callbacks. Do not save the Handle and use it at other times.
+class StateChangedSubject {
+ public:
+  using State = ::perfetto::consumer::State;
+  using Handle = ::perfetto::consumer::Handle;
+
+  StateChangedSubject(const ::perfetto::protos::TraceConfig& trace_config,
+                      rxcpp::subscriber<PerfettoStateChange> destination,
+                      std::shared_ptr<PerfettoConsumer> perfetto_consumer)
+    : deferred_start(trace_config.deferred_start()),
+      dest(std::move(destination)),
+      perfetto_consumer_(std::move(perfetto_consumer)) {
+    DCHECK(perfetto_consumer_ != nullptr);
+  }
+
+ private:
+  struct StateChangedError : public std::runtime_error {
+    explicit StateChangedError(const std::string& what_arg) : std::runtime_error(what_arg) {}
+  };
+
+  std::shared_ptr<PerfettoConsumerHandle> handle_;  // non-null after bound_ == true.
+  std::atomic<bool> bound_{false};  // synchronize-with for BindHandle -> OnStateChanged.
+
+  State last_state{State::kIdle};
+  bool deferred_start{false};
+
+  rxcpp::subscriber<PerfettoStateChange> dest;
+  std::shared_ptr<PerfettoConsumer> perfetto_consumer_;  // This is never null.
+
+  void DcheckBadStateTransition(State state, bool fail_unless = false) const {
+    DCHECK(fail_unless) << "Invalid state transition to " << state << " from " << last_state;
+  }
+
+  void DcheckValidStateTransition(State state) {
+    // State must not be out of range.
+    DCHECK_GE(state, State::kTraceFailed);
+    DCHECK_LE(state, State::kTraceEnded);
+
+    // Internal state that should never leak out into public perfetto API:
+    DCHECK_NE(state, State::kIdle);
+    // These can only be returned by PollState:
+    DCHECK_NE(state, State::kSessionNotFound);
+
+    // Validate state transitions as per the perfetto API contract.
+    // See the 'state diagram' in consumer_api.h
+    switch (last_state) {
+      case State::kTraceFailed:  // Final and unrecoverable.
+        // b/122548195: this can transition to 'kConnectionError' if selinux is disabled.
+        if (state == State::kConnectionError) {
+          LOG(WARNING) << "b/122548195: kTraceFailed is non-terminal, ignoring.";
+          // This is a bit awkward: rxcpp will drop the #on_error calls if its more than once.
+          break;
+        }
+        DcheckBadStateTransition(state);
+        break;
+      case State::kConnectionError:  // Final and unrecoverable.
+        DcheckBadStateTransition(state);
+        break;
+      case State::kSessionNotFound:
+        DcheckBadStateTransition(state);
+        break;
+      case State::kIdle:
+        // OK: we initialized our own state to idle prior to the first callback.
+        break;
+      case State::kConnecting:
+        switch (state) {
+          case State::kConfigured:
+            // kConfigured, if |deferred_start| == true in the trace config.
+            DcheckBadStateTransition(state, deferred_start);
+            break;
+          case State::kTracing:
+            // kTracing, if |deferred_start| == false.
+            DcheckBadStateTransition(state, !deferred_start);
+            break;
+          case State::kConnectionError:
+            // An error state, e.g. if cannot reach the traced daemon.
+            break;
+          default:
+            // Unconditionally invalid state transitions from kConnecting to anything else.
+            DcheckBadStateTransition(state);
+        }
+        break;
+      case State::kConfigured:
+        DCHECK(deferred_start);
+        if (state != State::kTracing  // OK: this is documented.
+            && state != State::kTraceFailed) {  // Undocumented selinux failure.
+            // Undocumented, but it appears to go directly from Configured->TraceEnded
+            // it can also go to kTraceFailed if e.g. there's an selinux violation
+            // however this appears to be underdocumented.
+            // b/122607276 #2
+
+          if (state != State::kTraceEnded) {  // b/122607276 #1
+            DcheckBadStateTransition(state);
+          }
+        }
+        break;
+      case State::kTracing:
+        switch (state) {
+          case State::kTraceEnded:
+            break;
+          case State::kTraceFailed:
+            break;
+          default:
+            DcheckBadStateTransition(state);
+        }
+        break;
+      case State::kTraceEnded:
+        // Cannot transition from terminal state to another state.
+        DcheckBadStateTransition(state);
+        break;
+
+      // default: This list is exhaustive
+    }
+  }
+
+  constexpr bool IsTerminalState() const {
+    switch (last_state) {
+      case State::kTraceFailed:
+      case State::kConnectionError:
+      case State::kTraceEnded:
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  // Returns true for non-terminal states (i.e. this callback will be invoked again).
+  // Returns false otherwise.
+  bool OnStateChanged(Handle handle, State state) {
+    using namespace ::perfetto::consumer;
+
+    // Block until 'BoundHandle' is called by the other thread.
+    while (!bound_.load()) {}  // seq_cst acquire.
+
+    std::shared_ptr<PerfettoConsumerHandle> handle_ptr = handle_;
+    DCHECK(handle_ptr != nullptr);
+
+    DCHECK_EQ(handle_ptr->GetHandle(), handle);
+    DcheckValidStateTransition(state);
+
+    switch (state) {
+      // Error states (terminal).
+      case State::kTraceFailed:
+        EmitError("kTraceFailed");
+        break;
+      case State::kConnectionError:
+        EmitError("kConnectionError");
+        break;
+
+      // Regular transitions (non-terminal).
+      case State::kConnecting:
+      case State::kConfigured:
+      case State::kTracing:
+        EmitNext(state);
+        break;
+      // Regular transitions (terminal).
+      case State::kTraceEnded:  // XX: do we even need to emit the 'TraceEnded' state?
+        EmitNext(state);
+        dest.on_completed();
+        break;
+      default:
+        DcheckBadStateTransition(state);
+    }
+
+    bool force_non_terminal = false;
+
+    if (last_state == State::kConfigured &&  state == State::kConnectionError) {
+      // b/122548195: this can transition to 'kConnectionError' if selinux is disabled.
+      force_non_terminal = true;
+      // This function must 'return true' in this buggy case, otherwise we will
+      // call the destructor too early and subsequent callbacks will crash.
+    }
+
+    // Remember the state to validate prior state transitions.
+    last_state = state;
+
+    // The owner of this class should avoid leaking memory once we reach a terminal state.
+    return !IsTerminalState() || force_non_terminal;
+  }
+
+ public:
+  // Thread safety: Called by main thread, terminates the rx stream.
+  // When this function is invoked, no calls to this class from other threads can occur.
+  void OnCreateFailed() {
+    // returned when an invalid handle is passed to PollState().
+    last_state = State::kSessionNotFound;
+    EmitError("Create returned kInvalidHandle");
+  }
+
+  // Thread safety: Called by main thread, this could be concurrent to
+  // 'CallbackOnStateChanged'.
+  void BindHandle(const std::shared_ptr<PerfettoConsumerHandle>& handle) {
+    handle_ = handle;
+
+    // Unblock OnStateChanged.
+    bound_.store(true);  // seq_cst release.
+  }
+
+  // Thread safety: Called by libperfetto background thread (same one every time).
+  static void CallbackOnStateChanged(Handle handle, State state, void* callback_arg) {
+    LOG(VERBOSE) << "CallbackOnStateChanged(handle=" << handle << ",state=" << state
+                 << ",callback_arg=" << callback_arg << ")";
+
+    // Validate OnStateChanged callback invariants, guaranteed by libperfetto.
+    DCHECK_NE(handle, ::perfetto::consumer::kInvalidHandle);
+
+    // Note: Perfetto guarantees this callback always occurs on the same thread,
+    // so we don't need to do any extra thread synchronization here since we are only mutating
+    // StateChangedSubject from within this function.
+
+    // TODO: the memory ordering guarantees should be explicitly specified in consumer_api.h:
+    // This isn't specific enough:
+    // "The callback will be invoked on an internal thread and must not block."
+    // However looking at the implementation it posts onto a single-thread task runner,
+    // so this must be the case.
+
+    StateChangedSubject* state_subject = reinterpret_cast<StateChangedSubject*>(callback_arg);
+    // This current thread owns 'StateChangedSubject', no other threads must access it.
+    // Explicit synchronization is not necessary.
+
+    if (!state_subject->OnStateChanged(handle, state)) {
+      // Clean up the state tracker when we reach a terminal state.
+      // This means that no future callbacks will occur anymore.
+      delete state_subject;
+    }
+  }
+
+ private:
+  void EmitError(const std::string& msg) {
+    // Sidenote: Exact error class does not matter, rxcpp only lets us access the error
+    // as a string (rxcpp::util::what).
+    //
+    // Either way, the recovery strategy is identical (log then try and restart).
+    dest.on_error(rxcpp::util::make_error_ptr(StateChangedError{msg}));
+  }
+
+  void EmitNext(State state) {
+    if (WOULD_LOG(VERBOSE) && !dest.is_subscribed()) {
+      // This is purely for logging: #on_next already filters out items after unsubscription.
+      LOG(VERBOSE) << "StateChangedSubject#EmitNext(" << state << ") - drop due to unsubscribe";
+    }
+
+    auto handle_ptr = handle_;
+    DCHECK(handle_ptr != nullptr);
+
+    // Non-null guarantee for the items emitted into this stream.
+    PerfettoStateChange state_change{state, handle_ptr};
+    dest.on_next(std::move(state_change));
+  }
+
+  // TODO: inherit from rx subject and handle #unsubscribe explicitly, instead
+  // of just being subject-like?
+};
+
+// Note: The states will be emitted on a separate thread, so e.g. #as_blocking()
+// needs to be used to avoid dropping everything on the floor.
+//
+// Important: The #on_error case must be handled explicitly by the observable,
+// because the default behavior is to 'throw' which will cause an std::terminate with -fno-except.
+static auto /*[observable<State>, shared_ptr<PerfettoConsumerHandle>]*/
+    CreatePerfettoStateStream(::perfetto::protos::TraceConfig perfetto_config,
+                              std::shared_ptr<PerfettoConsumer> perfetto_consumer) {
+  auto obs = rxcpp::observable<>::create<PerfettoStateChange>(
+    [perfetto_config = std::move(perfetto_config), perfetto_consumer = std::move(perfetto_consumer)]
+        (rxcpp::subscriber<PerfettoStateChange> subscriber) {
+      std::unique_ptr<StateChangedSubject> state_subject{
+          new StateChangedSubject{perfetto_config, subscriber, perfetto_consumer}};
+
+      // Perfetto API requires a pointer to a serialized protobuf, it doesn't accept
+      // the code-generated object.
+      std::string perfetto_config_str = perfetto_config.SerializeAsString();
+
+      ::perfetto::consumer::Handle handle =
+          perfetto_consumer->Create(perfetto_config_str.data(),
+                                    perfetto_config_str.size(),
+                                    // executes on the same background thread repeatedly.
+                                    &StateChangedSubject::CallbackOnStateChanged,
+                                    // inter-thread-move
+                                    reinterpret_cast<void*>(state_subject.get()));
+      // perfetto::consumer::Create synchronizes-with OnStateChanged callback, this means
+      // we don't need to explicitly synchronize state_subject here so long as we don't access
+      // it on this thread again.
+      LOG(DEBUG) << "Create Perfetto handle " << handle;
+
+      if (handle == ::perfetto::consumer::kInvalidHandle) {
+        LOG(ERROR) << "Failed to create Perfetto handle";
+        // No callbacks will occur, so our thread still owns the state subject.
+        state_subject->OnCreateFailed();
+        return;
+      }
+
+      std::shared_ptr<PerfettoConsumerHandle> safe_handle{
+          new PerfettoConsumerHandle{perfetto_consumer, handle}};
+
+      // Share ownership of the Handle with the StateSubject.
+      // This way we defer calling 'Destroy' until the callback reaches a terminal state
+      // *and* all users of the stream are done with the handle.
+      state_subject->BindHandle(safe_handle);
+
+      // state_subject ownership is taken over by OnStateChanged.
+      // It will also be touched in a separate thread, so we must never access it here again.
+      state_subject.release();
+
+      // 'subscriber#add' is actually a call to register an on_unsubscribe listener.
+      subscriber.add([safe_handle]() {
+        LOG(VERBOSE) << "PerfettoStateChange#unsubscribe";
+
+        // Release our ref-count to the handle.
+        // safe_handle.reset();  // This happens implicitly.
+
+        // TODO: I think this won't handle the case where we need to shut down early.
+        // Need to use the explicit kShutdown for that?
+      });
+
+      // TODO: this would be an excellent place to shuffle the perfetto config protobuf
+      // into a global debug state for dumpsys.
+    });
+
+  return obs;
+}
+
+template <typename T>
+bool BinaryWireProtobuf<T>::WriteFullyToFile(const std::string& path,
+                                             bool follow_symlinks) const {
+  // TODO: it would be great if android::base had a string_view overload to avoid copying
+  // data into an std::string.
+
+  // u  g  o
+  // rw-rw----
+  //
+  // Protobufs can be read/written but not executed.
+  static constexpr const mode_t kMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
+
+  int flags =
+      O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_BINARY | (follow_symlinks ? 0 : O_NOFOLLOW);
+  android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), flags, kMode)));
+
+  if (fd == -1) {
+    PLOG(ERROR) << "BinaryWireProtobuf::WriteFullyToFile open failed";
+    return false;
+  }
+
+  if (!::android::base::WriteFully(fd, data_.data(), size())) {
+    PLOG(ERROR) << "BinaryWireProtobuf::WriteFullyToFile write failed";
+    return CleanUpAfterFailedWrite(path);
+  }
+
+  return true;
+}
+
+template <typename T>
+bool BinaryWireProtobuf<T>::CleanUpAfterFailedWrite(const std::string& path) {
+  // Something went wrong. Let's not leave a corrupt file lying around.
+  int saved_errno = errno;
+  unlink(path.c_str());
+  errno = saved_errno;
+  return false;
+}
+
+template <typename T>
+bool BinaryWireProtobuf<T>::WriteStringToFd(int fd) const {
+  const char* p = reinterpret_cast<const char*>(data_.data());
+  size_t left = size();
+  while (left > 0) {
+    ssize_t n = TEMP_FAILURE_RETRY(write(fd, p, left));
+    if (n == -1) {
+      return false;
+    }
+    p += n;
+    left -= n;
+  }
+  return true;
+}
+
+// explicit template instantiation.
+template struct BinaryWireProtobuf<::google::protobuf::MessageLite>;
+// TODO: refactor this not to need the template instantiation.
+
+#if defined(__ANDROID__)
+// Copy of the 2.6.18 kernel header (linux/ioprio.h)
+
+#define IOPRIO_WHO_PROCESS (1)
+#define IOPRIO_CLASS_IDLE (3)
+
+#define IOPRIO_BITS		(16)
+#define IOPRIO_CLASS_SHIFT	(13)
+#define IOPRIO_PRIO_MASK	((1UL << IOPRIO_CLASS_SHIFT) - 1)
+
+#define IOPRIO_PRIO_CLASS(mask)	((mask) >> IOPRIO_CLASS_SHIFT)
+#define IOPRIO_PRIO_DATA(mask)	((mask) & IOPRIO_PRIO_MASK)
+#define IOPRIO_PRIO_VALUE(class, data)	(((class) << IOPRIO_CLASS_SHIFT) | data)
+#endif
+
+static int ioprio_get(int which, int who) {
+  return syscall(SYS_ioprio_get, which, who);
+}
+
+static int ioprio_set(int which, int who, int ioprio) {
+  return syscall(SYS_ioprio_set, which, who, ioprio);
+}
+
+// An rx Coordination, which will cause a new thread to spawn for each new Worker.
+//
+// Idle-class priority is set for the CPU and IO priorities on the new thread.
+rxcpp::observe_on_one_worker ObserveOnNewIoThread() {
+  // IO thread factory for idle-priority threads.
+  // Both the CPU scheduler and the IO scheduler are set to idle.
+  //
+  // Use this when needing to schedule disk access from a normal-priority thread onto a
+  // very low priority thread, but not so low that we need to use a BackgroundJobScheduler.
+  struct io_thread_factory {
+    std::thread operator()(std::function<void()> start) const {
+      return std::thread{
+        [start=std::move(start)]() {
+          // Set IO priority to idle.
+          do {
+            int value = ioprio_get(IOPRIO_WHO_PROCESS, /*pid*/0);
+            if (value == -1) {
+              PLOG(ERROR) << "io_thread_factory failed ioprio_get";
+              break;  // Can't set the ioprio, we don't know what data to use.
+            }
+
+            int data = IOPRIO_PRIO_DATA(value); // priority level
+            // This appears to be '4' in practice. We may want to raise to
+            // be the highest-priority within the idle class.
+
+            // idle scheduling class. only access disk when nobody else needs disk.
+            int res = ioprio_set(IOPRIO_WHO_PROCESS,
+                                 /*pid*/0,
+                                 IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, data));
+            if (res < 0) {
+              PLOG(ERROR) << "io_thread_factory failed ioprio_set";
+              break;
+            }
+
+            // Changing the IO priority only has any effect with cfq scheduler:
+            // $> cat /sys/block/sda/queue/scheduler
+            LOG(VERBOSE) << "ioprio_set(WHO_PROCESS, class=IDLE, data=" << data << ")";
+          } while (false);
+
+          // Set CPU priority to idle.
+          do {
+            struct sched_param param{};
+            param.sched_priority = 0;  // Required to be statically 0 when used with SCHED_IDLE.
+
+            if (sched_setscheduler(/*pid*/0,  // current thread,
+                                   SCHED_IDLE,
+                                   /*in*/&param) != 0) {
+              PLOG(ERROR) << "io_thread_factory failed sched_setscheduler";
+              break;
+            }
+
+            LOG(VERBOSE) << "sched_setscheduler(self, IDLE)";
+          } while (false);
+
+          // XX: if changing the scheduling is too aggressive (i.e. it causes starvation),
+          // we may want to stick with the default class and change the nice (priority) levels
+          // to the minimum.
+
+          // TODO: future work, maybe use cgroups configuration file instead?
+
+          // Call the rxcpp-supplied code.
+          start();
+        }
+      };
+    }
+  };
+
+  static rxcpp::schedulers::scheduler thread_scheduler =
+      rxcpp::schedulers::make_new_thread(io_thread_factory{});
+
+  static rxcpp::observe_on_one_worker observe_on_io_thread{thread_scheduler};
+
+  return observe_on_io_thread;
+}
+
+static auto/*observable<PerfettoTraceProto>*/
+    CreatePerfettoStream(rxcpp::observable<PerfettoStreamCommand> input,
+                         std::shared_ptr<PerfettoConsumer> perfetto_consumer,
+                         const ::perfetto::protos::TraceConfig& trace_config) {
+        // XX: should I also take a scheduler for input here???
+
+  auto /*observable<PerfettoStateChange>*/ perfetto_states =
+    CreatePerfettoStateStream(trace_config, perfetto_consumer);
+
+  using State = ::perfetto::consumer::State;
+
+  auto/*coordinator*/ serialize_coordinator = rxcpp::observe_on_new_thread();
+  // Rx note:
+  // The optimal thing to do would be to have a lock/unlock for an entire subset of a chain.
+  // This would avoid creating new threads, and could also be used to intentionally block
+  // the regular C-callback perfetto thread.
+  //
+  // It seems possible to create a coordinator to lock a single operator in a chain, but this
+  // appears to be unsound. In particular, it doesn't even make life any simpler below because
+  // it would only apply the synchronization to 'zip' but not 'flat_map' which is unsound.
+  //
+  // There is also the built-in 'serialize_new_thread' which seems to create a new thread but
+  // then never actually uses it, that seems unfortunate and wasteful.
+  //
+  // Instead, do the simple thing which is create a new thread and always queue on there.
+  // Execution an action on that worker is itself unsynchronized, but this doesn't matter since
+  // the worker is only backed by 1 thread (no 2 schedulables can be executed concurrently
+  // on the 'observe_new_thread' worker).
+  return input
+    .tap([](PerfettoStreamCommand command) {
+           LOG(VERBOSE) << "CreatePerfettoStreamCommand#tap(command=" << command << ")";
+         })
+    // Input A, thread tA. Input B, thread tB. Continue execution with (A,B) on thread tC.
+    .zip(serialize_coordinator,  // rest of chain is also executed on the same thread.
+         perfetto_states)
+    // Note: zip terminates when either of the streams complete.
+    .flat_map(
+         [](std::tuple<PerfettoStreamCommand, PerfettoStateChange> p) {
+           auto& [command, state_change] = p;
+           LOG(VERBOSE) << "CreatePerfettoStream#combine("
+                        << command << "," << state_change << ")";
+           if (command == PerfettoStreamCommand::kShutdown) {
+             // Perfetto: Always safe to call ::perfetto::consumer::Destroy
+             // at any time.
+             //
+             // XX: How do we clean up the StateChangedSubject without racing
+             // against the callback? It strikes me that we may need a 'kDestroyed'
+             // state that perfetto can transition to from kConfigured.
+             LOG(VERBOSE) << "Call Perfetto_Consumer->Destroy";
+             state_change.GetConsumer()->Destroy(state_change.GetHandle());
+
+             // XX: Do we even have any guarantees about not getting more callbacks?
+             // We could just say 'there can still be spurious output after Shutdown'
+             // and just ignore it (e.g. Shutdown and immediately unsubscribe).
+           } else if (command == PerfettoStreamCommand::kStartTracing
+                          && state_change.state == State::kConfigured) {
+             LOG(VERBOSE) << "Call Perfetto_Consumer->StartTracing";
+             state_change.GetConsumer()->StartTracing(state_change.GetHandle());
+           } else if (command == PerfettoStreamCommand::kStopTracing &&
+                          state_change.state == State::kTraceEnded) {
+             // TODO: if perfetto actually had a 'StopTracing' we could call that here.
+             // right now we just pretend it exists, but rely on the config timer instead.
+             ::perfetto::consumer::TraceBuffer trace_buffer =
+                 state_change.GetConsumer()->ReadTrace(state_change.GetHandle());
+
+             LOG(VERBOSE) << "Perfetto Trace ended"
+                          << ", addr=" << reinterpret_cast<void*>(trace_buffer.begin)
+                          << ",size= " << trace_buffer.size;
+
+             PerfettoTraceProto wire_proto{trace_buffer.begin, trace_buffer.size};
+             return rxcpp::observable<>::just(std::move(wire_proto)).as_dynamic();
+           }
+           return rxcpp::observable<>::empty<PerfettoTraceProto>().as_dynamic();
+         }
+    );
+}
+
+std::ostream& operator<<(std::ostream& os, PerfettoStreamCommand c) {
+  switch (c) {
+    case PerfettoStreamCommand::kStartTracing:
+      os << "kStartTracing";
+      break;
+    case PerfettoStreamCommand::kStopTracing:
+      os << "kStopTracing";
+      break;
+    case PerfettoStreamCommand::kShutdown:
+      os << "kShutdown";
+      break;
+    default:
+      os << "(unknown)";
+      break;
+  }
+  return os;
+}
+
+RxProducerFactory::RxProducerFactory(PerfettoDependencies::Injector& injector)
+  : injector_(injector) {
+}
+
+// TODO: (fruit) maybe this could be streamlined further by avoiding this boilerplate?
+rxcpp::observable<PerfettoTraceProto> RxProducerFactory::CreateTraceStream(
+    rxcpp::observable<PerfettoStreamCommand> commands) {
+  std::shared_ptr<PerfettoConsumer> perfetto_consumer =
+      injector_.get<std::shared_ptr<PerfettoConsumer>>();
+  const ::perfetto::protos::TraceConfig& trace_config =
+      injector_.get<::perfetto::protos::TraceConfig>();
+
+  DCHECK(perfetto_consumer != nullptr);
+  DCHECK(reinterpret_cast<volatile const void*>(&trace_config) != nullptr);
+
+  return CreatePerfettoStream(commands,
+                              perfetto_consumer,
+                              trace_config);
+}
+
+// For testing/debugging only.
+//
+// Saves protobuf results in file name specified by 'arg_output_proto'.
+void CollectPerfettoTraceBufferImmediately(
+    RxProducerFactory& producer_factory,
+    const std::string& arg_output_proto) {
+  LOG(VERBOSE) << "CollectPerfettoTraceBufferImmediately";
+
+  std::shared_ptr<PerfettoConsumer> perfetto_consumer =
+      producer_factory.injector_.get<std::shared_ptr<PerfettoConsumer>>();
+  const ::perfetto::protos::TraceConfig& trace_config =
+      producer_factory.injector_.get<const ::perfetto::protos::TraceConfig&>();
+
+  auto /*observable<PerfettoStateChange>*/ perfetto_states =
+    CreatePerfettoStateStream(trace_config, perfetto_consumer);
+
+  perfetto_states
+    .as_blocking()  // Wait for observable to terminate with on_completed or on_error.
+    .subscribe(/*on_next*/[&](auto state_change) {
+       LOG(VERBOSE) << "Perfetto post-processed State change: " << state_change;
+
+       using State = ::perfetto::consumer::State;
+       switch (state_change.state) {
+         case State::kConnecting:
+           LOG(VERBOSE) << "Perfetto Tracing is Connecting";
+           // Transitional state. No-op.
+           break;
+         case State::kConfigured:
+           state_change.GetConsumer()->StartTracing(state_change.GetHandle());
+           break;
+         case State::kTracing:
+           LOG(VERBOSE) << "Perfetto Tracing started";
+           // Transitional state. No-op.
+           break;
+         case State::kTraceEnded: {
+           ::perfetto::consumer::TraceBuffer trace_buffer =
+             state_change.GetConsumer()->ReadTrace(state_change.GetHandle());
+
+           LOG(VERBOSE) << "Perfetto Trace ended"
+                        << ", addr=" << reinterpret_cast<void*>(trace_buffer.begin)
+                        << ",size= " << trace_buffer.size;
+
+           if (!arg_output_proto.empty()) {
+             std::string trace_buffer_str;
+             trace_buffer_str.resize(trace_buffer.size);
+             std::copy(trace_buffer.begin,
+                       trace_buffer.begin + trace_buffer.size,
+                       trace_buffer_str.data());
+             if (!android::base::WriteStringToFile(trace_buffer_str, arg_output_proto)) {
+               LOG(ERROR) << "Failed to save TraceBuffer to " << arg_output_proto;
+             } else {
+               LOG(INFO) << "TraceBuffer saved to file: " << arg_output_proto;
+               LOG(INFO);
+               LOG(INFO) << "To print this in a human readable form, execute these commands:";
+               LOG(INFO) << "$> adb pull '" << arg_output_proto << "'";
+               LOG(INFO) << "$> trace_to_text systrace <filename.pb>";
+             }
+           }
+
+           // TODO: something more useful with this TraceBuffer, such as saving it to a file
+           // and printing the output.
+           break;
+         }
+         default:
+           // No other states are possible, because they go to #on_error or cause a dcheck.
+           DCHECK(false) << "Invalid state: " << state_change;
+       }
+
+       //INTENTIONAL_COMPILER_ERROR_HERE // lets make sure this code actually does a trace.
+
+     }, /*on_error*/[](rxcpp::util::error_ptr err) {
+       LOG(ERROR) << "Perfetto post-processed state change failed: " << rxcpp::util::what(err);
+     }, /*on_completed*/[]() {
+       LOG(VERBOSE) << "Perfetto post-processed State #on_completed";
+     });
+}
+
+
+}  // namespace iorap::perfetto
diff --git a/src/perfetto/rx_producer.h b/src/perfetto/rx_producer.h
new file mode 100644
index 0000000..f4c40b4
--- /dev/null
+++ b/src/perfetto/rx_producer.h
@@ -0,0 +1,165 @@
+// 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.
+
+#ifndef IORAP_SRC_PERFETTO_RX_PRODUCER_H_
+#define IORAP_SRC_PERFETTO_RX_PRODUCER_H_
+
+#include "perfetto/perfetto_consumer.h"       // libiorap
+
+#include <perfetto/config/trace_config.pb.h>  // libperfetto
+#include <rxcpp/rx.hpp>
+
+#include <iosfwd>
+#include <functional>
+#include <optional>
+#include <vector>
+
+namespace iorap::perfetto {
+
+struct PerfettoDependencies {
+  using Component =
+      fruit::Component<PerfettoConsumer, ::perfetto::protos::TraceConfig>;
+  using Injector =
+      fruit::Injector<PerfettoConsumer, ::perfetto::protos::TraceConfig>;
+  using NormalizedComponent =
+      fruit::NormalizedComponent<PerfettoConsumer, ::perfetto::protos::TraceConfig>;
+
+  // Create a 'live' component that will talk to perfetto via traced.
+  static Component CreateComponent(/*TODO: config params*/);
+
+  // Create perfetto.protos.TraceConfig , serialized as a (machine-readable) string.
+  //
+  // The following ftrace events are enabled:
+  // * mm_filemap_add_to_page_cache
+  // * mm_filemap_delete_from_page_cache
+  //
+  // If deferred starting is also enabled, no tracing will begin until
+  // ::perfetto::consumer::StartTracing is invoked.
+  static ::perfetto::protos::TraceConfig CreateConfig(uint32_t duration_ms,
+                                                      bool deferred_start = true,
+                                                      uint32_t buffer_size = 4096);
+};
+
+// This acts as a lightweight type marker so that we know what data has actually
+// encoded under the hood.
+template <typename T>
+struct BinaryWireProtobuf {
+  std::vector<std::byte>& data() {
+    return data_;
+  }
+
+  const std::vector<std::byte>& data() const {
+    return data_;
+  }
+
+  size_t size() const {
+    return data_.size();
+  }
+
+  explicit BinaryWireProtobuf(char* data, size_t size)
+    : BinaryWireProtobuf(reinterpret_cast<std::byte*>(data), size) {
+  }
+
+  explicit BinaryWireProtobuf(std::byte* data, size_t size) {
+    data_.resize(size);
+    std::copy(data,
+              data + size,
+              data_.data());
+  }
+
+  // Important: Deserialization could fail, for example data is truncated or
+  // some minor disc corruption occurred.
+  template <typename U>
+  std::optional<U> MaybeUnserialize() {
+    U unencoded;
+
+    if (!unencoded.ParseFromArray(data_.data(), data_.size())) {
+      return std::nullopt;
+    }
+
+    return {std::move(unencoded)};
+  }
+
+  bool WriteFullyToFile(const std::string& path,
+                        bool follow_symlinks = false) const;
+
+ private:
+  static bool CleanUpAfterFailedWrite(const std::string& path);
+  bool WriteStringToFd(int fd) const;
+
+  std::vector<std::byte> data_;
+};
+
+//using PerfettoTraceProto = BinaryWireProtobuf<::perfetto::protos::Trace>;
+using PerfettoTraceProto = BinaryWireProtobuf<::google::protobuf::MessageLite>;
+
+enum class PerfettoStreamCommand {
+  kStartTracing, // -> () | on_error
+  kStopTracing,  // -> on_next(PerfettoTraceProto) | on_error
+  kShutdown,     // -> on_completed | on_error
+  // XX: should shutdown be converted to use the rx suscriber#unsubscribe instead?
+};
+
+std::ostream& operator<<(std::ostream& os, PerfettoStreamCommand c);
+
+struct RxProducerFactory {
+  // Passing anything by value leads to a lot of pain and headache.
+  // Pass in the injector by reference because nothing else seems to work.
+  explicit RxProducerFactory(PerfettoDependencies::Injector& injector);
+
+  // Create a one-shot perfetto observable that will begin
+  // asynchronously producing a PerfettoTraceProto after the 'kStartTracing'
+  // command is observed.
+  //
+  // libperfetto is immediately primed (i.e. connected in a deferred state)
+  // upon calling this function, to reduce the latency of 'kStartTracing'.
+  //
+  // To finish the trace, push 'kStopTracing'. To cancel or tear down at any
+  // time, push 'kShutdown'.
+  //
+  // The TraceProto may come out at any time after 'kStartTracing',
+  // this is controlled by duration_ms in the TraceConfig.
+  //
+  // TODO: libperfetto should actually stop tracing when we ask it to,
+  // instead of using a hardcoded time.
+  //
+  // The observable may go into #on_error at any time, if the underlying
+  // libperfetto states transition to a failing state.
+  // This usually means the OS is not configured correctly.
+  rxcpp::observable<PerfettoTraceProto> CreateTraceStream(
+      rxcpp::observable<PerfettoStreamCommand> commands);
+
+  // TODO: is this refactor-able into a subscriber factory that takes
+  // the commands-observable as a parameter?
+
+  // TODO: infinite perfetto stream.
+
+ private:
+  // XX: why doesn't this just let me pass in a regular Component?
+  PerfettoDependencies::Injector& injector_;
+
+  friend void CollectPerfettoTraceBufferImmediately(
+      RxProducerFactory& producer_factory,
+      const std::string& arg_output_proto);
+};
+
+// An rx Coordination, which will cause a new thread to spawn for each new Worker.
+//
+// Idle-class priority is set for the CPU and IO priorities on the new thread.
+//
+// TODO: move to separate file
+rxcpp::observe_on_one_worker ObserveOnNewIoThread();
+
+}  // namespace iorap::perfetto
+#endif  // IORAP_SRC_PERFETTO_RX_PRODUCER_H_
diff --git a/tests/src/binder/app_launch_event_test.cc b/tests/src/binder/app_launch_event_test.cc
new file mode 100644
index 0000000..1d19bb1
--- /dev/null
+++ b/tests/src/binder/app_launch_event_test.cc
@@ -0,0 +1,202 @@
+/*
+ * 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.
+ */
+
+#include <binder/app_launch_event.h>
+
+#include <gtest/gtest.h>
+
+// TODO: move to app_launch_event.h
+#include <google/protobuf/util/message_differencer.h>
+
+using namespace iorap::binder;  // NOLINT
+
+using android::Parcel;
+
+using Type = AppLaunchEvent::Type;
+using Temperature = AppLaunchEvent::Temperature;
+
+namespace iorap::binder {
+
+inline bool ProtosEqual(const ::google::protobuf::Message& lhs,
+                        const ::google::protobuf::Message& rhs) {
+  return ::google::protobuf::util::MessageDifferencer::Equals(lhs, rhs);
+}
+
+inline bool ProtosEqual(const ::google::protobuf::MessageLite& lhs,
+                        const ::google::protobuf::MessageLite& rhs) {
+  // MessageLite does not support 'MessageDifferencer' which requires protobuf-full
+  // because it uses reflection.
+  //
+  // Serialize as a string and compare. This may lead to false inequality when protobufs
+  // are actually the same but their encodings are slightly different.
+  return lhs.GetTypeName() == rhs.GetTypeName()
+      && lhs.SerializeAsString() == rhs.SerializeAsString();
+}
+
+template <typename T>
+inline bool ProtoPointersEqual(const T& lhs_ptr, const T& rhs_ptr) {
+  if (lhs_ptr == nullptr && rhs_ptr == nullptr) {
+    return true;
+  }
+  else if (lhs_ptr != nullptr && rhs_ptr != nullptr) {
+    return ProtosEqual(*lhs_ptr, *rhs_ptr);
+  }
+  return false;
+}
+
+// Field-by-field equality.
+// Protos are compared according by checking that their serialized encodings are the same.
+inline bool operator==(const AppLaunchEvent& lhs, const AppLaunchEvent& rhs) {
+# define EQ_OR_RETURN(l, r, val) if (!(l.val == r.val)) { return false; }
+# define PROTO_EQ_OR_RETURN(l, r, val) if (!ProtoPointersEqual(l.val, r.val)) { return false; }
+
+  EQ_OR_RETURN(lhs, rhs, type);
+  EQ_OR_RETURN(lhs, rhs, sequence_id);
+  PROTO_EQ_OR_RETURN(lhs, rhs, intent_proto);
+  EQ_OR_RETURN(lhs, rhs, temperature);
+  PROTO_EQ_OR_RETURN(lhs, rhs, activity_record_proto);
+
+# undef EQ_OR_RETURN
+# undef PROTO_EQ_OR_RETURN
+
+  return true;
+}
+
+inline bool operator!=(const AppLaunchEvent& lhs, const AppLaunchEvent& rhs) {
+  return !(lhs == rhs);
+}
+
+static AppLaunchEvent MakeIntentStarted(int64_t sequence_id,
+                                            // non-null
+                                            std::unique_ptr<IntentProto> intent_proto) {
+  DCHECK(intent_proto != nullptr);
+
+  AppLaunchEvent e{Type::kIntentStarted, sequence_id, std::move(intent_proto)};
+  return e;
+}
+
+static AppLaunchEvent MakeIntentFailed(int64_t sequence_id) {
+  AppLaunchEvent e{Type::kIntentFailed, sequence_id};
+  return e;
+}
+
+static AppLaunchEvent
+MakeActivityLaunched(int64_t sequence_id,
+                     Temperature temperature,
+                     // non-null
+                     std::unique_ptr<ActivityRecordProto> activity_record_proto) {
+  DCHECK(activity_record_proto != nullptr);
+
+  AppLaunchEvent e{Type::kActivityLaunched,
+                   sequence_id,
+                   /*intent_proto*/nullptr,
+                   temperature,
+                   std::move(activity_record_proto)};
+  return e;
+}
+
+static AppLaunchEvent
+MakeActivityLaunchCancelled(int64_t sequence_id,
+                            // nullable
+                            std::unique_ptr<ActivityRecordProto> activity_record_proto = nullptr) {
+  AppLaunchEvent e{Type::kActivityLaunchCancelled,
+                   sequence_id,
+                   /*intent_proto*/nullptr,
+                   Temperature::kUninitialized,
+                   std::move(activity_record_proto)};
+  return e;
+}
+
+static AppLaunchEvent
+MakeActivityLaunchFinished(int64_t sequence_id,
+                           // non-null
+                           std::unique_ptr<ActivityRecordProto> activity_record_proto) {
+  DCHECK(activity_record_proto != nullptr);
+  AppLaunchEvent e{Type::kActivityLaunchFinished,
+                   sequence_id,
+                   /*intent_proto*/nullptr,
+                   Temperature::kUninitialized,
+                   std::move(activity_record_proto)};
+  return e;
+}
+
+}  // namespace iorap::binder
+
+auto MakeDummyIntent(std::string action = "package_name/.ClassName") {
+  std::unique_ptr<IntentProto> dummy_intent{new IntentProto{}};
+  dummy_intent->set_action(action);
+  return dummy_intent;
+}
+
+auto MakeDummyActivityRecord(std::string title = "package_name/.ClassName") {
+  std::unique_ptr<ActivityRecordProto> dummy{new ActivityRecordProto{}};
+
+  dummy->mutable_identifier()->set_title(title);
+
+  return dummy;
+}
+
+TEST(AppLaunchEventTest, Equals) {
+  EXPECT_EQ(MakeIntentStarted(456, MakeDummyIntent()), MakeIntentStarted(456, MakeDummyIntent()));
+  EXPECT_NE(MakeIntentStarted(45, MakeDummyIntent()), MakeIntentStarted(45, MakeDummyIntent("a")));
+
+  EXPECT_EQ(MakeIntentFailed(123), MakeIntentFailed(123));
+  EXPECT_NE(MakeIntentFailed(0), MakeIntentFailed(123));
+
+  EXPECT_EQ((MakeActivityLaunched(3, Temperature::kHot, MakeDummyActivityRecord())),
+            (MakeActivityLaunched(3, Temperature::kHot, MakeDummyActivityRecord())));
+  EXPECT_NE((MakeActivityLaunched(3, Temperature::kHot, MakeDummyActivityRecord())),
+            (MakeActivityLaunched(3, Temperature::kCold, MakeDummyActivityRecord())));
+  EXPECT_NE((MakeActivityLaunched(3, Temperature::kHot, MakeDummyActivityRecord())),
+            (MakeActivityLaunched(3, Temperature::kHot, MakeDummyActivityRecord("other title"))));
+
+  EXPECT_EQ((MakeActivityLaunchCancelled(4)),
+            (MakeActivityLaunchCancelled(4)));
+  EXPECT_EQ((MakeActivityLaunchCancelled(4, MakeDummyActivityRecord())),
+            (MakeActivityLaunchCancelled(4, MakeDummyActivityRecord())));
+  EXPECT_NE((MakeActivityLaunchCancelled(4, MakeDummyActivityRecord())),
+            (MakeActivityLaunchCancelled(4, MakeDummyActivityRecord("other"))));
+  EXPECT_NE((MakeActivityLaunchCancelled(4, MakeDummyActivityRecord())),
+            (MakeActivityLaunchCancelled(4)));
+
+  EXPECT_EQ((MakeActivityLaunchFinished(5, MakeDummyActivityRecord())),
+            (MakeActivityLaunchFinished(5, MakeDummyActivityRecord())));
+  EXPECT_NE((MakeActivityLaunchFinished(5, MakeDummyActivityRecord())),
+            (MakeActivityLaunchFinished(5, MakeDummyActivityRecord("other title"))));
+}
+
+template <typename T>
+T ValueParcelRoundTrip(const T& value) {
+  ::android::Parcel p;
+  CHECK_EQ(value.writeToParcel(&p), ::android::NO_ERROR);
+
+  T new_value;
+  p.setDataPosition(0);
+  CHECK_EQ(new_value.readFromParcel(&p), ::android::NO_ERROR);
+
+  return new_value;
+}
+
+#define EXPECT_PARCELING_ROUND_TRIP(a) EXPECT_EQ((a), ValueParcelRoundTrip((a)))
+
+TEST(AppLaunchEventTest, ParcelingRoundTrip) {
+  EXPECT_PARCELING_ROUND_TRIP(MakeIntentStarted(456, MakeDummyIntent()));
+  EXPECT_PARCELING_ROUND_TRIP(MakeIntentFailed(123));
+  EXPECT_PARCELING_ROUND_TRIP(MakeActivityLaunched(3, Temperature::kHot, MakeDummyActivityRecord()));
+  EXPECT_PARCELING_ROUND_TRIP(MakeActivityLaunchCancelled(4));
+  EXPECT_PARCELING_ROUND_TRIP(MakeActivityLaunchCancelled(4, MakeDummyActivityRecord()));
+  EXPECT_PARCELING_ROUND_TRIP(MakeActivityLaunchFinished(5, MakeDummyActivityRecord()));
+}
diff --git a/tests/src/inode2filename/search_directories_test.cc b/tests/src/inode2filename/search_directories_test.cc
new file mode 100644
index 0000000..d9903d6
--- /dev/null
+++ b/tests/src/inode2filename/search_directories_test.cc
@@ -0,0 +1,2819 @@
+/*
+ * 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.
+ */
+
+#include "inode2filename/search_directories.h"
+#include "inode2filename/system_call.h"
+
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+
+#include <fruit/fruit.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <optional>
+
+#include <sys/sysmacros.h>
+
+
+// Set this to 1 when debugging by hand to get more output.
+// Otherwise the spam might be too much when most tests are failing.
+#define LOG_WITH_VERBOSE 0
+// Set this to 1 when debugging by hand to have the logging output go to stderr.
+// TODO: I think the automated test bots have problems capturing non-logcat output.
+#define LOG_TO_STDERR 1
+
+// TODO: Might be nice to have these controlled by command line.
+
+using namespace std::literals::string_literals;  // NOLINT
+using namespace std::literals::string_view_literals;  // NOLINT
+using namespace iorap::inode2filename;  // NOLINT
+using namespace testing;  // NOLINT
+
+static void ConfigureLogging() {
+  if (LOG_TO_STDERR) {
+    ::android::base::SetLogger(::android::base::StderrLogger);
+  }
+  if (LOG_WITH_VERBOSE) {
+    ::android::base::SetMinimumLogSeverity(::android::base::VERBOSE);
+  } else {
+    ::android::base::SetMinimumLogSeverity(::android::base::DEBUG);
+  }
+}
+
+// Iterate substrings in 'what' that are separated by 'separator'.
+// Should be similar to the python 'str.split' behavior.
+//
+// Empty separators will have 0 iterations.
+//
+// NOTE: this could end up returning empty strings, e.g. '/'.split('/') -> ('', '')
+// Think of it more like splitting on "$/^" except the $ and ^ become empty strings in the end.
+//
+// Zero-copy guarantee (and no dynamic allocation).
+struct StringSplit {
+  struct SplitIterable;
+
+  // Return a 0-length substring whose address range is one past the end of 'what'.
+  // Logically equivalent to a "", but its real address will be within 'what'.
+  //
+  // Repeatedly applying this function on itself will return the same value.
+  //
+  // Do not use operator[] on the returned substring, as that would cause undefined
+  // behavior.
+  //
+  // To safely access the pointer, use #data(). The pointer must not be dereferenced,
+  // which would cause undefined behavior.
+  static constexpr std::string_view EmptySubstringAtEnd(std::string_view what) {
+    return what.substr(/*pos*/what.size(), /*count*/0);
+  }
+
+  // Create an Iterable that will iterate over substrings in 'what' separated by 'separator'.
+  //
+  // Each such 'value' emitted is guaranteed to be:
+  //  - a substring of 'what'
+  //  - not have any 'separator' substrings
+  //  - the address range of 'value' is within the address range of 'what' (or one-past-the-end)
+  //
+  // For example:
+  //
+  //   for (std::string_view substr : StringSplit("hello/world"sv, "/"sv)) {
+  //     ... // loop 0: substr == "hello"
+  //     ... // loop 1: substr == "world"
+  //   }
+  static constexpr SplitIterable Iterable(std::string_view what,
+                                          std::string_view separator) {
+    return SplitIterable{what, separator};
+  }
+
+  // Implement LegacyForwardIterator concept.
+  struct SplitIterator {
+    using value_type = std::string_view;
+    using reference = value_type&;
+    using pointer = value_type*;
+    using iterator_category = std::forward_iterator_tag;
+    using difference_type = std::ptrdiff_t;  // required by concept, but its meaningless.
+
+    constexpr bool operator==(const SplitIterator& other) const {
+      if (state != other.state) {
+        return false;
+      }
+      switch (state) {
+        case kNormal:
+        case kNearEnd:
+          return current_split.data() == other.current_split.data();
+        case kAtEnd:
+          return true;
+      }
+    }
+
+    constexpr bool operator!=(const SplitIterator& other) const {
+      return !(*this == other);
+    }
+
+    constexpr std::string_view& operator*() {
+        DCHECK(state != kAtEnd) << "Undefined behavior to dereference end() iterators";
+        return current_split;
+    }
+
+    constexpr std::string_view* operator->() {
+        DCHECK(state != kAtEnd) << "Undefined behavior to dereference end() iterators";
+        return &current_split;
+    }
+
+    /*
+    constexpr const std::string_view& operator*() const {
+        return current_split;
+    }
+
+    constexpr const std::string_view* operator->() const {
+        return &current_split;
+    }
+    */
+
+    constexpr SplitIterator& operator++() {
+      UpdateValues();
+      return *this;
+    }
+
+    constexpr SplitIterator operator++(int) {
+      SplitIterator copy{*this};
+      ++(*this);
+      return copy;
+    }
+
+   private:
+    // Avoid defining constructors etc. We get the default constructors and operator= then.
+
+    friend struct SplitIterable;  // Use below Make functions.
+
+    constexpr static SplitIterator MakeBegin(std::string_view whole, std::string_view separator) {
+      SplitIterator it;
+      it.state = kNormal;
+
+      if (separator == "") {
+        it.rest_of_string = StringSplit::EmptySubstringAtEnd(whole);
+        // point to one past-the end (which is legal), also equivalent to ""
+        // the difference being that the address range is guaranteed to be within 'whole'
+        // actually any 0-length subrange would be appropriate here, but just go with the 'end'
+        // because dereferencing it would be obviously bad.
+        it.state = kAtEnd;
+        // Empty separator -> empty # of visits. This seems the most composable.
+        // Note: Need to handle this case especially since find_first_of("") would return the
+        // entire string.
+      } else {
+        it.rest_of_string = whole;
+        it.separator = separator;
+        it.UpdateValues();
+      }
+      return it;
+    }
+
+    constexpr static SplitIterator MakeEnd() {
+      SplitIterator it;
+      it.state = kAtEnd;
+      return it;
+    }
+
+    constexpr void UpdateValues() {
+      switch (state) {
+        case kNormal:
+          break;
+        case kNearEnd:
+          // Address of emitted value is always within subrange of 'whole'.
+          current_split = StringSplit::EmptySubstringAtEnd(rest_of_string);
+          state = kAtEnd;
+          return;
+        case kAtEnd:
+          // Incrementing the 'end()' operator is undefined behavior.
+          DCHECK(false) << "Undefined behavior: Cannot increment end() iterator.";
+          return;
+      }
+
+      DCHECK(state == kNormal);
+
+      size_t pos = rest_of_string.find_first_of(separator);
+      if (std::string_view::npos == pos) {
+        // Always visit at least once for non-empty separators, even if the string is empty.
+
+        current_split = rest_of_string;
+        // Address of emitted value is always within subrange of 'whole'.
+        rest_of_string = rest_of_string.substr(/*pos*/0, /*count*/0);  // = ""
+        state = kNearEnd;
+      } else {
+        // includes the starting position of the needle
+        // e.g. "+x-".find_first_of('x') -> 1
+
+        // current_split = rest_of_string[0..pos)
+        current_split = rest_of_string.substr(/*pos*/0, pos);
+
+        // strip '${left}${separator}' from the left hand side.
+        // continue iterating.
+        rest_of_string = rest_of_string.substr(pos + separator.size());
+      }
+    }
+
+ public:
+
+    void PrintToStream(std::ostream& os) const {
+      os << "SplitIterator{";
+      os << "current_split:\"" << current_split << "\",";
+      os << "rest_of_string:\"" << rest_of_string << "\",";
+      os << "separator:\"" << separator << "\",";
+      os << "state:";
+      switch (state) {
+        case kNormal:
+          os << "kNormal";
+          break;
+        case kNearEnd:
+          os << "kNearEnd";
+          break;
+        case kAtEnd:
+          os << "kAtEnd";
+          break;
+      }
+      os << "}";
+    }
+ private:
+    // Not intended to be used directly.
+    // Public visibility to avoid making extra constructors.
+    std::string_view current_split;
+    std::string_view rest_of_string;
+    std::string_view separator;
+
+    enum State {
+      kNormal,
+      kNearEnd,
+      kAtEnd
+    };
+    State state{kNormal};
+    // This cannot have a field initializer due to a clang bug,
+    // https://bugs.llvm.org/show_bug.cgi?id=36684
+    // So define an explicit constructor below.
+
+    // This needs to go last:
+    // undefined constructor 'SplitIterator' cannot be used in a constant expression
+    // constexpr SplitIterator() : state{kNormal} {}
+   // constexpr SplitIterator() {}
+  };
+
+  friend struct SplitIterable;
+
+  struct SplitIterable {
+    std::string_view whole;
+    std::string_view separator;
+
+    constexpr SplitIterator begin() {
+      return SplitIterator::MakeBegin(whole, separator);
+    }
+
+    constexpr SplitIterator end() {
+      return SplitIterator::MakeEnd();
+    }
+  };
+};
+
+std::ostream& operator<<(std::ostream& os, const StringSplit::SplitIterator& it) {
+  it.PrintToStream(os);
+  return os;
+}
+
+static constexpr  const StringSplit::SplitIterator kBlankSplit;
+
+// Visit substrings in 'what' that are separated by 'separator'.
+// Should be similar to the python 'str.split' behavior.
+//
+// Empty separators will have 0 visits.
+//
+// 'f' is called back for each visit of a substring, this means there's 0 allocations here.
+//
+// NOTE: this could end up returning empty strings, e.g. '/'.split('/') -> ('', '')
+// Think of it more like splitting on "$/^" except the $ and ^ become empty strings in the end.
+//
+// (Dynamic allocation free)
+template <typename Fn>
+static constexpr void VisitSplitStringView(std::string_view what,
+                                           std::string_view separator,
+                                           Fn f) {
+  // Empty separator -> empty # of visits. This seems the most composable.
+  if (separator == "") {
+    // Note: Need to handle this case especially since find_first_of("") would return the
+    // entire string.
+    return;
+  }
+
+  size_t sep_length = separator.size();
+
+  do {
+    size_t pos = what.find_first_of(separator);
+    if (std::string_view::npos == pos) {
+      // Always visit at least once for non-empty separators, even if the string is empty.
+      f(what);
+      break;
+    } else {
+      // includes the starting position of the needle
+      // e.g. "+x-".find_first_of('x') -> 1
+
+      // left = what[0..pos)
+      std::string_view left_split = what.substr(/*pos*/0, pos);
+      f(left_split);
+
+      // strip '${left}${separator}' from the left hand side.
+      // continue iterating.
+      what = what.substr(pos + sep_length);
+    }
+  }
+  while (true);
+}
+
+std::vector<std::string> VisitSplitStringViewVec(std::string_view what,
+                                                 std::string_view separator) {
+  std::vector<std::string> vec;
+  VisitSplitStringView(what, separator, [&vec](auto&& part) {
+                         vec.push_back(std::string{part});
+                       });
+  return vec;
+}
+
+std::vector<std::string> IterableSplitStringViewVec(std::string_view what,
+                                                    std::string_view separator) {
+  auto iterable = StringSplit::Iterable(what, separator);
+  std::vector<std::string> vec{iterable.begin(), iterable.end()};
+
+  return vec;
+}
+
+TEST(SplitStringView, Tests) {
+  EXPECT_THAT(VisitSplitStringViewVec("", ""), IsEmpty());
+  EXPECT_THAT(VisitSplitStringViewVec("abcdef", ""), IsEmpty());
+  EXPECT_THAT(VisitSplitStringViewVec("", "/"), ElementsAre(""s));
+  EXPECT_THAT(VisitSplitStringViewVec("/", "/"), ElementsAre(""s, ""s));
+  EXPECT_THAT(VisitSplitStringViewVec("//", "/"), ElementsAre(""s, ""s, ""s));
+  EXPECT_THAT(VisitSplitStringViewVec("/hello", "/"), ElementsAre(""s, "hello"s));
+  EXPECT_THAT(VisitSplitStringViewVec("/hello/world", "/"), ElementsAre(""s, "hello"s, "world"s));
+  EXPECT_THAT(VisitSplitStringViewVec("bar", "/"), ElementsAre("bar"s));
+  EXPECT_THAT(VisitSplitStringViewVec("bar/baz", "/"), ElementsAre("bar"s, "baz"s));
+
+  EXPECT_THAT(IterableSplitStringViewVec("", ""), IsEmpty());
+  EXPECT_THAT(IterableSplitStringViewVec("abcdef", ""), IsEmpty());
+  EXPECT_THAT(IterableSplitStringViewVec("", "/"), ElementsAre(""s));
+  EXPECT_THAT(IterableSplitStringViewVec("/", "/"), ElementsAre(""s, ""s));
+  EXPECT_THAT(IterableSplitStringViewVec("//", "/"), ElementsAre(""s, ""s, ""s));
+  EXPECT_THAT(IterableSplitStringViewVec("/hello", "/"), ElementsAre(""s, "hello"s));
+  EXPECT_THAT(IterableSplitStringViewVec("/hello/world", "/"), ElementsAre(""s, "hello"s, "world"s));
+  EXPECT_THAT(IterableSplitStringViewVec("bar", "/"), ElementsAre("bar"s));
+  EXPECT_THAT(IterableSplitStringViewVec("bar/baz", "/"), ElementsAre("bar"s, "baz"s));
+
+  EXPECT_THAT(IterableSplitStringViewVec("/hello", "/"), ElementsAre(""sv, "hello"sv));
+  EXPECT_THAT(IterableSplitStringViewVec("/hello///", "/"), ElementsAre(""sv, "hello"sv, ""sv, ""sv, ""sv));
+ 
+}
+
+
+// Allocation-free immutable path representation and manipulation.
+//
+// A PurePath is logically represented by its 'parts', which is a view of each component.
+//
+// Examples:
+//   parts('foo/bar') -> ['foo', 'bar']
+//   parts('/bar') -> ['/', 'bar']
+//   parts('') -> []
+//   parts('.') -> []
+//   parts('baz//') -> ['baz']
+//   parts('hello/././world') -> ['hello', 'world']
+//   parts('../down/../down2') -> ['..', 'down', '..', 'down2']
+//
+// See also #VisitParts which allows an allocation-free traversal of the parts.
+//
+// Memory allocation/ownership guarantees:
+// * Functions marked as 'constexpr' are guaranteed never to allocate (zero-copy).
+// * Functions not marked as 'constexpr' and returning a PurePath will always return an object
+//   with its own internal copy of the underlying data (i.e. the memory is not borrowed).
+struct PurePath {
+  using part_type = std::string_view;
+
+  struct PartIterable;
+
+  // Create an empty PurePath.
+  //
+  // Empty paths are considered to have 0 parts, i.e.
+  //   PurePath{}.VisitParts() -> []
+  constexpr PurePath() : path_(".") {
+  }
+
+  // Create a PurePath from a string view.
+  //
+  // This borrows memory ownership of the string view. If you wish to make a copy,
+  // use the PurePath(std::string) constructor.
+  //
+  // Paths are non-normalized (i.e. redundant up-references, "..", are not stripped),
+  // you may wish to call 'NormalizePath' if this is important.
+  constexpr PurePath(std::string_view path)  : path_(path) {
+    /// : owner_(std::string(path)), path_(owner_.value()) {
+    // TODO: no copy
+  }
+
+  constexpr PurePath(const char* path) : PurePath(std::string_view(path)) {}
+
+  // Creates a PurePath from a string.
+  //
+  // The PurePath owns the memory of the string path.
+  //
+  // Only accepts movable strings, so that the cheaper borrowing (string_view)
+  // constructor is used by default.
+  PurePath(std::string&& path) : owner_(std::move(path)), path_(owner_.value()) {
+  }
+
+  // Return an Iterable, which upon traversing would
+  // return each part as an std::string_view.
+  //
+  // Empty and '.' path components are not visited,
+  // effectively ignoring redundant // and intermediate '.' components.
+  //
+  // To also ignore redundant up-references, see #NormalizePath.
+  //
+  // Example:
+  //   for (std::string_view part : PurePath("hello//world/./").IterateParts()) {
+  //     // loop 0, part == "hello"sv
+  //     // loop 1, part == "world"sv
+  //   }
+  constexpr PartIterable IterateParts() const {
+    return PartIterable::FromPath(*this);
+  }
+
+  // f is a function<void(std::string_view part)>
+  //
+  // Invoke 'f' repeatedly on each logical part of this path.
+  //
+  // Empty and '.' path components are not visited,
+  // effectively ignoring redundant // and intermediate '.' components.
+  //
+  // To also ignore redundant up-references, see #NormalizePath.
+  template <typename Fn>
+  constexpr void VisitParts(Fn f) const {
+    // Note: Near the top to avoid -Wundefined-inline warnings.
+    if (IsAbsolute()) {
+      f(kRoot);  // When we split, we no longer visit the '/' tokens. Handle root explicitly.
+    }
+    VisitSplitStringView(path_,
+                         kRoot,
+                         [&f](auto&& substr) {
+                           // Ignore duplicate /// and also .
+                           //
+                           // e.g.
+                           //   '//foo' -> ['/', 'foo']
+                           //   './foo' -> ['foo']
+                           //
+                           // This is consistent with PurePath.parts implementation.
+                           //
+                           // Note that redundant .. are not removed, e.g.
+                           //   '../foo/..' is not rewritten to ['..']
+                           //
+                           // Use 'NormalizePath' to do this explicitly.
+                           if (!substr.empty() && substr != ".") {
+                             f(substr);
+                           }
+                         });
+  }
+
+
+  // A path is considered equal to another path if all of the parts are identical.
+  /*constexpr*/ bool operator==(const PurePath& other) const {
+    /*if (path_ == other.path_) {
+      return true;
+    } else*/ {
+      auto this_range = IterateParts();
+      auto other_range = other.IterateParts();
+
+      return std::equal(this_range.begin(),
+                        this_range.end(),
+                        other_range.begin(),
+                        other_range.end());
+    }
+  }
+
+  // Returns the name component (if any).
+  //
+  // Logically equivalent to returning the last part unless:
+  //   - the last part is the root '/'
+  //   - there are no parts
+  //
+  // If the above conditions do not hold, return the empty string.
+  constexpr std::string_view Name() const {
+    std::string_view component = StringSplit::EmptySubstringAtEnd(path_);
+
+    size_t count = 0;
+    for (auto&& part : IterateParts()) {
+      if (count++ == 0 && part == kRoot) {
+        continue;  // '/' does not count as a name.
+      } else {
+        DCHECK_NE(part, kRoot);
+      }
+
+      component = part;
+    }
+
+    return component;
+  }
+
+  // Find the parent of this path.
+  //
+  // This is usually the path with the last part stripped off, with some special cases:
+  // - The parent of '/' is always '/' (recursive).
+  // - The parent of '' is always '..'.
+  // - The parent of '..[/..]*' is an additional '/..' appended.
+  //
+  // The parent is always distinct (i.e. not equal to this) except for '/', whose parent
+  // is itself.
+  /*constexpr*/ PurePath Parent() const {
+    size_t parts_count = 0;
+    size_t upreference_count = 0;
+    // TODO: this should be constexpr, but it complains about PurePath not being a literal type.
+
+    for (auto&& part : IterateParts()) {
+      ++parts_count;
+
+      if (part == "..") {
+        ++upreference_count;
+      }
+    }
+
+    if (upreference_count == parts_count) {  // Could also have 0 parts.
+      // "../../../" etc. No other parts are there.
+      // We need to add another '..'
+
+      // Explicitly handle a few iterations to remain constexpr.
+      switch (upreference_count) {
+        case 0:
+          return {".."};
+        case 1:
+          return {"../.."};
+        case 2:
+          return {"../../.."};
+        case 3:
+          return {"../../../.."};
+        default:
+          break;
+      }
+
+      // As a special case, this part of the function is not constexpr.
+      std::string built_parent_string = "..";
+      for (size_t i = 0; i < upreference_count; ++i) {
+        built_parent_string += kRoot;
+        built_parent_string += "..";
+      }
+
+      return PurePath{std::move(built_parent_string)};
+
+    } else if (parts_count == 1) {
+      if (IsAbsolute()) {
+        // "/" + ".." is still "/"
+        return {kRoot};
+      } else {
+        // <NOT-ROOT-OR-UP-REFERENCE> + ".." is just "."
+        return {};
+      }
+    } else {
+      DCHECK_GE(parts_count, 2u);
+
+      // Find the last iterator before we hit the end.
+      std::optional<std::string_view> last;
+      std::optional<std::string_view> prev_last;
+      for (auto&& part : IterateParts()) {
+        prev_last = last;
+        last = part;
+      }
+
+      DCHECK(last.has_value());
+      DCHECK(prev_last.has_value());
+
+      std::string_view& prev_last_view = *prev_last;
+      // prev_last_view must be within address of subrange_.
+      DCHECK_GE(prev_last_view.data(), path_.data());
+      DCHECK_LE(prev_last_view.data() + prev_last_view.size(), path_.data() + path_.size());
+
+      // take advantage of the address subrange property by calculating a new substring
+      // for the parent.
+      size_t length = prev_last_view.data() + prev_last_view.size() - path_.data();
+      std::string_view parent = std::string_view{path_.data(), length} ;
+
+      if ((false)) {
+        LOG(DEBUG) << "PurePath::Parent of \"" << path_ << "\" returns \"" << parent << "\"";
+      }
+      return { parent };
+    }
+  }
+
+  // A path is considered non-equal to another path if one or more of the parts differ.
+  constexpr bool operator!=(const PurePath& other) const {
+    return !(*this == other);
+  }
+
+  // Return the string view, i.e. to pass to other classes that need a string-like type.
+  //
+  // This passes in the original string as was passed into the constructor.
+  // The exact char-by-char representation may be different than concatenating all the parts
+  // together.
+  //
+  // See also #NormalizePath if you want to get a 1:1 mapping between a PurePath
+  // and a string.
+  constexpr std::string_view AsStringView() const {
+    // This is slightly inconsistent with PurePath#bytes because it actually collapses the string
+    // to the equivalent of concatenating the parts together. But we prefer not to do that,
+    // since it just causes more work and more allocations unnecessarily.
+    //
+    // This is generally not-noticeable when operating with the path at the logical layer.
+    return path_;
+  }
+
+  constexpr bool IsAbsolute() const {
+    return !path_.empty() && path_[0] == '/';  // left-whitespace is considered significant.
+  }
+
+  // Join one or more paths together.
+  //
+  // Logically equivalent to calling JoinPath(other) repeatedly.
+  template <typename It>
+  PurePath JoinPath(It begin, It end) const {
+    std::vector<std::string_view> parts_stack = PartsList();
+
+    while (begin != end) {
+      const PurePath& path = *begin;
+
+      if (path.IsAbsolute()) {
+        parts_stack = path.PartsList();
+      } else {
+        path.VisitParts([&parts_stack](auto&& part) {
+          parts_stack.push_back(part);
+        });
+      }
+
+      ++begin;
+    }
+
+    return {JoinPartsList(parts_stack)};
+  }
+
+  // Join two paths together:
+  //
+  // If 'other' is an absolute path, it is returned.
+  //
+  // Otherwise, return the concatenation of the parts (this and other) as a new path.
+  // (The returned path is always owned by the caller -- this is triggering an allocation every
+  // time).
+  PurePath JoinPath(const PurePath& other) const {
+    if (other.IsAbsolute()) {
+      return other.OwningCopy();
+    } else {
+      std::vector<std::string_view> parts_stack = PartsList();
+      other.VisitParts([&parts_stack](auto&& part) {
+        parts_stack.push_back(part);
+      });
+      return {JoinPartsList(parts_stack)};
+    }
+  }
+
+  constexpr PurePath(const PurePath& other) {
+    if (this == &other) {
+      return;
+    }
+    if (other.owner_) {  // stay constexpr for non-owning paths.
+      owner_ = other.owner_;
+      path_ = *owner_;   // path_ always points to owner if possible.
+    } else {
+      path_ = other.path_;
+    }
+  }
+
+  constexpr PurePath(PurePath&& other) {
+    if (this == &other) {
+      return;
+    }
+    if (other.owner_) {  // stay constexpr for non-owning paths.
+      owner_ = std::move(other.owner_);
+      path_ = *owner_;   // path_ always points to owner if possible.
+    } else {
+      path_ = std::move(other.path_);
+    }
+  }
+
+  // "/.." -> "/"
+  // "../foo/.." -> ".."
+  // etc.
+  //
+  // PurePath returned always owns its own memory (this always triggers an allocation).
+  PurePath NormalizePath() const {
+    if (IsNormalized()) {
+      return OwningCopy();  // Don't call this function if you want to avoid copies!
+    } else {
+      // Invariant: [/]? <UP-REFERENCE>* <NOT-AN-UP-REFERENCE>*
+      std::vector<std::string_view> parts_stack;
+      size_t not_an_up_reference = 0;
+
+      // Special handling of absolute paths:
+      //   '/' '..'* -> '/'
+      //
+      // Otherwise, remove the last part when encountering redundant up-references:
+      //   e.g. '../foo/bar/baz/..' -> '../foo/bar'
+      VisitParts([&](auto&& part) {
+                   if (part == "..") {
+                     // <UP-REFERENCE>
+                     if (not_an_up_reference > 0) {
+                       // Remove redundant up-references.
+                       DCHECK(!parts_stack.empty());
+
+                       // Could trigger de-normalization, remove redundant part from stack.
+
+                       if (parts_stack.back() != kRoot) {  // '/' '..'* -> '/'
+                         parts_stack.pop_back();
+                         --not_an_up_reference;            // '../foo/..' -> '..'
+                       }
+                     } else {
+                       // Did not trigger a denormalization.
+                       parts_stack.push_back(part);
+                     }
+                   } else {
+                     // <NOT-AN-UP-REFERENCE> or '/' (note: / is only visited the first time).
+                     parts_stack.push_back(part);
+                     ++not_an_up_reference;
+                   }
+                 });
+
+      // join again with empty delimiter.
+      std::string concat = JoinPartsList(std::move(parts_stack));
+
+      return PurePath(std::move(concat));
+    }
+  }
+
+  // Returns true if 'NormalizePath' would return a Path with a different parts representation.
+  //
+  // (This is not as strict as normalizing the underlying string, i.e. redundant '.' and "//"
+  // in AsStringView() could still be seen).
+  //
+  // A path is considered non-normalized unless all up-references are at the start.
+  //
+  //   NormalizedString := <UP-REFERENCE>* <NOT-AN-UP-REFERENCE>*
+  //
+  // where each token is a 'part' returned by VisitParts.
+  //
+  // Returning false here means that 'NormalizePath' will also trigger an extra allocation.
+  constexpr bool IsNormalized() const {
+    size_t not_an_up_reference = 0;
+    bool is_normalized = true;
+
+    // Note that this also handles '/' [..]* because '/' is treated identically to non-up-refs.
+    VisitParts([&](auto&& part) {
+                // Remove redundant up-references.
+                if (part != "..") {
+                  ++not_an_up_reference;
+                } else {  // part == ".."
+                  if (not_an_up_reference > 0) {   // <not-an-up-reference> <up-reference>
+                    is_normalized = false;
+                  }
+                }
+               });
+
+    return is_normalized;
+  }
+
+  // Implement LegacyForwardIterator concept.
+  struct PartIterator {
+    using value_type = std::string_view;
+    using reference = value_type&;
+    using pointer = value_type*;
+    using iterator_category = std::forward_iterator_tag;
+    using difference_type = std::ptrdiff_t;  // required by concept, but its meaningless.
+
+   private:
+    enum State {
+      kUninitialized,
+      kAtRoot,
+      kInitialized,
+      kAtEnd
+    };
+
+    using SplitIterable = StringSplit::SplitIterable;
+    using SplitIterator = StringSplit::SplitIterator;
+
+    State state{kUninitialized};
+    value_type cur_value;
+    SplitIterator cur;
+    SplitIterator end;
+
+    friend std::ostream& operator<<(std::ostream& os, const PartIterator& it);
+
+    // Print out extra debugging information when looping through the iterator.
+    static constexpr bool kLogDebug = false;
+
+   public:
+    void PrintToStream(std::ostream& os) const {
+      os << "PartIterator{";
+      os << "state:";
+      switch (state) {
+        case kUninitialized:
+          os << "kUninitialized";
+          break;
+        case kAtRoot:
+          os << "kAtRoot";
+          break;
+        case kInitialized:
+          os << "kInitialized";
+          break;
+        case kAtEnd:
+          os << "kAtEnd";
+          break;
+      }
+      os << ",";
+      os << "cur_value:\"" << cur_value << "\",";
+      os << "cur:" << cur << ",";
+      os << "end:" << end << ",";
+      os << "}";
+    }
+
+    /*constexpr*/ bool operator==(const PartIterator& other) const {
+      DCHECK(state != kUninitialized) << "State must be initialized";
+      DCHECK(other.state != kUninitialized) << "Other state must be initialized";
+
+      if (kLogDebug) {
+        LOG(DEBUG) << "PartIterator ==";
+      }
+
+      if (state != other.state) {
+        if (kLogDebug) {
+          LOG(DEBUG) << "State: " << static_cast<int>(state);
+          LOG(DEBUG) << "Other State: " << static_cast<int>(other.state);
+
+          LOG(DEBUG) << "== states differ (&self=" << this << ",&other=" << &other << ")";
+          LOG(DEBUG) << "Self=" << *this;
+          LOG(DEBUG) << "Other=" << other;
+        }
+        return false;
+      }
+
+      switch (state) {
+        case kAtRoot:
+          DCHECK(cur != end);
+          return cur == other.cur;
+        case kInitialized:
+          DCHECK(cur != end);
+          return cur == other.cur;
+        case kAtEnd:
+          DCHECK(cur == end);
+          DCHECK(cur == other.cur);
+          return true;
+        default:
+          DCHECK(false);  // -Werror -Wswitch
+          return true;
+      }
+    }
+
+    constexpr bool operator!=(const PartIterator& other) const {
+      return !(*this == other);
+    }
+
+    constexpr reference operator*() {
+        DCHECK(state != kAtEnd) << "Undefined behavior to dereference end() iterators";
+        return cur_value;  // Can't use *cur because we could yield a '/'.
+    }
+
+    constexpr pointer operator->() {
+        DCHECK(state != kAtEnd) << "Undefined behavior to dereference end() iterators";
+        return &cur_value;  // Can't use &*cur because we could yield a '/'.
+    }
+
+    /*
+    constexpr const reference operator*() const {
+        return *cur;
+    }
+
+    constexpr const pointer operator->() const {
+        return &*cur;
+    }*/
+
+    constexpr PartIterator& operator++() {
+      DCHECK(state != kAtEnd) << "Undefined behavior to increment end() iterators";
+      UpdateValues();
+      return *this;
+    }
+
+    constexpr PartIterator operator++(int) {
+      PartIterator copy{*this};
+      ++(*this);
+      return copy;
+    }
+
+    constexpr static PartIterator MakeBegin(SplitIterable& split_iterable,
+                                            std::string_view whole_path) {
+      SplitIterator begin = split_iterable.begin();
+      SplitIterator end = split_iterable.end();
+
+      PartIterator it;
+      it.end = end;
+
+      const bool is_absolute = !whole_path.empty() && whole_path[0] == '/';
+
+      if (begin == end) {
+        it.cur = end;
+        it.state = kAtEnd;
+        // I'm not sure this path is actually possible due to the nature of how StringSplit
+        // works, but it's better to cover this case just to be safe.
+        DCHECK(false) << "unreachable code, splitting by '/' always returns at least 1 split";
+      } else {
+        it.cur = begin;
+
+        if (is_absolute) {
+          // When we split, we no longer visit the '/' tokens. Handle root explicitly.
+          //
+          // All emitted values must be within the address range of the whole path.
+          it.cur_value = whole_path.substr(0, /*count*/1);  // '/'
+          DCHECK_EQ(it.cur_value, "/"sv);
+          it.state = kAtRoot;
+        } else {
+          it.state = kUninitialized;
+          it.UpdateValues();
+        }
+      }
+
+      return it;
+    }
+
+    constexpr static PartIterator MakeEnd(SplitIterable& split_iterable) {
+      SplitIterator end = split_iterable.end();
+
+      PartIterator it;
+      it.cur = end;
+      it.end = end;
+      it.state = kAtEnd;
+
+      return it;
+    }
+
+   private:
+    void UpdateValues() {
+      State previous_state = state;
+
+      if (kLogDebug) {
+        LOG(DEBUG) << "operator ++ // UpdateValues (&this=" << this << ")";
+      }
+
+      if (state == kAtEnd) {
+        return;
+      }
+
+      if (state == kInitialized) {
+        DCHECK(IsValidCurrent());
+      }
+
+      // '/' has no corresponding split, so it's handled as a special case.
+      // Furthermore, any splits that are empty or "." are skipped since they aren't
+      // considered to be a valid path component.
+      //
+      // The below code handles these special cases.
+
+      if (state == kAtRoot) {
+        state = kUninitialized;
+      }
+
+      if (state == kUninitialized) {
+        // If we are already at a valid value stop.
+        if (cur != end && IsValidCurrent()) {
+          state = kInitialized;
+          cur_value = *cur;
+          return;
+        }
+
+        // Otherwise we are either at the end, or
+        // the current value is invalid (e.g. empty or '.').
+        state = kInitialized;
+      }
+
+      DCHECK(state == kInitialized) << static_cast<int>(state);
+      if (previous_state == kInitialized) {
+        // If we fell-through from kAtRoot or kUninitialized
+        // then there's no guarantee that the current value is valid.
+        DCHECK(IsValidCurrent());
+      }
+
+      auto old_cur_value = *cur;
+
+      // Already at the end. Switch to end state.
+      if (cur == end) {
+        state = kAtEnd;
+        LOG(DEBUG) << "Updated state is: kAtEnd (1)";
+        return;
+      }
+
+      // Skip ahead.
+      // We may or may not be at a valid value now.
+      ++cur;
+
+      // If we aren't at a valid value yet, then keep going forward
+      // until we hit a valid value (or we exhaust the iterator).
+      while (cur != end && !IsValidCurrent()) {
+        ++cur;
+      }
+
+      if (cur == end) {
+        state = kAtEnd;
+      } else {
+        // We reached a valid value before exhausting the iterator.
+
+        // Stay in the 'Initialized' state.
+        DCHECK(IsValidCurrent()) << *cur;
+        cur_value = *cur;
+
+        // After we go forward, the old and current value cannot match.
+        DCHECK_NE(&cur_value[0], &old_cur_value[0]);
+      }
+
+      if (kLogDebug) {
+        LOG(DEBUG) << "Updated state is: " << state;
+      }
+    }
+
+    constexpr bool IsValidCurrent() {
+      if (cur->empty()) {
+        return false;
+      } else if (*cur == ".") {
+        return false;
+      }
+
+      return true;
+    }
+  };
+
+  friend struct PartIterable;
+
+  struct PartIterable {
+    constexpr static PartIterable FromPath(const PurePath& path) {
+      return PartIterable{
+          path.AsStringView(),
+          StringSplit::Iterable(path.AsStringView(), PurePath::kRoot),
+      };
+    }
+
+    constexpr PartIterator begin() {
+      return PartIterator::MakeBegin(split_iterable, whole_path);
+    }
+
+    constexpr PartIterator end() {
+      return PartIterator::MakeEnd(split_iterable);
+    }
+
+    std::string_view whole_path;
+    StringSplit::SplitIterable split_iterable;
+  };
+
+  // This isn't performance-efficient, but it might be needed by some functions
+  // that have to allocate anyway such as JoinPaths.
+  //
+  // Intended only for testing.
+  std::vector<std::string_view> PartsList() const {
+    PartIterable iterable = IterateParts();
+
+    std::vector<std::string_view> parts{iterable.begin(), iterable.end()};
+    return parts;
+  }
+
+  // Does this PurePath own the underlying memory?
+  //
+  // true = borrowing memory from someone else (might not be safe to retain this object)
+  // false = owns its own memory (can keep this object indefinitely long)
+  //
+  // Currently intended only for testing.
+  constexpr bool IsBorrowed() const {
+    return !owner_.has_value();
+  }
+
+ private:
+  // Return a PurePath that owns its own memory.
+  //
+  // This way functions which 'may' allocate memory turn into functions
+  // that always allocate memory, and avoid a dangling reference.
+  const PurePath OwningCopy() const {
+    std::string make_copy{path_};
+    return PurePath{std::move(make_copy)};
+  }
+
+  constexpr size_t PartsCount() const {
+    size_t count = 0;
+    VisitParts([&count](auto&& /*part*/) {
+      ++count;
+    });
+    return count;
+  }
+
+  // Basically a string join with an empty delimiter.
+  template <typename Container>
+  static std::string JoinPartsList(Container&& c) {
+    std::string build;
+    for (auto begin = c.begin(), end = c.end(); begin != end; ++begin) {
+      build += *begin;
+
+      // TODO: use forward_dependent here.
+    }
+
+    return build;
+  }
+
+  // This might be empty, in which case path_ is just a temporary borrow of path_.
+  std::optional<std::string> owner_;
+  std::string_view path_;  // points to owner_ if there's a value there.
+
+  // TODO: this is a bit error-prone, so we might want to refactor into a
+  // never-owning PathView and an always-owning PurePath.
+
+  static constexpr std::string_view kRoot = "/";
+};
+
+std::ostream& operator<<(std::ostream& os, const PurePath::PartIterator& it) {
+  it.PrintToStream(os);
+  return os;
+}
+
+static constexpr const PurePath::PartIterator kMakeMeABlank;
+
+std::ostream& operator<<(std::ostream& os, const PurePath& path) {
+  os << path.AsStringView();
+  return os;
+}
+
+TEST(PurePathTest, Ctor) {
+  ConfigureLogging();
+
+  EXPECT_EQ(PurePath{}.AsStringView(), "."sv);
+  EXPECT_EQ(PurePath{""}.AsStringView(), ""sv);
+  EXPECT_EQ(PurePath{""sv}.AsStringView(), ""sv);
+  EXPECT_EQ(PurePath{""s}.AsStringView(), ""sv);
+  EXPECT_EQ(PurePath{"/hello/world"}.AsStringView(), "/hello/world"sv);
+  EXPECT_EQ(PurePath{"/hello/world"s}.AsStringView(), "/hello/world"sv);
+  EXPECT_EQ(PurePath{"/hello/world"sv}.AsStringView(), "/hello/world"sv);
+  EXPECT_EQ(PurePath{"hello/world"}.AsStringView(), "hello/world"sv);
+  EXPECT_EQ(PurePath{"hello/world"s}.AsStringView(), "hello/world"sv);
+  EXPECT_EQ(PurePath{"hello/world"sv}.AsStringView(), "hello/world"sv);
+
+  // Ensure that std::string is only owning memory when we move a string into it.
+  // Otherwise, always take the string_view constructor.
+  EXPECT_FALSE(PurePath{std::string{"hello"}}.IsBorrowed());
+  std::string hello{"hello"};
+  EXPECT_TRUE(PurePath{hello}.IsBorrowed());
+  EXPECT_FALSE(PurePath{std::move(hello)}.IsBorrowed());
+}
+
+TEST(PurePathTest, Parts) {
+  ConfigureLogging();
+
+  EXPECT_THAT(PurePath{}.PartsList(), IsEmpty());
+  EXPECT_THAT(PurePath{"."}.PartsList(), IsEmpty());
+  EXPECT_THAT(PurePath{"./"}.PartsList(), IsEmpty());
+  EXPECT_THAT(PurePath{"./."}.PartsList(), IsEmpty());
+  EXPECT_THAT(PurePath{".///"}.PartsList(), IsEmpty());
+  EXPECT_THAT(PurePath{"./././."}.PartsList(), IsEmpty());
+  EXPECT_THAT(PurePath{"/"s}.PartsList(), ElementsAre("/"sv));
+  EXPECT_THAT(PurePath{"///"s}.PartsList(), ElementsAre("/"sv));
+  EXPECT_THAT(PurePath{"/hello"s}.PartsList(), ElementsAre("/"sv, "hello"sv));
+  EXPECT_THAT(PurePath{"/hello///"s}.PartsList(), ElementsAre("/"sv, "hello"sv));
+  EXPECT_THAT(PurePath{"/hello/world"s}.PartsList(), ElementsAre("/"sv, "hello"sv, "world"sv));
+  EXPECT_THAT(PurePath{"hello/world"sv}.PartsList(), ElementsAre("hello"sv, "world"sv));
+  EXPECT_THAT(PurePath{"hello/world"sv}.PartsList(), ElementsAre("hello"sv, "world"sv));
+  EXPECT_THAT(PurePath{"hello//world"sv}.PartsList(), ElementsAre("hello"sv, "world"sv));
+  EXPECT_THAT(PurePath{"hello/./world"sv}.PartsList(), ElementsAre("hello"sv, "world"sv));
+  EXPECT_THAT(PurePath{"hello/./world/././"sv}.PartsList(), ElementsAre("hello"sv, "world"sv));
+}
+
+#define EXPECT_PATH_EQ(lhs, rhs) EXPECT_EQ(PurePath{lhs}, PurePath{rhs})
+#define EXPECT_PATH_NE(lhs, rhs) EXPECT_NE(PurePath{lhs}, PurePath{rhs})
+
+TEST(PurePathTest, Equals) {
+  ConfigureLogging();
+
+  EXPECT_PATH_EQ("", "");
+  EXPECT_PATH_EQ(".", ".");
+  EXPECT_PATH_EQ("", ".");
+  EXPECT_PATH_EQ("./", ".");
+  EXPECT_PATH_EQ(".////", ".");
+  EXPECT_PATH_EQ(".//././", ".");
+  EXPECT_PATH_EQ("hello/world//", "hello/world");
+  EXPECT_PATH_EQ("hello/world//", "./hello/world");
+  EXPECT_PATH_EQ("//hello/world//", "/hello/world");
+  EXPECT_PATH_EQ("/./hello/world//", "/hello/world/./");
+  EXPECT_PATH_EQ("..", ".././.");
+  EXPECT_PATH_EQ("../..//", "../..");
+
+  // Also make sure that the path is not equal to its parent [which is a substring].
+  EXPECT_PATH_NE("/data", "/data/baz");
+  EXPECT_PATH_NE("/data/././baz", "/data/baz/bar");
+
+  // Also make sure its not equal when the other path shares the same underlying starting data().
+  {
+    std::string_view view = "/data/bar";
+    EXPECT_PATH_NE(PurePath{view}, PurePath{view.substr(/*pos*/0, /*count*/5)});
+  }
+}
+
+// A parent is always different than its child (except for '/').
+#define EXPECT_PATH_PARENT_EQ(actual, expected) \
+    EXPECT_EQ(PurePath{actual}.Parent(), PurePath{expected}); \
+    { auto act = PurePath{actual};   \
+      EXPECT_NE(act, act.Parent());  \
+    }
+TEST(PurePathTest, Parent) {
+  ConfigureLogging();
+
+  // Special recursive case: parent of '/' is still '/'.
+  EXPECT_EQ(PurePath{"/"}, PurePath{"/"}.Parent());
+  EXPECT_NE(PurePath{""}, PurePath{"/"}.Parent());
+
+  // All other cases are non-recursive.
+  EXPECT_PATH_PARENT_EQ("", "..");
+  EXPECT_PATH_PARENT_EQ("..", "../..");
+  EXPECT_PATH_PARENT_EQ("../..", "../../..");
+  EXPECT_PATH_PARENT_EQ("../../../../../../../../..", "../../../../../../../../../..");
+
+  EXPECT_PATH_PARENT_EQ("/abc", "/");
+  EXPECT_PATH_PARENT_EQ("abc", "");
+
+  EXPECT_PATH_PARENT_EQ("/foo/bar", "/foo");
+  EXPECT_PATH_PARENT_EQ("/foo/bar/b", "/foo/bar");
+  EXPECT_PATH_PARENT_EQ("/foo/bar///baz///././/nay", "/foo/bar/baz");
+
+  EXPECT_PATH_PARENT_EQ("foo/bar", "foo");
+  EXPECT_PATH_PARENT_EQ("foo/bar/b", "foo/bar");
+  EXPECT_PATH_PARENT_EQ("foo/bar///baz///././/nay", "foo/bar/baz");
+
+  EXPECT_PATH_PARENT_EQ("../foo/bar", "../foo");
+  EXPECT_PATH_PARENT_EQ("../foo/bar/b", "../foo/bar");
+  EXPECT_PATH_PARENT_EQ("../foo/bar///baz///././/nay", "../foo/bar/baz");
+}
+
+#define EXPECT_PATH_NAME_EQ(expected, actual) EXPECT_EQ(PurePath{actual}, PurePath{expected}.Name())
+TEST(PurePathTest, Name) {
+  ConfigureLogging();
+
+  EXPECT_PATH_NAME_EQ("", "");
+  EXPECT_PATH_NAME_EQ("..", "..");
+  EXPECT_PATH_NAME_EQ("../..", "..");
+  EXPECT_PATH_NAME_EQ("../../../../../../../../..", "..");
+
+  EXPECT_PATH_NAME_EQ("/", "");
+  EXPECT_PATH_NAME_EQ("/abc", "abc");
+  EXPECT_PATH_NAME_EQ("abc", "abc");
+
+  EXPECT_PATH_NAME_EQ("/foo/bar", "bar");
+  EXPECT_PATH_NAME_EQ("/foo/bar/b", "b");
+  EXPECT_PATH_NAME_EQ("/foo/bar///baz///././/nay", "nay");
+  EXPECT_PATH_NAME_EQ("/foo/bar///baz///././/nay//./.", "nay");
+
+  EXPECT_PATH_NAME_EQ("foo/bar", "bar");
+  EXPECT_PATH_NAME_EQ("foo/bar/b", "b");
+  EXPECT_PATH_NAME_EQ("foo/bar///baz///././/nay", "nay");
+
+  EXPECT_PATH_NAME_EQ("../foo/bar", "bar");
+  EXPECT_PATH_NAME_EQ("../foo/bar/b", "b");
+  EXPECT_PATH_NAME_EQ("../foo/bar///baz///././/nay", "nay");
+}
+
+
+struct PathEntry {
+  Inode inode;
+  PurePath path;  // full path
+
+  static std::vector<PathEntry> Zip(std::vector<Inode>& inodes, std::vector<std::string>& paths) {
+    CHECK_EQ(inodes.size(), paths.size());
+
+    std::vector<PathEntry> entries;
+
+    static bool debug = true;  // Print only once.
+
+    if (debug) {
+      LOG(DEBUG) << "PathEntry::Zip (begin)";
+    }
+
+    for (size_t i = 0; i < inodes.size(); ++i) {
+      entries.push_back(PathEntry{inodes[i], PurePath{std::string{paths[i]}}});
+
+      // TODO: this seems awkward, maybe refactor into PurePath + PurePathView ?
+      DCHECK(entries[i].path.IsBorrowed() == false);
+
+      if (debug) {
+        LOG(DEBUG) << "PathEntry - add " << inodes[i] << " at '" << paths[i] << "'";
+      }
+    }
+
+    debug = false;
+
+    return entries;
+  }
+};
+
+std::ostream& operator<<(std::ostream& os, const PathEntry& path_entry) {
+  os << "PathEntry{inode=" << path_entry.inode << ",path=\"" << path_entry.path << "\"}";
+  return os;
+}
+
+// This super-inefficient class models a Tree to a list of absolute path names.
+// Obviously intended only for testing, since its algorithmically suboptimal.
+struct PathEntryTree {
+  std::vector<PathEntry> entries;
+
+  static constexpr bool debug{false};
+#define PET_LOG_DEBUG if (debug) LOG(DEBUG)
+
+  std::optional<PathEntry> GetEntryFor(const std::string& path_name) {
+    PurePath path{path_name};
+    for (auto&& entry : entries) {
+      if (entry.path == path) {
+        return entry;
+      }
+    }
+    return {};
+  }
+
+  bool HasDirectory(const std::string& path_name) {
+    PurePath path{path_name};
+    for (auto&& entry : entries) {
+      if (entry.path == path) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  std::vector<PathEntry> OpenDirectory(const std::string& path_name) {
+    PurePath path{path_name};
+    return OpenDirectory(path);
+  }
+
+  std::vector<PathEntry> OpenDirectory(const PurePath& path) {
+    std::vector<PathEntry> children;
+
+    PET_LOG_DEBUG << "OpenDirectory(" << path << ")";
+
+    for (auto&& entry : entries) {
+      // Only find the immediate children, don't find any other offspring.
+      PurePath parent = entry.path.Parent();
+      if (parent == path) {
+        if (parent == entry.path) {
+          // Ignore recursive parents, e.g. '/'
+          PET_LOG_DEBUG << "OpenDirectory - Ignore recursive parent " << parent;
+          continue;
+        }
+
+        children.push_back(entry);
+
+        DCHECK(!children.back().path.IsBorrowed());
+
+        PET_LOG_DEBUG << "OpenDirectory - Child added = " << entry;
+      }
+    }
+
+
+    return children;
+  }
+
+  size_t size() const {
+    return entries.size();
+  }
+};
+
+
+static std::vector<std::string> ParseLines(const char* what) {
+  std::vector<std::string> do_split = android::base::Split(what, "\n");
+
+  std::vector<std::string> output;
+  for (std::string& s : do_split) {
+    if (s.size() != 0) {
+      output.push_back(s);
+    }
+  }
+
+  return output;
+}
+
+static std::vector<Inode> ParseInodes(std::vector<std::string> inode_strings) {
+  std::vector<Inode> results;
+
+  for (std::string& s : inode_strings) {
+    Inode inode;
+
+    std::string error_msg;
+    bool inode_parse_succeeded = Inode::Parse(s, /*out*/&inode, /*out*/&error_msg);
+    CHECK(inode_parse_succeeded) << s << ", error: " << error_msg;
+
+    results.push_back(inode);
+  }
+
+  return results;
+}
+
+
+
+static PathEntryTree CreateFakePathEntries() {
+#if 1
+    // adb shell 'find /data/data/com.google.android.googlequicksearchbox/ | xargs stat -c "%d@%i"'
+    static const char* kInodeValues = R"1N0D3(
+66323@1117133
+66323@1127133
+66323@1137133
+66323@1327133
+66323@1336383
+66323@1376559
+66323@1376448
+66323@1376446
+66323@1376596
+66323@1376638
+66323@1376438
+66323@1376444
+66323@1376563
+66323@1376434
+66323@1376439
+66323@1336384
+66323@1335704
+66323@1336031
+66323@1335751
+66323@1337692
+66323@1336090
+66323@1336385
+66323@1376543
+66323@1376449
+66323@1376544
+66323@1376547
+66323@1376436
+66323@1336619
+66323@1336070
+66323@1336681
+66323@1336064
+66323@1336088
+66323@1336470
+66323@1335570
+66323@1335668
+66323@1336471
+66323@1335514
+66323@1376475
+66323@1376462
+66323@1376435
+66323@1376476
+66323@1376632
+66323@1351934
+66323@1351948
+66323@1351949
+66323@1351950
+66323@1351939
+66323@1376479
+66323@1376437
+66323@1376450
+66323@1376480
+66323@1376442
+66323@1376451
+66323@1376454
+66323@1376457
+66323@1376452
+66323@1376546
+66323@1335629
+66323@1343800
+66323@1343801
+66323@1336890
+66323@1336616
+66323@1336921
+66323@1327135
+66323@1335862
+66323@1336547
+66323@1351681
+66323@1351684
+66323@1351744
+66323@1351705
+66323@1351699
+66323@1351711
+66323@1351748
+66323@1351734
+66323@1351682
+66323@1351683
+66323@1351719
+66323@1351739
+66323@1351689
+66323@1351724
+66323@1351690
+66323@1351745
+66323@1351686
+66323@1351691
+66323@1351741
+66323@1351687
+66323@1351747
+66323@1351736
+66323@1351698
+66323@1351697
+66323@1351730
+66323@1351712
+66323@1351703
+66323@1351721
+66323@1351701
+66323@1351717
+66323@1351716
+66323@1351695
+66323@1351720
+66323@1351688
+66323@1351685
+66323@1351727
+66323@1351738
+66323@1351729
+66323@1351704
+66323@1351743
+66323@1351723
+66323@1351700
+66323@1351713
+66323@1351707
+66323@1351709
+66323@1351731
+66323@1351732
+66323@1351693
+66323@1351726
+66323@1351708
+66323@1351714
+66323@1351728
+66323@1351694
+66323@1351706
+66323@1351722
+66323@1351696
+66323@1351715
+66323@1351740
+66323@1351725
+66323@1351702
+66323@1351710
+66323@1351737
+66323@1351742
+66323@1351746
+66323@1351735
+66323@1351733
+66323@1351692
+66323@1351718
+66323@1336864
+66323@1335446
+66323@1337584
+66323@1335740
+66323@1335854
+66323@1336644
+66323@1376553
+66323@1376554
+66323@1376469
+66323@1376637
+66323@1376555
+66323@1376556
+66323@1376570
+66323@1376565
+66323@1376557
+66323@1376558
+66323@1376432
+66323@1376567
+66323@1376440
+66323@1343805
+66323@1336646
+66323@1336947
+66323@1336393
+66323@1336394
+66323@1335920
+66323@1336041
+66323@1335650
+66323@1336667
+66323@1336665
+66323@1335760
+66323@1343802
+66323@1343803
+66323@1344013
+66323@1344134
+66323@1376276
+66323@1336598
+66323@1336634
+66323@1336652
+66323@1336656
+66323@1336446
+66323@1336863
+66323@1337682
+66323@1336866
+66323@1336867
+66323@1335678
+66323@1336865
+66323@1327631
+66323@1327664
+66323@1327660
+66323@1327134
+66323@1336825
+66323@1337969
+66323@1335938
+66323@1337849
+66323@1337839
+66323@1337866
+66323@1337122
+66323@1337756
+66323@1336966
+66323@1337982
+66323@1337097
+66323@1336683
+66323@1337824
+66323@1337460
+66323@1337775
+66323@1337810
+66323@1337847
+66323@1335853
+66323@1337594
+66323@1337808
+66323@1337817
+66323@1337092
+66323@1337699
+66323@1337593
+66323@1337089
+66323@1335959
+66323@1337788
+66323@1337181
+66323@1337610
+66323@1336980
+66323@1337972
+66323@1337554
+66323@1337661
+66323@1337770
+66323@1335951
+66323@1337984
+66323@1336061
+66323@1337497
+66323@1337835
+66323@1337805
+66323@1336557
+66323@1336780
+66323@1337816
+66323@1337732
+66323@1337983
+66323@1337954
+66323@1337713
+66323@1337687
+66323@1337597
+66323@1337466
+66323@1337814
+66323@1337603
+66323@1337031
+66323@1336784
+66323@1337534
+66323@1337727
+66323@1337693
+66323@1337791
+66323@1337567
+66323@1337748
+66323@1337777
+66323@1336194
+66323@1337843
+66323@1336971
+66323@1337974
+66323@1336785
+66323@1337871
+66323@1337815
+66323@1337709
+66323@1337551
+66323@1337088
+66323@1337776
+66323@1337672
+66323@1335979
+66323@1337823
+66323@1336028
+66323@1337526
+66323@1337971
+66323@1337853
+66323@1337596
+66323@1337901
+66323@1337572
+66323@1335921
+66323@1336954
+66323@1337820
+66323@1335492
+66323@1337809
+66323@1337696
+66323@1335636
+66323@1337608
+66323@1335746
+66323@1337731
+66323@1337967
+66323@1337769
+66323@1337751
+66323@1337973
+66323@1337697
+66323@1335939
+66323@1336001
+66323@1337598
+66323@1336713
+66323@1337702
+66323@1337844
+66323@1337862
+66323@1336978
+66323@1337975
+66323@1336798
+66323@1337858
+66323@1337605
+66323@1337510
+66323@1337914
+66323@1376548
+66323@1376549
+66323@1376550
+66323@1376564
+66323@1376571
+66323@1376683
+66323@1376681
+66323@1376652
+66323@1376682
+66323@1376684
+66323@1376649
+66323@1376568
+66323@1376569
+66323@1376576
+66323@1376578
+66323@1376579
+66323@1376581
+66323@1376582
+66323@1376577
+66323@1376580
+66323@1376597
+66323@1376598
+66323@1376602
+66323@1376599
+66323@1376600
+66323@1376601
+66323@1376583
+66323@1376551
+66323@1376552
+66323@1376560
+66323@1376561
+66323@1376562
+66323@1376591
+66323@1376497
+66323@1376482
+66323@1376536
+66323@1376533
+66323@1376532
+66323@1336380
+66323@1336425
+66323@1337738
+66323@1337978
+66323@1337796
+66323@1337819
+66323@1337781
+66323@1337857
+66323@1337963
+66323@1335777
+66323@1337569
+66323@1337818
+66323@1337758
+66323@1337742
+66323@1336950
+66323@1337730
+66323@1337021
+66323@1335774
+66323@1337813
+66323@1337755
+66323@1337964
+66323@1337860
+66323@1338005
+66323@1336592
+66323@1336428
+66323@1335779
+66323@1337976
+66323@1337461
+66323@1337789
+66323@1337745
+66323@1337602
+66323@1337698
+66323@1336813
+66323@1337606
+66323@1337896
+66323@1337712
+66323@1337970
+66323@1337981
+66323@1335435
+66323@1337587
+66323@1337821
+66323@1337716
+66323@1337754
+66323@1337786
+66323@1337778
+66323@1336032
+66323@1338029
+66323@1337550
+66323@1337783
+66323@1337609
+66323@1337107
+66323@1337841
+66323@1337557
+66323@1337700
+66323@1337604
+66323@1337920
+66323@1337469
+66323@1337811
+66323@1337715
+66323@1337980
+66323@1336949
+66323@1337812
+66323@1337806
+66323@1337779
+66323@1337600
+66323@1336080
+66323@1337601
+66323@1336920
+66323@1337703
+66323@1337033
+66323@1336824
+66323@1337104
+66323@1337854
+66323@1336078
+66323@1336970
+66323@1337917
+66323@1337671
+66323@1337926
+66323@1336802
+66323@1337797
+66323@1338031
+66323@1337095
+66323@1337676
+66323@1337708
+66323@1335905
+66323@1336124
+66323@1337859
+66323@1337784
+66323@1337795
+66323@1337724
+66323@1337822
+66323@1336426
+66323@1337852
+66323@1337856
+66323@1337855
+66323@1337780
+66323@1337607
+66323@1336956
+66323@1337038
+66323@1336513
+66323@1336918
+66323@1336739
+66323@1337924
+66323@1337530
+66323@1337757
+66323@1337850
+66323@1337701
+66323@1336613
+66323@1337737
+66323@1336817
+66323@1337977
+66323@1336314
+66323@1337465
+66323@1336991
+66323@1337279
+66323@1337922
+66323@1337710
+66323@1337599
+66323@1337861
+66323@1336388
+66323@1336389
+66323@1336084
+66323@1335615
+66323@1336375
+66323@1335759
+66323@1336036
+66323@1336433
+66323@1335649
+66323@1337744
+66323@1336008
+66323@1336004
+66323@1336026
+66323@1335834
+66323@1336376
+66323@1336377
+66323@1336505
+66323@1336378
+66323@1335382
+66323@1337015
+66323@1336108
+66323@1337103
+66323@1335413
+66323@1335935
+66323@1335429
+66323@1337733
+66323@1336382
+66323@1336381
+66323@1336633
+66323@1337522
+66323@1336694
+66323@1335428
+)1N0D3";
+
+    const char* kPathNames = R"F1L3N4M3(
+/
+/data/
+/data/data/
+/data/data/com.google.android.googlequicksearchbox/
+/data/data/com.google.android.googlequicksearchbox/app_si
+/data/data/com.google.android.googlequicksearchbox/app_si/searchbox_stats_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/searchbox_stats_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/searchbox_stats_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/searchbox_stats_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/searchbox_stats_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/shortcuts_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/shortcuts_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/shortcuts_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/shortcuts_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/shortcuts_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store/now_content_store_blob_9060309284749123123.bin
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store/now_content_store_blob_9184734810098631032.bin
+/data/data/com.google.android.googlequicksearchbox/app_si/now_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/proactive_key_value_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/proactive_key_value_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/proactive_key_value_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/proactive_key_value_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/proactive_key_value_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/srp_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/srp_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/srp_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/srp_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/srp_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/state_dump_event_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/state_dump_event_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/state_dump_event_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/state_dump_event_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/state_dump_event_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/homescreen_shortcut_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/homescreen_shortcut_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/homescreen_shortcut_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/homescreen_shortcut_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/homescreen_shortcut_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/search_widget_overlay_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/search_widget_overlay_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/search_widget_overlay_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/search_widget_overlay_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/search_widget_overlay_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/opa_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/opa_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/opa_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_si/opa_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/opa_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/accl_conv_client_content_store
+/data/data/com.google.android.googlequicksearchbox/app_si/accl_conv_client_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/app_si/accl_conv_client_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/app_si/accl_conv_client_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/app_si/accl_conv_client_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/app_session
+/data/data/com.google.android.googlequicksearchbox/app_monet_init_data
+/data/data/com.google.android.googlequicksearchbox/app_monet_init_data/search.TYPE_SEARCHNOW.binarypb
+/data/data/com.google.android.googlequicksearchbox/no_backup
+/data/data/com.google.android.googlequicksearchbox/no_backup/com.google.InstanceId.properties
+/data/data/com.google.android.googlequicksearchbox/no_backup/com.google.android.gms.appid-no-backup
+/data/data/com.google.android.googlequicksearchbox/code_cache
+/data/data/com.google.android.googlequicksearchbox/app_sid
+/data/data/com.google.android.googlequicksearchbox/app_g3_models
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/CLG.prewalk.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/verbalizer_terse.mfar
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_app-actions_prompted-app-name_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/contacts.abnf
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_monastery_contact-disambig-static_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/wordlist.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/norm_fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/APP_NAME.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/APP_NAME.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/ep_portable_mean_stddev
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/portable_meanstddev
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/SONG_NAME.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/g2p_phonemes.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/TERSE_LSTM_LM.lstm_lm.main_model.uint8.data
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/voice_actions.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/CONTACT_NAME.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/TERSE_LSTM_LM.lstm_lm.self_normalized_model.uint8.data
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/pumpkin.mmap
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/CONTACT_NAME.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/word_confidence_classifier
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/offline_action_data.pb
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/config.pumpkin
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/compile_grammar.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/lstm_model.uint8.data
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_read-items_SearchMessageAction-Prompted-Read_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/embedded_class_denorm.mfar
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/g2p.data
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/dictation.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/endpointer_model.mmap
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/endpointer_model
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/c_fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/ep_portable_model.uint8.mmap
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/SONG_NAME.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/CONTACT.transform.mfar
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/hmmlist
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/portable_lstm
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/lexicon.U.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/embedded_normalizer.mfar
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/semantics.pumpkin
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/g2p_graphemes.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/dict
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_read-items_SearchMessageAction-Prompted-Skip_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_confirmation_confirmation-cancellation_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_media-actions_music-service-controllable_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/magic_mic.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/metadata
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/am_phonemes.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/hmm_symbols
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_gmm-actions_gmm-nav-actions_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_time-actions_time-context_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/input_mean_std_dev
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/benchmark.volvo.txt
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_calendar-actions_AddCalendarEvent-Prompted-FieldToChange_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/g2p_fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/commands.abnf
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/endpointer_dictation.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/prons_exception_dictionary_file.txt
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/grammar.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/dnn
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/en-US_monastery_GenericAction-Prompted-ContactName_TWIDDLER_FST.fst
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/phonelist
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/rescoring.fst.compact
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/voice_actions_compiler.config
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/offensive_word_normalizer.mfar
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/monastery_config.pumpkin
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/TERSE_LSTM_LM.lstm_lm.syms
+/data/data/com.google.android.googlequicksearchbox/app_g3_models/en-US/endpointer_voicesearch.config
+/data/data/com.google.android.googlequicksearchbox/app_textures
+/data/data/com.google.android.googlequicksearchbox/files
+/data/data/com.google.android.googlequicksearchbox/files/current_experiments.bin
+/data/data/com.google.android.googlequicksearchbox/files/training_question_data
+/data/data/com.google.android.googlequicksearchbox/files/now_request_queue
+/data/data/com.google.android.googlequicksearchbox/files/velour
+/data/data/com.google.android.googlequicksearchbox/files/velour/preferences
+/data/data/com.google.android.googlequicksearchbox/files/velour/preferences/ipa
+/data/data/com.google.android.googlequicksearchbox/files/velour/preferences/ipa/IpaBgTask
+/data/data/com.google.android.googlequicksearchbox/files/velour/preferences/wernicke_player
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ipa_0p_instant_cache
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ZeroPrefixContacts
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ipa_content_store
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ipa_content_store/content_store.db
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ipa_content_store/content_store.db-wal
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ipa_content_store/content_store.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/files/velour/feature_data/ipa/ipa_content_store/content_store.db-shm
+/data/data/com.google.android.googlequicksearchbox/files/velour/jar_data
+/data/data/com.google.android.googlequicksearchbox/files/velour/verified_jars
+/data/data/com.google.android.googlequicksearchbox/files/velour/dex_cache
+/data/data/com.google.android.googlequicksearchbox/files/native_crash_dir
+/data/data/com.google.android.googlequicksearchbox/files/native_crash_dir/com.google.android.googlequicksearchbox:search
+/data/data/com.google.android.googlequicksearchbox/files/current_configuration.bin
+/data/data/com.google.android.googlequicksearchbox/files/dynamic_update_config_log
+/data/data/com.google.android.googlequicksearchbox/files/brainsuggest
+/data/data/com.google.android.googlequicksearchbox/files/brainsuggest/libbrainsuggest.so
+/data/data/com.google.android.googlequicksearchbox/files/brainsuggest/tensors.bin
+/data/data/com.google.android.googlequicksearchbox/files/persisted_profiling_statistics
+/data/data/com.google.android.googlequicksearchbox/files/en-US
+/data/data/com.google.android.googlequicksearchbox/files/en-US/x_hotword.data
+/data/data/com.google.android.googlequicksearchbox/files/recently
+/data/data/com.google.android.googlequicksearchbox/files/recently/libcore.test@gmail.com
+/data/data/com.google.android.googlequicksearchbox/files/dump
+/data/data/com.google.android.googlequicksearchbox/files/bloblobber
+/data/data/com.google.android.googlequicksearchbox/files/bloblobber/pending
+/data/data/com.google.android.googlequicksearchbox/files/web_suggest_model
+/data/data/com.google.android.googlequicksearchbox/files/web_suggest_model/index.bin
+/data/data/com.google.android.googlequicksearchbox/files/client_data_request_log
+/data/data/com.google.android.googlequicksearchbox/app_webview
+/data/data/com.google.android.googlequicksearchbox/app_webview/variations_seed_new
+/data/data/com.google.android.googlequicksearchbox/app_webview/Cookies-journal
+/data/data/com.google.android.googlequicksearchbox/app_webview/variations_stamp
+/data/data/com.google.android.googlequicksearchbox/app_webview/variations_seed
+/data/data/com.google.android.googlequicksearchbox/app_webview/Cookies
+/data/data/com.google.android.googlequicksearchbox/app_shared_prefs
+/data/data/com.google.android.googlequicksearchbox/app_shared_prefs/StartupSettings.bin
+/data/data/com.google.android.googlequicksearchbox/app_shared_prefs/SearchSettings.bin
+/data/data/com.google.android.googlequicksearchbox/cache
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/fd12e9a1ba593cbe075c925a95626534054861f9dd82fa27f656ac0088648fd9.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/4a19d733c917d730443eaff509ee0496e116f79c69d0d2fa54a594f5accd19d1.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/76f6a9848373162cd602a03e00e363ad8455e62293e9218d57da728f7382ee34.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/666d7a8c0d257a9a9f1457a1bb04b8eda821966283d466db872d5693de42d29b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/0f714cd570228ce48e2741fd6ff959bcbbab49e40427b6eb5c4b1ff3aae4ad40.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/458acc9b996dc815a7259a2c9dbf5b94ae549da3d66f3649d1e0a1e239214390.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/13ec5eaf61460a0be11467ba2e0efad6602142e45fd1c97988bc42aa54832407.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/99b468ff1549d8e260ce274f7afdaaf32fb70064c31596173b812ea2d11c8097.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/40c737d31b2d843c5772648820deeb4c8d5bef9426b025f75bdc72ba7913e0fd.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/41186543405abfea16b57da566ce9e509826f9b1b6337473d05d9421f53912aa.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/cda361ffde7db6bedfeb9f01a63dd51ebbe4b733d3c6be69cede7a681d20b583.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/108adf201cd9b479777504c8e7fb74299bbc2b51082d2872a34616abe6c743fb.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/47a3358d7989bf06c4ce1ecb350149b1669bf16073ea1d321371a02ad3507d63.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/0c25c0bd4df514cdd4531872553e053b74a3b9a60476871184b7e8c2a1b67048.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/936a9280b8b33ffaf98e9497927d7d84cfc87757bf235569fa950c55922ab76c.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/4cb136284aa9e19be2492e23d421297736f888ddf03cd7eebdb690d4f3b929c1.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/43f3dcad386912254bc1b2a6cd8414c3373f465665655584a0cf31b9c2f6ce6f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/73c62366e6dda1ee960692d71e4ff9ba92d193b966986866793e64dec10fdc9b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/d361cbbc4c1c8a5eeda8dda6173926c112f28f0bc49efc7946a0c218b4195fa3.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/eb0939417658eea85bdccd5b4c1c460f836649e442cad3100393e039f8f82fe6.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/5979b3d43ade0ddbf8aa86e3ef5e2951fb677bcc0a39d133433bd58989328130.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/82f2aaeedc6ddb9a8086912bd8064c5aac85437814d7ef6e5a6fb5e22bd71491.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/cf4ed99e5d88c5bbf18d812c6eb7a5b90f12e47f346b33061f6ad6c073d81be7.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/f5f95fe8ca532f13faeb1209981138d786f0df2e061d151fe655a571a8ddd88c.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/5a5f20fcb280a697e2f22c460d796fbd291481760480a764d6fe6a6c83e6380a.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/dba86bd8a4f75d2148b80fb04d525193b29b060fbf8a4743fe1b41e39c4fb25d.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/d7017ff3c9fbda9a376ff72d8a95eb9e0a5858cf50ee82a5b92d6e995550867d.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/a6bf758115a73beafa9124803667e93729442e7cad367a86608ad9ad8655b08b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/24ae44003669f9f9640832b4c9cf9acb8ac3c2adf5ab5a2444a7715b090b3f67.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/b39aac6d9d0b5ea2ce78831602e48e0a48f7f2c792697e9c58de1d45b27a792a.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/9194df1b37d6a7da9ee8fd03abebdc3e81ec6ea366224eecb0cd7d3d66024062.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/f64b3e72b098f53f10ca3f3031b93df60c8ef407510bab8a003c9747e82f6043.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/6ff297c691797ab5fd5222b0c1fa13abc750fc031685a29589eace7748864318.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/8704bbe8a29455b6034d773c57246b1633da5393fd102f87fcb6eca819b82753.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ef4146d94b8e32988b3cb0eba7e967cfa9627a683a1359cf00a1d76aa5022680.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ebf974f9350e2f784145463f0afcccac69f265af0e8b233813617829684a290f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/06a6aaede02e9527e1eb6dad977a7889e22d2dfcc098f9342eacc134c31363df.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/d915555e83e27c4d5f6dcd1badaaed666fa80e5ae11d6d2382e666efe606bd1f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/a189554aa8b3185799fdb5bfb89cc42698c544a1041e65709b0e79d267cabdc5.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/0f310a87cae45484d5da35274ff89463eb966a1aeb32d53a2fb8350cf9d836b2.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/df7598066371b7da446954feb42a1febdb8921cacf436285e85606cae9de4bb5.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ff8d8869f35a9a4243fac1ce8ab5deff7ee161dfc8c2ea1107099ec1cf74e100.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/75d36a907297689726ca96ef721c091c04a879f1f096f503076dd172834a27dc.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/5e6fee09c93c6f1d493bd04dd18cc8043c0b40093d85ed94c2df28ab129b226b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/d7ddd06a2070b76dc05fe12741d7882df5a4312b174a11ce3d98d059fcc17173.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/7bcac404ee6981364837ac1a89033184ff65939507a81caa7c43ea52a195e215.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/392ac948386e63063f941449eeb9199c3f1a05959934c47c5987bbf6aa0721a3.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/df13e0aac1211176a1939bf2198f9e0e7dedd1f043875a4093ce0265cd02744e.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/052649971efbb0e18631044219b92ab68f12dc244041042b24203c88a62377b6.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/1e86ba82b4061a7ef799089ce29826fcba0ba07c77aec6638f20e850d1864144.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/7fe25319e6b2049b96c6659264568defe6a7e21bb3817685970b3a3aa511d8c8.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/3a66049907f84778aa548c747d9a52c2e67ce880c19fa4b0a8b4e58ae96fd9ef.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/54f622cb04525cc1953cc1ffbe12646be3290de6ca378ce5508869860df761e5.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/6fa117d5fcf115733a154f5d0911ce05b103ea33d5eee65d2b08a83605cf6e80.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/cc1d2b8828dc4dba9f0ea7d980eb8b24d2b0792c3282725552a56f7c8929459f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/9b59194c025f9e6fc5c1d60ee444be69f14546b7efa4389a30ee6db88fbc207f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/8686d00f5e82bb2343d8154fad3d66afca1420c45e4f63fe5decb6b9f5b84d2b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/6b7ac224bb235b03798c358125eadc5d805445744543c368ebc0a4f7bb7a4328.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/dfb89354e0b433989eab276004296575f5b5e3ecb8c700cfcee620765ef0e74d.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ef0656c2dce6c462c2e2591b5c43b76c1fe83ebebbaa778c1194706577c46d5c.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/7bfb6c4c45d0dcfbd66456456c5300b3ba83d52f37c5434e3e78baf0b54e5c07.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/c61ee02616c8d1c87be89499cb1ebdcd7267e47e51fa53e10578e8935a8b7aa9.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/f32062ec857d3d40d3a82359511167113065019d09d10755764aec91ba37bb32.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/d678ca3b06ea1872ebb20236ed1099e1b6e1451c51d78ab98d914abead7e4651.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ee4c2e6785e3062be57305190a480ee437f6569471534533dd3524794b125ace.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/9a3ee31c30754a89da9280cbff44440e8e974bfac4c815376a0df768ef926590.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/334ae045a86998d77ed3fca093d7c43dfd5be53f939d156d9e71a885263739c1.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/65b4c081cd081eb8716e435c51abf7882f9aacb47005d77aa336ddf8190f2249.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/e2d38a33de7d378bf8a2989ae6db7b20d9168dd9b4c078225ced7aef9154a370.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/988ece4e5c191b30ff71dd2ce3fa3dc16f22dd3076702c17e9f3373612765c9c.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/272e01e003b0a4ed9b8ebb908d88cf1fbc841f65cf3c5994e99c7e5b10332209.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/eb1fd91a2d9b5feba4504c7ee8182ec680121e83a1571a292d52bf7cac12c396.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ea4967bd3dec3fdcf48ba9682777ae09b1be48ae861a5fb55f8650fccb24aeb5.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/23653a7282567de2cc678619554153f1aff5409061b2a08ffedf208b10c7fd9f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/6e4b9df052f0c4c0b726301c66e4aedc596a01416268857f3742c1eaf6760c64.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/7f82912066c187783d04de0b189314fcde9d33208335827a6d0fa8755637a136.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/f07293e9a6dc9a826363cb52fbc0db1f75fbba49814cb626db63affa74dbc9bc.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/46df84d2163240ae59efe0c91b94b3d23018daa816de1d44405275e17b5f4e77.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/1101fd494d1a226a790d5091f04fae9bcab5543eec8c80a0d3dd8b83a8d31c14.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/2f405ef994ff2c7470116d092ec7e9a8833400354479760742e616f800e19831.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/5f1efddc7e6c6bb7984813794cd275f0cf46d2bf598ac16cc7adc05c9878eb22.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/c641b68789fcc05feb518d6ea7dd5ffa1344c124263e67573c86786766547fa8.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/f9b35c5e0adbd9f0084559cd909d57a1dc8928b5c48887a1119d226226664270.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/3ab2ef4b9b5f1900ff51f892d531b2abc539f9acfb728c841f35dab93bde160f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/c92c58cb868b0c698c9e24ea9dfb63f1f4587a04fbc0cc6d495058ebb7534f69.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/46cb9725c59d409c7e7603f9eab6d2dbd3b29e5d4aef2fb154d5ceda40a33f85.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/b4fd15d151bbcf2f299a24ccd2c7e94afae7d6eccf7208f65b06febc5958d95d.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/191e06e43ac8a9618a5c1f10178e7cdf6e609f14dc7ec56be2ea89ac19b5e253.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/69191aa596bfd2633279e0488152f67565ae47bf3e9e728b9c57376bfd2abdc3.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/222870114e13280aa20077b14588e6d2fa8e7a7b347cde4a01553e395fb40a22.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/ba3636d73e375a6f16a878752464adaa57a03614dbb3e2d68e26d08d686262c3.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/b9b7fb42891473d4906c7acb11f8680565eb02eabce71a8645f917bb1375c0d2.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/7d62853edb09b73996a0d4bb369067e45fc229926a8961207596a3162941dccd.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/c0bcad354275f905e235af359dd789db4110201a4ef1fd7f8d4aae3563ff06a5.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/66397bf25b427e4d910ca117e3ce8afb8e19012403c7a1716696eca2da261884.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/487b35586c4591772393a2d2f430d00f97c3d36ad8ee7be784f130249cac7c31.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/6b664a4b20fde7754d5448f129532fded9103284aa101b50b79f810246f75a3b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/2752173075a4ef17451a0a3db546956389eff82db209711e4a1fff47e90b6065.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/265252b3db2c2501483502b6aadab3ea891f32cae539b5ccc7ef8295b63f4018.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/b5cc6b97c28bd853ebf7d853a7b19e4b5018c01eae8823ac537800c2cbb06011.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/c9b094709f2b2773b5e5258716df0663b2aee98a6ea47c3cb4040322123cb99c.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/7f158d1bfbafe42c7b3118d2b9ea701bda16df10dbdf9c4f2779ea15595a331f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/518548841153bce3488afa3a36ad6e6cbddb4f1689f5e9366ee80e206b6a1ffe.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/629e460d17bf23cce5d75bbe8672e037db86d8d757cc4efd9a1a0f53d435425b.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/667ddeda218b4f95a47792345550d546e00fe2a52a505437d30608cefd0fc4bd.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/20d9f1018284a761162108e9a82d6a73b1fa8a9fd6866a506db77ed07ec5e578.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/1f35f42d565dd11860e30c41241c78bc5f06d724117bfc83b3784c66d52e332f.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/58cdc6432a1cd95f7f6427744019b59d164384edfc54aa51537d4685f847ba39.0
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/journal
+/data/data/com.google.android.googlequicksearchbox/cache/image_manager_disk_cache/61f774ab3a005d56366b267b08b994e5b035ff8eab1e454f00fb2cf7b356a46f.0
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/prons.cache
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559/digest
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559/semantic_fst
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559/grammar_symbols
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559/semantic_symbols
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559/metadata
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contacts/v1539635905559/grammar_clg
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984/grammar_clg
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984/semantic_fst
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984/semantic_symbols
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984/digest
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984/metadata
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/v1536705472984/grammar_symbols
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/hands_free_commands/prons.cache
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/music_names
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/music_names/v1536705480879
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/music_names/v1536705480879/metadata
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/music_names/v1536705480879/SONG_NAME.fst
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/music_names/v1536705480879/SONG_NAME.syms
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/music_names/v1536705480879/digest
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contact_names
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contact_names/v1539635914600
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contact_names/v1539635914600/CONTACT_NAME.fst
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contact_names/v1539635914600/CONTACT_NAME.syms
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contact_names/v1539635914600/digest
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/contact_names/v1539635914600/metadata
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/app_names
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/app_names/v1543480552712
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/app_names/v1543480552712/APP_NAME.fst
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/app_names/v1543480552712/digest
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/app_names/v1543480552712/metadata
+/data/data/com.google.android.googlequicksearchbox/cache/g3_grammars/en-US/app_names/v1543480552712/APP_NAME.syms
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/79b5269c206115a4_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/aa9db037f918da1f_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/aebae57f6f7dcdb9_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e446f170a9c613a1_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/2f06680d22ff6fbf_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ffa3f495612db016_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/69c20684e88c955b_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ef65bf506ba3e339_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/014821f96953c508_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/1180e087d9bd1160_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3b36cd7b2f6df416_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e5701f55e9ce22c8_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/9ec568d6b3dc0762_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/88f57d1088993219_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/83fd8318538fbe29_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/22930ed83887868c_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/333bf7ac47cc9770_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/adfd903f6a8ce876_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/2a1237e13688c120_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3f3f18bf8e704931_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/b8262fc8c9591057_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/59829c5897cb9d93_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/index-dir
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/index-dir/the-real-index
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/feb5af6bca039e09_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/0050e1bcb6d6546c_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3a3d22ec4fc7ad21_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/5f269f49d811cd82_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e1cf52389fbebceb_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/359582e09cf26c7a_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/964347070fc23ea0_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/fb7d48b4e068afda_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/a5c586e8f0aeb850_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/37fadb6203e4e379_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/a7c25e80d95ef15d_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/a11922fc39ec0249_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/bebb870e573c852c_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/89c95cdfc9b59f48_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/fcb3fff3117a2d12_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/5c48229cf8e35d0b_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/55a3abf82b2a626c_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/f79ff1a77d9e9492_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ca9ccf019443fb16_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/986246894e9084ec_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e546ff051d5dbafc_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/23b6960b741da560_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/8c9509f47aa07ed8_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/81be3f3a1ebb3222_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/4edb09d9737acff2_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e89950485ea68183_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e40df7fec15afae9_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/cc338158aa28d723_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/4797a2fb8c7eac6b_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/411f64d386b7c4fe_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/29f6d5d8b27eb0c5_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e99ae68f3e468751_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/502dafda143b5a74_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/d1783d0a170fdb8e_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ac27a389f7bf6b67_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/40300177b5c0050d_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/4a8de756f1428237_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e029cf1b0932611f_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/c69ff5c7e450ab22_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/bdfd0aa008d40005_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/40dd89dd968602d3_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/d04eb6456f31d2f7_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ff4e7b79b6327627_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/b136f3771ffb9958_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/207bb56723cc5c3a_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/95035b9448e65cf2_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/73afbe7f6b7a496c_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/84b41c998e542199_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/d68d127f97a27059_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/037a0f2e4460355f_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/da584b3cb202e078_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/2d6c5245e29028b2_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ca9b25d228896196_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/0e2708cf50936235_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ab39283d30a39dd1_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/f721910d7c288b54_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/e1cd683779c2ea08_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/d1a8c9a323296d5b_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/4821c08320e603ae_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/01a2afcf422b3b4f_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/d41f0a4d475402fd_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/fcacc70d27c27f8b_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/f76d072b8c546a89_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/aff6b5b6e20cc2fa_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/8037b4d4c7774071_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/index
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/1e6d9e3ecd002bc9_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3cfe648fbdd026a7_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/ab5d3ea4f0904068_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/9a785469b604c8af_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/472d78242cce2d22_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/503a7645e7f2d973_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/593a42d396c32634_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/b6ee82fb12843073_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3561efa2281c73ed_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/318755c427839e86_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3ef890a79ddb7e0c_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/d9772c6ee701ad39_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/5b3eff799688e021_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/30bb71565ae0cc27_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/54afe61c6fcf0e3f_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/8c9d078e6dbc501d_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/32a4f6fc17306385_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/0111bfd7286ca658_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/55ccaf33bd76fb46_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/65b80b5a552aaeaf_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/30fffcc41f7846bf_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/5377846224f95fc9_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/f9c93b74a177706b_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/94239554b50b59b5_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/14ced047ba93cdd3_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/2ceea49fe8c9e2fe_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/disk_cache/3ec7cdbc127eae35_0
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/version
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/prefs
+/data/data/com.google.android.googlequicksearchbox/cache/cronet-async/prefs/local_prefs.json
+/data/data/com.google.android.googlequicksearchbox/cache/si
+/data/data/com.google.android.googlequicksearchbox/databases
+/data/data/com.google.android.googlequicksearchbox/databases/icing-mdh.db-wal
+/data/data/com.google.android.googlequicksearchbox/databases/google_app_measurement.db-shm
+/data/data/com.google.android.googlequicksearchbox/databases/icing-mdh.db-shm
+/data/data/com.google.android.googlequicksearchbox/databases/google_app_measurement_local.db
+/data/data/com.google.android.googlequicksearchbox/databases/google_app_measurement_local.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/databases/google_app_measurement.db-wal
+/data/data/com.google.android.googlequicksearchbox/databases/google_app_measurement.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/databases/google_app_measurement.db
+/data/data/com.google.android.googlequicksearchbox/databases/icing-mdh.db-wipecheck
+/data/data/com.google.android.googlequicksearchbox/databases/launcher.db
+/data/data/com.google.android.googlequicksearchbox/databases/launcher.db-wal
+/data/data/com.google.android.googlequicksearchbox/databases/icing-mdh.db
+/data/data/com.google.android.googlequicksearchbox/databases/launcher.db-shm
+/data/data/com.google.android.googlequicksearchbox/shared_prefs
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/AccountSwitcherDrawerPresenter.Prefs.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/com.google.android.gms.appid.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/consecutive_crash_stats.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/uncaught_exception_handler_stats.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/VoiceInteractionService.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/interactor_process_uncaught_exception_handler_stats.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/WebViewChromiumPrefs.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/com.android.launcher3.managedusers.prefs.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/com.android.launcher3.prefs.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/GEL.GSAPrefs.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/com.google.android.gms.measurement.prefs.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/ThrottlingLogger.xml
+/data/data/com.google.android.googlequicksearchbox/shared_prefs/default_process_uncaught_exception_handler_stats.xml
+)F1L3N4M3";
+
+#else
+
+    static const char* kInodeValues = R"1N0D3(
+66323@1117133
+66323@1127134
+66323@1137135
+66323@1137136
+66323@1137137
+)1N0D3";
+
+    const char* kPathNames = R"F1L3N4M3(
+/
+/data/
+/data/data/
+/data/data/file
+/data/data/last_file
+)F1L3N4M3";
+
+#endif
+
+
+    std::vector<std::string> inode_values = ParseLines(kInodeValues);
+    std::vector<std::string> path_names = ParseLines(kPathNames);
+
+    std::vector<Inode> inodes = ParseInodes(inode_values);
+
+    return PathEntryTree{ PathEntry::Zip(inodes, path_names) };
+}
+
+class FakeSystemCall : public SystemCall {
+ public:
+  // stat(2)
+  virtual int stat(const char *pathname, struct stat *statbuf) override {
+    if (pathname == nullptr || statbuf == nullptr) {
+      errno = EINVAL;
+      return -1;
+    }
+
+    std::optional<PathEntry> maybe_path_entry = path_entries_.GetEntryFor(pathname);
+
+    if (!maybe_path_entry) {
+      errno = ENOENT;
+      return -1;
+    }
+
+    memset(statbuf, 0, sizeof(*statbuf));
+
+    Inode inode = maybe_path_entry->inode;
+    statbuf->st_dev = makedev(static_cast<int>(inode.device_major),
+                              static_cast<int>(inode.device_minor));
+    statbuf->st_ino = static_cast<ino_t>(inode.inode);
+
+    return 0;
+  }
+
+  static constexpr bool debug{false};
+
+#define FS_LOG_DEBUG if (debug) LOG(DEBUG)
+
+  // opendir(3)
+  virtual DIR *opendir(const char *name) override {
+
+    FS_LOG_DEBUG << "opendir(" << name << ")";
+
+    std::string name_str{name};
+    if (path_entries_.HasDirectory(name_str)) {
+        CHECK(!state_.open_);
+
+        std::vector<PathEntry> children = path_entries_.OpenDirectory(name_str);
+
+        state_ = State::Open(name_str, std::move(children));
+
+        FS_LOG_DEBUG << "opendir - success, state address: " << &state_;
+
+        return get_state_as_dir();
+    }
+
+    FS_LOG_DEBUG << "opendir - no matching entry, scanned " << path_entries_.size();
+
+    // TODO. errno.
+    errno = EINVAL;
+    return nullptr;
+  }
+
+  // readdir(3)
+  virtual struct dirent *readdir(DIR *dirp) override {
+    DCHECK(dirp != nullptr);
+    // We could also errno=EBADF but this seems more apropro to test.
+
+    State* state = dir_to_state(dirp);
+    (void) state;
+    DCHECK(state != nullptr);
+
+    std::optional<PathEntry> path_entry_opt = state->ReadDir();
+
+    if (!path_entry_opt) {
+      FS_LOG_DEBUG << "readdir(" << &state << ") - no children left ";
+
+      // No more children left. We have exhausted them all.
+      return nullptr;
+    }
+
+    PathEntry path_entry = *path_entry_opt;
+
+    FS_LOG_DEBUG << "readdir(" << &state << ") - called for " << path_entry.path;
+
+    // TODO. impelment this.
+    static struct dirent dir_ent{};
+
+    // Clear it again.
+    memset(&dir_ent, 0, sizeof(dir_ent));
+
+    dir_ent.d_ino = path_entry.inode.inode;
+
+    FS_LOG_DEBUG << "readdir(" << &state << ") - children check" << path_entry.path;
+
+    // Is this a file (no children) or a directory (some children)?
+    //
+    // In reality some directories might be empty too, but lets not worry about it yet.
+    std::vector<PathEntry> children = path_entries_.OpenDirectory(path_entry.path);
+
+    if (children.empty()) {
+      dir_ent.d_type = DT_REG;
+    } else {
+      dir_ent.d_type = DT_DIR;
+    }
+
+    // the d_name must be just the final name component of a path.
+    // Do not include the full path.
+
+    std::string_view name_view = path_entry.path.Name();
+    DCHECK_LT(name_view.size(), sizeof(dir_ent.d_name));
+
+    std::copy(name_view.begin(),
+              name_view.end(),
+              &dir_ent.d_name[0]);
+    dir_ent.d_name[name_view.size()] = '\0';
+
+    FS_LOG_DEBUG << "readdir(" << &state << ") - return , d_name=\"" << dir_ent.d_name << "\""
+                 << ", d_type=" << (dir_ent.d_type == DT_REG ? "DT_REG" : "DT_DIR");
+
+    return &dir_ent;
+  }
+
+  // closedir(3)
+  virtual int closedir(DIR *dirp) override {
+    CHECK(dirp != nullptr);
+    State* state = dir_to_state(dirp);
+    state->Close();
+
+    return 0;
+  }
+
+  FakeSystemCall() {
+    path_entries_ = CreateFakePathEntries();
+  }
+
+ private:
+  struct State {
+    std::string name_;
+    bool open_{false};
+    std::vector<PathEntry> children;
+
+    static State Open(std::string name, std::vector<PathEntry> children) {
+      return State{name, /*open*/true, std::move(children)};
+    }
+
+    std::optional<PathEntry> ReadDir() {
+      if (children.empty()) {
+        return {};
+      }
+
+      PathEntry last = children.back();
+      children.pop_back();
+
+      return { std::move(last) };
+    }
+
+    void Close() {
+      CHECK(open_);
+      open_ = false;
+    }
+  };
+
+  DIR* get_state_as_dir() {
+    return reinterpret_cast<DIR*>(reinterpret_cast<void*>(&state_));
+  }
+
+  State* dir_to_state(DIR* dirp) {
+    return reinterpret_cast<State*>(reinterpret_cast<void*>(dirp));
+  }
+
+  State state_;
+
+  PathEntryTree path_entries_;
+};
+
+class MockSystemCall : public SystemCall {
+ public:
+  INJECT(MockSystemCall()) {
+    // Delegate calls to a fake (see the googlemock CookBook for more details).
+    // https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md#delegating-calls-to-a-fake
+    DelegateToFake();
+
+    WorkAroundForNiceMock();
+  }
+
+  ~MockSystemCall() {
+  }
+
+  MOCK_METHOD2(stat, int(const char *, struct stat *));
+  MOCK_METHOD1(opendir, DIR*(const char *));
+  MOCK_METHOD1(readdir, struct dirent*(DIR*));
+  MOCK_METHOD1(closedir, int(DIR*));
+
+  // Delegates the default actions of the methods to a FakeSystemCall object.
+  // This must be called *before* the custom ON_CALL() statements.
+  void DelegateToFake() {
+    ON_CALL(*this, stat(_,_))
+        .WillByDefault(Invoke(&fake_, &FakeSystemCall::stat));
+    ON_CALL(*this, opendir(_))
+        .WillByDefault(Invoke(&fake_, &FakeSystemCall::opendir));
+    ON_CALL(*this, readdir(_))
+        .WillByDefault(Invoke(&fake_, &FakeSystemCall::readdir));
+    ON_CALL(*this, closedir(_))
+        .WillByDefault(Invoke(&fake_, &FakeSystemCall::closedir));
+  }
+
+  void WorkAroundForNiceMock();
+
+ private:
+  FakeSystemCall fake_;
+};
+
+// Don't print any warnings when methods are executed without EXPECT_CALL.
+//using NiceMockSystemCall = NiceMock<MockSystemCall>;
+
+// Can't use NiceMock<MockSystemCall> here, fails with this compilation error
+//
+// external/google-fruit/include/fruit/impl/injection_errors.h:107:3: error: static_assert failed due to requirement 'AlwaysFalse<NiceMock<MockSystemCall> >::value' "C::Inject is a signature, but does not return a C. Maybe the class C has no Inject typedef and inherited the base class' one? If that's not the case, make sure it returns just C, not C* or other types."
+using NiceMockSystemCall = MockSystemCall;
+
+void MockSystemCall::WorkAroundForNiceMock() {
+  // Should be able to use NiceMock instead, but fruit is having problems compiling.
+  EXPECT_CALL(*this, stat).Times(AtLeast(0));
+  EXPECT_CALL(*this, opendir).Times(AtLeast(0));
+  EXPECT_CALL(*this, readdir).Times(AtLeast(0));
+  EXPECT_CALL(*this, closedir).Times(AtLeast(0));
+}
+
+fruit::Component<SearchDirectories, NiceMockSystemCall> getTestComponents() {
+    return fruit::createComponent()
+        .bind<SystemCall, NiceMockSystemCall>();
+}
+
+// TODO: there might be a helper or similar to do this instead.
+template <typename T>
+static std::vector<T> subscribe_drain(std::pair<rxcpp::observable<T>,
+                                                std::unique_ptr<SearchDirectories::RxAnyConnectable>> pair) {
+  rxcpp::observable<T>& obs = pair.first;
+  std::unique_ptr<SearchDirectories::RxAnyConnectable>& connectable_ptr = pair.second;
+
+  std::vector<T> vec;
+
+  obs.subscribe([&vec](auto&& x) {
+    vec.push_back(IORAP_FORWARD_LAMBDA(x));
+  });
+
+  CHECK(connectable_ptr != nullptr);
+
+  // Execute above lambda, blocking until all values are drained.
+  connectable_ptr->connect();
+
+  return vec;
+}
+
+struct SearchDirectoriesParam {
+  std::vector<std::string> root_directories;
+  std::vector<Inode> search_inodes;
+  std::vector<InodeResult> expected_results;
+};
+
+template <typename It>
+std::ostream& iterator_to_stream(std::ostream& os, It begin, It end) {
+  os << "{";
+  while (begin != end) {
+    os << *begin;
+    os << ",";
+
+    ++begin;
+  }
+  os << "}";
+
+  return os;
+}
+
+template <typename T>
+std::ostream& container_to_stream(std::ostream& os, T&& c) {
+  return iterator_to_stream(os, c.begin(), c.end());
+}
+
+std::ostream& operator<<(std::ostream& os, const SearchDirectoriesParam& p) {
+  os << "{";
+  os << "root_directories:";
+  container_to_stream(os, p.root_directories);
+  os << ", ";
+  os << "search_inodes:";
+  container_to_stream(os, p.search_inodes) << ", ";
+  os << "expected_results:";
+  container_to_stream(os, p.expected_results);
+  os << "}";
+  return os;
+}
+
+struct SearchDirectoriesTest :
+    public ::testing::TestWithParam<SearchDirectoriesParam> {
+
+  static void SetUpTestCase() {
+    ConfigureLogging();
+  }
+
+  virtual void SetUp() override {
+    auto pair =
+        search.FindFilenamesFromInodesPair(GetParam().root_directories,
+                                           GetParam().search_inodes,
+                                           SearchMode::kInProcessDirect);
+
+    actual = subscribe_drain(std::move(pair));
+    expected = GetParam().expected_results;
+  }
+
+  virtual void TearDown() override {
+    // TODO.
+  }
+
+ protected:
+  fruit::Injector<SearchDirectories, NiceMockSystemCall> injector{getTestComponents};
+
+  SearchDirectories& search = injector.get<SearchDirectories&>();
+  MockSystemCall& mock_syscall = injector.get<NiceMockSystemCall&>();
+
+  std::vector<InodeResult> actual;
+  std::vector<InodeResult> expected;
+};
+
+TEST_P(SearchDirectoriesTest, ElementsAreArrayMatcher) {
+  EXPECT_THAT(actual, ElementsAreArray(expected));
+}
+
+auto MakeEmptyInodes(std::vector<std::string> root_dirs) {
+  return SearchDirectoriesParam{root_dirs, /*inodes*/{}, /*actual*/{}};
+}
+
+// When are are 0 inodes to search for, the results will be empty.
+INSTANTIATE_TEST_CASE_P(EmptyResults,
+                        SearchDirectoriesTest,
+                        ::testing::Values(
+                            MakeEmptyInodes(/*root_dirs*/{}),
+                            MakeEmptyInodes(/*root_dirs*/{""}),
+                            MakeEmptyInodes(/*root_dirs*/{"/"}),
+                            MakeEmptyInodes(/*root_dirs*/{"/abc"})
+                        ));
+
+
+auto MakeAllFailInodes(std::vector<std::string> root_dirs, std::vector<Inode> inodes) {
+  std::vector<InodeResult> results;
+  for (const Inode& inode : inodes) {
+    results.push_back(InodeResult::makeFailure(inode, InodeResult::kCouldNotFindFilename));
+  }
+
+  return SearchDirectoriesParam{root_dirs, inodes, results};
+}
+
+// TODO: fixme
+
+#if 1
+
+// When none of the inodes can be found, all results will be failing results.
+INSTANTIATE_TEST_CASE_P(AllResultsAreErrorCouldNotFindFilename,
+                        SearchDirectoriesTest,
+                        ::testing::Values(
+                            // TODO: why is empty root dir failing?
+                            // MakeAllFailInodes(/*root_dirs*/{}, {Inode{1,2,3}}),
+                            MakeAllFailInodes(/*root_dirs*/{"/"}, {Inode{1,2,3}}),
+                            MakeAllFailInodes(/*root_dirs*/{"/data"}, {Inode{1,2,3}}),
+                            MakeAllFailInodes(/*root_dirs*/{"/data/data"}, {Inode{1,2,3}})
+                        ));
+
+auto MakeAllPassInodes(std::vector<std::string> root_dirs, std::vector<std::string> inodes, std::vector<std::string> paths) {
+  std::vector<InodeResult> results;
+
+  std::vector<Inode> inodes_actual;
+
+  size_t i = 0;
+  for (const std::string& inode_str : inodes) {
+    Inode inode;
+    std::string error_msg;
+
+    CHECK(Inode::Parse(inode_str, &inode, &error_msg));
+
+    inodes_actual.push_back(inode);
+
+    std::string& path = paths[i];
+    results.push_back(InodeResult::makeSuccess(inode, path));
+
+    ++i;
+  }
+
+  return SearchDirectoriesParam{root_dirs, inodes_actual, results};
+}
+
+// Find all the inodes. Yay.
+INSTANTIATE_TEST_CASE_P(AllResultsAreSuccess,
+                        SearchDirectoriesTest,
+                        ::testing::Values(
+                            MakeAllPassInodes(/*root_dirs*/{"/"}, {"66323@1127133"}, {"/data"})
+                        ));
+
+#endif