Test ownership migration rules am: 1c4892a15a

Original change: https://android-review.googlesource.com/c/platform/system/chre/+/2967882

Change-Id: Ia6da0c8a2d5ee52da203f40c1e0b075392985177
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 28b7151..da0dc83 100644
--- a/Android.bp
+++ b/Android.bp
@@ -19,20 +19,6 @@
     default_applicable_licenses: ["system_chre_license"],
 }
 
-// Added automatically by a large-scale-change that took the approach of
-// 'apply every license found to every target'. While this makes sure we respect
-// every license restriction, it may not be entirely correct.
-//
-// e.g. GPL in an MIT project might only apply to the contrib/ directory.
-//
-// Please consider splitting the single license below into multiple licenses,
-// taking care not to lose any license_kind information, and overriding the
-// default license using the 'licenses: [...]' property on targets as needed.
-//
-// For unused files, consider creating a 'fileGroup' with "//visibility:private"
-// to attach the license to, and including a comment whether the files may be
-// used in the current project.
-// See: http://go/android-license-faq
 license {
     name: "system_chre_license",
     visibility: [":__subpackages__"],
@@ -46,22 +32,36 @@
 }
 
 filegroup {
-    name: "contexthub_generic_aidl_hal_core",
+    name: "contexthub_hal_socket",
+    srcs: ["host/common/socket_server.cc"]
+}
+
+filegroup {
+    name: "contexthub_hal_wifi",
+    srcs: ["host/common/wifi_ext_hal_handler.cc"]
+}
+
+filegroup {
+    name: "contexthub_hal_core",
     srcs: [
+        "host/common/bt_snoop_log_parser.cc",
+        "host/common/config_util.cc",
+        "host/common/log.cc",
+        "host/common/log_message_parser.cc",
         "host/common/preloaded_nanoapp_loader.cc",
         "host/common/time_syncer.cc",
-        "host/common/config_util.cc",
-        "host/common/log_message_parser.cc",
-        "host/common/bt_snoop_log_parser.cc",
-        "host/hal_generic/common/permissions_util.cc",
         "host/hal_generic/common/hal_client_manager.cc",
         "host/hal_generic/common/multi_client_context_hub_base.cc",
+        "host/hal_generic/common/permissions_util.cc",
     ],
 }
 
 cc_library_static {
     name: "chre_client",
     vendor_available: true,
+    local_include_dirs: [
+        "chre_api/include/chre_api",
+    ],
     export_include_dirs: [
         "host/common/include",
         "platform/shared/include",
@@ -70,17 +70,55 @@
     srcs: [
         "host/common/file_stream.cc",
         "host/common/fragmented_load_transaction.cc",
+        "host/common/hal_client.cc",
         "host/common/host_protocol_host.cc",
+        "host/common/log.cc",
+        "host/common/pigweed/hal_channel_output.cc",
+        "host/common/pigweed/hal_rpc_client.cc",
         "host/common/socket_client.cc",
         "platform/shared/host_protocol_common.cc",
     ],
     header_libs: ["chre_flatbuffers"],
     export_header_lib_headers: ["chre_flatbuffers"],
     shared_libs: [
+        "android.hardware.contexthub-V3-ndk",
+        "libbinder_ndk",
         "libcutils",
         "liblog",
         "libutils",
     ],
+    cflags: [
+        "-DCHRE_IS_HOST_BUILD",
+        "-DCHRE_MESSAGE_TO_HOST_MAX_SIZE=4000", // Needed to import CHRE APIs.
+        "-Wall",
+        "-Werror",
+        "-Wthread-safety", // Need to be explicitly set
+    ],
+    defaults: [
+        "pw_rpc_cflags_chre",
+        "pw_rpc_nanopb_lib_dependencies",
+    ],
+}
+
+cc_library {
+    name: "chre_metrics_reporter",
+    export_include_dirs: [
+        "host/common/include",
+    ],
+    srcs: [
+        "host/common/metrics_reporter.cc",
+        "host/common/log.cc",
+    ],
+    shared_libs: [
+        "android.frameworks.stats-V1-ndk",
+        "chremetrics-cpp",
+        "chre_atoms_log",
+        "libbinder_ndk",
+        "libcutils",
+        "liblog",
+        "libutils",
+    ],
+    vendor: true,
     cflags: ["-Wall", "-Werror"]
 }
 
@@ -100,7 +138,76 @@
         "liblog",
         "libutils",
     ],
-    static_libs: ["chre_client"],
+    static_libs: [
+        "chre_client",
+        "chre_host_common"
+    ],
+}
+
+genrule {
+    name: "rpc_world_proto_header",
+    defaults: [
+        "pw_rpc_generate_nanopb_proto",
+    ],
+    srcs: ["apps/rpc_world/rpc/rpc_world.proto"],
+    out: [
+        "rpc_world.pb.h",
+    ],
+}
+
+genrule {
+    name: "rpc_world_proto_source",
+    defaults: [
+        "pw_rpc_generate_nanopb_proto",
+    ],
+    srcs: ["apps/rpc_world/rpc/rpc_world.proto"],
+    out: [
+        "rpc_world.pb.c",
+    ],
+}
+
+genrule {
+    name: "rpc_world_rpc_header",
+    defaults: [
+        "pw_rpc_generate_nanopb_rpc_header",
+    ],
+    srcs: ["apps/rpc_world/rpc/rpc_world.proto"],
+    out: [
+        "rpc_world.rpc.pb.h",
+    ],
+}
+
+cc_binary {
+    name: "chre_test_rpc",
+    vendor: true,
+    local_include_dirs: [
+        "chre_api/include/chre_api",
+        "util/include",
+    ],
+    srcs: [
+        "host/common/test/chre_test_rpc.cc",
+    ],
+    cflags: ["-Wall", "-Werror"],
+    shared_libs: [
+        "libcutils",
+        "liblog",
+        "libutils",
+    ],
+    static_libs: [
+        "chre_client",
+        "libprotobuf-c-nano",
+    ],
+    generated_sources: [
+        "rpc_world_proto_source",
+    ],
+    generated_headers: [
+        "rpc_world_proto_header",
+        "rpc_world_rpc_header",
+    ],
+    defaults: [
+        "pw_rpc_cflags_chre",
+        "pw_rpc_nanopb_lib_dependencies",
+    ],
 }
 
 cc_binary {
@@ -131,23 +238,33 @@
 cc_binary {
     name: "chre_aidl_hal_client",
     vendor: true,
+    cpp_std: "c++20",
     local_include_dirs: [
         "host/common/include",
         "chre_api/include",
     ],
     srcs: [
-        "host/common/file_stream.cc",
         "host/common/chre_aidl_hal_client.cc",
+        "host/common/file_stream.cc",
+        "host/common/log.cc",
     ],
     shared_libs: [
-        "android.hardware.contexthub-V2-ndk",
+        "android.hardware.contexthub-V3-ndk",
         "libbase",
         "libbinder_ndk",
         "libjsoncpp",
         "liblog",
         "libutils",
     ],
-    cflags: ["-Wall", "-Werror", "-fexceptions"],
+    static_libs: [
+        "chre_client",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-fexceptions",
+        "-DLOG_TAG=\"CHRE.HAL.CLIENT\"",
+    ],
 }
 
 cc_test {
@@ -314,7 +431,7 @@
         "host/common/include",
     ],
     shared_libs: [
-        "android.hardware.contexthub-V2-ndk",
+        "android.hardware.contexthub-V3-ndk",
         "libcutils",
         "libutils",
     ],
@@ -328,27 +445,65 @@
     ],
 }
 
-cc_test_host {
-    name: "hal_unit_tests",
+cc_library_static {
+    name: "chre_host_common",
     vendor: true,
+    host_supported: true,
+    cpp_std: "c++20",
     srcs: [
-        "host/test/**/*_test.cc",
+        "host/common/log.cc",
     ],
     local_include_dirs: [
         "util/include",
         "host/common/include",
     ],
+    shared_libs: [
+        "libcutils",
+        "liblog",
+        "libutils",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-DCHRE_IS_HOST_BUILD",
+    ],
+}
+
+cc_test_host {
+    name: "hal_unit_tests",
+    vendor: true,
+    srcs: [
+        "host/test/**/*_test.cc",
+        "host/hal_generic/common/hal_client_manager.cc",
+        "host/common/fragmented_load_transaction.cc",
+        "host/common/hal_client.cc",
+    ],
+    local_include_dirs: [
+        "host/common/include",
+        "host/hal_generic/common/",
+        "util/include/",
+        "host/hal_generic/aidl/",
+        "platform/shared/include/",
+    ],
     static_libs: [
-        "android.hardware.contexthub-V2-ndk",
+        "android.hardware.contexthub-V3-ndk",
+        "chre_flags_c_lib",
+        "chre_host_common",
         "event_logger",
         "libgmock",
     ],
     shared_libs: [
         "libcutils",
         "libutils",
+        "android.hardware.contexthub-V3-ndk",
+        "liblog",
+        "libjsoncpp",
+        "libbinder_ndk",
+        "server_configurable_flags",
     ],
     header_libs: [
         "chre_flatbuffers",
+        "chre_api",
     ],
     defaults: [
         "chre_linux_cflags",
@@ -358,6 +513,9 @@
         "-Werror",
         "-DCHRE_IS_HOST_BUILD",
     ],
+    test_options: {
+        unit_test: true,
+    },
 }
 
 genrule {
@@ -505,15 +663,20 @@
         "platform/include",
         "platform/linux/include",
         "platform/shared/include",
+        "platform/shared/pw_trace/include",
         "util/include",
     ],
     cflags: [
         "-DCHRE_MESSAGE_TO_HOST_MAX_SIZE=4096",
         "-DCHRE_MINIMUM_LOG_LEVEL=CHRE_LOG_LEVEL_DEBUG",
         "-DCHRE_ASSERTIONS_ENABLED=true",
+        "-DCHRE_BLE_SUPPORT_ENABLED=true",
         "-DCHRE_FILENAME=__FILE__",
         "-DGTEST",
     ],
+    header_libs: [
+        "chre_flatbuffers",
+    ],
     static_libs: [
         "chre_linux",
         "libgmock",
@@ -523,23 +686,39 @@
     },
 }
 
-// pw_rpc rules instantiation
+// PW_RPC rules.
 
 cc_defaults {
     name: "pw_rpc_cflags_chre",
     cflags: [
         "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-        "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+        "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
         "-DPW_RPC_DYNAMIC_ALLOCATION",
     ],
 }
 
+// Lib dependencies for apps and libs using PW_RPC with nanopb.
+cc_defaults {
+    name: "pw_rpc_nanopb_lib_dependencies",
+    static_libs: [
+        "pw_containers",
+        "pw_protobuf",
+        "pw_rpc_chre",
+        "pw_rpc_nanopb_chre",
+        "pw_status",
+        "pw_stream",
+        "pw_varint",
+    ],
+}
+
 cc_library_static {
     name: "pw_rpc_chre",
     defaults: [
         "pw_rpc_cflags_chre",
         "pw_rpc_defaults",
     ],
+    host_supported: true,
+    vendor_available: true,
 }
 
 cc_library_static {
@@ -556,6 +735,8 @@
         "pw_rpc_raw_chre",
         "pw_rpc_chre",
     ],
+    host_supported: true,
+    vendor_available: true,
 }
 
 cc_library_static {
@@ -567,6 +748,8 @@
     static_libs: [
         "pw_rpc_chre",
     ],
+    host_supported: true,
+    vendor_available: true,
 }
 
 genrule {
@@ -589,7 +772,6 @@
     out: [
         "rpc_test.pb.c",
     ],
-
 }
 
 genrule {
@@ -609,7 +791,9 @@
     isolated: false,
     test_suites: ["general-tests"],
     srcs: [
-        "test/simulation/*.cc",
+        "test/simulation/test_base.cc",
+        "test/simulation/test_util.cc",
+        "test/simulation/*_test.cc",
     ],
     generated_sources: [
         "rpc_test_proto_source",
@@ -626,16 +810,11 @@
         "chre_linux",
         "chre_pal_linux",
         "libprotobuf-c-nano",
-        "pw_containers",
-        "pw_protobuf",
-        "pw_rpc_nanopb_chre",
-        "pw_rpc_chre",
-        "pw_stream",
-        "pw_varint",
     ],
     defaults: [
         "chre_linux_cflags",
         "pw_rpc_cflags_chre",
+        "pw_rpc_nanopb_lib_dependencies",
     ],
     sanitize: {
         address: true,
@@ -708,7 +887,6 @@
         "platform/shared/platform_gnss.cc",
         "platform/shared/platform_wifi.cc",
         "platform/shared/system_time.cc",
-        "platform/shared/tracing.cc",
         "platform/shared/version.cc",
         "platform/shared/sensor_pal/platform_sensor.cc",
         "platform/shared/sensor_pal/platform_sensor_type_helpers.cc",
@@ -762,10 +940,11 @@
         "-DCHRE_SENSORS_SUPPORT_ENABLED",
         "-DCHRE_WIFI_SUPPORT_ENABLED",
         "-DCHRE_WIFI_NAN_SUPPORT_ENABLED",
-        "-DCHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS=3000000000",
-        "-DCHRE_TEST_WIFI_RANGING_RESULT_TIMEOUT_NS=3000000000",
-        "-DCHRE_TEST_ASYNC_RESULT_TIMEOUT_NS=3000000000",
+        "-DCHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS=300000000",
+        "-DCHRE_TEST_WIFI_RANGING_RESULT_TIMEOUT_NS=300000000",
+        "-DCHRE_TEST_ASYNC_RESULT_TIMEOUT_NS=300000000",
         "-DCHRE_BLE_READ_RSSI_SUPPORT_ENABLED",
+        "-Wextra-semi",
     ],
 }
 
@@ -800,9 +979,12 @@
         "libjsoncpp",
         "libutils",
         "liblog",
+        "chre_metrics_reporter",
+        "server_configurable_flags",
     ],
     static_libs: [
         "chre_config_util",
+        "chre_flags_c_lib",
     ]
 }
 
@@ -856,8 +1038,12 @@
     host_supported: true,
     proto: {
         type: "lite",
+        include_dirs: ["external/protobuf/src"],
     },
-    srcs: ["apps/test/common/chre_api_test/rpc/chre_api_test.proto"],
+    srcs: [
+        "apps/test/common/chre_api_test/rpc/chre_api_test.proto",
+        ":libprotobuf-internal-protos",
+    ],
     sdk_version: "current",
 }
 
@@ -876,3 +1062,16 @@
     ],
     cflags: ["-Wall", "-Werror"]
 }
+
+aconfig_declarations {
+    name: "chre_flags",
+    package: "android.chre.flags",
+    srcs: ["chre_flags.aconfig"],
+}
+
+cc_aconfig_library {
+    name: "chre_flags_c_lib",
+    aconfig_declarations: "chre_flags",
+    host_supported: true,
+    vendor: true,
+}
diff --git a/Android.mk b/Android.mk
index f1145ad..a25d1ca 100644
--- a/Android.mk
+++ b/Android.mk
@@ -98,7 +98,12 @@
     chremetrics-cpp \
     chre_atoms_log \
     android.frameworks.stats-V1-ndk \
-    libbinder_ndk
+    libbinder_ndk \
+    chre_metrics_reporter \
+    server_configurable_flags
+
+LOCAL_STATIC_LIBRARIES := \
+    chre_flags_c_lib
 
 LOCAL_SRC_FILES += $(MSM_SRC_FILES)
 LOCAL_C_INCLUDES += $(MSM_INCLUDES)
@@ -107,20 +112,33 @@
 LOCAL_CFLAGS += -Wno-sign-compare
 LOCAL_CFLAGS += -Wno-c++11-narrowing
 LOCAL_CFLAGS += -Wno-deprecated-volatile
-PIGWEED_DIR = external/pigweed
-PIGWEED_DIR_RELPATH = ../../$(PIGWEED_DIR)
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_bytes/public
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public_overrides
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/standard_library_public
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_preprocessor/public
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_tokenizer/public
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_varint/public
-LOCAL_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public
 
-LOCAL_SRC_FILES += $(PIGWEED_DIR_RELPATH)/pw_tokenizer/detokenize.cc
-LOCAL_SRC_FILES += $(PIGWEED_DIR_RELPATH)/pw_tokenizer/decode.cc
-LOCAL_SRC_FILES += $(PIGWEED_DIR_RELPATH)/pw_varint/varint.cc
+# Pigweed (PW)
+PW_DIR = external/pigweed
+PW_DIR_RELPATH = ../../$(PW_DIR)
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_assert/assert_compatibility_public_overrides
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_assert/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_base64/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_bytes/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_containers/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_log_tokenized/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_log/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_polyfill/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_polyfill/public_overrides
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_polyfill/standard_library_public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_preprocessor/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_result/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_span/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_status/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_string/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_tokenizer/public
+LOCAL_CFLAGS += -I$(PW_DIR)/pw_varint/public
+LOCAL_CFLAGS += -I$(PW_DIR)/third_party/fuchsia/repo/sdk/lib/stdcompat/include
+
+LOCAL_SRC_FILES += $(PW_DIR_RELPATH)/pw_tokenizer/decode.cc
+LOCAL_SRC_FILES += $(PW_DIR_RELPATH)/pw_tokenizer/detokenize.cc
+LOCAL_SRC_FILES += $(PW_DIR_RELPATH)/pw_varint/varint_c.c
+LOCAL_SRC_FILES += $(PW_DIR_RELPATH)/pw_varint/varint.cc
 
 ifeq ($(CHRE_DAEMON_USE_SDSPRPC),true)
 LOCAL_SHARED_LIBRARIES += libsdsprpc
diff --git a/Makefile b/Makefile
index 604c4ac..c3543d1 100644
--- a/Makefile
+++ b/Makefile
@@ -68,6 +68,12 @@
 include $(CHRE_PREFIX)/external/pigweed/pw_tokenizer.mk
 endif
 
+# Optional tokenized tracing support.
+ifeq ($(CHRE_TRACING_ENABLED), true)
+COMMON_CFLAGS += -DCHRE_TRACING_ENABLED
+include $(CHRE_PREFIX)/external/pigweed/pw_trace.mk
+endif
+
 # Optional on-device unit tests support
 include $(CHRE_PREFIX)/test/test.mk
 
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 64a49d5..ca8b740 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -11,4 +11,6 @@
 
 todo_checker_hook = ${REPO_ROOT}/system/chre/tools/todo_checker.py
 
-run_sim = ${REPO_ROOT}/system/chre/run_sim.sh -b
\ No newline at end of file
+run_sim = ${REPO_ROOT}/system/chre/run_sim.sh -b
+run_tests = ${REPO_ROOT}/system/chre/run_tests.sh -b
+run_pal_impl_tests = ${REPO_ROOT}/system/chre/run_pal_impl_tests.sh -b
\ No newline at end of file
diff --git a/api_parser/.gitignore b/api_parser/.gitignore
new file mode 100644
index 0000000..48fb193
--- /dev/null
+++ b/api_parser/.gitignore
@@ -0,0 +1 @@
+/parser_cache
diff --git a/chpp/api_parser/README.md b/api_parser/README.md
similarity index 100%
rename from chpp/api_parser/README.md
rename to api_parser/README.md
diff --git a/api_parser/api_parser.py b/api_parser/api_parser.py
new file mode 100644
index 0000000..343bcc9
--- /dev/null
+++ b/api_parser/api_parser.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+
+from collections import defaultdict
+from pyclibrary import CParser
+
+from utils import system_chre_abs_path
+
+
+class ApiParser:
+    """Given a file-specific set of annotations (extracted from JSON annotations file), parses a
+    single API header file into data structures suitable for use with code generation. This class
+    will contain the parsed representation of the headers when instantiated.
+    """
+
+    def __init__(self, json_obj):
+        """Initialize and parse the API file described in the provided JSON-derived object.
+
+        :param json_obj: Extracted file-specific annotations from JSON
+        """
+
+        self.json = json_obj
+        self.structs_and_unions = {}
+        self._parse_annotations()
+        self._parse_api()
+
+    def _parse_annotations(self):
+        # Converts annotations list to a more usable data structure: dict keyed by structure name,
+        # containing a dict keyed by field name, containing a list of annotations (as they
+        # appear in the JSON). In other words, we can easily get all of the annotations for the
+        # "version" field in "chreWwanCellInfoResult" via
+        # annotations['chreWwanCellInfoResult']['version']. This is also a defaultdict, so it's safe
+        # to access if there are no annotations for this structure + field; it'll just give you
+        # an empty list in that case.
+
+        self.annotations = defaultdict(lambda: defaultdict(list))
+        for struct_info in self.json['struct_info']:
+            for annotation in struct_info['annotations']:
+                self.annotations[struct_info['name']
+                                 ][annotation['field']].append(annotation)
+
+    def _files_to_parse(self):
+        """Returns a list of files to supply as input to CParser"""
+
+        # Input paths for CParser are stored in JSON relative to <android_root>/system/chre
+        # Reformulate these to absolute paths, and add in some default includes that we always
+        # supply
+        chre_project_base_dir = system_chre_abs_path()
+        default_includes = ['api_parser/parser_defines.h',
+                            'chre_api/include/chre_api/chre/version.h']
+        files = default_includes + \
+            self.json['includes'] + [self.json['filename']]
+        return [os.path.join(chre_project_base_dir, file) for file in files]
+
+    def _parse_structs_and_unions(self):
+        # Starts with the root structures (i.e. those that will appear at the top-level in one
+        # or more CHPP messages), build a data structure containing all of the information we'll
+        # need to emit the CHPP structure definition and conversion code.
+
+        structs_and_unions_to_parse = self.json['root_structs'].copy()
+        while len(structs_and_unions_to_parse) > 0:
+            type_name = structs_and_unions_to_parse.pop()
+            if type_name in self.structs_and_unions:
+                continue
+
+            entry = {
+                'appears_in': set(),  # Other types this type is nested within
+                'dependencies': set(),  # Types that are nested in this type
+                'has_vla_member': False,  # True if this type or any dependency has a VLA member
+                'members': [],  # Info about each member of this type
+            }
+            if type_name in self.parser.defs['structs']:
+                defs = self.parser.defs['structs'][type_name]
+                entry['is_union'] = False
+            elif type_name in self.parser.defs['unions']:
+                defs = self.parser.defs['unions'][type_name]
+                entry['is_union'] = True
+            else:
+                raise RuntimeError(
+                    "Couldn't find {} in parsed structs/unions".format(type_name))
+
+            for member_name, member_type, _ in defs['members']:
+                member_info = {
+                    'name': member_name,
+                    'type': member_type,
+                    'annotations': self.annotations[type_name][member_name],
+                    'is_nested_type': False,
+                }
+
+                if member_type.type_spec.startswith('struct ') or \
+                        member_type.type_spec.startswith('union '):
+                    member_info['is_nested_type'] = True
+                    member_type_name = member_type.type_spec.split(' ')[1]
+                    member_info['nested_type_name'] = member_type_name
+                    entry['dependencies'].add(member_type_name)
+                    structs_and_unions_to_parse.append(member_type_name)
+
+                entry['members'].append(member_info)
+
+                # Flip a flag if this structure has at least one variable-length array member, which
+                # means that the encoded size can only be computed at runtime
+                if not entry['has_vla_member']:
+                    for annotation in self.annotations[type_name][member_name]:
+                        if annotation['annotation'] == 'var_len_array':
+                            entry['has_vla_member'] = True
+
+            self.structs_and_unions[type_name] = entry
+
+        # Build reverse linkage of dependency chain (i.e. lookup between a type and the other types
+        # it appears in)
+        for type_name, type_info in self.structs_and_unions.items():
+            for dependency in type_info['dependencies']:
+                self.structs_and_unions[dependency]['appears_in'].add(
+                    type_name)
+
+        # Bubble up "has_vla_member" to types each type it appears in, i.e. if this flag is set to
+        # True on a leaf node, then all its ancestors should also have the flag set to True
+        for type_name, type_info in self.structs_and_unions.items():
+            if type_info['has_vla_member']:
+                types_to_mark = list(type_info['appears_in'])
+                while len(types_to_mark) > 0:
+                    type_to_mark = types_to_mark.pop()
+                    self.structs_and_unions[type_to_mark]['has_vla_member'] = True
+                    types_to_mark.extend(
+                        list(self.structs_and_unions[type_to_mark]['appears_in']))
+
+    def _parse_api(self):
+        """
+        Parses the API and stores the structs and unions.
+        """
+
+        file_to_parse = self._files_to_parse()
+        self.parser = CParser(file_to_parse, cache='parser_cache')
+        self._parse_structs_and_unions()
diff --git a/api_parser/chpp_code_generator.py b/api_parser/chpp_code_generator.py
new file mode 100644
index 0000000..5861951
--- /dev/null
+++ b/api_parser/chpp_code_generator.py
@@ -0,0 +1,994 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import subprocess
+
+from datetime import datetime
+
+from utils import CHPP_PARSER_INCLUDE_PATH
+from utils import CHPP_PARSER_SOURCE_PATH
+from utils import LICENSE_HEADER
+from utils import android_build_top_abs_path
+from utils import system_chre_abs_path
+
+
+class CodeGenerator:
+    """Given an ApiParser object, generates a header file with structure definitions in CHPP format.
+    """
+
+    def __init__(self, api, commit_hash):
+        """
+        :param api: ApiParser object
+        """
+
+        self.api = api
+        self.json = api.json
+        # Turn "chre_api/include/chre_api/chre/wwan.h" into "wwan"
+        self.service_name = self.json['filename'].split('/')[-1].split('.')[0]
+        self.capitalized_service_name = self.service_name.capitalize()
+        self.commit_hash = commit_hash
+
+    # ----------------------------------------------------------------------------------------------
+    # Header generation methods (plus some methods shared with encoder generation)
+    # ----------------------------------------------------------------------------------------------
+
+    def _autogen_notice(self):
+        out = []
+        out.append('// This file was automatically generated by {}\n'.format(
+            os.path.basename(__file__)))
+        out.append(
+            '// Date: {} UTC\n'.format(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')))
+        out.append(
+            '// Source: {} @ commit {}\n\n'.format(self.json['filename'], self.commit_hash))
+        out.append(
+            '// DO NOT modify this file directly, as those changes will be lost the next\n')
+        out.append('// time the script is executed\n\n')
+        return out
+
+    def _dump_to_file(self, output_filename, contents, dry_run, skip_clang_fomat):
+        """Outputs contents to output_filename, or prints contents if dry_run is True"""
+
+        if dry_run:
+            print('---- {} ----'.format(output_filename))
+            print(contents)
+            print('---- end of {} ----\n'.format(output_filename))
+        else:
+            with open(output_filename, 'w') as f:
+                f.write(contents)
+
+            if not skip_clang_fomat:
+                clang_format_path = (android_build_top_abs_path() +
+                                     '/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format')
+                args = [clang_format_path, '-i', output_filename]
+                result = subprocess.run(args)
+                result.check_returncode()
+
+    def _is_array_type(self, type_info):
+        # If this is an array type, declarators will be a tuple containing a list of
+        # a single int element giving the size of the array
+        return len(type_info.declarators) == 1 and isinstance(type_info.declarators[0], list)
+
+    def _get_array_len(self, type_info):
+        return type_info.declarators[0][0]
+
+    def _get_chpp_type_from_chre(self, chre_type):
+        """Returns 'struct ChppWwanCellInfo', etc. given 'chreWwanCellInfo'"""
+
+        prefix = self._get_struct_or_union_prefix(chre_type)
+
+        # First see if we have an explicit name override (e.g. for anonymous types)
+        for annotation in self.api.annotations[chre_type]['.']:
+            if annotation['annotation'] == 'rename_type':
+                return prefix + annotation['type_override']
+
+        # Otherwise, use the existing type name, just replace the "chre" prefix with "Chpp"
+        if chre_type.startswith('chre'):
+            return prefix + 'Chpp' + chre_type[4:]
+        else:
+            raise RuntimeError(
+                "Couldn't figure out new type name for {}".format(chre_type))
+
+    def _get_chre_type_with_prefix(self, chre_type):
+        """Returns 'struct chreWwanCellInfo', etc. given 'chreWwanCellInfo'"""
+
+        return self._get_struct_or_union_prefix(chre_type) + chre_type
+
+    def _get_chpp_header_type_from_chre(self, chre_type):
+        """Returns 'struct ChppWwanCellInfoWithHeader', etc. given 'chreWwanCellInfo'"""
+
+        return self._get_chpp_type_from_chre(chre_type) + 'WithHeader'
+
+    def _get_member_comment(self, member_info):
+        for annotation in member_info['annotations']:
+            if annotation['annotation'] == 'fixed_value':
+                return '  // Input ignored; always set to {}'.format(annotation['value'])
+            elif annotation['annotation'] == 'var_len_array':
+                return '  // References {} instances of {}'.format(
+                    annotation['length_field'], self._get_member_type(member_info))
+        return ''
+
+    def _get_member_type(self, member_info, underlying_vla_type=False):
+        """Gets the CHPP type specification prefix for a struct/union member.
+
+        :param member_info: a dict element from self.api.structs_and_unions[struct]['members']
+        :param underlying_vla_type: (used only for var-len array types) False to output
+            'struct ChppOffset', and True to output the type that ChppOffset references
+        :return: type specification string that prefixes the field name, e.g. 'uint8_t'
+        """
+
+        # 4 cases to handle:
+        #   1) Annotation gives explicit type that we should use
+        #   2) Annotation says this is a variable length array (so use ChppOffset if
+        #      underlying_vla_type is False)
+        #   3) This is a struct/union type, so use the renamed (CHPP) type name
+        #   4) Regular type, e.g. uint32_t, so just use the type spec as-is
+        for annotation in member_info['annotations']:
+            if annotation['annotation'] == 'rewrite_type':
+                return annotation['type_override']
+            elif not underlying_vla_type and annotation['annotation'] in ['var_len_array', 'string']:
+                return 'struct ChppOffset'
+
+        if not underlying_vla_type and len(member_info['type'].declarators) > 0 and \
+                member_info['type'].declarators[0] == '*':
+            # This case should either be handled by rewrite_type (e.g. to uint32_t as
+            # opaque/ignored), or var_len_array
+            raise RuntimeError('Pointer types require annotation\n{}'.format(
+                member_info))
+
+        if member_info['is_nested_type']:
+            return self._get_chpp_type_from_chre(member_info['nested_type_name'])
+
+        return member_info['type'].type_spec
+
+    def _get_member_type_suffix(self, member_info):
+        if self._is_array_type(member_info['type']):
+            return '[{}]'.format(self._get_array_len(member_info['type']))
+        return ''
+
+    def _get_struct_or_union_prefix(self, chre_type):
+        return 'struct ' if not self.api.structs_and_unions[chre_type]['is_union'] else 'union '
+
+    def _gen_header_includes(self):
+        """Generates #include directives for use in <service>_types.h"""
+
+        out = ['#include <stdbool.h>\n#include <stddef.h>\n#include <stdint.h>\n\n']
+
+        includes = ['chpp/app.h', 'chpp/macros.h', 'chre_api/chre/version.h']
+        includes.extend(self.json['output_includes'])
+        for incl in sorted(includes):
+            out.append('#include "{}"\n'.format(incl))
+        out.append('\n')
+        return out
+
+    def _gen_struct_or_union(self, name):
+        """Generates the definition for a single struct/union type."""
+
+        out = []
+        if not name.startswith('anon'):
+            out.append('//! See {{@link {}}} for details\n'.format(name))
+        out.append('{} {{\n'.format(self._get_chpp_type_from_chre(name)))
+        for member_info in self.api.structs_and_unions[name]['members']:
+            out.append('  {} {}{};{}\n'.format(self._get_member_type(member_info),
+                                               member_info['name'],
+                                               self._get_member_type_suffix(
+                                                   member_info),
+                                               self._get_member_comment(member_info)))
+
+        out.append('} CHPP_PACKED_ATTR;\n\n')
+        return out
+
+    def _gen_header_struct(self, chre_type):
+        """Generates the definition for the type with header (WithHeader)."""
+
+        out = []
+        out.append('//! CHPP app header plus {}\n'.format(
+            self._get_chpp_header_type_from_chre(chre_type)))
+
+        out.append('{} {{\n'.format(
+            self._get_chpp_header_type_from_chre(chre_type)))
+        out.append('  struct ChppAppHeader header;\n')
+        out.append('  {} payload;\n'.format(
+            self._get_chpp_type_from_chre(chre_type)))
+        out.append('} CHPP_PACKED_ATTR;\n\n')
+
+        return out
+
+    def _gen_structs_and_unions(self):
+        """Generates definitions for all struct/union types required for the root structs."""
+
+        out = []
+        out.append('CHPP_PACKED_START\n\n')
+
+        sorted_structs = self._sorted_structs(self.json['root_structs'])
+        for type_name in sorted_structs:
+            out.extend(self._gen_struct_or_union(type_name))
+
+        for chre_type in self.json['root_structs']:
+            out.extend(self._gen_header_struct(chre_type))
+
+        out.append('CHPP_PACKED_END\n\n')
+        return out
+
+    def _sorted_structs(self, root_nodes):
+        """Implements a topological sort on self.api.structs_and_unions.
+
+        Elements are ordered by definition dependency, i.e. if A includes a field of type B,
+        then B will appear before A in the returned list.
+        :return: list of keys in self.api.structs_and_unions, sorted by dependency order
+        """
+
+        result = []
+        visited = set()
+
+        def sort_helper(collection, key):
+            for dep in sorted(collection[key]['dependencies']):
+                if dep not in visited:
+                    visited.add(dep)
+                    sort_helper(collection, dep)
+            result.append(key)
+
+        for node in sorted(root_nodes):
+            sort_helper(self.api.structs_and_unions, node)
+        return result
+
+    # ----------------------------------------------------------------------------------------------
+    # Encoder function generation methods (CHRE --> CHPP)
+    # ----------------------------------------------------------------------------------------------
+
+    def _get_chpp_member_sizeof_call(self, member_info):
+        """Returns invocation used to determine the size of the provided member when encoded.
+
+        Will be either sizeof(<type in CHPP struct>) or a function call if the member contains a VLA
+        :param member_info: a dict element from self.api.structs_and_unions[struct]['members']
+        :return: string
+        """
+
+        type_name = None
+        if member_info['is_nested_type']:
+            chre_type = member_info['nested_type_name']
+            if self.api.structs_and_unions[chre_type]['has_vla_member']:
+                return '{}(in->{})'.format(self._get_chpp_sizeof_function_name(chre_type),
+                                           member_info['name'])
+            else:
+                type_name = self._get_chpp_type_from_chre(chre_type)
+        else:
+            type_name = member_info['type'].type_spec
+        return 'sizeof({})'.format(type_name)
+
+    def _gen_chpp_sizeof_function(self, chre_type):
+        """Generates a function to determine the encoded size of the CHRE struct, if necessary."""
+
+        out = []
+
+        # Note that this function *should* work with unions as well, but at the time of writing
+        # it'll only be used with structs, so names, etc. are written with that in mind
+        struct_info = self.api.structs_and_unions[chre_type]
+        if not struct_info['has_vla_member']:
+            # No codegen necessary, just sizeof on the CHPP structure name is sufficient
+            return out
+
+        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(
+            chre_type)
+        parameter_name = core_type_name[0].lower() + core_type_name[1:]
+        chpp_type_name = self._get_chpp_header_type_from_chre(chre_type)
+        out.append('//! @return number of bytes required to represent the given\n'
+                   '//! {} along with the CHPP header as\n'
+                   '//! {}\n'
+                   .format(chre_type, chpp_type_name))
+        out.append('static size_t {}(\n        const {}{} *{}) {{\n'
+                   .format(self._get_chpp_sizeof_function_name(chre_type),
+                           self._get_struct_or_union_prefix(
+                               chre_type), chre_type,
+                           parameter_name))
+
+        # sizeof(this struct)
+        out.append('  size_t encodedSize = sizeof({});\n'.format(chpp_type_name))
+
+        # Plus count * sizeof(type) for each var-len array included in this struct
+        for member_info in self.api.structs_and_unions[chre_type]['members']:
+            for annotation in member_info['annotations']:
+                if annotation['annotation'] == 'var_len_array':
+                    # If the VLA field itself contains a VLA, then we'd need to generate a for
+                    # loop to calculate the size of each element individually - I don't think we
+                    # have any of these in the CHRE API today, so leaving this functionality out.
+                    # Also note that to support that case we'd also want to recursively call this
+                    # function to generate sizeof functions for nested fields.
+                    if member_info['is_nested_type'] and \
+                            self.api.structs_and_unions[member_info['nested_type_name']][
+                                'has_vla_member']:
+                        raise RuntimeError(
+                            'Nested variable-length arrays is not currently supported ({} '
+                            'in {})'.format(member_info['name'], chre_type))
+
+                    out.append('  encodedSize += {}->{} * sizeof({});\n'.format(
+                        parameter_name, annotation['length_field'],
+                        self._get_member_type(member_info, True)))
+                elif annotation['annotation'] == 'string':
+                    out.append('  if ({}->{} != NULL) {{'.format(
+                        parameter_name, annotation['field']))
+                    out.append('    encodedSize += strlen({}->{}) + 1;\n'.format(
+                        parameter_name, annotation['field']))
+                    out.append('  }\n')
+
+        out.append('  return encodedSize;\n}\n\n')
+        return out
+
+    def _gen_chpp_sizeof_functions(self):
+        """For each root struct, generate necessary functions to determine their encoded size."""
+
+        out = []
+        for struct in self.json['root_structs']:
+            out.extend(self._gen_chpp_sizeof_function(struct))
+        return out
+
+    def _gen_conversion_includes(self):
+        """Generates #include directives for the conversion source file."""
+
+        out = ['#include "chpp/macros.h"\n'
+               '#include "chpp/memory.h"\n'
+               '#include "chpp/common/{}_types.h"\n\n'.format(self.service_name)]
+        out.append(
+            '#include <stddef.h>\n#include <stdint.h>\n#include <string.h>\n\n')
+        return out
+
+    def _get_chpp_sizeof_function_name(self, chre_struct):
+        """Returns the function name used to compute the encoded size of the given struct at
+        runtime.
+        """
+
+        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(
+            chre_struct)
+        return 'chpp{}SizeOf{}FromChre'.format(self.capitalized_service_name, core_type_name)
+
+    def _get_encoding_function_name(self, chre_type):
+        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(
+            chre_type)
+        return 'chpp{}Convert{}FromChre'.format(self.capitalized_service_name, core_type_name)
+
+    def _gen_encoding_function_signature(self, chre_type):
+        out = []
+        out.append(
+            'void {}(\n'.format(self._get_encoding_function_name(chre_type)))
+        out.append('    const {}{} *in,\n'.format(
+            self._get_struct_or_union_prefix(chre_type), chre_type))
+        out.append('    {} *out'.format(self._get_chpp_type_from_chre(chre_type)))
+        if self.api.structs_and_unions[chre_type]['has_vla_member']:
+            out.append(',\n')
+            out.append('    uint8_t *payload,\n')
+            out.append('    size_t payloadSize,\n')
+            out.append('    uint16_t *vlaOffset')
+        out.append(')')
+        return out
+
+    def _gen_string_encoding(self, member_info, annotation):
+        out = []
+        # Might want to revisit this if we ever end up supporting NULL strings
+        # in our API. We can assert here since there's currently no API that
+        # does so.
+        member_name = member_info['name']
+        out.append('  if (in->{} != NULL) {{\n'.format(member_name))
+        out.append(
+            '    size_t strSize = strlen(in->{}) + 1;\n'.format(member_name))
+        out.append('    memcpy(&payload[*vlaOffset], in->{}, strSize);\n'.format(
+            member_name))
+        out.append('    out->{}.length = (uint16_t)(strSize);\n'.format(
+            member_name))
+        out.append('    out->{}.offset = *vlaOffset;\n'.format(member_name))
+        out.append('    *vlaOffset += out->{}.length;\n'.format(member_name))
+        out.append('  } else {\n')
+        out.append('    out->{}.length = 0;\n'.format(member_name))
+        out.append('    out->{}.offset = 0;\n'.format(member_name))
+        out.append('  }\n\n')
+
+        return out
+
+    def _gen_vla_encoding(self, member_info, annotation):
+        out = []
+
+        variable_name = member_info['name']
+        chpp_type = self._get_member_type(member_info, True)
+
+        if member_info['is_nested_type']:
+            out.append('\n  {} *{} = ({} *) &payload[*vlaOffset];\n'.format(
+                chpp_type, variable_name, chpp_type))
+
+        out.append('  out->{}.length = (uint16_t)(in->{} * {});\n'.format(
+            member_info['name'], annotation['length_field'],
+            self._get_chpp_member_sizeof_call(member_info)))
+
+        out.append('  CHPP_ASSERT((size_t)(*vlaOffset + out->{}.length) <= payloadSize);\n'.format(
+            member_info['name']))
+
+        out.append('  if (out->{}.length > 0 &&\n'
+                   '      *vlaOffset + out->{}.length <= payloadSize) {{\n'.format(
+                       member_info['name'], member_info['name']))
+
+        if member_info['is_nested_type']:
+            out.append('    for (size_t i = 0; i < in->{}; i++) {{\n'.format(
+                annotation['length_field']))
+            out.append('      {}'.format(
+                self._get_assignment_statement_for_field(member_info, in_vla_loop=True)))
+            out.append('    }\n')
+        else:
+            out.append('memcpy(&payload[*vlaOffset], in->{}, in->{} * sizeof({}));\n'.format(
+                member_info['name'], annotation['length_field'], chpp_type))
+
+        out.append(
+            '    out->{}.offset = *vlaOffset;\n'.format(member_info['name']))
+        out.append(
+            '    *vlaOffset += out->{}.length;\n'.format(member_info['name']))
+
+        out.append('  } else {\n')
+        out.append('    out->{}.offset = 0;\n'.format(member_info['name']))
+        out.append('  }\n')
+
+        return out
+
+    # ----------------------------------------------------------------------------------------------
+    # Encoder / decoder function generation methods (CHRE <--> CHPP)
+    # ----------------------------------------------------------------------------------------------
+
+    def _get_assignment_statement_for_field(self, member_info,
+                                            in_vla_loop=False,
+                                            containing_field_name=None,
+                                            decode_mode=False):
+        """Returns a statement to assign the provided member
+
+        :param member_info:
+        :param in_vla_loop: True if we're currently inside a loop and should append [i]
+        :param containing_field_name: Additional member name to use to access the target field, or
+        None; for example the normal case is "out->field = in->field", but if we're generating
+        assignments in the parent conversion function (e.g. as used for union variants), we need to
+        do "out->nested_field.field = in->nested_field.field"
+        :param decode_mode: True converts from CHPP to CHRE. False from CHRE to CHPP
+        :return: assignment statement as a string
+        """
+
+        array_index = '[i]' if in_vla_loop else ''
+        output_accessor = '' if in_vla_loop else 'out->'
+        containing_field = containing_field_name + \
+            '.' if containing_field_name is not None else ''
+
+        output_variable = '{}{}{}{}'.format(output_accessor, containing_field, member_info['name'],
+                                            array_index)
+        input_variable = 'in->{}{}{}'.format(containing_field,
+                                             member_info['name'], array_index)
+
+        if decode_mode and in_vla_loop:
+            output_variable = '{}Out{}'.format(
+                member_info['name'], array_index)
+            input_variable = '{}In{}'.format(member_info['name'], array_index)
+
+        if member_info['is_nested_type']:
+            chre_type = member_info['nested_type_name']
+            has_vla_member = self.api.structs_and_unions[chre_type]['has_vla_member']
+            if decode_mode:
+                # Use decoding function
+                vla_params = ', inSize' if has_vla_member else ''
+                out = 'if (!{}(&{}, &{}{})) {{\n'.format(
+                    self._get_decoding_function_name(
+                        chre_type), input_variable,
+                    output_variable, vla_params)
+                if has_vla_member:
+                    out += '  CHPP_FREE_AND_NULLIFY({}Out);\n'.format(
+                        member_info['name'])
+                out += '  return false;\n'
+                out += '}\n'
+                return out
+            else:
+                # Use encoding function
+                vla_params = ', payload, payloadSize, vlaOffset' if has_vla_member else ''
+                return '{}(&{}, &{}{});\n'.format(
+                    self._get_encoding_function_name(
+                        chre_type), input_variable, output_variable,
+                    vla_params)
+        elif self._is_array_type(member_info['type']):
+            # Array of primitive type (e.g. uint32_t[8]) - use memcpy
+            return 'memcpy({}, {}, sizeof({}));\n'.format(output_variable, input_variable,
+                                                          output_variable)
+        else:
+            # Regular assignment
+            return '{} = {};\n'.format(output_variable, input_variable)
+
+    def _gen_union_variant_conversion_code(self, member_info, annotation, decode_mode):
+        """Generates a switch statement to encode the "active"/"used" field within a union.
+
+        Handles cases where a union has multiple types, but there's another peer/adjacent field
+        which tells you which field in the union is to be used. Outputs code like this:
+        switch (in->{discriminator field}) {
+            case {first discriminator value associated with a fields}:
+                {conversion code for the field associated with this discriminator value}
+                ...
+        :param chre_type: CHRE type of the union
+        :param annotation: Reference to JSON annotation data with the discriminator mapping data
+        :param decode_mode: False encodes from CHRE to CHPP. True decodes from CHPP to CHRE
+        :return: list of strings
+        """
+
+        out = []
+        chre_type = member_info['nested_type_name']
+        struct_info = self.api.structs_and_unions[chre_type]
+
+        # Start off by zeroing out the union field so any padding is set to a consistent value
+        out.append('  memset(&out->{}, 0, sizeof(out->{}));\n'.format(member_info['name'],
+                                                                      member_info['name']))
+
+        # Next, generate the switch statement that will copy over the proper values
+        out.append(
+            '  switch (in->{}) {{\n'.format(annotation['discriminator']))
+        for value, field_name in annotation['mapping']:
+            out.append('    case {}:\n'.format(value))
+
+            found = False
+            for nested_member_info in struct_info['members']:
+                if nested_member_info['name'] == field_name:
+                    out.append('      {}'.format(
+                        self._get_assignment_statement_for_field(
+                            nested_member_info,
+                            containing_field_name=member_info['name'],
+                            decode_mode=decode_mode)))
+                    found = True
+                    break
+
+            if not found:
+                raise RuntimeError("Invalid mapping - couldn't find target field {} in struct {}"
+                                   .format(field_name, chre_type))
+
+            out.append('      break;\n')
+
+        out.append('    default:\n'
+                   '      CHPP_ASSERT(false);\n'
+                   '  }\n')
+
+        return out
+
+    def _gen_conversion_function(self, chre_type, already_generated, decode_mode):
+        out = []
+
+        struct_info = self.api.structs_and_unions[chre_type]
+        for dependency in sorted(struct_info['dependencies']):
+            if dependency not in already_generated:
+                out.extend(
+                    self._gen_conversion_function(dependency, already_generated, decode_mode))
+
+        # Skip if we've already generated code for this type, or if it's a union (in which case we
+        # handle the assignment in the parent structure to enable support for discrimination of
+        # which field in the union to use)
+        if chre_type in already_generated or struct_info['is_union']:
+            return out
+        already_generated.add(chre_type)
+
+        out.append('static ')
+        if decode_mode:
+            out.extend(self._gen_decoding_function_signature(chre_type))
+        else:
+            out.extend(self._gen_encoding_function_signature(chre_type))
+        out.append(' {\n')
+
+        for member_info in self.api.structs_and_unions[chre_type]['members']:
+            generated_by_annotation = False
+            for annotation in member_info['annotations']:
+                if annotation['annotation'] == 'fixed_value':
+                    if self._is_array_type(member_info['type']):
+                        out.append('  memset(&out->{}, {}, sizeof(out->{}));\n'.format(
+                            member_info['name'], annotation['value'], member_info['name']))
+                    else:
+                        out.append('  out->{} = {};\n'.format(member_info['name'],
+                                                              annotation['value']))
+                    generated_by_annotation = True
+                    break
+                elif annotation['annotation'] == 'enum':
+                    # Note: We could generate range verification code here, but it has not
+                    # been considered necessary thus far.
+                    pass
+                elif annotation['annotation'] == 'var_len_array':
+                    if decode_mode:
+                        out.extend(self._gen_vla_decoding(
+                            member_info, annotation))
+                    else:
+                        out.extend(self._gen_vla_encoding(
+                            member_info, annotation))
+                    generated_by_annotation = True
+                    break
+                elif annotation['annotation'] == 'string':
+                    if decode_mode:
+                        out.extend(self._gen_string_decoding(
+                            member_info, annotation))
+                    else:
+                        out.extend(self._gen_string_encoding(
+                            member_info, annotation))
+                    generated_by_annotation = True
+                    break
+                elif annotation['annotation'] == 'union_variant':
+                    out.extend(self._gen_union_variant_conversion_code(
+                        member_info, annotation, decode_mode))
+                    generated_by_annotation = True
+                    break
+
+            if not generated_by_annotation:
+                out.append('  {}'.format(
+                    self._get_assignment_statement_for_field(member_info, decode_mode=decode_mode)))
+
+        if decode_mode:
+            out.append('\n  return true;\n')
+
+        out.append('}\n\n')
+        return out
+
+    def _gen_conversion_functions(self, decode_mode):
+        out = []
+        already_generated = set()
+        for struct in self.json['root_structs']:
+            out.extend(self._gen_conversion_function(
+                struct, already_generated, decode_mode))
+        return out
+
+    def _strip_prefix_and_service_from_chre_struct_name(self, struct):
+        """Strips 'chre' and service prefix, e.g. 'chreWwanCellResultInfo' -> 'CellResultInfo'."""
+
+        chre_stripped = struct[4:]
+        upcased_service_name = self.service_name[0].upper(
+        ) + self.service_name[1:]
+        if not struct.startswith('chre') or not chre_stripped.startswith(upcased_service_name):
+            # If this happens, we need to update the script to handle it. Right we assume struct
+            # naming follows the pattern "chre<Service_name><Thing_name>"
+            raise RuntimeError('Unexpected structure name {}'.format(struct))
+
+        return chre_stripped[len(self.service_name):]
+
+    # ----------------------------------------------------------------------------------------------
+    # Memory allocation generation methods
+    # ----------------------------------------------------------------------------------------------
+
+    def _get_chpp_sizeof_call(self, chre_type):
+        """Returns invocation used to determine the size of the provided CHRE struct (with the CHPP
+        app header) after encoding.
+
+        Like _get_chpp_member_sizeof_call(), except for a top-level type assigned to the variable
+        "in" rather than a member within a structure (e.g. a VLA field)
+        :param chre_type: CHRE type name
+        :return: string
+        """
+
+        if self.api.structs_and_unions[chre_type]['has_vla_member']:
+            return '{}(in)'.format(self._get_chpp_sizeof_function_name(chre_type))
+        else:
+            return 'sizeof({})'.format(self._get_chpp_header_type_from_chre(chre_type))
+
+    def _get_encode_allocation_function_name(self, chre_type):
+        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(
+            chre_type)
+        return 'chpp{}{}FromChre'.format(self.capitalized_service_name, core_type_name)
+
+    def _gen_encode_allocation_function_signature(self, chre_type, gen_docs=False):
+        out = []
+        if gen_docs:
+            out.append('/**\n'
+                       ' * Converts from given CHRE structure to serialized CHPP type.\n'
+                       ' *\n'
+                       ' * @param in Fully-formed CHRE structure.\n'
+                       ' * @param out Upon success, will point to a buffer allocated with '
+                       'chppMalloc().\n'
+                       ' * It is the responsibility of the caller to set the values of the CHPP '
+                       'app layer header, and to free the buffer when it is no longer needed via '
+                       'chppFree() or CHPP_FREE_AND_NULLIFY().\n'
+                       ' * @param outSize Upon success, will be set to the size of the output '
+                       'buffer, in bytes.\n'
+                       ' *\n'
+                       ' * @return true on success, false if memory allocation failed.\n'
+                       ' */\n')
+        out.append(
+            'bool {}(\n'.format(self._get_encode_allocation_function_name(chre_type)))
+        out.append('    const {}{} *in,\n'.format(
+            self._get_struct_or_union_prefix(chre_type), chre_type))
+        out.append(
+            '    {} **out,\n'.format(self._get_chpp_header_type_from_chre(chre_type)))
+        out.append('    size_t *outSize)')
+        return out
+
+    def _gen_encode_allocation_function(self, chre_type):
+        out = []
+        out.extend(self._gen_encode_allocation_function_signature(chre_type))
+        out.append(' {\n')
+        out.append('  CHPP_NOT_NULL(out);\n')
+        out.append('  CHPP_NOT_NULL(outSize);\n\n')
+        out.append('  size_t payloadSize = {};\n'.format(
+            self._get_chpp_sizeof_call(chre_type)))
+        out.append('  *out = chppMalloc(payloadSize);\n')
+
+        out.append('  if (*out != NULL) {\n')
+
+        struct_info = self.api.structs_and_unions[chre_type]
+        if struct_info['has_vla_member']:
+            out.append('    uint8_t *payload = (uint8_t *) &(*out)->payload;\n')
+            out.append('    uint16_t vlaOffset = sizeof({});\n'.format(
+                self._get_chpp_type_from_chre(chre_type)))
+
+        out.append('    {}(in, &(*out)->payload'.format(
+            self._get_encoding_function_name(chre_type)))
+        if struct_info['has_vla_member']:
+            out.append(', payload, payloadSize, &vlaOffset')
+        out.append(');\n')
+        out.append('    *outSize = payloadSize;\n')
+        out.append('    return true;\n')
+        out.append('  }\n')
+
+        out.append('  return false;\n}\n\n')
+        return out
+
+    def _gen_encode_allocation_functions(self):
+        out = []
+        for chre_type in self.json['root_structs']:
+            out.extend(self._gen_encode_allocation_function(chre_type))
+        return out
+
+    def _gen_encode_allocation_function_signatures(self):
+        out = []
+        for chre_type in self.json['root_structs']:
+            out.extend(
+                self._gen_encode_allocation_function_signature(chre_type, True))
+            out.append(';\n\n')
+        return out
+
+    # ----------------------------------------------------------------------------------------------
+    # Decoder function generation methods (CHPP --> CHRE)
+    # ----------------------------------------------------------------------------------------------
+
+    def _get_decoding_function_name(self, chre_type):
+        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(
+            chre_type)
+        return 'chpp{}Convert{}ToChre'.format(self.capitalized_service_name, core_type_name)
+
+    def _gen_decoding_function_signature(self, chre_type):
+        out = []
+        out.append(
+            'bool {}(\n'.format(self._get_decoding_function_name(chre_type)))
+        out.append(
+            '    const {} *in,\n'.format(self._get_chpp_type_from_chre(chre_type)))
+        out.append(
+            '    {} *out'.format(self._get_chre_type_with_prefix(chre_type)))
+        if self.api.structs_and_unions[chre_type]['has_vla_member']:
+            out.append(',\n')
+            out.append('    size_t inSize')
+        out.append(')')
+        return out
+
+    def _gen_string_decoding(self, member_info, annotation):
+        out = []
+        variable_name = member_info['name']
+        out.append('\n')
+        out.append('  if (in->{}.length == 0) {{\n'.format(variable_name))
+        out.append('    out->{} = NULL;\n'.format(variable_name))
+        out.append('  } else {\n')
+        out.append('    char *{}Out = chppMalloc(in->{}.length);\n'.format(
+            variable_name, variable_name))
+        out.append('    if ({}Out == NULL) {{\n'.format(variable_name))
+        out.append('      return false;\n')
+        out.append('    }\n\n')
+        out.append('    memcpy({}Out, &((const uint8_t *)in)[in->{}.offset],\n'.format(
+            variable_name, variable_name))
+        out.append('      in->{}.length);\n'.format(variable_name))
+        out.append('    out->{} = {}Out;\n'.format(variable_name, variable_name))
+        out.append('  }\n')
+
+        return out
+
+    def _gen_vla_decoding(self, member_info, annotation):
+        out = []
+
+        variable_name = member_info['name']
+        chpp_type = self._get_member_type(member_info, True)
+        if member_info['is_nested_type']:
+            chre_type = self._get_chre_type_with_prefix(
+                member_info['nested_type_name'])
+        else:
+            chre_type = chpp_type
+
+        out.append('\n')
+        out.append('  if (in->{}.length == 0) {{\n'.format(variable_name))
+        out.append('    out->{} = NULL;\n'.format(variable_name))
+        out.append('  }\n')
+        out.append('  else {\n')
+        out.append('    if (in->{}.offset + in->{}.length > inSize ||\n'.format(
+            variable_name, variable_name))
+        out.append('        in->{}.length != in->{} * sizeof({})) {{\n'.format(
+            variable_name, annotation['length_field'], chpp_type))
+
+        out.append('      return false;\n')
+        out.append('    }\n\n')
+
+        if member_info['is_nested_type']:
+            out.append(
+                '    const {} *{}In =\n'.format(chpp_type, variable_name))
+            out.append('        (const {} *) &((const uint8_t *)in)[in->{}.offset];\n\n'.format(
+                chpp_type, variable_name))
+
+        out.append('    {} *{}Out = chppMalloc(in->{} * sizeof({}));\n'.format(
+            chre_type, variable_name, annotation['length_field'], chre_type))
+        out.append('    if ({}Out == NULL) {{\n'.format(variable_name))
+        out.append('      return false;\n')
+        out.append('    }\n\n')
+
+        if member_info['is_nested_type']:
+            out.append('    for (size_t i = 0; i < in->{}; i++) {{\n'.format(
+                annotation['length_field'], variable_name))
+            out.append('      {}'.format(self._get_assignment_statement_for_field(
+                member_info, in_vla_loop=True, decode_mode=True)))
+            out.append('    }\n')
+        else:
+            out.append('    memcpy({}Out, &((const uint8_t *)in)[in->{}.offset],\n'.format(
+                variable_name, variable_name))
+            out.append('      in->{} * sizeof({}));\n'.format(
+                annotation['length_field'], chre_type))
+
+        out.append('    out->{} = {}Out;\n'.format(variable_name, variable_name))
+        out.append('  }\n\n')
+
+        return out
+
+    def _get_decode_allocation_function_name(self, chre_type):
+        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(
+            chre_type)
+        return 'chpp{}{}ToChre'.format(self.capitalized_service_name, core_type_name)
+
+    def _gen_decode_allocation_function_signature(self, chre_type, gen_docs=False):
+        out = []
+        if gen_docs:
+            out.append('/**\n'
+                       ' * Converts from serialized CHPP structure to a CHRE type.\n'
+                       ' *\n'
+                       ' * @param in Fully-formed CHPP structure.\n'
+                       ' * @param in Size of the CHPP structure in bytes.\n'
+                       ' *\n'
+                       ' * @return If successful, a pointer to a CHRE structure allocated with '
+                       'chppMalloc(). If unsuccessful, null.\n'
+                       ' * It is the responsibility of the caller to free the buffer when it is no '
+                       'longer needed via chppFree() or CHPP_FREE_AND_NULLIFY().\n'
+                       ' */\n')
+
+        out.append('{} *{}(\n'.format(
+            self._get_chre_type_with_prefix(chre_type),
+            self._get_decode_allocation_function_name(chre_type)))
+        out.append(
+            '    const {} *in,\n'.format(self._get_chpp_type_from_chre(chre_type)))
+        out.append('    size_t inSize)')
+        return out
+
+    def _gen_decode_allocation_function(self, chre_type):
+        out = []
+
+        out.extend(self._gen_decode_allocation_function_signature(chre_type))
+        out.append(' {\n')
+
+        out.append('  {} *out = NULL;\n\n'.format(
+            self._get_chre_type_with_prefix(chre_type)))
+
+        out.append('  if (inSize >= sizeof({})) {{\n'.format(
+            self._get_chpp_type_from_chre(chre_type)))
+
+        out.append('    out = chppMalloc(sizeof({}));\n'.format(
+            self._get_chre_type_with_prefix(chre_type)))
+        out.append('    if (out != NULL) {\n')
+
+        struct_info = self.api.structs_and_unions[chre_type]
+
+        out.append('      if (!{}(in, out'.format(
+            self._get_decoding_function_name(chre_type)))
+        if struct_info['has_vla_member']:
+            out.append(', inSize')
+        out.append(')) {')
+        out.append('        CHPP_FREE_AND_NULLIFY(out);\n')
+        out.append('      }\n')
+
+        out.append('    }\n')
+        out.append('  }\n\n')
+        out.append('  return out;\n')
+        out.append('}\n')
+        return out
+
+    def _gen_decode_allocation_functions(self):
+        out = []
+        for chre_type in self.json['root_structs']:
+            out.extend(self._gen_decode_allocation_function(chre_type))
+        return out
+
+    def _gen_decode_allocation_function_signatures(self):
+        out = []
+        for chre_type in self.json['root_structs']:
+            out.extend(
+                self._gen_decode_allocation_function_signature(chre_type, True))
+            out.append(';\n\n')
+        return out
+
+    # ----------------------------------------------------------------------------------------------
+    # Public methods
+    # ----------------------------------------------------------------------------------------------
+
+    def generate_header_file(self, dry_run=False, skip_clang_format=False):
+        """Creates a C header file for this API and writes it to the file indicated in the JSON."""
+
+        filename = self.service_name + '_types.h'
+        if not dry_run:
+            print('Generating {} ... '.format(filename), end='', flush=True)
+        output_file = os.path.join(
+            system_chre_abs_path(), CHPP_PARSER_INCLUDE_PATH, filename)
+        header = self.generate_header_string()
+        self._dump_to_file(output_file, header, dry_run, skip_clang_format)
+        if not dry_run:
+            print('done')
+
+    def generate_header_string(self):
+        """Returns a C header with structure definitions for this API."""
+
+        # To defer concatenation (speed things up), build the file as a list of strings then only
+        # concatenate once at the end
+        out = [LICENSE_HEADER]
+
+        header_guard = 'CHPP_{}_TYPES_H_'.format(self.service_name.upper())
+
+        out.append('#ifndef {}\n#define {}\n\n'.format(
+            header_guard, header_guard))
+        out.extend(self._autogen_notice())
+        out.extend(self._gen_header_includes())
+        out.append('#ifdef __cplusplus\nextern "C" {\n#endif\n\n')
+        out.extend(self._gen_structs_and_unions())
+
+        out.append('\n// Encoding functions (CHRE --> CHPP)\n\n')
+        out.extend(self._gen_encode_allocation_function_signatures())
+
+        out.append('\n// Decoding functions (CHPP --> CHRE)\n\n')
+        out.extend(self._gen_decode_allocation_function_signatures())
+
+        out.append('#ifdef __cplusplus\n}\n#endif\n\n')
+        out.append('#endif  // {}\n'.format(header_guard))
+        return ''.join(out)
+
+    def generate_conversion_file(self, dry_run=False, skip_clang_format=False):
+        """Generates a .c file with functions for encoding CHRE structs into CHPP and vice versa."""
+
+        filename = self.service_name + '_convert.c'
+        if not dry_run:
+            print('Generating {} ... '.format(filename), end='', flush=True)
+        contents = self.generate_conversion_string()
+        output_file = os.path.join(
+            system_chre_abs_path(), CHPP_PARSER_SOURCE_PATH, filename)
+        self._dump_to_file(output_file, contents, dry_run, skip_clang_format)
+        if not dry_run:
+            print('done')
+
+    def generate_conversion_string(self):
+        """Returns C code for encoding CHRE structs into CHPP and vice versa."""
+
+        out = [LICENSE_HEADER, '\n']
+
+        out.extend(self._autogen_notice())
+        out.extend(self._gen_conversion_includes())
+
+        out.append('\n// Encoding (CHRE --> CHPP) size functions\n\n')
+        out.extend(self._gen_chpp_sizeof_functions())
+        out.append('\n// Encoding (CHRE --> CHPP) conversion functions\n\n')
+        out.extend(self._gen_conversion_functions(False))
+        out.append('\n// Encoding (CHRE --> CHPP) top-level functions\n\n')
+        out.extend(self._gen_encode_allocation_functions())
+
+        out.append('\n// Decoding (CHPP --> CHRE) conversion functions\n\n')
+        out.extend(self._gen_conversion_functions(True))
+        out.append('\n// Decoding (CHPP --> CHRE) top-level functions\n\n')
+        out.extend(self._gen_decode_allocation_functions())
+
+        return ''.join(out)
diff --git a/api_parser/chre_api_annotations.json b/api_parser/chre_api_annotations.json
new file mode 100644
index 0000000..f54ff5c
--- /dev/null
+++ b/api_parser/chre_api_annotations.json
@@ -0,0 +1,316 @@
+[
+  {
+    "filename": "chre_api/include/chre_api/chre/wwan.h",
+    "includes": [
+      "chre_api/include/chre_api/chre/common.h"
+    ],
+    "output_includes": [
+      "chpp/common/common_types.h",
+      "chre_api/chre/wwan.h"
+    ],
+    "struct_info": [
+      {
+        "name": "chreWwanCellInfoResult",
+        "annotations": [
+          {
+            "field": "version",
+            "annotation": "fixed_value",
+            "value": "CHRE_WWAN_CELL_INFO_RESULT_VERSION"
+          },
+          {
+            "field": "errorCode",
+            "annotation": "enum",
+            "enum_type": "chreError"
+          },
+          {
+            "field": "cookie",
+            "annotation": "fixed_value",
+            "value": "0"
+          },
+          {
+            "field": "cookie",
+            "annotation": "rewrite_type",
+            "type_override": "uint32_t"
+          },
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          },
+          {
+            "field": "cells",
+            "annotation": "var_len_array",
+            "length_field": "cellInfoCount"
+          }
+        ]
+      },
+      {
+        "name": "chreWwanCellInfo",
+        "annotations": [
+          {
+            "field": "cellInfoType",
+            "annotation": "enum",
+            "enum_type": "chreWwanCellInfoType"
+          },
+          {
+            "field": "CellInfo",
+            "annotation": "union_variant",
+            "discriminator": "cellInfoType",
+            "mapping": [
+              [
+                "CHRE_WWAN_CELL_INFO_TYPE_GSM",
+                "gsm"
+              ],
+              [
+                "CHRE_WWAN_CELL_INFO_TYPE_CDMA",
+                "cdma"
+              ],
+              [
+                "CHRE_WWAN_CELL_INFO_TYPE_LTE",
+                "lte"
+              ],
+              [
+                "CHRE_WWAN_CELL_INFO_TYPE_WCDMA",
+                "wcdma"
+              ],
+              [
+                "CHRE_WWAN_CELL_INFO_TYPE_TD_SCDMA",
+                "tdscdma"
+              ],
+              [
+                "CHRE_WWAN_CELL_INFO_TYPE_NR",
+                "nr"
+              ]
+            ]
+          },
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      },
+      {
+        "name": "chreWwanCellIdentityGsm",
+        "annotations": [
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      }
+    ],
+    "root_structs": [
+      "chreWwanCellInfoResult"
+    ]
+  },
+  {
+    "filename": "chre_api/include/chre_api/chre/wifi.h",
+    "includes": [
+      "chre_api/include/chre_api/chre/common.h"
+    ],
+    "output_includes": [
+      "chpp/common/common_types.h",
+      "chre_api/chre/wifi.h"
+    ],
+    "struct_info": [
+      {
+        "name": "chreWifiScanEvent",
+        "annotations": [
+          {
+            "field": "version",
+            "annotation": "fixed_value",
+            "value": "CHRE_WIFI_SCAN_EVENT_VERSION"
+          },
+          {
+            "field": "scannedFreqList",
+            "annotation": "var_len_array",
+            "length_field": "scannedFreqListLen"
+          },
+          {
+            "field": "results",
+            "annotation": "var_len_array",
+            "length_field": "resultCount"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiScanResult",
+        "annotations": [
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiScanParams",
+        "annotations": [
+          {
+            "field": "frequencyList",
+            "annotation": "var_len_array",
+            "length_field": "frequencyListLen"
+          },
+          {
+            "field": "ssidList",
+            "annotation": "var_len_array",
+            "length_field": "ssidListLen"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiRangingEvent",
+        "annotations": [
+          {
+            "field": "version",
+            "annotation": "fixed_value",
+            "value": "CHRE_WIFI_RANGING_EVENT_VERSION"
+          },
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          },
+          {
+            "field": "results",
+            "annotation": "var_len_array",
+            "length_field": "resultCount"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiRangingResult",
+        "annotations": [
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiRangingParams",
+        "annotations": [
+          {
+            "field": "targetList",
+            "annotation": "var_len_array",
+            "length_field": "targetListLen"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiRangingTarget",
+        "annotations": [
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiNanSubscribeConfig",
+        "annotations": [
+          {
+            "field": "subscribeType",
+            "annotation": "enum",
+            "enum_type": "chreWifiNanSubscribeType"
+          },
+          {
+            "field": "service",
+            "annotation": "string"
+          },
+          {
+            "field": "serviceSpecificInfo",
+            "annotation": "var_len_array",
+            "length_field": "serviceSpecificInfoSize"
+          },
+          {
+            "field": "matchFilter",
+            "annotation": "var_len_array",
+            "length_field": "matchFilterLength"
+          }
+        ]
+      },
+      {
+        "name": "chreWifiNanDiscoveryEvent",
+        "annotations": [
+          {
+            "field": "serviceSpecificInfo",
+            "annotation": "var_len_array",
+            "length_field": "serviceSpecificInfoSize"
+          }
+        ]
+      }
+    ],
+    "root_structs": [
+      "chreWifiScanEvent",
+      "chreWifiScanParams",
+      "chreWifiRangingEvent",
+      "chreWifiRangingParams",
+      "chreWifiNanSubscribeConfig",
+      "chreWifiNanDiscoveryEvent",
+      "chreWifiNanSessionLostEvent",
+      "chreWifiNanSessionTerminatedEvent",
+      "chreWifiNanRangingParams"
+    ]
+  },
+  {
+    "filename": "chre_api/include/chre_api/chre/gnss.h",
+    "includes": [
+      "chre_api/include/chre_api/chre/common.h"
+    ],
+    "output_includes": [
+      "chpp/common/common_types.h",
+      "chre_api/chre/gnss.h"
+    ],
+    "struct_info": [
+      {
+        "name": "chreGnssDataEvent",
+        "annotations": [
+          {
+            "field": "version",
+            "annotation": "fixed_value",
+            "value": "CHRE_GNSS_DATA_EVENT_VERSION"
+          },
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          },
+          {
+            "field": "measurements",
+            "annotation": "var_len_array",
+            "length_field": "measurement_count"
+          }
+        ]
+      },
+      {
+        "name": "chreGnssLocationEvent",
+        "annotations": [
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      },
+      {
+        "name": "chreGnssClock",
+        "annotations": [
+          {
+            "field": "reserved",
+            "annotation": "fixed_value",
+            "value": "0"
+          }
+        ]
+      }
+    ],
+    "root_structs": [
+      "chreGnssDataEvent",
+      "chreGnssLocationEvent"
+    ]
+  }
+]
diff --git a/api_parser/chre_api_to_chpp.py b/api_parser/chre_api_to_chpp.py
new file mode 100755
index 0000000..c330204
--- /dev/null
+++ b/api_parser/chre_api_to_chpp.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+import os
+import json
+import subprocess
+
+from api_parser import ApiParser
+from chpp_code_generator import CodeGenerator
+from utils import system_chre_abs_path
+
+
+def run(args):
+    with open(system_chre_abs_path() + '/api_parser/chre_api_annotations.json') as f:
+        js = json.load(f)
+
+    commit_hash = subprocess.getoutput(
+        "git describe --always --long --dirty --exclude '*'")
+    for file in js:
+        if args.file_filter:
+            matched = False
+            for matcher in args.file_filter:
+                if matcher in file['filename']:
+                    matched = True
+                    break
+            if not matched:
+                print("Skipping {} - doesn't match filter(s) {}".format(file['filename'],
+                                                                        args.file_filter))
+                continue
+        print('Parsing {} ... '.format(file['filename']), end='', flush=True)
+        api_parser = ApiParser(file)
+        code_gen = CodeGenerator(api_parser, commit_hash)
+        print('done')
+        code_gen.generate_header_file(args.dry_run, args.skip_clang_format)
+        code_gen.generate_conversion_file(args.dry_run, args.skip_clang_format)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Generate CHPP serialization code from CHRE APIs.')
+    parser.add_argument('-n', dest='dry_run', action='store_true',
+                        help='Print the output instead of writing to a file')
+    parser.add_argument('--skip-clang-format', dest='skip_clang_format', action='store_true',
+                        help='Skip running clang-format on the output files (doesn\'t apply to dry '
+                             'runs)')
+    parser.add_argument('file_filter', nargs='*',
+                        help='Filters the input files (filename field in the JSON) to generate a '
+                             'subset of the typical output, e.g. "wifi" to just generate conversion'
+                             ' routines for wifi.h')
+    args = parser.parse_args()
+    run(args)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/chpp/api_parser/parser_defines.h b/api_parser/parser_defines.h
similarity index 100%
rename from chpp/api_parser/parser_defines.h
rename to api_parser/parser_defines.h
diff --git a/api_parser/requirements.txt b/api_parser/requirements.txt
new file mode 100644
index 0000000..e61f6e6
--- /dev/null
+++ b/api_parser/requirements.txt
@@ -0,0 +1,2 @@
+future==0.18.3
+pyclibrary==0.2.1
diff --git a/api_parser/utils.py b/api_parser/utils.py
new file mode 100644
index 0000000..51279b3
--- /dev/null
+++ b/api_parser/utils.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys
+import os
+
+# Paths for output, relative to system/chre
+CHPP_PARSER_INCLUDE_PATH = 'chpp/include/chpp/common'
+CHPP_PARSER_SOURCE_PATH = 'chpp/common'
+
+LICENSE_HEADER = """/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+"""
+
+
+def android_build_top_abs_path():
+    """Gets the absolute path to the Android build top directory or exits the program."""
+
+    android_build_top = os.environ.get('ANDROID_BUILD_TOP')
+    if android_build_top == None or len(android_build_top) == 0:
+        print('ANDROID_BUILD_TOP not found. Please source build/envsetup.sh.')
+        sys.exit(1)
+
+    return android_build_top
+
+
+def system_chre_abs_path():
+    """Gets the absolute path to the system/chre directory or exits this program."""
+
+    return android_build_top_abs_path() + '/system/chre'
diff --git a/apps/ble_world/ble_world.cc b/apps/ble_world/ble_world.cc
index 63bf5fa..0470a0e 100644
--- a/apps/ble_world/ble_world.cc
+++ b/apps/ble_world/ble_world.cc
@@ -213,6 +213,10 @@
        event->eventType);
 }
 
+void handleFlushCompleteEvent(const chreAsyncResult *event) {
+  LOGI("Received flush complete event with status 0x%" PRIx8, event->errorCode);
+}
+
 void nanoappHandleEvent(uint32_t senderInstanceId, uint16_t eventType,
                         const void *eventData) {
   LOGI("Received event 0x%" PRIx16 " from 0x%" PRIx32 " at time %" PRIu64 " ms",
@@ -230,7 +234,7 @@
       handleTimerEvent(eventData);
       break;
     case CHRE_EVENT_BLE_FLUSH_COMPLETE:
-      LOGI("Received flush complete");
+      handleFlushCompleteEvent(static_cast<const chreAsyncResult *>(eventData));
       break;
     case CHRE_EVENT_BLE_RSSI_READ:
       handleRssiEvent(static_cast<const chreBleReadRssiEvent *>(eventData));
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.cc
index e766639..6878e26 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.cc
@@ -18,6 +18,7 @@
 
 #include <utility>
 
+#include "chre_api/chre.h"
 #include "third_party/contexthub/chre/util/include/chre/util/nanoapp/log.h"
 
 #define LOG_TAG "[NEARBY][ADV_CACHE]"
@@ -47,18 +48,23 @@
         cache_expire_nanosec_) {
       // TODO(b/285043291): Refactor cache element by wrapper struct/class
       // which deallocates data in its destructor.
-      if (cache_reports_[index].data != nullptr) {
-        chreHeapFree(const_cast<uint8_t *>(cache_reports_[index].data));
-      }
-      // Don't require to increase index because all elements after the indexed
-      // one are moved forward one position.
-      cache_reports_.erase(index);
+      chreHeapFree(const_cast<uint8_t *>(cache_reports_[index].data));
+      // The index does not need to increase because the current element is
+      // replaced by the end of the element and the list is resized.
+      cache_reports_.swap(index, cache_reports_.size() - 1);
+      cache_reports_.resize(cache_reports_.size() - 1);
     } else {
       ++index;
     }
   }
 }
 
+void AdvReportCache::RefreshIfNeeded() {
+  if (cache_reports_.size() > kRefreshCacheCountThreshold) {
+    Refresh();
+  }
+}
+
 void AdvReportCache::Push(const chreBleAdvertisingReport &event_report) {
 #ifdef NEARBY_PROFILE
   ashProfileBegin(&profile_data_);
@@ -97,6 +103,8 @@
           static_cast<uint8_t *>(chreHeapAlloc(sizeof(uint8_t) * dataLength));
       if (data == nullptr) {
         LOGE("Memory allocation failed!");
+        // Clean up expired cache elements for which heap memory is allocated.
+        Refresh();
         return;
       }
       memcpy(data, event_report.data, dataLength);
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.h b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.h
index 5561483..400d0ce 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.h
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/adv_report_cache.h
@@ -89,6 +89,10 @@
   // Removes cached elements older than the cache timeout.
   void Refresh();
 
+  // Removes cached elements older than the cache timeout if cache count is
+  // larger than threshold.
+  void RefreshIfNeeded();
+
  private:
   // Weight for a current data point in moving average.
   static constexpr float kMovingAverageWeight = 0.3f;
@@ -98,6 +102,11 @@
   static constexpr uint64_t kMaxExpireTimeNanoSec =
       std::numeric_limits<uint64_t>::max();
 
+  // Default value for threshold cache count to trigger refresh.
+  // At the worst case, roughly 2KB ( = 255 byte * 8) could be allocated for the
+  // cache elements exired.
+  static constexpr size_t kRefreshCacheCountThreshold = 8;
+
   chre::DynamicVector<chreBleAdvertisingReport> cache_reports_;
   // Current cache timeout value.
   uint64_t cache_expire_nanosec_ = kMaxExpireTimeNanoSec;
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_main.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_main.cc
index ff01df1..7d8fe02 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_main.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_main.cc
@@ -34,7 +34,7 @@
   // Initialize the AppManager singleton.
   // Must be done before invoking AppManagerSingleton::get().
   ::nearby::AppManagerSingleton::init();
-  return true;
+  return ::nearby::AppManagerSingleton::get()->IsInitialized();
 }
 
 void nanoappEnd(void) {
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.cc
index f59a8d3..491f69d 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.cc
@@ -22,7 +22,12 @@
 
 #include <utility>
 
+#include "chre_api/chre.h"
 #include "location/lbs/contexthub/nanoapps/common/math/macros.h"
+#ifdef ENABLE_EXTENSION
+#include "location/lbs/contexthub/nanoapps/nearby/nearby_extension.h"
+#include "location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.nanopb.h"
+#endif
 #include "location/lbs/contexthub/nanoapps/proto/filter.nanopb.h"
 #include "third_party/contexthub/chre/util/include/chre/util/macros.h"
 #include "third_party/contexthub/chre/util/include/chre/util/nanoapp/log.h"
@@ -41,6 +46,10 @@
       false /* report_total_thread_cycles */, true /* printCsvFormat */);
 #endif
 }
+bool AppManager::IsInitialized() {
+  // AppManager initialized successfully only when BLE scan is available.
+  return ble_scanner_.isAvailable();
+}
 
 void AppManager::HandleEvent(uint32_t sender_instance_id, uint16_t event_type,
                              const void *event_data) {
@@ -132,22 +141,22 @@
 #ifdef ENABLE_EXTENSION
   // Matches extended filters.
   chre::DynamicVector<FilterExtensionResult> filter_extension_results;
-  chre::DynamicVector<FilterExtensionResult> screen_on_filter_extension_results;
   filter_extension_.Match(adv_reports_cache.GetAdvReports(),
                           &filter_extension_results,
-                          &screen_on_filter_extension_results);
+                          &screen_on_filter_extension_results_);
   if (!filter_extension_results.empty()) {
     SendFilterExtensionResultToHost(filter_extension_results);
   }
-  if (!screen_on_filter_extension_results.empty()) {
+  if (!screen_on_filter_extension_results_.empty()) {
     if (screen_on_) {
       LOGD("Send screen on filter extension results back");
-      SendFilterExtensionResultToHost(screen_on_filter_extension_results);
-    } else {
-      LOGD("Update filter extension result cache");
+      SendFilterExtensionResultToHost(screen_on_filter_extension_results_);
       screen_on_filter_extension_results_.clear();
-      screen_on_filter_extension_results_ =
-          std::move(screen_on_filter_extension_results);
+    } else {
+      for (auto &filter_result : screen_on_filter_extension_results_) {
+        filter_result.RefreshIfNeeded();
+      }
+      LOGD("Updated filter extension result cache");
     }
   }
 #endif
@@ -179,10 +188,8 @@
                               event->messageSize);
       break;
 #ifdef ENABLE_EXTENSION
-    case lbs_FilterMessageType_MESSAGE_FILTER_EXTENSIONS:
-      if (UpdateFilterExtension(event)) {
-        UpdateBleScanState();
-      }
+    case lbs_FilterMessageType_MESSAGE_EXT_CONFIG_REQUEST:
+      HandleHostExtConfigRequest(event);
       break;
 #endif
   }
@@ -228,7 +235,9 @@
     LOGD("received screen config %d", screen_on_);
     if (screen_on_) {
       fp_screen_on_sent_ = false;
-      ble_scanner_.Flush();
+      if (ble_scanner_.isScanning()) {
+        ble_scanner_.Flush();
+      }
       // TODO(b/255338604): used the default report delay value only because
       // FP offload scan doesn't use low latency report delay.
       // when the flushed packet droping issue is resolved, try to reconfigure
@@ -248,7 +257,7 @@
       }
 #ifdef ENABLE_EXTENSION
       if (!screen_on_filter_extension_results_.empty()) {
-        LOGD("send filter extension result from cache");
+        LOGD("try to send filter extension result from cache");
         SendFilterExtensionResultToHost(screen_on_filter_extension_results_);
         screen_on_filter_extension_results_.clear();
       }
@@ -428,43 +437,87 @@
 }
 
 #ifdef ENABLE_EXTENSION
-bool AppManager::UpdateFilterExtension(const chreMessageFromHostData *event) {
+void AppManager::HandleHostExtConfigRequest(
+    const chreMessageFromHostData *event) {
   chreHostEndpointInfo host_info;
-  chre::DynamicVector<chreBleGenericFilter> generic_filters;
-  nearby_extension_FilterConfigResult config_result =
-      nearby_extension_FilterConfigResult_init_zero;
-  if (chreGetHostEndpointInfo(event->hostEndpoint, &host_info)) {
-    if (host_info.isNameValid) {
-      LOGD("host package name %s", host_info.packageName);
-      // TODO(b/283035791) replace "android" with the package name of Nearby
-      // Mainline host.
-      // The event is sent from Nearby Mainline host, not OEM services.
-      if (strcmp(host_info.packageName, "android") == 0) {
-        return false;
-      }
-      filter_extension_.Update(host_info, *event, &generic_filters,
-                               &config_result);
-      if (config_result.result == CHREX_NEARBY_RESULT_OK) {
-        if (!ble_scanner_.UpdateFilters(event->hostEndpoint,
-                                        &generic_filters)) {
-          config_result.result = CHREX_NEARBY_RESULT_INTERNAL_ERROR;
-        }
-      }
-    } else {
-      LOGE("host package name invalid.");
-      config_result.result = CHREX_NEARBY_RESULT_UNKNOWN_PACKAGE;
-    }
-  } else {
-    config_result.result = CHREX_NEARBY_RESULT_INTERNAL_ERROR;
+  pb_istream_t stream =
+      pb_istream_from_buffer(static_cast<const uint8_t *>(event->message),
+                             static_cast<size_t>(event->messageSize));
+  nearby_extension_ExtConfigRequest config =
+      nearby_extension_ExtConfigRequest_init_default;
+  nearby_extension_ExtConfigResponse config_response =
+      nearby_extension_ExtConfigResponse_init_zero;
+
+  if (!pb_decode(&stream, nearby_extension_ExtConfigRequest_fields, &config)) {
+    LOGE("Failed to decode extended config msg: %s", PB_GET_ERROR(&stream));
+  } else if (!chreGetHostEndpointInfo(event->hostEndpoint, &host_info)) {
     LOGE("Failed to get host info.");
+    config_response.result = CHREX_NEARBY_RESULT_INTERNAL_ERROR;
+  } else if (!host_info.isNameValid) {
+    LOGE("Failed to get package name");
+    config_response.result = CHREX_NEARBY_RESULT_UNKNOWN_PACKAGE;
+  } else {
+    LOGD("*** Receiving %s extended config ***",
+         GetExtConfigNameFromTag(config.which_config));
+
+    switch (config.which_config) {
+      case nearby_extension_ExtConfigRequest_filter_config_tag:
+        if (!HandleExtFilterConfig(host_info, config.config.filter_config,
+                                   &config_response)) {
+          LOGE("Failed to handle extended filter config");
+        }
+        break;
+      case nearby_extension_ExtConfigRequest_service_config_tag:
+        if (!HandleExtServiceConfig(host_info, config.config.service_config,
+                                    &config_response)) {
+          LOGE("Failed to handle extended service config");
+        }
+        break;
+      default:
+        LOGE("Unknown extended config %d", config.which_config);
+        config_response.result = CHREX_NEARBY_RESULT_FEATURE_NOT_SUPPORTED;
+        break;
+    }
   }
-  SendFilterExtensionConfigResultToHost(event->hostEndpoint, config_result);
+  SendExtConfigResponseToHost(config.request_id, event->hostEndpoint,
+                              config_response);
+}
+
+bool AppManager::HandleExtFilterConfig(
+    const chreHostEndpointInfo &host_info,
+    const nearby_extension_ExtConfigRequest_FilterConfig &config,
+    nearby_extension_ExtConfigResponse *config_response) {
+  chre::DynamicVector<chreBleGenericFilter> generic_filters;
+
+  filter_extension_.Update(host_info, config, &generic_filters,
+                           config_response);
+  if (config_response->result != CHREX_NEARBY_RESULT_OK) {
+    return false;
+  }
+  if (!ble_scanner_.UpdateFilters(host_info.hostEndpointId, &generic_filters)) {
+    config_response->result = CHREX_NEARBY_RESULT_INTERNAL_ERROR;
+    return false;
+  }
+  UpdateBleScanState();
   return true;
 }
 
-void AppManager::SendFilterExtensionConfigResultToHost(
-    uint16_t host_end_point,
-    const nearby_extension_FilterConfigResult &config_result) {
+bool AppManager::HandleExtServiceConfig(
+    const chreHostEndpointInfo &host_info,
+    const nearby_extension_ExtConfigRequest_ServiceConfig &config,
+    nearby_extension_ExtConfigResponse *config_response) {
+  filter_extension_.ConfigureService(host_info, config, config_response);
+  if (config_response->result != CHREX_NEARBY_RESULT_OK) {
+    return false;
+  }
+  return true;
+}
+
+void AppManager::SendExtConfigResponseToHost(
+    uint32_t request_id, uint16_t host_end_point,
+    nearby_extension_ExtConfigResponse &config_response) {
+  config_response.has_request_id = true;
+  config_response.request_id = request_id;
   uint8_t *msg_buf = (uint8_t *)chreHeapAlloc(kFilterResultsBufSize);
   if (msg_buf == nullptr) {
     LOGE("Failed to allocate message buffer of size %zu for dispatch.",
@@ -472,23 +525,23 @@
     return;
   }
   size_t encoded_size;
-  if (!FilterExtension::EncodeConfigResult(
-          config_result, ByteArray(msg_buf, kFilterResultsBufSize),
+  if (!FilterExtension::EncodeConfigResponse(
+          config_response, ByteArray(msg_buf, kFilterResultsBufSize),
           &encoded_size)) {
     chreHeapFree(msg_buf);
     return;
   }
-  auto resp_type = (config_result.result == CHREX_NEARBY_RESULT_OK
-                        ? lbs_FilterMessageType_MESSAGE_SUCCESS
-                        : lbs_FilterMessageType_MESSAGE_FAILURE);
-
   if (chreSendMessageWithPermissions(
-          msg_buf, encoded_size, resp_type, host_end_point,
+          msg_buf, encoded_size,
+          lbs_FilterMessageType_MESSAGE_EXT_CONFIG_RESPONSE, host_end_point,
           CHRE_MESSAGE_PERMISSION_BLE,
           [](void *msg, size_t /*size*/) { chreHeapFree(msg); })) {
-    LOGD("Successfully sent the filter extension config result.");
+    LOGD("Successfully sent the extended config response for request %" PRIu32
+         ".",
+         request_id);
   } else {
-    LOGE("Failed to send filter extension config result.");
+    LOGE("Failed to send extended config response for request %" PRIu32 ".",
+         request_id);
   }
 }
 
@@ -499,27 +552,41 @@
     if (reports.empty()) {
       continue;
     }
-    uint8_t *msg_buf = (uint8_t *)chreHeapAlloc(kFilterResultsBufSize);
-    if (msg_buf == nullptr) {
-      LOGE("Failed to allocate message buffer of size %zu for dispatch.",
-           kFilterResultsBufSize);
-      return;
+    for (auto &report : reports) {
+      size_t encoded_size;
+      uint8_t *msg_buf = (uint8_t *)chreHeapAlloc(kFilterResultsBufSize);
+      if (msg_buf == nullptr) {
+        LOGE("Failed to allocate message buffer of size %zu for dispatch.",
+             kFilterResultsBufSize);
+        return;
+      }
+      if (!FilterExtension::EncodeAdvReport(
+              report, ByteArray(msg_buf, kFilterResultsBufSize),
+              &encoded_size)) {
+        chreHeapFree(msg_buf);
+        return;
+      }
+      if (chreSendMessageWithPermissions(
+              msg_buf, encoded_size,
+              lbs_FilterMessageType_MESSAGE_FILTER_RESULTS, result.end_point,
+              CHRE_MESSAGE_PERMISSION_BLE,
+              [](void *msg, size_t /*size*/) { chreHeapFree(msg); })) {
+        LOGD("Successfully sent the filter extension result.");
+      } else {
+        LOGE("Failed to send filter extension result.");
+      }
     }
-    size_t encoded_size;
-    if (!FilterExtension::Encode(reports,
-                                 ByteArray(msg_buf, kFilterResultsBufSize),
-                                 &encoded_size)) {
-      chreHeapFree(msg_buf);
-      return;
-    }
-    if (chreSendMessageWithPermissions(
-            msg_buf, encoded_size, lbs_FilterMessageType_MESSAGE_FILTER_RESULTS,
-            result.end_point, CHRE_MESSAGE_PERMISSION_BLE,
-            [](void *msg, size_t /*size*/) { chreHeapFree(msg); })) {
-      LOGD("Successfully sent the filter extension result.");
-    } else {
-      LOGE("Failed to send filter extension result.");
-    }
+  }
+}
+
+const char *AppManager::GetExtConfigNameFromTag(pb_size_t config_tag) {
+  switch (config_tag) {
+    case nearby_extension_ExtConfigRequest_filter_config_tag:
+      return "FilterConfig";
+    case nearby_extension_ExtConfigRequest_service_config_tag:
+      return "ServiceConfig";
+    default:
+      return "Unknown";
   }
 }
 #endif
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.h b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.h
index bed03e4..27d0f58 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.h
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/app_manager.h
@@ -40,6 +40,9 @@
 
  public:
   AppManager();
+  // Returns true if AppManager is initialized successfully.
+  bool IsInitialized();
+
   // Handles an event from CHRE.
   void HandleEvent(uint32_t sender_instance_id, uint16_t event_type,
                    const void *event_data);
@@ -47,10 +50,13 @@
  private:
   // Handles a message from host.
   void HandleMessageFromHost(const chreMessageFromHostData *event);
+
   // Acknowledge a host's SET_FILTER_REQUEST to indicate success or failure.
   void RespondHostSetFilterRequest(bool success);
+
   // Handles config request from the host.
   void HandleHostConfigRequest(const uint8_t *message, uint32_t message_size);
+
   // Handles advertise reports to match filters.
   // Advertise reports will be cleared at the end of this function.
   void HandleMatchAdvReports(AdvReportCache &adv_reports);
@@ -94,12 +100,29 @@
       size_t &encoded_size);
 
 #ifdef ENABLE_EXTENSION
-  static void SendFilterExtensionConfigResultToHost(
-      uint16_t host_end_point,
-      const nearby_extension_FilterConfigResult &config_result);
+  // Handles extended config request from the host.
+  void HandleHostExtConfigRequest(const chreMessageFromHostData *event);
+
+  // Handles extended filter config request from the host.
+  bool HandleExtFilterConfig(
+      const chreHostEndpointInfo &host_info,
+      const nearby_extension_ExtConfigRequest_FilterConfig &config,
+      nearby_extension_ExtConfigResponse *config_response);
+
+  // Handles extended service config request from the host.
+  bool HandleExtServiceConfig(
+      const chreHostEndpointInfo &host_info,
+      const nearby_extension_ExtConfigRequest_ServiceConfig &config,
+      nearby_extension_ExtConfigResponse *config_response);
+
+  static void SendExtConfigResponseToHost(
+      uint32_t request_id, uint16_t host_end_point,
+      nearby_extension_ExtConfigResponse &config_response);
 
   static void SendFilterExtensionResultToHost(
       chre::DynamicVector<FilterExtensionResult> &filter_results);
+
+  static const char *GetExtConfigNameFromTag(pb_size_t config_tag);
 #endif
   // TODO(b/193756395): Find the optimal size or compute the size in runtime.
   // Note: the nanopb API pb_get_encoded_size
@@ -114,7 +137,11 @@
   static constexpr size_t kFilterResultsBufSize = 300;
   // Default value for Fast Pair cache to expire.
   static constexpr uint64_t kFpFilterResultExpireTimeNanoSec =
+#ifdef USE_SHORT_FP_CACHE_TO
+      3 * chre::kOneSecondInNanoseconds;
+#else
       5 * chre::kOneSecondInNanoseconds;
+#endif
 
   Filter filter_;
 #ifdef ENABLE_EXTENSION
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.cc
index f77c0cf..d4e09f4 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.cc
@@ -222,6 +222,7 @@
     }
   }
   chreBleScanFilter scan_filter;
+  memset(&scan_filter, 0, sizeof(scan_filter));
   scan_filter.rssiThreshold = CHRE_BLE_RSSI_THRESHOLD_NONE;
   scan_filter.scanFilters = generic_filters.data();
   scan_filter.scanFilterCount = static_cast<uint8_t>(generic_filters.size());
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.h b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.h
index 08c51d1..fb06f53 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.h
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/ble_scanner.h
@@ -64,6 +64,16 @@
     return is_batch_flushing_;
   }
 
+  // Returns whether BLE scan is running.
+  bool isScanning() {
+    return is_started_;
+  }
+
+  // Returns true if BLE scan is available in the device.
+  bool isAvailable() {
+    return is_ble_scan_supported_;
+  }
+
   // Returns whether BLE batch scan is supported.
   bool IsBatchSupported() {
     return is_batch_supported_;
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_account_data.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_account_data.cc
index af8c51c..b564cb8 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_account_data.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_account_data.cc
@@ -99,7 +99,7 @@
   }
   // filter and salt are required.
   if (filter.length == 0 || salt.length == 0) {
-    LOGE(
+    LOGD(
         "Invalid Fast Pair service data with filter length %zu and salt length "
         "%zu.",
         filter.length, salt.length);
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_filter.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_filter.cc
index 9407495..f7513f3 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_filter.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/fast_pair_filter.cc
@@ -154,6 +154,12 @@
          kFastPairUuid);
     return false;
   }
+  if (service_data.length == kFastPairModelIdLength) {
+    LOGD(
+        "Initial Pair advertisements, not proceed to subsequent pair "
+        "filtering.");
+    return false;
+  }
   FastPairAccountData account_data = FastPairAccountData::Parse(
       ByteArray(const_cast<uint8_t *>(service_data.data), service_data.length));
   if (!account_data.is_valid) {
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter.cc
index 7aded38..9254217 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter.cc
@@ -20,8 +20,10 @@
 #include <inttypes.h>
 #include <pb_decode.h>
 
+#include <cstdint>
 #include <iterator>
 
+#include "chre_api/chre.h"
 #include "location/lbs/contexthub/nanoapps/nearby/ble_scan_record.h"
 #include "location/lbs/contexthub/nanoapps/nearby/fast_pair_filter.h"
 #ifdef ENABLE_PRESENCE
@@ -111,6 +113,10 @@
     // The buffer size has already been checked.
     static_assert(std::size(result.bluetooth_address) == CHRE_BLE_ADDRESS_LEN);
     memcpy(result.bluetooth_address, report.address, std::size(report.address));
+    result.has_timestamp_ns = true;
+    result.timestamp_ns =
+        report.timestamp +
+        static_cast<uint64_t>(chreGetEstimatedHostTimeOffset());
     if (MatchFastPair(ble_filters_.filter[filter_index], record, &result)) {
       LOGD("Add a matched Fast Pair filter result");
       fp_filter_results->push_back(result);
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.cc
index 11e490c..abc004e 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.cc
@@ -5,8 +5,12 @@
 #include <pb_encode.h>
 
 #include <cstddef>
+#include <cstdint>
 #include <utility>
 
+#include "chre_api/chre.h"
+#include "location/lbs/contexthub/nanoapps/nearby/nearby_extension.h"
+#include "location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.nanopb.h"
 #include "third_party/contexthub/chre/util/include/chre/util/nanoapp/log.h"
 
 #define LOG_TAG "[NEARBY][FILTER_EXTENSION]"
@@ -15,34 +19,23 @@
 
 const size_t kChreBleGenericFilterDataSize = 29;
 
-constexpr nearby_extension_FilterConfig kEmptyFilterConfig =
-    nearby_extension_FilterConfig_init_zero;
-
 constexpr nearby_extension_FilterResult kEmptyFilterResult =
     nearby_extension_FilterResult_init_zero;
 
 void FilterExtension::Update(
-    const chreHostEndpointInfo &host_info, const chreMessageFromHostData &event,
+    const chreHostEndpointInfo &host_info,
+    const nearby_extension_ExtConfigRequest_FilterConfig &filter_config,
     chre::DynamicVector<chreBleGenericFilter> *generic_filters,
-    nearby_extension_FilterConfigResult *config_result) {
+    nearby_extension_ExtConfigResponse *config_response) {
   LOGD("Update extension filter");
-  nearby_extension_FilterConfig filter_config = kEmptyFilterConfig;
-  pb_istream_t stream = pb_istream_from_buffer(
-      static_cast<const uint8_t *>(event.message), event.messageSize);
-  if (!pb_decode(&stream, nearby_extension_FilterConfig_fields,
-                 &filter_config)) {
-    LOGE("Failed to decode a Filters message.");
-    return;
-  }
   const int32_t host_index = FindOrCreateHostIndex(host_info);
   if (host_index < 0) {
     LOGE("Failed to find or create the host.");
     return;
   }
-  const chreHostEndpointInfo &host =
-      host_list_[static_cast<size_t>(host_index)];
-  config_result->has_result = true;
-  config_result->has_vendor_status = true;
+  HostEndpointInfo &host = host_list_[static_cast<size_t>(host_index)];
+  config_response->has_result = true;
+  config_response->has_vendor_status = true;
 
   // Returns hardware filters.
   for (int i = 0; i < filter_config.hardware_filter_count; i++) {
@@ -64,32 +57,52 @@
   chrexNearbyExtendedFilterConfig config;
   config.data = filter_config.oem_filter;
   config.data_length = filter_config.oem_filter_length;
+  host.cache_expire_ms = filter_config.cache_expire_ms;
 
-  config_result->result =
-      static_cast<int32_t>(chrexNearbySetExtendedFilterConfig(
-          &host, &scan_filter, &config, &config_result->vendor_status));
-  if (config_result->result != CHREX_NEARBY_RESULT_OK) {
-    LOGE("Failed to config filters, result %" PRId32, config_result->result);
+  config_response->result = static_cast<int32_t>(
+      chrexNearbySetExtendedFilterConfig(&host.host_info, &scan_filter, &config,
+                                         &config_response->vendor_status));
+  if (config_response->result != CHREX_NEARBY_RESULT_OK) {
+    LOGE("Failed to config filters, result %" PRId32, config_response->result);
     host_list_.erase(static_cast<size_t>(host_index));
     return;
   }
   // Removes the host if both hardware and oem filters are empty.
   if (filter_config.hardware_filter_count == 0 &&
       filter_config.oem_filter_length == 0) {
-    LOGD("Remove host: id (%d), package name (%s)", host.hostEndpointId,
-         host.isNameValid ? host.packageName : "unknown");
+    LOGD("Remove host: id (%d), package name (%s)",
+         host.host_info.hostEndpointId,
+         host.host_info.isNameValid ? host.host_info.packageName : "unknown");
     host_list_.erase(static_cast<size_t>(host_index));
   }
 }
 
+void FilterExtension::ConfigureService(
+    const chreHostEndpointInfo &host_info,
+    const nearby_extension_ExtConfigRequest_ServiceConfig &service_config,
+    nearby_extension_ExtConfigResponse *config_response) {
+  LOGD("Configure extension service");
+  config_response->has_result = true;
+  config_response->has_vendor_status = true;
+
+  chrexNearbyExtendedServiceConfig config;
+  config.data = service_config.data;
+  config.data_length = service_config.data_length;
+
+  config_response->result =
+      static_cast<int32_t>(chrexNearbySetExtendedServiceConfig(
+          &host_info, &config, &config_response->vendor_status));
+}
+
 int32_t FilterExtension::FindOrCreateHostIndex(
     const chreHostEndpointInfo &host_info) {
   for (size_t index = 0; index < host_list_.size(); index++) {
-    if (host_info.hostEndpointId == host_list_[index].hostEndpointId) {
+    if (host_info.hostEndpointId ==
+        host_list_[index].host_info.hostEndpointId) {
       return static_cast<int32_t>(index);
     }
   }
-  if (!host_list_.push_back(host_info)) {
+  if (!host_list_.push_back(HostEndpointInfo(host_info))) {
     LOGE("Failed to add new host info.");
     return -1;
   }
@@ -101,9 +114,10 @@
  * Returns the index of the entry.
  */
 size_t AddToFilterResults(
-    uint16_t endponit_id,
+    const HostEndpointInfo &host,
     chre::DynamicVector<FilterExtensionResult> *filter_results) {
-  FilterExtensionResult result(endponit_id);
+  FilterExtensionResult result(host.host_info.hostEndpointId,
+                               host.cache_expire_ms);
   size_t idx = filter_results->find(result);
   if (filter_results->size() == idx) {
     filter_results->push_back(std::move(result));
@@ -115,12 +129,12 @@
     const chre::DynamicVector<chreBleAdvertisingReport> &ble_adv_list,
     chre::DynamicVector<FilterExtensionResult> *filter_results,
     chre::DynamicVector<FilterExtensionResult> *screen_on_filter_results) {
-  for (const chreHostEndpointInfo &host_info : host_list_) {
-    size_t idx = AddToFilterResults(host_info.hostEndpointId, filter_results);
-    size_t screen_on_idx =
-        AddToFilterResults(host_info.hostEndpointId, screen_on_filter_results);
+  for (const HostEndpointInfo &host : host_list_) {
+    size_t idx = AddToFilterResults(host, filter_results);
+    size_t screen_on_idx = AddToFilterResults(host, screen_on_filter_results);
     for (const auto &ble_adv_report : ble_adv_list) {
-      switch (chrexNearbyMatchExtendedFilter(&host_info, &ble_adv_report)) {
+      switch (
+          chrexNearbyMatchExtendedFilter(&host.host_info, &ble_adv_report)) {
         case CHREX_NEARBY_FILTER_ACTION_IGNORE:
           continue;
         case CHREX_NEARBY_FILTER_ACTION_DELIVER_ON_WAKE:
@@ -137,20 +151,20 @@
   }
 }
 
-bool FilterExtension::EncodeConfigResult(
-    const nearby_extension_FilterConfigResult &config_result,
+bool FilterExtension::EncodeConfigResponse(
+    const nearby_extension_ExtConfigResponse &config_response,
     ByteArray data_buf, size_t *encoded_size) {
   if (!pb_get_encoded_size(encoded_size,
-                           nearby_extension_FilterConfigResult_fields,
-                           &config_result)) {
-    LOGE("Failed to get filter config result size.");
+                           nearby_extension_ExtConfigResponse_fields,
+                           &config_response)) {
+    LOGE("Failed to get extended config response size.");
     return false;
   }
   pb_ostream_t ostream = pb_ostream_from_buffer(data_buf.data, data_buf.length);
 
-  if (!pb_encode(&ostream, nearby_extension_FilterConfigResult_fields,
-                 &config_result)) {
-    LOGE("Unable to encode protobuf for FilterConfigResult, error %s",
+  if (!pb_encode(&ostream, nearby_extension_ExtConfigResponse_fields,
+                 &config_response)) {
+    LOGE("Unable to encode protobuf for ExtConfigResponse, error %s",
          PB_GET_ERROR(&ostream));
     return false;
   }
@@ -207,4 +221,52 @@
   return true;
 }
 
+bool FilterExtension::EncodeAdvReport(chreBleAdvertisingReport &report,
+                                      ByteArray data_buf,
+                                      size_t *encoded_size) {
+  nearby_extension_FilterResult filter_result = kEmptyFilterResult;
+  nearby_extension_ChreBleAdvertisingReport &report_proto =
+      filter_result.report[0];
+  report_proto.has_timestamp = true;
+  report_proto.timestamp =
+      report.timestamp +
+      static_cast<uint64_t>(chreGetEstimatedHostTimeOffset());
+  report_proto.has_event_type_and_data_status = true;
+  report_proto.event_type_and_data_status = report.eventTypeAndDataStatus;
+  report_proto.has_address = true;
+  for (size_t i = 0; i < 6; i++) {
+    report_proto.address[i] = report.address[i];
+  }
+  report_proto.has_tx_power = true;
+  report_proto.tx_power = report.txPower;
+  report_proto.has_rssi = true;
+  report_proto.rssi = report.rssi;
+  report_proto.has_data_length = true;
+  report_proto.data_length = report.dataLength;
+  if (report.dataLength > 0) {
+    report_proto.has_data = true;
+  }
+  for (size_t i = 0; i < report.dataLength; i++) {
+    report_proto.data[i] = report.data[i];
+  }
+  filter_result.report_count = 1;
+  filter_result.has_error_code = true;
+  filter_result.error_code = nearby_extension_FilterResult_ErrorCode_SUCCESS;
+
+  if (!pb_get_encoded_size(encoded_size, nearby_extension_FilterResult_fields,
+                           &filter_result)) {
+    LOGE("Failed to get filter extension result size.");
+    return false;
+  }
+  pb_ostream_t ostream = pb_ostream_from_buffer(data_buf.data, data_buf.length);
+
+  if (!pb_encode(&ostream, nearby_extension_FilterResult_fields,
+                 &filter_result)) {
+    LOGE("Unable to encode protobuf for FilterExtensionResults, error %s",
+         PB_GET_ERROR(&ostream));
+    return false;
+  }
+  return true;
+}
+
 }  // namespace nearby
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.h b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.h
index 996e4b9..99c9b20 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.h
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/filter_extension.h
@@ -2,6 +2,7 @@
 #define LOCATION_LBS_CONTEXTHUB_NANOAPPS_NEARBY_FILTER_EXTENSION_H_
 #include <chre.h>
 
+#include <cstdint>
 #include <utility>
 
 #include "location/lbs/contexthub/nanoapps/nearby/adv_report_cache.h"
@@ -13,16 +14,28 @@
 
 namespace nearby {
 
-struct FilterExtensionResult {
-  // Default value for filter extension result to expire.
-  static constexpr uint64_t kFilterExtensionReportExpireTimeMilliSec =
-      5 * chre::kOneSecondInMilliseconds;
+// Default value for filter extension result to expire.
+static constexpr uint64_t kFilterExtensionReportExpireTimeMilliSec =
+    5 * chre::kOneSecondInMilliseconds;
 
+struct HostEndpointInfo {
+  chreHostEndpointInfo host_info;
+  // Host-specific configurations.
+  uint32_t cache_expire_ms;
+
+  explicit HostEndpointInfo(chreHostEndpointInfo host_info)
+      : host_info(std::move(host_info)) {}
+};
+
+struct FilterExtensionResult {
   const uint16_t end_point;
   AdvReportCache reports;
 
-  explicit FilterExtensionResult(uint16_t end_point) : end_point(end_point) {
-    reports.SetCacheTimeout(kFilterExtensionReportExpireTimeMilliSec);
+  explicit FilterExtensionResult(
+      uint16_t end_point,
+      uint64_t expire_time_ms = kFilterExtensionReportExpireTimeMilliSec)
+      : end_point(end_point) {
+    reports.SetCacheTimeout(expire_time_ms);
   }
 
   FilterExtensionResult(FilterExtensionResult &&src)
@@ -40,6 +53,17 @@
     reports.Clear();
   }
 
+  // Removes advertising reports older than the cache timeout.
+  void Refresh() {
+    reports.Refresh();
+  }
+
+  // Removes advertising reports older than the cache timeout if the cache size
+  // hits a threshold.
+  void RefreshIfNeeded() {
+    reports.RefreshIfNeeded();
+  }
+
   // Returns advertise reports in cache.
   chre::DynamicVector<chreBleAdvertisingReport> &GetAdvReports() {
     return reports.GetAdvReports();
@@ -60,14 +84,21 @@
 
 class FilterExtension {
  public:
-  // Updates extended filters (passed in the event) for each end host.
+  // Updates extended filters for each end host.
   // Returns generic_filters, which can be used to restart BLE scan.
-  // If config_result->result is not CHREX_NEARBY_RESULT_OK, the returned
+  // If config_response->result is not CHREX_NEARBY_RESULT_OK, the returned
   // generic_filters should be ignored.
-  void Update(const chreHostEndpointInfo &host_info,
-              const chreMessageFromHostData &event,
-              chre::DynamicVector<chreBleGenericFilter> *generic_filters,
-              nearby_extension_FilterConfigResult *config_result);
+  void Update(
+      const chreHostEndpointInfo &host_info,
+      const nearby_extension_ExtConfigRequest_FilterConfig &filter_config,
+      chre::DynamicVector<chreBleGenericFilter> *generic_filters,
+      nearby_extension_ExtConfigResponse *config_response);
+
+  // Configures OEM service data.
+  void ConfigureService(
+      const chreHostEndpointInfo &host_info,
+      const nearby_extension_ExtConfigRequest_ServiceConfig &service_config,
+      nearby_extension_ExtConfigResponse *config_response);
 
   // Matches BLE advertisements. Returns matched advertisements in
   // filter_results. If the results is only delivered when screen is on,
@@ -77,10 +108,10 @@
       chre::DynamicVector<FilterExtensionResult> *filter_results,
       chre::DynamicVector<FilterExtensionResult> *screen_on_filter_results);
 
-  // Serializes config_result into data_buf. The encoded size is filled in
-  // encoded_size. Returns true for successful encoding.
-  static bool EncodeConfigResult(
-      const nearby_extension_FilterConfigResult &config_result,
+  // Serializes extended config response into data_buf. The encoded size is
+  // filled in encoded_size. Returns true for successful encoding.
+  static bool EncodeConfigResponse(
+      const nearby_extension_ExtConfigResponse &config_response,
       ByteArray data_buf, size_t *encoded_size);
 
   // Encodes reports into data_buf. The reports are converted to
@@ -89,6 +120,11 @@
       const chre::DynamicVector<chreBleAdvertisingReport> &reports,
       ByteArray data_buf, size_t *encoded_size);
 
+  // Encodes a single report into data_buf. The report are converted to
+  // nearby_extension_FilterResult before the serialization.
+  static bool EncodeAdvReport(chreBleAdvertisingReport &report,
+                              ByteArray data_buf, size_t *encoded_size);
+
   // Whether host list is empty. The host which doesn't have filter
   // configuration or was disconnected should be removed in the host list.
   bool IsEmpty() const {
@@ -100,7 +136,7 @@
   int32_t FindOrCreateHostIndex(const chreHostEndpointInfo &host_info);
 
  private:
-  chre::DynamicVector<chreHostEndpointInfo> host_list_;
+  chre::DynamicVector<HostEndpointInfo> host_list_;
 };
 
 }  // namespace nearby
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.c b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.c
index 4b9370e..5407592 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.c
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.c
@@ -1,7 +1,9 @@
 #include "location/lbs/contexthub/nanoapps/nearby/nearby_extension.h"
 
-#include "stddef.h"
+#include <string.h>
 
+#include "chre_api/chre.h"
+#include "stddef.h"
 #include "third_party/contexthub/chre/util/include/chre/util/nanoapp/log.h"
 
 #define LOG_TAG "[NEARBY][FILTER_EXTENSION]"
@@ -28,6 +30,7 @@
 static uint8_t EXT_FILTER_DATA = 0;
 static uint8_t EXT_FILTER_DATA_MASK = 0;
 #define MAX_GENERIC_FILTER_COUNT 10
+#define MAX_SERVICE_CONFIG_LEN 10
 
 struct hwBleScanFilter {
   int8_t rssi_threshold;
@@ -38,6 +41,7 @@
     .rssi_threshold = CHRE_BLE_RSSI_THRESHOLD_NONE,
     .scan_filter_count = 0,
 };
+static uint8_t EXT_SERVICE_CONFIG[MAX_SERVICE_CONFIG_LEN] = {0};
 const char kHostPackageName[] = "com.google.android.nearby.offload.reference";
 
 uint32_t chrexNearbySetExtendedFilterConfig(
@@ -50,7 +54,12 @@
     LOGE("Invalid scan_filter configuration");
     return CHREX_NEARBY_RESULT_INTERNAL_ERROR;
   }
-  // Performs a deep copy of the hardware scan filter structure
+  if (!host_info->isNameValid ||
+      strcmp(host_info->packageName, kHostPackageName) != 0) {
+    LOGE("Unknown package: %s", host_info->packageName);
+    return CHREX_NEARBY_RESULT_UNKNOWN_PACKAGE;
+  }
+  // Performs a deep copy of the hardware scan filter structure.
   HW_SCAN_FILTER.rssi_threshold = scan_filter->rssiThreshold;
   HW_SCAN_FILTER.scan_filter_count = scan_filter->scanFilterCount;
   for (size_t i = 0; i < HW_SCAN_FILTER.scan_filter_count; ++i) {
@@ -71,17 +80,33 @@
          HW_SCAN_FILTER.scan_filters[i].type,
          HW_SCAN_FILTER.scan_filters[i].len);
   }
-  if (host_info->isNameValid &&
-      strcmp(host_info->packageName, kHostPackageName) == 0) {
-    EXT_FILTER_DATA = config->data[EXT_FILTER_CONFIG_DATA_INDEX];
-    EXT_FILTER_DATA_MASK = config->data[EXT_FILTER_CONFIG_DATA_MASK_INDEX];
-  }
+  EXT_FILTER_DATA = config->data[EXT_FILTER_CONFIG_DATA_INDEX];
+  EXT_FILTER_DATA_MASK = config->data[EXT_FILTER_CONFIG_DATA_MASK_INDEX];
   *vendorStatusCode = 0;
   LOGD("Set EXT_FILTER_DATA 0x%02X", EXT_FILTER_DATA);
   LOGD("Set EXT_FILTER_DATA_MASK 0x%02X", EXT_FILTER_DATA_MASK);
   return CHREX_NEARBY_RESULT_OK;
 }
 
+uint32_t chrexNearbySetExtendedServiceConfig(
+    const struct chreHostEndpointInfo *host_info,
+    const struct chrexNearbyExtendedServiceConfig *config,
+    uint32_t *vendorStatusCode) {
+  if (!host_info->isNameValid ||
+      strcmp(host_info->packageName, kHostPackageName) != 0) {
+    LOGE("Unknown package: %s", host_info->packageName);
+    return CHREX_NEARBY_RESULT_UNKNOWN_PACKAGE;
+  }
+  if (config->data_length > MAX_SERVICE_CONFIG_LEN) {
+    return CHREX_NEARBY_RESULT_OUT_OF_RESOURCES;
+  }
+  // Performs a deep copy of the service configuration.
+  memcpy(EXT_SERVICE_CONFIG, config->data, config->data_length);
+  *vendorStatusCode = 0;
+  LOGD("Set EXT_SERVICE_CONFIG 0x%02X", EXT_SERVICE_CONFIG[0]);
+  return CHREX_NEARBY_RESULT_OK;
+}
+
 uint32_t chrexNearbyMatchExtendedFilter(
     const struct chreHostEndpointInfo *host_info,
     const struct chreBleAdvertisingReport *report) {
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.h b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.h
index 7d26d68..a34a1d9 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.h
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension.h
@@ -13,6 +13,12 @@
   const uint8_t *data;  //!< Vendor-defined payload
 };
 
+//! Contains vendor-defined data for configuring vendor service in library
+struct chrexNearbyExtendedServiceConfig {
+  size_t data_length;   //!< Number of bytes in data
+  const uint8_t *data;  //!< Vendor-defined payload
+};
+
 enum chrexNearbyResult {
   //! Operation completed successfully
   CHREX_NEARBY_RESULT_OK = 0,
@@ -73,6 +79,32 @@
     const struct chrexNearbyExtendedFilterConfig *config,
     uint32_t *vendorStatusCode);
 
+/**
+ * Configures vendor-defined service data sent by a vendor/OEM service on the
+ * host. This is called by the Nearby nanoapp when it receives a
+ * ChreNearbyExtendedService message, and the result is sent back to the host
+ * endpoint that made the request. The pointers supplied for the
+ * parameters are not guaranteed to be valid after this call. The OEM library
+ * should perform a deep copy of the structure if we want to store them in the
+ * library.
+ *
+ * @param host_info The meta data for a host end point that sent the message,
+ *     obtained from chreHostEndpointInfo. The implementation must ensure that
+ *     messages from a given host end point are only provided to the vendor
+ *     library explicitly associated with that host end point.
+ * @param config Configuration data in a vendor-defined format.
+ * @param[out] vendorStatusCode Optional vendor-defined status code that will be
+ *     returned to the vendor service regardless of the return value of this
+ *     function. The value 0 is reserved to indicate that a vendor status code
+ * was not provided or is not relevant. All other values have a vendor-defined
+ *     meaning.
+ * @return A value from enum chrexNearbyResult.
+ */
+uint32_t chrexNearbySetExtendedServiceConfig(
+    const struct chreHostEndpointInfo *host_info,
+    const struct chrexNearbyExtendedServiceConfig *config,
+    uint32_t *vendorStatusCode);
+
 enum chrexNearbyFilterAction {
   //! Ignore/drop this advertising report
   CHREX_NEARBY_FILTER_ACTION_IGNORE = 0,
@@ -111,6 +143,7 @@
  * @param report Contains data for a BLE advertisement.
  * @return A value from enum chrexNearbyFilterAction.
  */
+// TODO(b/305277310): Pass OEM extension API version to OEM library
 uint32_t chrexNearbyMatchExtendedFilter(
     const struct chreHostEndpointInfo *host_info,
     const struct chreBleAdvertisingReport *report);
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension_support_lib.cc b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension_support_lib.cc
index 2445bd4..62634e3 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension_support_lib.cc
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/nearby_extension_support_lib.cc
@@ -49,6 +49,17 @@
 }
 
 WEAK_SYMBOL
+uint32_t chrexNearbySetExtendedServiceConfig(
+    const struct chreHostEndpointInfo *host_info,
+    const struct chrexNearbyExtendedServiceConfig *config,
+    uint32_t *vendorStatusCode) {
+  auto *fptr = CHRE_NSL_LAZY_LOOKUP(chrexNearbySetExtendedServiceConfig);
+  return (fptr != nullptr)
+             ? fptr(host_info, config, vendorStatusCode)
+             : chrexNearbyResult::CHREX_NEARBY_RESULT_FEATURE_NOT_SUPPORTED;
+}
+
+WEAK_SYMBOL
 uint32_t chrexNearbyMatchExtendedFilter(
     const struct chreHostEndpointInfo *host_info,
     const struct chreBleAdvertisingReport *report) {
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/ble_filter.proto b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/ble_filter.proto
index 0f938c4..6a9e734 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/ble_filter.proto
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/ble_filter.proto
@@ -114,6 +114,9 @@
   repeated DataElement data_element = 7;
   optional bytes ble_service_data = 8;
   optional ResultType result_type = 9;
+  // Timestamp the advertisement was received, in nanoseconds, relative to
+  // Android SystemClock.elapsedRealtimeNanos().
+  optional uint64 timestamp_ns = 10;
 }
 
 message BleFilterResults {
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.options b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.options
index 769841b..b0c8d25 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.options
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.options
@@ -11,11 +11,14 @@
 //  See the License for the specific language governing permissions and
 //  limitations under the License.
 
-nearby_extension.FilterConfig.oem_filter max_size:512
-nearby_extension.FilterConfig.oem_filter type:FT_INLINE
+nearby_extension.ExtConfigRequest.FilterConfig.oem_filter max_size:800
+nearby_extension.ExtConfigRequest.FilterConfig.oem_filter type:FT_INLINE
 
-nearby_extension.FilterConfig.hardware_filter max_count:10
-nearby_extension.FilterConfig.hardware_filter type:FT_STATIC
+nearby_extension.ExtConfigRequest.FilterConfig.hardware_filter max_count:10
+nearby_extension.ExtConfigRequest.FilterConfig.hardware_filter type:FT_STATIC
+
+nearby_extension.ExtConfigRequest.ServiceConfig.data max_size:800
+nearby_extension.ExtConfigRequest.ServiceConfig.data type:FT_INLINE
 
 nearby_extension.ChreBleGenericFilter.data max_size:29
 nearby_extension.ChreBleGenericFilter.data type:FT_INLINE
@@ -23,13 +26,13 @@
 nearby_extension.ChreBleGenericFilter.data_mask max_size:29
 nearby_extension.ChreBleGenericFilter.data_mask type:FT_INLINE
 
-nearby_extension.FilterResult.report max_count:20
+nearby_extension.FilterResult.report max_count:1
 nearby_extension.FilterResult.report type:FT_STATIC
 
 nearby_extension.ChreBleAdvertisingReport.address max_size:6
 nearby_extension.ChreBleAdvertisingReport.address type:FT_INLINE
 
-nearby_extension.ChreBleAdvertisingReport.data max_size:29
+nearby_extension.ChreBleAdvertisingReport.data max_size:255
 nearby_extension.ChreBleAdvertisingReport.data type:FT_INLINE
 
 
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.proto b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.proto
index f289c57..6be8352 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.proto
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/nearby/proto/nearby_extension.proto
@@ -21,39 +21,73 @@
 option java_package = "com.google.location.lbs.contexthub";
 option java_outer_classname = "NearbyOffloadExtension";
 
-message FilterConfig {
-  // Vendor-specific configuration data for extended filter. Byte array opaque
-  // to Nearby nanoapp, which will be forwarded through
-  // chrexNearbySetExtendedFilterConfig().
-  // If the OEM service wishes to send more data than can fit in a single
-  // message, or update previous configuration, it can send additional messages.
-  optional bytes oem_filter = 1;
-  optional uint32 oem_filter_length = 2;
+message ExtConfigRequest {
+  message FilterConfig {
+    // Vendor-specific configuration data for extended filter. Byte array opaque
+    // to Nearby nanoapp, which will be forwarded through
+    // chrexNearbySetExtendedFilterConfig().
+    // If both hardware and oem filters are empty, the scan requested by this
+    // host will stop. Otherwise, the scan will start with the scan filters.
+    // If the OEM service wishes to send more data than can fit in a single
+    // message, or update previous configuration, it can send additional
+    // messages.
+    optional bytes oem_filter = 1;
+    optional uint32 oem_filter_length = 2;
 
-  // List of Hardware filters (follows chreBleScanFilter defined in CHRE BLE
-  // API). Resource for hardware filters is constrained in CHRE, and hardware
-  // filtering is best effort, i.e. advertisements may still be forwarded for
-  // inspection if they do not match the configured hardware filters. It is
-  // expected that an OEM service will include at least one hardware filter in
-  // the first message. Subsequent messages that do not include this field will
-  // not impact previously configured filters. But if this field is populated in
-  // a subsequent message, its contents will replace any previously set filters.
-  // To remove all previously set hardware filters, reset extended filtering by
-  // closing the ContextHubClient connection.
-  repeated ChreBleGenericFilter hardware_filter = 3;
+    // List of Hardware filters (follows chreBleScanFilter defined in CHRE BLE
+    // API). Resource for hardware filters is constrained in CHRE, and hardware
+    // filtering is best effort, i.e. advertisements may still be forwarded for
+    // inspection if they do not match the configured hardware filters. It is
+    // expected that an OEM service will include at least one hardware filter in
+    // the first message. Subsequent messages that do not include this field
+    // will not impact previously configured filters. But if this field is
+    // populated in a subsequent message, its contents will replace any
+    // previously set filters. To remove all previously set hardware filters,
+    // reset extended filtering by closing the ContextHubClient connection.
+    repeated ChreBleGenericFilter hardware_filter = 3;
 
-  // Maximum time to batch BLE scan results before processing in the nanoapp, in
-  // milliseconds. For optimal power, specify the longest value that the use
-  // case permits. If not provided, either the last provided value will continue
-  // to be used, or if no previous value was given, defaults defined in the
-  // Nearby nanoapp will be used.
-  optional uint32 report_delay_ms = 4;
+    // Maximum time to batch BLE scan results before processing in the nanoapp,
+    // in milliseconds. For optimal power, specify the longest value that the
+    // use case permits. If not provided, either the last provided value will
+    // continue to be used, or if no previous value was given, defaults defined
+    // in the Nearby nanoapp will be used.
+    optional uint32 report_delay_ms = 4;
 
-  // BLE scan modes identify functional scan levels without specifying or
-  // guaranteeing particular scan parameters (e.g. duty cycle, interval, radio
-  // chain). The actual scan parameters may be platform dependent and may change
-  // without notice in real time based on contextual cues, etc.
-  optional ChreBleScanMode scan_mode = 5;
+    // BLE scan modes identify functional scan levels without specifying or
+    // guaranteeing particular scan parameters (e.g. duty cycle, interval, radio
+    // chain). The actual scan parameters may be platform dependent and may
+    // change without notice in real time based on contextual cues, etc.
+    optional ChreBleScanMode scan_mode = 5;
+
+    // BLE advertising report cache expires after this time period.
+    // The expired reports are descarded and not delivered to the OEM service.
+    optional uint32 cache_expire_ms = 6 [default = 5000];
+  }
+
+  message ServiceConfig {
+    // Vendor-specific configuration data for OEM service. Byte array opaque
+    // to Nearby nanoapp, which will be forwarded through
+    // chrexNearbySetServiceConfig().
+    // If OEM service cannot pass service config data through oem_filter in
+    // FilterConfig, or if OEM service want to pass it at another time, it can
+    // pass the service config data to the OEM library through ServiceConfig.
+    // ServiceConfig is only responsible for passing service config data to the
+    // OEM library, regardless of the LE scan start/stop behavior and
+    // hardware_filter.
+    // If the OEM service wishes to send more data than can fit in a single
+    // message, or update previous configuration, it can send additional
+    // messages.
+    optional bytes data = 1;
+    optional uint32 data_length = 2;
+  }
+
+  // Request ID specified by the client to pair Request/Response messages.
+  optional uint32 request_id = 1;
+
+  oneof config {
+    FilterConfig filter_config = 2;
+    ServiceConfig service_config = 3;
+  }
 }
 
 message ChreBleGenericFilter {
@@ -95,13 +129,14 @@
   CHRE_BLE_SCAN_MODE_AGGRESSIVE = 3;
 }
 
-// Sent in response to FilterConfig
-message FilterConfigResult {
-  // Value from enum chrexNearbyResult that was returned by
-  // chrexNearbySetExtendedFilterConfig()
-  optional int32 result = 1;
-  // Vendor-defined status code provided in chrexNearbySetExtendedFilterConfig()
-  optional uint32 vendor_status = 2;
+// Sent in response to ExtConfigRequest
+message ExtConfigResponse {
+  // Request ID of the corresponding Request message.
+  optional uint32 request_id = 1;
+  // Value from enum chrexNearbyResult that was returned from OEM library.
+  optional int32 result = 2;
+  // Vendor-defined status code provided from OEM library.
+  optional uint32 vendor_status = 3;
 }
 
 // Sent when one or more advertisements matched an extended filter
diff --git a/apps/nearby/location/lbs/contexthub/nanoapps/proto/filter.proto b/apps/nearby/location/lbs/contexthub/nanoapps/proto/filter.proto
index f051f54..34be88e 100644
--- a/apps/nearby/location/lbs/contexthub/nanoapps/proto/filter.proto
+++ b/apps/nearby/location/lbs/contexthub/nanoapps/proto/filter.proto
@@ -20,6 +20,8 @@
   MESSAGE_FILTER_RESULTS = 4;
   // Config the filtering, including start/stop filtering.
   MESSAGE_CONFIG = 5;
-  // Message from host to CHRE to set Filter extensions.
-  MESSAGE_FILTER_EXTENSIONS = 6;
+  // Message from host to CHRE to request extended configuration.
+  MESSAGE_EXT_CONFIG_REQUEST = 6;
+  // Message from CHRE to host for the response of extended configuration.
+  MESSAGE_EXT_CONFIG_RESPONSE = 7;
 }
diff --git a/apps/rpc_world/src/rpc_world_manager.cc b/apps/rpc_world/src/rpc_world_manager.cc
index d658678..182e028 100644
--- a/apps/rpc_world/src/rpc_world_manager.cc
+++ b/apps/rpc_world/src/rpc_world_manager.cc
@@ -103,7 +103,7 @@
     mAddCall.Write(addRequest);
     mAddCall.Write(addRequest);
     mAddCall.Write(addRequest);
-    mAddCall.CloseClientStream();
+    mAddCall.RequestCompletion();
   } else {
     LOGE("Error while retrieving the client");
   }
@@ -146,6 +146,8 @@
 }
 
 void RpcWorldManager::end() {
+  mServer.close();
+  mClient.close();
   if (mTimerId != CHRE_TIMER_INVALID) {
     chreTimerCancel(mTimerId);
   }
@@ -168,7 +170,7 @@
   reader.set_on_next([](const chre_rpc_NumberMessage &request) {
     RpcWorldManagerSingleton::get()->mSum += request.number;
   });
-  reader.set_on_client_stream_end([]() {
+  reader.set_on_completion_requested([]() {
     chre_rpc_NumberMessage response;
     response.number = RpcWorldManagerSingleton::get()->mSum;
     RpcWorldManagerSingleton::get()->setPermissionForNextMessage(
diff --git a/apps/test/chqts/src/general_test/basic_ble_test.cc b/apps/test/chqts/src/general_test/basic_ble_test.cc
index 891ad65..4d01722 100644
--- a/apps/test/chqts/src/general_test/basic_ble_test.cc
+++ b/apps/test/chqts/src/general_test/basic_ble_test.cc
@@ -138,6 +138,11 @@
     }
     mFlushWasCalled = true;
   } else {
+    if (chreBleFlushAsync(&gFlushCookie)) {
+      sendFatalFailureToHost(
+          "chreBleFlushAsync should return false if batching is not supported");
+    }
+
     if (!chreBleStopScanAsync()) {
       sendFatalFailureToHost("Failed to stop a BLE scan session");
     }
diff --git a/apps/test/chqts/src/general_test/send_event_stress_test.cc b/apps/test/chqts/src/general_test/send_event_stress_test.cc
index 6bc7e63..41e8dfe 100644
--- a/apps/test/chqts/src/general_test/send_event_stress_test.cc
+++ b/apps/test/chqts/src/general_test/send_event_stress_test.cc
@@ -84,8 +84,12 @@
     sendFatalFailureToHost("Insufficient events available");
   }
 
-  // sCompleteCallbacksLeft may be 0 or 1 at this point.  We don't care.
-  // We just know we also expect all the sEventsLeft to have callbacks.
+  // If kMaxEventsToSend events are sent, we need to reset
+  // sCompleteCallbacksLeft because we only expect at most sEventsLeft to have
+  // callbacks.
+  if (sEventsLeft == kMaxEventsToSend) {
+    sCompleteCallbacksLeft = 0;
+  }
   sCompleteCallbacksLeft += sEventsLeft;
 
   sInMethod = false;
diff --git a/apps/test/common/chre_api_test/chre_api_test.mk b/apps/test/common/chre_api_test/chre_api_test.mk
index 5f8559f..c9ad8c2 100644
--- a/apps/test/common/chre_api_test/chre_api_test.mk
+++ b/apps/test/common/chre_api_test/chre_api_test.mk
@@ -25,6 +25,7 @@
 # Utilities ####################################################################
 
 COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/ble.cc
+COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/string.cc
 
 # Compiler Flags ###############################################################
 
@@ -44,3 +45,4 @@
 # PW RPC protos ################################################################
 
 PW_RPC_SRCS = $(NANOAPP_PATH)/rpc/chre_api_test.proto
+PW_RPC_SRCS += $(ANDROID_BUILD_TOP)/external/protobuf/src/google/protobuf/empty.proto
diff --git a/apps/test/common/chre_api_test/inc/chre_api_test_manager.h b/apps/test/common/chre_api_test/inc/chre_api_test_manager.h
index 8380e8a..0e4543b 100644
--- a/apps/test/common/chre_api_test/inc/chre_api_test_manager.h
+++ b/apps/test/common/chre_api_test/inc/chre_api_test_manager.h
@@ -38,29 +38,16 @@
   /**
    * Returns the BLE capabilities.
    */
-  pw::Status ChreBleGetCapabilities(const chre_rpc_Void &request,
+  pw::Status ChreBleGetCapabilities(const google_protobuf_Empty &request,
                                     chre_rpc_Capabilities &response);
 
   /**
    * Returns the BLE filter capabilities.
    */
-  pw::Status ChreBleGetFilterCapabilities(const chre_rpc_Void &request,
+  pw::Status ChreBleGetFilterCapabilities(const google_protobuf_Empty &request,
                                           chre_rpc_Capabilities &response);
 
   /**
-   * Starts a BLE scan.
-   */
-  pw::Status ChreBleStartScanAsync(
-      const chre_rpc_ChreBleStartScanAsyncInput &request,
-      chre_rpc_Status &response);
-
-  /**
-   * Stops a BLE scan.
-   */
-  pw::Status ChreBleStopScanAsync(const chre_rpc_Void &request,
-                                  chre_rpc_Status &response);
-
-  /**
    * Finds the default sensor and returns the handle in the output.
    */
   pw::Status ChreSensorFindDefault(
@@ -108,13 +95,6 @@
       chre_rpc_Status &response);
 
   /**
-   * Retrieve the last host endpoint notification.
-   */
-  pw::Status RetrieveLatestDisconnectedHostEndpointEvent(
-      const chre_rpc_Void &request,
-      chre_rpc_RetrieveLatestDisconnectedHostEndpointEventOutput &response);
-
-  /**
    * Gets the host endpoint info for a given host endpoint id.
    */
   pw::Status ChreGetHostEndpointInfo(
@@ -132,7 +112,7 @@
    * Stops a BLE scan synchronously. Waits for the CHRE_EVENT_BLE_ASYNC_RESULT
    * event.
    */
-  void ChreBleStopScanSync(const chre_rpc_Void &request,
+  void ChreBleStopScanSync(const google_protobuf_Empty &request,
                            ServerWriter<chre_rpc_GeneralSyncMessage> &writer);
 
   /**
@@ -174,16 +154,6 @@
 
  private:
   /**
-   * Copies a string from source to destination up to the length of the source
-   * or the max value. Pads with null characters.
-   *
-   * @param destination         the destination string.
-   * @param source              the source string.
-   * @param maxChars            the maximum number of chars.
-   */
-  void copyString(char *destination, const char *source, size_t maxChars);
-
-  /**
    * Sets the synchronous timeout timer for the active sync message.
    *
    * @return                     if the operation was successful.
@@ -200,17 +170,17 @@
    *                             false otherwise.
    */
   bool validateInputAndCallChreBleGetCapabilities(
-      const chre_rpc_Void &request, chre_rpc_Capabilities &response);
+      const google_protobuf_Empty &request, chre_rpc_Capabilities &response);
 
   bool validateInputAndCallChreBleGetFilterCapabilities(
-      const chre_rpc_Void &request, chre_rpc_Capabilities &response);
+      const google_protobuf_Empty &request, chre_rpc_Capabilities &response);
 
   bool validateInputAndCallChreBleStartScanAsync(
       const chre_rpc_ChreBleStartScanAsyncInput &request,
       chre_rpc_Status &response);
 
-  bool validateInputAndCallChreBleStopScanAsync(const chre_rpc_Void &request,
-                                                chre_rpc_Status &response);
+  bool validateInputAndCallChreBleStopScanAsync(
+      const google_protobuf_Empty &request, chre_rpc_Status &response);
 
   bool validateInputAndCallChreSensorFindDefault(
       const chre_rpc_ChreSensorFindDefaultInput &request,
@@ -240,14 +210,24 @@
       const chre_rpc_ChreConfigureHostEndpointNotificationsInput &request,
       chre_rpc_Status &response);
 
-  bool validateInputAndRetrieveLatestDisconnectedHostEndpointEvent(
-      const chre_rpc_Void &request,
-      chre_rpc_RetrieveLatestDisconnectedHostEndpointEventOutput &response);
-
   bool validateInputAndCallChreGetHostEndpointInfo(
       const chre_rpc_ChreGetHostEndpointInfoInput &request,
       chre_rpc_ChreGetHostEndpointInfoOutput &response);
 
+  /**
+   * Validates the BLE scan filters and creates a generic filter in the
+   * outputScanFilters array. scanFilters and outputScanFilters must be of size
+   * scanFilterCount or greater.
+   *
+   * @param scanFilters          the input scan filters.
+   * @param outputScanFilters    the output scan filters.
+   * @param scanFilterCount      the number of scan filters.
+   * @return                     whether the validation was successful.
+   */
+  bool validateBleScanFilters(const chre_rpc_ChreBleGenericFilter *scanFilters,
+                              chreBleGenericFilter *outputScanFilters,
+                              uint32_t scanFilterCount);
+
   constexpr static uint32_t kMaxNumEventTypes =
       10;  // declared in chre_api_test.options
 
@@ -259,12 +239,6 @@
   uint32_t mSyncTimerHandle = CHRE_TIMER_INVALID;
   uint8_t mRequestType;
 
-  /**
-   * Variables to store disconnected host endpoint notification.
-   */
-  uint32_t mReceivedHostEndpointDisconnectedNum = 0;
-  chreHostEndpointNotification mLatestHostEndpointNotification;
-
   /*
    * Variables to control synchronization for sync events calls.
    * Only one sync event call may be made at a time.
diff --git a/apps/test/common/chre_api_test/rpc/chre_api_test.options b/apps/test/common/chre_api_test/rpc/chre_api_test.options
index 6ec7e7c..6c3bc2c 100644
--- a/apps/test/common/chre_api_test/rpc/chre_api_test.options
+++ b/apps/test/common/chre_api_test/rpc/chre_api_test.options
@@ -7,3 +7,7 @@
 chre.rpc.ChreGetHostEndpointInfoOutput.endpointTag max_size:51 # CHRE_MAX_ENDPOINT_TAG_LEN
 chre.rpc.ChreSensorThreeAxisData.readings max_count:10
 chre.rpc.GatherEventsInput.eventTypes max_count:10
+chre.rpc.ChreBleAdvertisingReport.address max_size:6 # CHRE_BLE_ADDRESS_LEN
+chre.rpc.ChreBleAdvertisingReport.directAddress max_size:6 # CHRE_BLE_ADDRESS_LEN
+chre.rpc.ChreBleAdvertisingReport.data max_size:255 # extended range is [0, 255]
+chre.rpc.ChreBleAdvertisementEvent.reports max_count:10
diff --git a/apps/test/common/chre_api_test/rpc/chre_api_test.proto b/apps/test/common/chre_api_test/rpc/chre_api_test.proto
index 57b7dbc..54c716e 100644
--- a/apps/test/common/chre_api_test/rpc/chre_api_test.proto
+++ b/apps/test/common/chre_api_test/rpc/chre_api_test.proto
@@ -2,26 +2,17 @@
 
 package chre.rpc;
 
+import "google/protobuf/empty.proto";
+
 option java_package = "dev.chre.rpc.proto";
 
 service ChreApiTestService {
   // Returns the BLE capabilities.
-  rpc ChreBleGetCapabilities(Void) returns (Capabilities) {}
+  rpc ChreBleGetCapabilities(google.protobuf.Empty) returns (Capabilities) {}
 
   // Returns the BLE filter capabilities.
-  rpc ChreBleGetFilterCapabilities(Void) returns (Capabilities) {}
-
-  /* Starts a BLE scan asynchronously. This will not wait for the
-   * event but will return success if the chreBleStartScanAsync
-   * function returns success.
-   */
-  rpc ChreBleStartScanAsync(ChreBleStartScanAsyncInput) returns (Status) {}
-
-  /* Stops a BLE scan asynchronously. This will not wait for the
-   * event but will return success if the chreBleStopScanAsync
-   * function returns success.
-   */
-  rpc ChreBleStopScanAsync(Void) returns (Status) {}
+  rpc ChreBleGetFilterCapabilities(google.protobuf.Empty)
+      returns (Capabilities) {}
 
   // Finds the default sensor for the given sensor type.
   rpc ChreSensorFindDefault(ChreSensorFindDefaultInput)
@@ -49,12 +40,6 @@
   rpc ChreConfigureHostEndpointNotifications(
       ChreConfigureHostEndpointNotificationsInput) returns (Status) {}
 
-  // TODO(b/274791978): Deprecate this once we can capture event
-  // Retrieves the latest disconnected host endpoint event and the number of
-  // disconnect event received.
-  rpc RetrieveLatestDisconnectedHostEndpointEvent(Void)
-      returns (RetrieveLatestDisconnectedHostEndpointEventOutput) {}
-
   // Gets the host endpoint info for a given host endpoint id.
   rpc ChreGetHostEndpointInfo(ChreGetHostEndpointInfoInput)
       returns (ChreGetHostEndpointInfoOutput) {}
@@ -74,7 +59,8 @@
    * function returns success and the event is seen. This will
    * return the event's success field.
    */
-  rpc ChreBleStopScanSync(Void) returns (stream GeneralSyncMessage) {}
+  rpc ChreBleStopScanSync(google.protobuf.Empty)
+      returns (stream GeneralSyncMessage) {}
 
   // Returns events that match the eventType filter
   rpc GatherEvents(GatherEventsInput) returns (stream GeneralEventsMessage) {}
@@ -82,9 +68,6 @@
 
 // General messages
 
-// Empty message (void)
-message Void {}
-
 // Contains a capabilities uint32
 message Capabilities {
   uint32 capabilities = 1;
@@ -110,7 +93,9 @@
 // Contains event filters for gathering events
 message GatherEventsInput {
   repeated uint32 eventTypes = 1;
-  uint32 eventTypeCount = 2;
+
+  // Deprecated: We use the built-in count variable now
+  uint32 eventTypeCount = 2 [deprecated = true];
   uint32 eventCount = 3;
   uint64 timeoutInNs = 4;
 }
@@ -122,6 +107,8 @@
   oneof data {
     ChreSensorThreeAxisData chreSensorThreeAxisData = 2;
     ChreSensorSamplingStatusEvent chreSensorSamplingStatusEvent = 3;
+    ChreHostEndpointNotification chreHostEndpointNotification = 4;
+    ChreBleAdvertisementEvent chreBleAdvertisementEvent = 5;
   }
 }
 
@@ -153,7 +140,7 @@
   uint32 reserved = 5;
 }
 
-// Individual sample data for a three-axis sensor.
+// Individual sample data for a three-axis sensor
 message ChreSensorThreeAxisSampleData {
   uint32 timestampDelta = 1;
   float x = 2;
@@ -161,6 +148,30 @@
   float z = 4;
 }
 
+// A BLE advertising event
+message ChreBleAdvertisementEvent {
+  uint32 reserved = 1;
+  repeated ChreBleAdvertisingReport reports = 2;
+}
+
+// A BLE advertising report
+message ChreBleAdvertisingReport {
+  uint64 timestamp = 1;
+  uint32 eventTypeAndDataStatus = 2;
+  uint32 addressType = 3;
+  bytes address = 4;
+  uint32 primaryPhy = 5;
+  uint32 secondaryPhy = 6;
+  uint32 advertisingSid = 7;
+  int32 txPower = 8;
+  uint32 periodicAdvertisingInterval = 9;
+  int32 rssi = 10;
+  uint32 directAddressType = 11;
+  bytes directAddress = 12;
+  bytes data = 13;
+  uint32 reserved = 14;
+}
+
 // Function specific messages
 
 // Input value for ChreSensorFindDefault
@@ -181,6 +192,11 @@
   uint32 hostEndpointId = 2;
 }
 
+message ChreHostEndpointNotification {
+  uint32 hostEndpointId = 1;
+  uint32 notificationType = 2;
+}
+
 // Input value for ChreGetHostEndpointInfo
 message ChreGetHostEndpointInfoInput {
   uint32 hostEndpointId = 1;
@@ -271,7 +287,9 @@
 // Scan filter for BLE scanning
 message ChreBleScanFilter {
   int32 rssiThreshold = 1;
-  uint32 scanFilterCount = 2;
+
+  // Deprecated: We use the built-in count variable now
+  uint32 scanFilterCount = 2 [deprecated = true];
   repeated ChreBleGenericFilter scanFilters = 3;
 }
 
diff --git a/apps/test/common/chre_api_test/src/chre_api_test_manager.cc b/apps/test/common/chre_api_test/src/chre_api_test_manager.cc
index 3e4a38f..695255d 100644
--- a/apps/test/common/chre_api_test/src/chre_api_test_manager.cc
+++ b/apps/test/common/chre_api_test/src/chre_api_test_manager.cc
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include <algorithm>
+
 #include "chre_api_test_manager.h"
 
 #include "chre.h"
@@ -27,6 +29,7 @@
  * The following constants are defined in chre_api_test.options.
  */
 constexpr uint32_t kThreeAxisDataReadingsMaxCount = 10;
+constexpr uint32_t kChreBleAdvertisementReportMaxCount = 10;
 
 /**
  * Closes the writer and invalidates the writer.
@@ -86,7 +89,7 @@
 // Start ChreApiTestService RPC generated functions
 
 pw::Status ChreApiTestService::ChreBleGetCapabilities(
-    const chre_rpc_Void &request, chre_rpc_Capabilities &response) {
+    const google_protobuf_Empty &request, chre_rpc_Capabilities &response) {
   ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
       CHRE_MESSAGE_PERMISSION_NONE);
   return validateInputAndCallChreBleGetCapabilities(request, response)
@@ -95,7 +98,7 @@
 }
 
 pw::Status ChreApiTestService::ChreBleGetFilterCapabilities(
-    const chre_rpc_Void &request, chre_rpc_Capabilities &response) {
+    const google_protobuf_Empty &request, chre_rpc_Capabilities &response) {
   ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
       CHRE_MESSAGE_PERMISSION_NONE);
   return validateInputAndCallChreBleGetFilterCapabilities(request, response)
@@ -103,25 +106,6 @@
              : pw::Status::InvalidArgument();
 }
 
-pw::Status ChreApiTestService::ChreBleStartScanAsync(
-    const chre_rpc_ChreBleStartScanAsyncInput &request,
-    chre_rpc_Status &response) {
-  ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
-      CHRE_MESSAGE_PERMISSION_NONE);
-  return validateInputAndCallChreBleStartScanAsync(request, response)
-             ? pw::OkStatus()
-             : pw::Status::InvalidArgument();
-}
-
-pw::Status ChreApiTestService::ChreBleStopScanAsync(
-    const chre_rpc_Void &request, chre_rpc_Status &response) {
-  ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
-      CHRE_MESSAGE_PERMISSION_NONE);
-  return validateInputAndCallChreBleStopScanAsync(request, response)
-             ? pw::OkStatus()
-             : pw::Status::InvalidArgument();
-}
-
 pw::Status ChreApiTestService::ChreSensorFindDefault(
     const chre_rpc_ChreSensorFindDefaultInput &request,
     chre_rpc_ChreSensorFindDefaultOutput &response) {
@@ -193,17 +177,6 @@
              : pw::Status::InvalidArgument();
 }
 
-pw::Status ChreApiTestService::RetrieveLatestDisconnectedHostEndpointEvent(
-    const chre_rpc_Void &request,
-    chre_rpc_RetrieveLatestDisconnectedHostEndpointEventOutput &response) {
-  ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
-      CHRE_MESSAGE_PERMISSION_NONE);
-  return validateInputAndRetrieveLatestDisconnectedHostEndpointEvent(request,
-                                                                     response)
-             ? pw::OkStatus()
-             : pw::Status::InvalidArgument();
-}
-
 pw::Status ChreApiTestService::ChreGetHostEndpointInfo(
     const chre_rpc_ChreGetHostEndpointInfoInput &request,
     chre_rpc_ChreGetHostEndpointInfoOutput &response) {
@@ -243,7 +216,7 @@
 }
 
 void ChreApiTestService::ChreBleStopScanSync(
-    const chre_rpc_Void &request,
+    const google_protobuf_Empty &request,
     ServerWriter<chre_rpc_GeneralSyncMessage> &writer) {
   if (mWriter.has_value()) {
     ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
@@ -281,26 +254,18 @@
     return;
   }
 
-  if (request.eventTypeCount > kMaxNumEventTypes) {
-    LOGE("GatherEvents: request.eventTypeCount is out of bounds");
+  if (request.eventTypes_count == 0) {
+    LOGE("GatherEvents: request.eventTypes_count == 0");
     ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
         CHRE_MESSAGE_PERMISSION_NONE);
     writer.Finish();
     return;
   }
 
-  if (request.eventTypeCount == 0) {
-    LOGE("GatherEvents: request.eventTypeCount == 0");
-    ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
-        CHRE_MESSAGE_PERMISSION_NONE);
-    writer.Finish();
-    return;
-  }
-
-  for (uint32_t i = 0; i < request.eventTypeCount; ++i) {
+  for (uint32_t i = 0; i < request.eventTypes_count; ++i) {
     if (request.eventTypes[i] < std::numeric_limits<uint16_t>::min() ||
         request.eventTypes[i] > std::numeric_limits<uint16_t>::max()) {
-      LOGE("GatherEvents: invalid request.eventTypes: i: %" PRIu32, i);
+      LOGE("GatherEvents: invalid request.eventTypes at index: %" PRIu32, i);
       ChreApiTestManagerSingleton::get()->setPermissionForNextMessage(
           CHRE_MESSAGE_PERMISSION_NONE);
       writer.Finish();
@@ -321,7 +286,7 @@
     sendFailureAndFinishCloseWriter(mEventWriter);
     mEventTimerHandle = CHRE_TIMER_INVALID;
   } else {
-    mEventTypeCount = request.eventTypeCount;
+    mEventTypeCount = request.eventTypes_count;
     mEventExpectedCount = request.eventCount;
     mEventSentCount = 0;
     LOGD("GatherEvents: mEventTypeCount: %" PRIu32
@@ -379,7 +344,7 @@
       message.which_data =
           chre_rpc_GeneralEventsMessage_chreSensorThreeAxisData_tag;
 
-      const struct chreSensorThreeAxisData *data =
+      const auto *data =
           static_cast<const struct chreSensorThreeAxisData *>(eventData);
       message.data.chreSensorThreeAxisData.header.baseTimestamp =
           data->header.baseTimestamp;
@@ -408,7 +373,7 @@
       break;
     }
     case CHRE_EVENT_SENSOR_SAMPLING_CHANGE: {
-      const struct chreSensorSamplingStatusEvent *data =
+      const auto *data =
           static_cast<const struct chreSensorSamplingStatusEvent *>(eventData);
       message.data.chreSensorSamplingStatusEvent.sensorHandle =
           data->sensorHandle;
@@ -424,6 +389,78 @@
           chre_rpc_GeneralEventsMessage_chreSensorSamplingStatusEvent_tag;
       break;
     }
+    case CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION: {
+      const auto *data =
+          static_cast<const struct chreHostEndpointNotification *>(eventData);
+      message.data.chreHostEndpointNotification.hostEndpointId =
+          data->hostEndpointId;
+      message.data.chreHostEndpointNotification.notificationType =
+          data->notificationType;
+
+      message.status = true;
+      message.which_data =
+          chre_rpc_GeneralEventsMessage_chreHostEndpointNotification_tag;
+      break;
+    }
+    case CHRE_EVENT_BLE_ADVERTISEMENT: {
+      const auto *data =
+          static_cast<const struct chreBleAdvertisementEvent *>(eventData);
+      message.data.chreBleAdvertisementEvent.reserved = data->reserved;
+
+      uint32_t numReports =
+          MIN(kChreBleAdvertisementReportMaxCount, data->numReports);
+      message.data.chreBleAdvertisementEvent.reports_count = numReports;
+      for (uint32_t i = 0; i < numReports; ++i) {
+        message.data.chreBleAdvertisementEvent.reports[i].timestamp =
+            data->reports[i].timestamp;
+        message.data.chreBleAdvertisementEvent.reports[i]
+            .eventTypeAndDataStatus = data->reports[i].eventTypeAndDataStatus;
+        message.data.chreBleAdvertisementEvent.reports[i].addressType =
+            data->reports[i].addressType;
+
+        message.data.chreBleAdvertisementEvent.reports[i].address.size =
+            CHRE_BLE_ADDRESS_LEN;
+        std::memcpy(
+            message.data.chreBleAdvertisementEvent.reports[i].address.bytes,
+            data->reports[i].address, CHRE_BLE_ADDRESS_LEN);
+
+        message.data.chreBleAdvertisementEvent.reports[i].primaryPhy =
+            data->reports[i].primaryPhy;
+        message.data.chreBleAdvertisementEvent.reports[i].secondaryPhy =
+            data->reports[i].secondaryPhy;
+        message.data.chreBleAdvertisementEvent.reports[i].advertisingSid =
+            data->reports[i].advertisingSid;
+        message.data.chreBleAdvertisementEvent.reports[i].txPower =
+            data->reports[i].txPower;
+        message.data.chreBleAdvertisementEvent.reports[i]
+            .periodicAdvertisingInterval =
+            data->reports[i].periodicAdvertisingInterval;
+        message.data.chreBleAdvertisementEvent.reports[i].rssi =
+            data->reports[i].rssi;
+        message.data.chreBleAdvertisementEvent.reports[i].directAddressType =
+            data->reports[i].directAddressType;
+
+        message.data.chreBleAdvertisementEvent.reports[i].directAddress.size =
+            CHRE_BLE_ADDRESS_LEN;
+        std::memcpy(message.data.chreBleAdvertisementEvent.reports[i]
+                        .directAddress.bytes,
+                    data->reports[i].directAddress, CHRE_BLE_ADDRESS_LEN);
+
+        message.data.chreBleAdvertisementEvent.reports[i].data.size =
+            data->reports[i].dataLength;
+        std::memcpy(
+            message.data.chreBleAdvertisementEvent.reports[i].data.bytes,
+            data->reports[i].data, data->reports[i].dataLength);
+
+        message.data.chreBleAdvertisementEvent.reports[i].reserved =
+            data->reports[i].reserved;
+      }
+
+      message.status = true;
+      message.which_data =
+          chre_rpc_GeneralEventsMessage_chreBleAdvertisementEvent_tag;
+      break;
+    }
     default: {
       LOGE("GatherEvents: event type: %" PRIu16 " not implemented", eventType);
     }
@@ -461,34 +498,6 @@
   }
 }
 
-void ChreApiTestService::handleHostEndpointNotificationEvent(
-    const chreHostEndpointNotification *data) {
-  if (data->notificationType != HOST_ENDPOINT_NOTIFICATION_TYPE_DISCONNECT) {
-    LOGW("Received non disconnected event");
-    return;
-  }
-
-  ++mReceivedHostEndpointDisconnectedNum;
-  mLatestHostEndpointNotification = *data;
-}
-
-void ChreApiTestService::copyString(char *destination, const char *source,
-                                    size_t maxChars) {
-  CHRE_ASSERT_NOT_NULL(destination);
-  CHRE_ASSERT_NOT_NULL(source);
-
-  if (maxChars == 0) {
-    return;
-  }
-
-  uint32_t i;
-  for (i = 0; i < maxChars - 1 && source[i] != '\0'; ++i) {
-    destination[i] = source[i];
-  }
-
-  memset(&destination[i], 0, maxChars - i);
-}
-
 bool ChreApiTestService::startSyncTimer() {
   mSyncTimerHandle = chreTimerSet(
       kSyncFunctionTimeout, &mSyncTimerHandle /* cookie */, true /* oneShot */);
@@ -510,7 +519,7 @@
 }
 
 void ChreApiTestManager::end() {
-  // do nothing
+  mServer.close();
 }
 
 void ChreApiTestManager::handleEvent(uint32_t senderInstanceId,
@@ -530,10 +539,6 @@
     case CHRE_EVENT_TIMER:
       mChreApiTestService.handleTimerEvent(eventData);
       break;
-    case CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION:
-      mChreApiTestService.handleHostEndpointNotificationEvent(
-          static_cast<const chreHostEndpointNotification *>(eventData));
-      break;
     default: {
       // ignore
     }
diff --git a/apps/test/common/chre_api_test/src/chre_api_test_service.cc b/apps/test/common/chre_api_test/src/chre_api_test_service.cc
index b045551..56a3402 100644
--- a/apps/test/common/chre_api_test/src/chre_api_test_service.cc
+++ b/apps/test/common/chre_api_test/src/chre_api_test_service.cc
@@ -18,7 +18,9 @@
 
 #include "chre/util/nanoapp/ble.h"
 #include "chre/util/nanoapp/log.h"
+#include "chre/util/nanoapp/string.h"
 
+using ::chre::copyString;
 using ::chre::createBleGenericFilter;
 
 namespace {
@@ -26,19 +28,22 @@
 /**
  * The following constants are defined in chre_api_test.options.
  */
-constexpr uint32_t kMaxBleScanFilters = 10;
-constexpr uint32_t kMaxNameStringSize = 100;
+constexpr size_t kMaxNameStringBufferSize = 100;
+constexpr size_t kMaxHostEndpointNameBufferSize = 51;
+constexpr size_t kMaxHostEndpointTagBufferSize = 51;
 }  // namespace
 
 bool ChreApiTestService::validateInputAndCallChreBleGetCapabilities(
-    const chre_rpc_Void & /* request */, chre_rpc_Capabilities &response) {
+    const google_protobuf_Empty & /* request */,
+    chre_rpc_Capabilities &response) {
   response.capabilities = chreBleGetCapabilities();
   LOGD("ChreBleGetCapabilities: capabilities: %" PRIu32, response.capabilities);
   return true;
 }
 
 bool ChreApiTestService::validateInputAndCallChreBleGetFilterCapabilities(
-    const chre_rpc_Void & /* request */, chre_rpc_Capabilities &response) {
+    const google_protobuf_Empty & /* request */,
+    chre_rpc_Capabilities &response) {
   response.capabilities = chreBleGetFilterCapabilities();
   LOGD("ChreBleGetFilterCapabilities: capabilities: %" PRIu32,
        response.capabilities);
@@ -48,13 +53,15 @@
 bool ChreApiTestService::validateInputAndCallChreBleStartScanAsync(
     const chre_rpc_ChreBleStartScanAsyncInput &request,
     chre_rpc_Status &response) {
-  bool success = false;
   if (request.mode < _chre_rpc_ChreBleScanMode_MIN ||
       request.mode > _chre_rpc_ChreBleScanMode_MAX ||
       request.mode == chre_rpc_ChreBleScanMode_INVALID) {
     LOGE("ChreBleStartScanAsync: invalid mode");
-  } else if (!request.hasFilter) {
-    chreBleScanMode mode = static_cast<chreBleScanMode>(request.mode);
+    return false;
+  }
+
+  if (!request.hasFilter) {
+    auto mode = static_cast<chreBleScanMode>(request.mode);
     response.status =
         chreBleStartScanAsync(mode, request.reportDelayMs, nullptr);
 
@@ -65,68 +72,47 @@
              : (mode == CHRE_BLE_SCAN_MODE_FOREGROUND ? "foreground"
                                                       : "aggressive"),
          request.reportDelayMs, response.status ? "true" : "false");
-    success = true;
-  } else if (request.filter.rssiThreshold <
-                 std::numeric_limits<int8_t>::min() ||
-             request.filter.rssiThreshold >
-                 std::numeric_limits<int8_t>::max()) {
-    LOGE("ChreBleStartScanAsync: invalid filter.rssiThreshold");
-  } else if (request.filter.scanFilterCount == 0 ||
-             request.filter.scanFilterCount > kMaxBleScanFilters) {
-    LOGE("ChreBleStartScanAsync: invalid filter.scanFilterCount");
-  } else {
-    chreBleGenericFilter genericFilters[request.filter.scanFilterCount];
-    bool validateFiltersSuccess = true;
-    for (uint32_t i = 0;
-         validateFiltersSuccess && i < request.filter.scanFilterCount; ++i) {
-      const chre_rpc_ChreBleGenericFilter &scanFilter =
-          request.filter.scanFilters[i];
-      if (scanFilter.type > std::numeric_limits<uint8_t>::max() ||
-          scanFilter.length > std::numeric_limits<uint8_t>::max()) {
-        LOGE(
-            "ChreBleStartScanAsync: invalid request.filter.scanFilters member: "
-            "type: %" PRIu32 " or length: %" PRIu32,
-            scanFilter.type, scanFilter.length);
-        validateFiltersSuccess = false;
-      } else if (scanFilter.data.size < scanFilter.length ||
-                 scanFilter.mask.size < scanFilter.length) {
-        LOGE(
-            "ChreBleStartScanAsync: invalid request.filter.scanFilters member: "
-            "data or mask size");
-        validateFiltersSuccess = false;
-      } else {
-        genericFilters[i] = createBleGenericFilter(
-            scanFilter.type, scanFilter.length, scanFilter.data.bytes,
-            scanFilter.mask.bytes);
-      }
-    }
-
-    if (validateFiltersSuccess) {
-      struct chreBleScanFilter filter;
-      filter.rssiThreshold = request.filter.rssiThreshold;
-      filter.scanFilterCount = request.filter.scanFilterCount;
-      filter.scanFilters = genericFilters;
-
-      chreBleScanMode mode = static_cast<chreBleScanMode>(request.mode);
-      response.status =
-          chreBleStartScanAsync(mode, request.reportDelayMs, &filter);
-
-      LOGD("ChreBleStartScanAsync: mode: %s, reportDelayMs: %" PRIu32
-           ", scanFilterCount: %" PRIu32 ", status: %s",
-           mode == CHRE_BLE_SCAN_MODE_BACKGROUND
-               ? "background"
-               : (mode == CHRE_BLE_SCAN_MODE_FOREGROUND ? "foreground"
-                                                        : "aggressive"),
-           request.reportDelayMs, request.filter.scanFilterCount,
-           response.status ? "true" : "false");
-      success = true;
-    }
+    return true;
   }
-  return success;
+
+  if (request.filter.rssiThreshold < std::numeric_limits<int8_t>::min() ||
+      request.filter.rssiThreshold > std::numeric_limits<int8_t>::max()) {
+    LOGE("ChreBleStartScanAsync: invalid filter.rssiThreshold");
+    return false;
+  }
+
+  if (request.filter.scanFilters_count == 0) {
+    LOGE("ChreBleStartScanAsync: invalid filter.scanFilters_count");
+    return false;
+  }
+
+  chreBleGenericFilter genericFilters[request.filter.scanFilters_count];
+  if (!validateBleScanFilters(request.filter.scanFilters, genericFilters,
+                              request.filter.scanFilters_count)) {
+    return false;
+  }
+
+  struct chreBleScanFilter filter;
+  filter.rssiThreshold = request.filter.rssiThreshold;
+  filter.scanFilterCount = request.filter.scanFilters_count;
+  filter.scanFilters = genericFilters;
+
+  auto mode = static_cast<chreBleScanMode>(request.mode);
+  response.status = chreBleStartScanAsync(mode, request.reportDelayMs, &filter);
+
+  LOGD("ChreBleStartScanAsync: mode: %s, reportDelayMs: %" PRIu32
+       ", scanFilterCount: %" PRIu16 ", status: %s",
+       mode == CHRE_BLE_SCAN_MODE_BACKGROUND
+           ? "background"
+           : (mode == CHRE_BLE_SCAN_MODE_FOREGROUND ? "foreground"
+                                                    : "aggressive"),
+       request.reportDelayMs, request.filter.scanFilters_count,
+       response.status ? "true" : "false");
+  return true;
 }
 
 bool ChreApiTestService::validateInputAndCallChreBleStopScanAsync(
-    const chre_rpc_Void & /* request */, chre_rpc_Status &response) {
+    const google_protobuf_Empty & /* request */, chre_rpc_Status &response) {
   response.status = chreBleStopScanAsync();
   LOGD("ChreBleStopScanAsync: status: %s", response.status ? "true" : "false");
   return true;
@@ -139,7 +125,7 @@
     return false;
   }
 
-  uint8_t sensorType = (uint8_t)request.sensorType;
+  auto sensorType = static_cast<uint8_t>(request.sensorType);
   response.foundSensor =
       chreSensorFindDefault(sensorType, &response.sensorHandle);
 
@@ -157,7 +143,8 @@
   response.status = chreGetSensorInfo(request.handle, &sensorInfo);
 
   if (response.status) {
-    copyString(response.sensorName, sensorInfo.sensorName, kMaxNameStringSize);
+    copyString(response.sensorName, sensorInfo.sensorName,
+               kMaxNameStringBufferSize);
     response.sensorType = sensorInfo.sensorType;
     response.isOnChange = sensorInfo.isOnChange;
     response.isOneShot = sensorInfo.isOneShot;
@@ -210,8 +197,7 @@
 bool ChreApiTestService::validateInputAndCallChreSensorConfigure(
     const chre_rpc_ChreSensorConfigureInput &request,
     chre_rpc_Status &response) {
-  chreSensorConfigureMode mode =
-      static_cast<chreSensorConfigureMode>(request.mode);
+  auto mode = static_cast<chreSensorConfigureMode>(request.mode);
   response.status = chreSensorConfigure(request.sensorHandle, mode,
                                         request.interval, request.latency);
 
@@ -222,8 +208,7 @@
 bool ChreApiTestService::validateInputAndCallChreSensorConfigureModeOnly(
     const chre_rpc_ChreSensorConfigureModeOnlyInput &request,
     chre_rpc_Status &response) {
-  chreSensorConfigureMode mode =
-      static_cast<chreSensorConfigureMode>(request.mode);
+  auto mode = static_cast<chreSensorConfigureMode>(request.mode);
   response.status = chreSensorConfigureModeOnly(request.sensorHandle, mode);
 
   LOGD("ChreSensorConfigureModeOnly: status: %s",
@@ -239,7 +224,7 @@
   response.status = chreAudioGetSource(request.handle, &audioSource);
 
   if (response.status) {
-    copyString(response.name, audioSource.name, kMaxNameStringSize);
+    copyString(response.name, audioSource.name, kMaxNameStringBufferSize);
     response.sampleRate = audioSource.sampleRate;
     response.minBufferDuration = audioSource.minBufferDuration;
     response.maxBufferDuration = audioSource.maxBufferDuration;
@@ -273,15 +258,6 @@
   return true;
 }
 
-bool ChreApiTestService::
-    validateInputAndRetrieveLatestDisconnectedHostEndpointEvent(
-        const chre_rpc_Void & /* request */,
-        chre_rpc_RetrieveLatestDisconnectedHostEndpointEventOutput &response) {
-  response.disconnectedCount = mReceivedHostEndpointDisconnectedNum;
-  response.hostEndpointId = mLatestHostEndpointNotification.hostEndpointId;
-  return true;
-}
-
 bool ChreApiTestService::validateInputAndCallChreGetHostEndpointInfo(
     const chre_rpc_ChreGetHostEndpointInfoInput &request,
     chre_rpc_ChreGetHostEndpointInfoOutput &response) {
@@ -302,15 +278,15 @@
     response.isTagValid = hostEndpointInfo.isTagValid;
     if (hostEndpointInfo.isNameValid) {
       copyString(response.endpointName, hostEndpointInfo.endpointName,
-                 CHRE_MAX_ENDPOINT_NAME_LEN);
+                 kMaxHostEndpointNameBufferSize);
     } else {
-      memset(response.endpointName, 0, CHRE_MAX_ENDPOINT_NAME_LEN);
+      memset(response.endpointName, 0, kMaxHostEndpointNameBufferSize);
     }
     if (hostEndpointInfo.isTagValid) {
       copyString(response.endpointTag, hostEndpointInfo.endpointTag,
-                 CHRE_MAX_ENDPOINT_TAG_LEN);
+                 kMaxHostEndpointTagBufferSize);
     } else {
-      memset(response.endpointTag, 0, CHRE_MAX_ENDPOINT_TAG_LEN);
+      memset(response.endpointTag, 0, kMaxHostEndpointTagBufferSize);
     }
 
     LOGD("ChreGetHostEndpointInfo: status: true, hostEndpointID: %" PRIu32
@@ -325,3 +301,37 @@
   }
   return true;
 }
+
+bool ChreApiTestService::validateBleScanFilters(
+    const chre_rpc_ChreBleGenericFilter *scanFilters,
+    chreBleGenericFilter *outputScanFilters, uint32_t scanFilterCount) {
+  if (scanFilters == nullptr || outputScanFilters == nullptr) {
+    return false;
+  }
+
+  for (uint32_t i = 0; i < scanFilterCount; ++i) {
+    const chre_rpc_ChreBleGenericFilter &scanFilter = scanFilters[i];
+    if (scanFilter.type > std::numeric_limits<uint8_t>::max() ||
+        scanFilter.length > std::numeric_limits<uint8_t>::max()) {
+      LOGE(
+          "validateBleScanFilters: invalid request.filter.scanFilters member: "
+          "type: %" PRIu32 " or length: %" PRIu32,
+          scanFilter.type, scanFilter.length);
+      return false;
+    }
+
+    if (scanFilter.data.size < scanFilter.length ||
+        scanFilter.mask.size < scanFilter.length) {
+      LOGE(
+          "validateBleScanFilters: invalid request.filter.scanFilters member: "
+          "data or mask size");
+      return false;
+    }
+
+    outputScanFilters[i] =
+        createBleGenericFilter(scanFilter.type, scanFilter.length,
+                               scanFilter.data.bytes, scanFilter.mask.bytes);
+  }
+
+  return true;
+}
diff --git a/apps/test/common/chre_cross_validator_wifi/inc/chre_cross_validator_wifi_manager.h b/apps/test/common/chre_cross_validator_wifi/inc/chre_cross_validator_wifi_manager.h
index ffc2bae..3f47746 100644
--- a/apps/test/common/chre_cross_validator_wifi/inc/chre_cross_validator_wifi_manager.h
+++ b/apps/test/common/chre_cross_validator_wifi/inc/chre_cross_validator_wifi_manager.h
@@ -84,22 +84,6 @@
   bool mChreDataCollectionDone = false;
 
   /**
-   * This is the fraction of the number of results in the greater set of
-   * scan results between the AP and CHRE that the lesser set can differ by.
-   * Increasing this value will increase the relative amount that AP and CHRE
-   * results sizes can differ by.
-   *
-   * Ex: AP_results_size = 8
-   *     CHRE_results_size = 7
-   *     kMaxDiffNumResultsFraction = 0.25
-   *
-   *     CHRE_results_size is valid because it is >= 8 - 8 * 0.25 = 6
-   */
-  // TODO(b/185026344): Perfect this number. Consider using an abolute
-  // difference instead of a percentage difference also.
-  static constexpr float kMaxDiffNumResultsFraction = 0.25f;
-
-  /**
    * Handle a message from the host.
    * @param senderInstanceId The instance id of the sender.
    * @param data The message from the host's data.
diff --git a/apps/test/common/chre_cross_validator_wifi/src/chre_cross_validator_wifi_manager.cc b/apps/test/common/chre_cross_validator_wifi/src/chre_cross_validator_wifi_manager.cc
index 49af65e..7351bc2 100644
--- a/apps/test/common/chre_cross_validator_wifi/src/chre_cross_validator_wifi_manager.cc
+++ b/apps/test/common/chre_cross_validator_wifi/src/chre_cross_validator_wifi_manager.cc
@@ -180,9 +180,11 @@
     LOGE("AP and CHRE wifi scan result counts differ, AP = %" PRIu8
          ", CHRE = %" PRIu8,
          mApScanResultsSize, mChreScanResultsSize);
-  } else {
-    verifyScanResults(&testResult);
+
+    return;
   }
+
+  verifyScanResults(&testResult);
   test_shared::sendMessageToHost(
       mCrossValidatorState.hostEndpoint, &testResult,
       chre_test_common_TestResult_fields,
diff --git a/apps/test/common/chre_settings_test/inc/chre_settings_test_manager.h b/apps/test/common/chre_settings_test/inc/chre_settings_test_manager.h
index 89863dc..602b60d 100644
--- a/apps/test/common/chre_settings_test/inc/chre_settings_test_manager.h
+++ b/apps/test/common/chre_settings_test/inc/chre_settings_test_manager.h
@@ -184,6 +184,10 @@
 
   //! The number of scan result received when after getting a wifi async result.
   uint16_t mReceivedScanResults;
+
+  //! True if we have received a chreAudioSourceStatusEvent with suspended ==
+  //! false.
+  bool mAudioSamplingEnabled;
 };
 
 // The settings test manager singleton.
diff --git a/apps/test/common/chre_settings_test/src/chre_settings_test_manager.cc b/apps/test/common/chre_settings_test/src/chre_settings_test_manager.cc
index 77b8ac6..2f55d30 100644
--- a/apps/test/common/chre_settings_test/src/chre_settings_test_manager.cc
+++ b/apps/test/common/chre_settings_test/src/chre_settings_test_manager.cc
@@ -44,6 +44,9 @@
 constexpr uint32_t kGnssMeasurementCookie = 0x4567;
 constexpr uint32_t kWwanCellInfoCookie = 0x5678;
 
+// The default audio handle.
+constexpr uint32_t kAudioHandle = 0;
+
 // Flag to verify if an audio data event was received after a valid sampling
 // change event (i.e., we only got the data event after a source-enabled-and-
 // not-suspended event).
@@ -182,7 +185,7 @@
     }
     case Feature::AUDIO: {
       struct chreAudioSource source;
-      supported = chreAudioGetSource(0 /* handle */, &source);
+      supported = chreAudioGetSource(kAudioHandle, &source);
       break;
     }
     case Feature::BLE_SCANNING: {
@@ -348,8 +351,8 @@
 
     case Feature::AUDIO: {
       struct chreAudioSource source;
-      if ((success = chreAudioGetSource(0 /* handle */, &source))) {
-        success = chreAudioConfigureSource(0 /* handle */, true /* enable */,
+      if ((success = chreAudioGetSource(kAudioHandle, &source))) {
+        success = chreAudioConfigureSource(kAudioHandle, true /* enable */,
                                            source.minBufferDuration,
                                            source.minBufferDuration);
       }
@@ -547,52 +550,61 @@
 
 void Manager::handleAudioSourceStatusEvent(
     const struct chreAudioSourceStatusEvent *event) {
-  bool success = false;
-  if (mTestSession.has_value()) {
-    if (mTestSession->featureState == FeatureState::ENABLED) {
-      if (event->status.suspended) {
-        struct chreAudioSource source;
-        if (chreAudioGetSource(0 /* handle */, &source)) {
-          const uint64_t duration =
-              source.minBufferDuration + kOneSecondInNanoseconds;
-          gAudioDataTimerHandle = chreTimerSet(duration, &kAudioDataTimerCookie,
-                                               true /* oneShot */);
+  LOGI("Received sampling status event suspended %d", event->status.suspended);
+  mAudioSamplingEnabled = !event->status.suspended;
+  if (!mTestSession.has_value()) {
+    return;
+  }
 
-          if (gAudioDataTimerHandle == CHRE_TIMER_INVALID) {
-            LOGE("Failed to set data check timer");
-          } else {
-            success = true;
-          }
+  bool success = false;
+  if (mTestSession->featureState == FeatureState::ENABLED) {
+    if (event->status.suspended) {
+      if (gAudioStatusTimerHandle != CHRE_TIMER_INVALID) {
+        chreTimerCancel(gAudioStatusTimerHandle);
+        gAudioStatusTimerHandle = CHRE_TIMER_INVALID;
+      }
+
+      struct chreAudioSource source;
+      if (chreAudioGetSource(kAudioHandle, &source)) {
+        const uint64_t duration =
+            source.minBufferDuration + kOneSecondInNanoseconds;
+        gAudioDataTimerHandle =
+            chreTimerSet(duration, &kAudioDataTimerCookie, true /* oneShot */);
+
+        if (gAudioDataTimerHandle == CHRE_TIMER_INVALID) {
+          LOGE("Failed to set data check timer");
         } else {
-          LOGE("Failed to query audio source");
-        }
-      } else {
-        // There might be a corner case where CHRE might have queued an audio
-        // available event just as the microphone disable setting change is
-        // received that might wrongfully indicate that microphone access
-        // wasn't disabled when it is dispatched. We add a 2 second timer to
-        // allow CHRE to send the source status change event to account for
-        // this, and fail the test if the timer expires without getting said
-        // event.
-        LOGW("Source wasn't suspended when Mic Access disabled, waiting 2 sec");
-        gAudioStatusTimerHandle =
-            chreTimerSet(2 * kOneSecondInNanoseconds, &kAudioStatusTimerCookie,
-                         true /* oneShot */);
-        if (gAudioStatusTimerHandle == CHRE_TIMER_INVALID) {
-          LOGE("Failed to set audio status check timer");
-        } else {
-          // continue the test, fail on timeout.
           success = true;
         }
+      } else {
+        LOGE("Failed to query audio source");
       }
     } else {
-      gGotSourceEnabledEvent = true;
-      success = true;
+      // There might be a corner case where CHRE might have queued an audio
+      // available event just as the microphone disable setting change is
+      // received that might wrongfully indicate that microphone access
+      // wasn't disabled when it is dispatched. We add a 2 second timer to
+      // allow CHRE to send the source status change event to account for
+      // this, and fail the test if the timer expires without getting said
+      // event.
+      LOGW("Source wasn't suspended when Mic Access disabled, waiting 2 sec");
+      gAudioStatusTimerHandle =
+          chreTimerSet(2 * kOneSecondInNanoseconds, &kAudioStatusTimerCookie,
+                       true /* oneShot */);
+      if (gAudioStatusTimerHandle == CHRE_TIMER_INVALID) {
+        LOGE("Failed to set audio status check timer");
+      } else {
+        // continue the test, fail on timeout.
+        success = true;
+      }
     }
+  } else {
+    gGotSourceEnabledEvent = true;
+    success = true;
+  }
 
-    if (!success) {
-      sendTestResult(mTestSession->hostEndpointId, success);
-    }
+  if (!success) {
+    sendTestResult(mTestSession->hostEndpointId, success);
   }
 }
 
@@ -609,7 +621,7 @@
     } else if (gGotSourceEnabledEvent) {
       success = true;
     }
-    chreAudioConfigureSource(0 /* handle */, false /* enable */,
+    chreAudioConfigureSource(kAudioHandle, false /* enable */,
                              0 /* minBufferDuration */,
                              0 /* maxbufferDuration */);
     sendTestResult(mTestSession->hostEndpointId, success);
@@ -619,18 +631,18 @@
 void Manager::handleTimeout(const void *eventData) {
   bool testSuccess = false;
   auto *cookie = static_cast<const uint32_t *>(eventData);
+  // Ignore the audio status timer if the suspended status was received.
+  if (*cookie == kAudioStatusTimerCookie && !mAudioSamplingEnabled) {
+    gAudioStatusTimerHandle = CHRE_TIMER_INVALID;
+    return;
+  }
 
   if (*cookie == kAudioDataTimerCookie) {
     gAudioDataTimerHandle = CHRE_TIMER_INVALID;
     testSuccess = true;
-    if (gAudioStatusTimerHandle != CHRE_TIMER_INVALID) {
-      chreTimerCancel(gAudioStatusTimerHandle);
-      gAudioStatusTimerHandle = CHRE_TIMER_INVALID;
-    }
   } else if (*cookie == kAudioStatusTimerCookie) {
-    LOGE("Source wasn't suspended when Mic Access was disabled");
     gAudioStatusTimerHandle = CHRE_TIMER_INVALID;
-    testSuccess = false;
+    LOGE("Source wasn't suspended when Mic Access was disabled");
   } else {
     LOGE("Invalid timer cookie: %" PRIx32, *cookie);
   }
diff --git a/apps/test/common/rpc_service_test/inc/rpc_service_manager.h b/apps/test/common/rpc_service_test/inc/rpc_service_manager.h
index f092c86..4807c60 100644
--- a/apps/test/common/rpc_service_test/inc/rpc_service_manager.h
+++ b/apps/test/common/rpc_service_test/inc/rpc_service_manager.h
@@ -60,6 +60,11 @@
                    const void *eventData);
 
   /**
+   * Cleanup on nanoapp end.
+   */
+  void end();
+
+  /**
    * Sets the permission for the next server message.
    *
    * @params permission Bitmasked CHRE_MESSAGE_PERMISSION_.
diff --git a/apps/test/common/rpc_service_test/src/rpc_service_manager.cc b/apps/test/common/rpc_service_test/src/rpc_service_manager.cc
index bea1036..956aad6 100644
--- a/apps/test/common/rpc_service_test/src/rpc_service_manager.cc
+++ b/apps/test/common/rpc_service_test/src/rpc_service_manager.cc
@@ -57,6 +57,10 @@
   }
 }
 
+void RpcServiceManager::end() {
+  mServer.close();
+}
+
 void RpcServiceManager::setPermissionForNextMessage(uint32_t permission) {
   mServer.setPermissionForNextMessage(permission);
 }
diff --git a/apps/test/common/rpc_service_test/src/rpc_service_test.cc b/apps/test/common/rpc_service_test/src/rpc_service_test.cc
index da709ff..1381288 100644
--- a/apps/test/common/rpc_service_test/src/rpc_service_test.cc
+++ b/apps/test/common/rpc_service_test/src/rpc_service_test.cc
@@ -31,6 +31,7 @@
 }
 
 extern "C" void nanoappEnd(void) {
+  RpcServiceManagerSingleton::get()->end();
   RpcServiceManagerSingleton::deinit();
 }
 
diff --git a/build/build_template.mk b/build/build_template.mk
index 4907577..18bf30b 100644
--- a/build/build_template.mk
+++ b/build/build_template.mk
@@ -51,6 +51,9 @@
 
 # Target Objects ###############################################################
 
+# Remove duplicates
+COMMON_SRCS := $(sort $(COMMON_SRCS))
+
 # Source files.
 $(1)_CC_SRCS = $$(filter %.cc, $(COMMON_SRCS) $(8))
 $(1)_CPP_SRCS = $$(filter %.cpp, $(COMMON_SRCS) $(8))
@@ -229,18 +232,33 @@
 # Token Mapping ################################################################
 
 $$($(1)_TOKEN_MAP): $$($(1)_AR)
-	@echo " [TOKEN_MAP_GEN] $<"
-	$(V)mkdir -p $(OUT)/$(1)
-	$(V)$(TOKEN_MAP_GEN_CMD) $$($(1)_TOKEN_MAP) $$($(1)_AR)
-	$(V)$(TOKEN_MAP_CSV_GEN_CMD) $$($(1)_TOKEN_MAP_CSV) $$($(1)_AR)
+	@echo " [TOKEN_MAP_GEN] $$@"
+	$(V)mkdir -p $$(@D)
+	$(V)$(TOKEN_MAP_GEN_CMD) $$($(1)_TOKEN_MAP) $$($(1)_AR) 2>&1
+	$(V)$(TOKEN_MAP_CSV_GEN_CMD) $$($(1)_TOKEN_MAP_CSV) $$($(1)_AR) 2>&1
+
+# Rust #########################################################################
+
+ifeq ($(IS_BUILD_REQUIRING_RUST),)
+RUST_DEPENDENCIES =
+else
+RUST_DEPENDENCIES = rust_archive_$(1)
+endif
+
+# Always invoke the cargo build, let cargo decide if updates are needed
+.PHONY: rust_archive_$(1)
+rust_archive_$(1):
+	@echo " [Rust Archive] $$@"
+	$(RUST_FLAGS) cargo +nightly build -Z build-std=core,alloc \
+	    --$(RUST_OPT_LEVEL) --target $(RUST_TARGET_DIR)/$(RUST_TARGET).json
 
 # Link #########################################################################
 
 $$($(1)_SO): $$($(1)_CC_DEPS) \
               $$($(1)_CPP_DEPS) $$($(1)_C_DEPS) $$($(1)_S_DEPS) \
               $$($(1)_CC_OBJS) $$($(1)_CPP_OBJS) $$($(1)_C_OBJS) \
-              $$($(1)_S_OBJS) | $$(OUT)/$(1) $$($(1)_DIRS)
-	$(V)$(5) $(4) -o $$@ $(11) $$(filter %.o, $$^) $(12)
+              $$($(1)_S_OBJS) $(RUST_DEPENDENCIES) | $$(OUT)/$(1) $$($(1)_DIRS)
+	$(5) $(4) -o $$@ $(11) $$(filter %.o, $$^) $(12)
 
 $$($(1)_BIN): $$($(1)_CC_DEPS) \
                $$($(1)_CPP_DEPS) $$($(1)_C_DEPS) $$($(1)_S_DEPS) \
diff --git a/build/clang.mk b/build/clang.mk
index 8481c9d..0b5a265 100644
--- a/build/clang.mk
+++ b/build/clang.mk
@@ -10,5 +10,5 @@
 endif
 
 # Clang toolchain path ########################################################
-CLANG_TOOLCHAIN_PATH=$(ANDROID_BUILD_TOP)/prebuilts/clang/host/linux-x86/clang-r475365b
+CLANG_TOOLCHAIN_PATH=$(ANDROID_BUILD_TOP)/prebuilts/clang/host/linux-x86/clang-r498229b
 IS_CLANG_TOOLCHAIN=true
diff --git a/build/common.mk b/build/common.mk
index e99c67a..38a6743 100644
--- a/build/common.mk
+++ b/build/common.mk
@@ -51,4 +51,7 @@
 include $(CHRE_PREFIX)/build/nanopb.mk
 
 # TFLM Sources
-include $(CHRE_PREFIX)/external/tflm/tflm.mk
\ No newline at end of file
+include $(CHRE_PREFIX)/external/tflm/tflm.mk
+
+# Rust config
+include $(CHRE_PREFIX)/build/rust/common_rust_config.mk
\ No newline at end of file
diff --git a/build/nanoapp/app.mk b/build/nanoapp/app.mk
index 3f82778..fa35eb2 100644
--- a/build/nanoapp/app.mk
+++ b/build/nanoapp/app.mk
@@ -92,6 +92,11 @@
 COMMON_CFLAGS += -DCHRE_NANOAPP_USES_WWAN
 endif
 
+ifneq ($(CHRE_NANOAPP_USES_TOKENIZED_LOGGING),)
+COMMON_CFLAGS += -DCHRE_TOKENIZED_LOGGING_ENABLED
+include $(CHRE_PREFIX)/external/pigweed/pw_tokenizer.mk
+endif
+
 # Common Compiler Flags ########################################################
 
 # Add the CHRE API to the include search path.
@@ -126,8 +131,9 @@
 # Variant-specific Nanoapp Support Source Files ################################
 
 APP_SUPPORT_PATH = $(CHRE_PREFIX)/build/app_support
-DSO_SUPPORT_LIB_PATH = $(CHRE_PREFIX)/platform/shared/nanoapp
-DSO_SUPPORT_LIB_SRCS = $(DSO_SUPPORT_LIB_PATH)/nanoapp_support_lib_dso.cc
+SHARED_NANOAPP_LIB_PATH = $(CHRE_PREFIX)/platform/shared/nanoapp
+DSO_SUPPORT_LIB_SRCS = $(SHARED_NANOAPP_LIB_PATH)/nanoapp_support_lib_dso.cc
+STACK_CHECK_SRCS =  $(SHARED_NANOAPP_LIB_PATH)/nanoapp_stack_check.cc
 
 # Required includes for nanoapp_support_lib_dso.cc, but using a special prefix
 # directory and symlinks to effectively hide them from nanoapps
@@ -171,9 +177,6 @@
 include $(CHRE_PREFIX)/build/defs.mk
 include $(CHRE_PREFIX)/build/common.mk
 
-# Pigweed module includes
-include $(CHRE_PREFIX)/external/pigweed/pw_rpc.mk
-
 # CHRE API version.
 include $(CHRE_PREFIX)/chre_api/chre_api_version.mk
 
diff --git a/build/nanopb.mk b/build/nanopb.mk
index 012d7c6..012d683 100644
--- a/build/nanopb.mk
+++ b/build/nanopb.mk
@@ -1,16 +1,20 @@
 #
-# Nanoapp/CHRE NanoPB Makefile
+# Nanoapp/CHRE NanoPB and Pigweed RPC Makefile
 #
-# Include this file in your nanoapp Makefile to produce pb.c and pb.h (or
-# $NANOPB_EXTENSION.c and $NANOPB_EXTENSION.h if $NANOPB_EXTENSION is defined)
-# for .proto files specified in the NANOPB_SRCS variable. The produced pb.c or
-# $NANOPB_EXTENSION.c files are automatically added to the COMMON_SRCS variable
+# Include this file in your nanoapp Makefile to generate .c source and .h header
+# files. ($NANOPB_EXTENSION.c and $NANOPB_EXTENSION.h if $NANOPB_EXTENSION
+# is defined) for .proto files specified in the NANOPB_SRCS and PW_RPC_SRCS
+# variables.
+#
+# The generated source files are automatically added to the COMMON_SRCS variable
 # for the nanoapp build.
 #
+# The path to the generated header files is similarly added to the COMMON_CFLAGS.
+#
 # The NANOPB_OPTIONS variable can be used to supply an .options file to use when
 # generating code for all .proto files. Alternatively, if an .options file has
-# the same name as a .proto file in NANOPB_SRCS, it'll be automatically picked
-# up when generating code **only** for that .proto file.
+# the same name as a .proto file, it'll be automatically picked up when generating
+# code **only** for that .proto file.
 #
 # NANOPB_FLAGS can be used to supply additional command line arguments to the
 # nanopb compiler. Note that this is global and applies to all protobuf
@@ -25,20 +29,34 @@
 
 # Environment Checks ###########################################################
 
+HAS_PROTO_SRC = false
+
 ifneq ($(NANOPB_SRCS),)
 ifeq ($(NANOPB_PREFIX),)
 $(error "NANOPB_SRCS is non-empty. You must supply a NANOPB_PREFIX environment \
          variable containing a path to the nanopb project. Example: \
          export NANOPB_PREFIX=$$HOME/path/to/nanopb/nanopb-c")
 endif
+HAS_PROTO_SRC = true
 endif
 
+ifneq ($(PW_RPC_SRCS),)
+ifeq ($(NANOPB_PREFIX),)
+$(error "PW_RPC_SRCS is non-empty. You must supply a NANOPB_PREFIX environment \
+         variable containing a path to the nanopb project. Example: \
+         export NANOPB_PREFIX=$$HOME/path/to/nanopb/nanopb-c")
+endif
+HAS_PROTO_SRC = true
+endif
+
+################################################################################
+# Common #######################################################################
+################################################################################
+
 ifeq ($(PROTOC),)
 PROTOC=protoc
 endif
 
-# Generated Source Files #######################################################
-
 NANOPB_GEN_PATH = $(OUT)/nanopb_gen
 
 ifeq ($(NANOPB_EXTENSION),)
@@ -51,10 +69,16 @@
                               $(NANOPB_GEN_PATH)/%.$(NANOPB_EXTENSION).c, \
                               $(NANOPB_SRCS))
 
-ifneq ($(NANOPB_GEN_SRCS),)
+# Add Google proto well-known types. See https://protobuf.dev/reference/protobuf/google.protobuf/.
+PROTOBUF_DIR = $(ANDROID_BUILD_TOP)/external/protobuf
+COMMON_CFLAGS += -I$(NANOPB_GEN_PATH)/$(PROTOBUF_DIR)/src
+
+################################################################################
+# Common to nanopb & rpc #######################################################
+################################################################################
+
+ifeq ($(HAS_PROTO_SRC),true)
 COMMON_CFLAGS += -I$(NANOPB_PREFIX)
-COMMON_CFLAGS += -I$(NANOPB_GEN_PATH)
-COMMON_CFLAGS += $(addprefix -I$(NANOPB_GEN_PATH)/, $(NANOPB_INCLUDES))
 
 ifneq ($(NANOPB_INCLUDE_LIBRARY),false)
 COMMON_SRCS += $(NANOPB_PREFIX)/pb_common.c
@@ -62,14 +86,22 @@
 COMMON_SRCS += $(NANOPB_PREFIX)/pb_encode.c
 endif
 
-endif
-
-# NanoPB Compiler Flags ########################################################
-
-ifneq ($(NANOPB_GEN_SRCS),)
+# NanoPB Compiler Flags
 ifneq ($(NANOPB_INCLUDE_LIBRARY),false)
 COMMON_CFLAGS += -DPB_NO_PACKED_STRUCTS=1
 endif
+
+NANOPB_PROTOC = $(NANOPB_PREFIX)/generator/protoc-gen-nanopb
+
+endif # ifeq ($(HAS_PROTO_SRC),true)
+
+################################################################################
+# nanopb #######################################################################
+################################################################################
+
+ifneq ($(NANOPB_GEN_SRCS),)
+COMMON_CFLAGS += -I$(NANOPB_GEN_PATH)
+COMMON_CFLAGS += $(addprefix -I$(NANOPB_GEN_PATH)/, $(NANOPB_INCLUDES))
 endif
 
 # NanoPB Generator Setup #######################################################
@@ -78,7 +110,7 @@
 NANOPB_GENERATOR_SRCS += $(NANOPB_PREFIX)/generator/proto/plugin_pb2.py
 
 $(NANOPB_GENERATOR_SRCS):
-	cd $(NANOPB_PREFIX)/generator/proto && make
+	cd $(NANOPB_PREFIX)/generator/proto && $(MAKE)
 
 ifneq ($(NANOPB_OPTIONS),)
 NANOPB_OPTIONS_FLAG = --options-file=$(NANOPB_OPTIONS)
@@ -100,10 +132,12 @@
                                                     $(NANOPB_GENERATOR_SRCS)
 	@echo " [NANOPB] $<"
 	$(V)mkdir -p $(dir $@)
-	$(V)$(PROTOC) --plugin=protoc-gen-nanopb=$(NANOPB_PROTOC) \
+	$(V)PYTHONPATH=$(PYTHONPATH) $(PROTOC) \
+	  --plugin=protoc-gen-nanopb=$(NANOPB_PROTOC) \
 	  --proto_path=$(abspath $(dir $<)) \
 	  $(NANOPB_FLAGS) \
-	  --nanopb_out="$(NANOPB_GENERATOR_FLAGS) --options-file=$(basename $<).options:$(dir $@)" \
+	  --nanopb_out="$(NANOPB_GENERATOR_FLAGS) \
+	  --options-file=$(basename $<).options:$(dir $@)" \
 	  $(abspath $<)
 
 $(NANOPB_GEN_PATH)/%.$(NANOPB_EXTENSION).c \
@@ -112,8 +146,238 @@
                                                     $(NANOPB_GENERATOR_SRCS)
 	@echo " [NANOPB] $<"
 	$(V)mkdir -p $(dir $@)
-	$(V)$(PROTOC) --plugin=protoc-gen-nanopb=$(NANOPB_PROTOC) \
+	$(V)PYTHONPATH=$(PYTHONPATH) $(PROTOC) \
+	  --plugin=protoc-gen-nanopb=$(NANOPB_PROTOC) \
 	  --proto_path=$(abspath $(dir $<)) \
 	  $(NANOPB_FLAGS) \
 	  --nanopb_out="$(NANOPB_GENERATOR_FLAGS) $(NANOPB_OPTIONS_FLAG):$(dir $@)" \
 	  $(abspath $<)
+
+################################################################################
+# Specific to pigweed RPC ######################################################
+################################################################################
+ifneq ($(PW_RPC_SRCS),)
+
+# Location of various Pigweed modules
+PIGWEED_DIR = $(ANDROID_BUILD_TOP)/external/pigweed
+PROTOBUF_DIR = $(ANDROID_BUILD_TOP)/external/protobuf
+CHRE_PREFIX = $(ANDROID_BUILD_TOP)/system/chre
+CHRE_UTIL_DIR = $(CHRE_PREFIX)/util
+CHRE_API_DIR = $(CHRE_PREFIX)/chre_api
+PIGWEED_CHRE_DIR=$(CHRE_PREFIX)/external/pigweed
+PIGWEED_CHRE_UTIL_DIR = $(CHRE_UTIL_DIR)/pigweed
+
+PW_RPC_GEN_PATH = $(OUT)/pw_rpc_gen
+
+# Create proto used for header generation ######################################
+
+PW_RPC_PROTO_GENERATOR = $(PIGWEED_DIR)/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
+PW_RPC_GENERATOR_PROTO = $(PIGWEED_DIR)/pw_rpc/internal/packet.proto
+PW_RPC_GENERATOR_COMPILED_PROTO = $(PW_RPC_GEN_PATH)/py/pw_rpc/internal/packet_pb2.py
+PW_PROTOBUF_PROTOS = $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos/common.proto \
+	  $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos/field_options.proto \
+	  $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos/status.proto
+
+# Modifies PYTHONPATH so that python can see all of pigweed's modules used by
+# their protoc plugins
+PW_RPC_GENERATOR_CMD = PYTHONPATH=$$PYTHONPATH:$(PW_RPC_GEN_PATH)/py:$\
+  $(PIGWEED_DIR)/pw_status/py:$(PIGWEED_DIR)/pw_protobuf/py:$\
+  $(PIGWEED_DIR)/pw_protobuf_compiler/py $(PYTHON)
+
+$(PW_RPC_GENERATOR_COMPILED_PROTO): $(PW_RPC_GENERATOR_PROTO)
+	@echo " [PW_RPC] $<"
+	$(V)mkdir -p $(PW_RPC_GEN_PATH)/py/pw_rpc/internal
+	$(V)mkdir -p $(PW_RPC_GEN_PATH)/py/pw_protobuf_codegen_protos
+	$(V)mkdir -p $(PW_RPC_GEN_PATH)/py/pw_protobuf_protos
+	$(V)cp -R $(PIGWEED_DIR)/pw_rpc/py/pw_rpc $(PW_RPC_GEN_PATH)/py/
+
+	$(PROTOC) -I$(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos \
+	  --experimental_allow_proto3_optional \
+	  --python_out=$(PW_RPC_GEN_PATH)/py/pw_protobuf_protos \
+	  $(PW_PROTOBUF_PROTOS)
+
+	$(PROTOC) -I$(PIGWEED_DIR)/pw_protobuf/pw_protobuf_codegen_protos \
+	  --experimental_allow_proto3_optional \
+	  --python_out=$(PW_RPC_GEN_PATH)/py/pw_protobuf_codegen_protos \
+	  $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_codegen_protos/codegen_options.proto
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --out-dir=$(PW_RPC_GEN_PATH)/py/pw_rpc/internal \
+	  --compile-dir=$(dir $<) --sources $(PW_RPC_GENERATOR_PROTO) \
+	  --language python
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_protobuf/py/pw_protobuf/plugin.py \
+	  --compile-dir=$(dir $<) --sources $(PW_RPC_GENERATOR_PROTO) \
+	  --language pwpb
+
+# Generated PW RPC Files #######################################################
+
+PW_RPC_GEN_SRCS = $(patsubst %.proto, \
+                             $(PW_RPC_GEN_PATH)/%.pb.c, \
+                             $(PW_RPC_SRCS))
+
+# Include to-be-generated files
+COMMON_CFLAGS += -I$(PW_RPC_GEN_PATH)
+COMMON_CFLAGS += -I$(PW_RPC_GEN_PATH)/$(PIGWEED_DIR)
+
+# Add include paths to reference protos directly
+COMMON_CFLAGS += $(addprefix -I$(PW_RPC_GEN_PATH)/, $(abspath $(dir $(PW_RPC_SRCS))))
+
+# Add include paths to import protos
+ifneq ($(PW_RPC_INCLUDE_DIRS),)
+COMMON_CFLAGS += $(addprefix -I$(PW_RPC_GEN_PATH)/, $(abspath $(PW_RPC_INCLUDE_DIRS)))
+endif
+
+# Add Google proto well-known types. See https://protobuf.dev/reference/protobuf/google.protobuf/.
+COMMON_CFLAGS += -I$(PW_RPC_GEN_PATH)/$(PROTOBUF_DIR)/src
+
+COMMON_SRCS += $(PW_RPC_GEN_SRCS)
+
+# PW RPC library ###############################################################
+
+# Pigweed RPC include paths
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_assert/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_bytes/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_containers/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_function/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_log/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public_overrides
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/standard_library_public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_preprocessor/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_protobuf/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_result/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/nanopb/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/pwpb/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/raw/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public_overrides
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_status/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_stream/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_string/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_sync/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_toolchain/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_varint/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/third_party/fuchsia/repo/sdk/lib/fit/include
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/third_party/fuchsia/repo/sdk/lib/stdcompat/include
+
+# Pigweed RPC sources
+COMMON_SRCS += $(PIGWEED_DIR)/pw_assert_log/assert_log.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_containers/intrusive_list.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_protobuf/decoder.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_protobuf/encoder.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_protobuf/stream_decoder.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/call.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/channel.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/channel_list.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/client.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/client_call.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/endpoint.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/packet.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/server.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/server_call.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/service.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/nanopb/common.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/nanopb/method.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/nanopb/server_reader_writer.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/pwpb/server_reader_writer.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_stream/memory_stream.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/stream.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/varint_c.c
+COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/varint.cc
+# Pigweed configuration
+COMMON_CFLAGS += -DPW_RPC_USE_GLOBAL_MUTEX=0
+COMMON_CFLAGS += -DPW_RPC_YIELD_MODE=PW_RPC_YIELD_MODE_BUSY_LOOP
+
+# Enable closing a client stream.
+COMMON_CFLAGS += -DPW_RPC_COMPLETION_REQUEST_CALLBACK
+
+# Use dynamic channel allocation
+COMMON_CFLAGS += -DPW_RPC_DYNAMIC_ALLOCATION
+COMMON_CFLAGS += -DPW_RPC_DYNAMIC_CONTAINER\(type\)="chre::DynamicVector<type>"
+COMMON_CFLAGS += -DPW_RPC_DYNAMIC_CONTAINER_INCLUDE='"chre/util/dynamic_vector.h"'
+
+# Add CHRE Pigweed util sources since nanoapps should always use these
+COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/chre_channel_output.cc
+COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/rpc_client.cc
+COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/rpc_helper.cc
+COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/rpc_server.cc
+COMMON_SRCS += $(CHRE_UTIL_DIR)/nanoapp/callbacks.cc
+COMMON_SRCS += $(CHRE_UTIL_DIR)/dynamic_vector_base.cc
+
+# CHRE Pigweed overrides
+COMMON_CFLAGS += -I$(PIGWEED_CHRE_DIR)/pw_log_nanoapp/public_overrides
+COMMON_CFLAGS += -I$(PIGWEED_CHRE_DIR)/pw_assert_nanoapp/public_overrides
+
+# Generate PW RPC headers ######################################################
+
+$(PW_RPC_GEN_PATH)/%.pb.c \
+        $(PW_RPC_GEN_PATH)/%.pb.h \
+        $(PW_RPC_GEN_PATH)/%.rpc.pb.h \
+        $(PW_RPC_GEN_PATH)/%.raw_rpc.pb.h: %.proto \
+                                           %.options \
+                                           $(NANOPB_GENERATOR_SRCS) \
+                                           $(PW_RPC_GENERATOR_COMPILED_PROTO)
+	@echo " [PW_RPC] $<"
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(NANOPB_PROTOC) \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_protobuf/py/pw_protobuf/plugin.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_nanopb.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb_rpc \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_raw.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language raw_rpc \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_pwpb.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb_rpc \
+	  --sources $<
+
+$(PW_RPC_GEN_PATH)/%.pb.c \
+        $(PW_RPC_GEN_PATH)/%.pb.h \
+        $(PW_RPC_GEN_PATH)/%.rpc.pb.h \
+        $(PW_RPC_GEN_PATH)/%.raw_rpc.pb.h: %.proto \
+                                           $(NANOPB_OPTIONS) \
+                                           $(NANOPB_GENERATOR_SRCS) \
+                                           $(PW_RPC_GENERATOR_COMPILED_PROTO)
+	@echo " [PW_RPC] $<"
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(NANOPB_PROTOC) \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_protobuf/py/pw_protobuf/plugin.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_nanopb.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb_rpc \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_raw.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language raw_rpc \
+	  --sources $<
+
+	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
+	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_pwpb.py \
+	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb_rpc \
+	  --sources $<
+
+endif # ifneq ($(PW_RPC_SRCS),)
diff --git a/build/rust/common_rust_config.mk b/build/rust/common_rust_config.mk
new file mode 100644
index 0000000..c562445
--- /dev/null
+++ b/build/rust/common_rust_config.mk
@@ -0,0 +1,4 @@
+RUST_TARGET_DIR = $(CHRE_PRIV_DIR)/build/rust
+RUST_OPT_LEVEL = release
+# Required for linking of composite Rust + C binaries
+RUST_FLAGS = RUSTFLAGS='-C relocation-model=pic'
\ No newline at end of file
diff --git a/build/variant/aosp_riscv55e03_tinysys.mk b/build/variant/aosp_riscv55e03_tinysys.mk
index 5513f95..92d8a62 100644
--- a/build/variant/aosp_riscv55e03_tinysys.mk
+++ b/build/variant/aosp_riscv55e03_tinysys.mk
@@ -34,11 +34,10 @@
 
 # chre platform
 TARGET_CFLAGS += -DCHRE_FIRST_SUPPORTED_API_VERSION=CHRE_API_VERSION_1_7
-# TODO(b/254121302): Needs to confirm with MTK about the max message size below
 TARGET_CFLAGS += -DCHRE_MESSAGE_TO_HOST_MAX_SIZE=4096
 TARGET_CFLAGS += -DCHRE_USE_BUFFERED_LOGGING
-# TODO(b/256870101): create mutex on heap for now
-TARGET_CFLAGS += -DCHRE_CREATE_MUTEX_ON_HEAP
+# enable static allocation in freertos
+TINYSYS_CFLAGS += -DCFG_STATIC_ALLOCATE
 
 # Compiling flags ##############################################################
 
diff --git a/chpp/Android.bp b/chpp/Android.bp
index 99036e6..5d1c6b4 100644
--- a/chpp/Android.bp
+++ b/chpp/Android.bp
@@ -52,6 +52,8 @@
         "-D_POSIX_C_SOURCE=199309L",
         // Required for pthread_setname_np()
         "-D_GNU_SOURCE",
+        // Enable assert (i.e. CHPP_ASSERT, ...)
+        "-UNDEBUG",
     ],
     conlyflags: [
         "-std=c11",
@@ -155,7 +157,10 @@
         "test/app_test.cpp",
         "test/gnss_test.cpp",
         "test/transport_test.cpp",
-        "test/clients_test.cpp",
+        "test/app_timeout_test.cpp",
+        "test/app_discovery_test.cpp",
+        "test/app_req_resp_test.cpp",
+        "test/app_notification_test.cpp",
     ],
     static_libs: [
         "chre_chpp_linux",
@@ -186,9 +191,10 @@
     name: "chre_chpp_fake_link_sync_tests",
     defaults: ["chre_chpp_core_without_link"],
     cflags: [
-        // Speed up tests by setting timeouts to 10 ms
-        "-DCHPP_TRANSPORT_TX_TIMEOUT_NS=10000000",
-        "-DCHPP_TRANSPORT_RX_TIMEOUT_NS=10000000",
+        // Speed up tests by setting timeouts to 50 ms.
+        // Note: the value shouldn't be too low to avoid timeouts on slow test servers.
+        "-DCHPP_TRANSPORT_TX_TIMEOUT_NS=50000000",
+        "-DCHPP_TRANSPORT_RX_TIMEOUT_NS=50000000",
     ],
     local_include_dirs: [
         "include",
diff --git a/chpp/RELEASE_NOTES.md b/chpp/RELEASE_NOTES.md
index 75d4440..bc1cf4d 100644
--- a/chpp/RELEASE_NOTES.md
+++ b/chpp/RELEASE_NOTES.md
@@ -263,4 +263,77 @@
 The handle which used to be returned is now populated in `serviceState`.
 `service->appContext` is also initialized to the passed `appContext`.
 
-This change makes the signature and behavior consistent with `chreRegisterClient`.
\ No newline at end of file
+This change makes the signature and behavior consistent with `chreRegisterClient`.
+
+### 2023-08
+
+Services can now send requests and receive responses from clients.
+
+The changes to the public API of the different layers are described below.
+Check the inline documentation for more information.
+
+**Breaking changes**
+
+- `ChppClientState` and `ChppServiceState` have been unified into `ChppEndpointState`.
+
+#### app.c / app.h
+
+**Breaking changes**
+
+- Move all sync primitives to a new `struct ChppSyncResponse`,
+- Renamed `ChppClient.rRStateCount` to `ChppClient.outReqCount`. The content and meaning stay the same,
+- Split `struct ChppRequestResponseState` to `struct ChppOutgoingRequestState` and `struct ChppIncomingRequestState`. Both the struct have the same layout and usage as the former `struct`. Having different types is only to make code clearer as both clients and services now support both incoming and outgoing requests,
+- Renamed `ChppAppState.nextClientRequestTimeoutNs` to `ChppAppState.nextRequestTimeoutNs`. The content and meaning stay the same,
+- Renamed `CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE` to `CHPP_REQUEST_TIMEOUT_INFINITE`.
+
+**Added APIs**
+
+- Added `chppAllocResponseTypedArray` and `chppAllocResponseFixed` to allocate responses. Those can be used by both clients and services. They call the added `chppAllocResponse`,
+- Added `CHPP_MESSAGE_TYPE_SERVICE_REQUEST` and  `CHPP_MESSAGE_TYPE_CLIENT_RESPONSE` to the message types (`ChppMessageType`). Used for requests sent by the services and the corresponding responses sent by the client,
+- Added `ChppService.responseDispatchFunctionPtr` to handle client responses,
+- Added `ChppService.outReqCount` holding the number of commands supported by the service (0 when the service can not send requests),
+- Added `ChppClient.requestDispatchFunctionPtr` to handle service requests,
+- Added `ChppAppState.registeredServiceStates` to track service states,
+- Added `ChppAppState.nextServiceRequestTimeoutNs` to track when the next service sent request will timeout,
+- Added `chppTimestampIncomingRequest` to be used by both clients and services,
+- Added `chppTimestampOutgoingRequest` to be used by both clients and services,
+- Added `chppTimestampIncomingResponse` to be used by both clients and services,
+- Added `chppTimestampOutgoingResponse` to be used by both clients and services,
+- Added `chppSendTimestampedResponseOrFail` to be used by both clients and services,
+- Added `chppSendTimestampedRequestOrFail` to be used by both clients and services,
+- Added `chppWaitForResponseWithTimeout` to be used by both clients and services.
+
+#### clients.c / clients.h
+
+**Breaking changes**
+
+- Renamed `ChppClientState.rRStates` to `outReqStates` - the new type is `ChppOutgoingRequestState`. The content and meaning stay the same,
+- Nest all sync primitive in `ChppClientState` into a `struct ChppSyncResponse syncResponse`,
+- `chppRegisterClient` takes a `ChppOutgoingRequestState` instead of the former `ChppRequestResponseState`,
+- `chppClientTimestampRequest` is replaced with `chppTimestampOutgoingRequest` in the app layer. Parameters are different,
+- `chppClientTimestampResponse` is replaced with `chppTimestampIncomingResponse` in the app layer. Parameters are different,
+- `chppSendTimestampedRequestOrFail` is renamed to `chppClientSendTimestampedRequestOrFail`. It takes a `ChppOutgoingRequestState` instead of the former `ChppRequestResponseState`,
+- `chppSendTimestampedRequestAndWait` is renamed to `chppClientSendTimestampedRequestAndWait`. It takes a `ChppOutgoingRequestState` instead of the former `ChppRequestResponseState`,
+- `chppSendTimestampedRequestAndWaitTimeout` is renamed to `chppClientSendTimestampedRequestAndWaitTimeout`. It takes a `ChppOutgoingRequestState` instead of the former `ChppRequestResponseState`,
+- `chppClientSendOpenRequest` takes a `ChppOutgoingRequestState` instead of the former `ChppRequestResponseState`.
+
+#### services.c / services.h
+
+**Breaking changes**
+
+- Replaced `chppAllocServiceResponseTypedArray` with `chppAllocResponseTypedArray` in the app layer,
+- Replaced `chppAllocServiceResponseFixed` with `chppAllocResponseFixed` in the app layer,
+- Replaced `chppAllocServiceResponse` with `chppAllocResponse` in the app layer,
+- `chppRegisterService` now takes and additional `struct ChppOutgoingRequestState *` to track outgoing requests. `NULL` when the service do not send requests.
+
+**Added APIs**
+
+- Added `chppAllocServiceRequestFixed` and `chppAllocServiceRequestTypedArray` to allocate service requests. They call the added `chppAllocServiceRequest`,
+- Added `ChppServiceState.outReqStates` to track outgoing requests,
+- Added `ChppServiceState.transaction` to track app layer packet number,
+- Added `ChppServiceState.syncResponse` for sync responses,
+- Added `chppAllocServiceRequest`,
+- Added `chppAllocServiceRequestCommand`,
+- Added `chppServiceSendTimestampedRequestOrFail`,
+- Added `chppServiceSendTimestampedRequestAndWaitTimeout`,
+- Added `chppServiceCloseOpenRequests` to close pending requests on reset.
diff --git a/chpp/api_parser/.gitignore b/chpp/api_parser/.gitignore
deleted file mode 100644
index e431c53..0000000
--- a/chpp/api_parser/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-parser_cache
diff --git a/chpp/api_parser/chre_api_annotations.json b/chpp/api_parser/chre_api_annotations.json
deleted file mode 100644
index c396bb5..0000000
--- a/chpp/api_parser/chre_api_annotations.json
+++ /dev/null
@@ -1,297 +0,0 @@
-[
-  {
-  "filename": "chre_api/include/chre_api/chre/wwan.h",
-  "includes": [
-    "chre_api/include/chre_api/chre/common.h"
-  ],
-  "output_includes": [
-    "chpp/common/common_types.h",
-    "chre_api/chre/wwan.h"
-  ],
-  "struct_info": [
-    {
-      "name": "chreWwanCellInfoResult",
-      "annotations": [
-        {
-          "field": "version",
-          "annotation": "fixed_value",
-          "value": "CHRE_WWAN_CELL_INFO_RESULT_VERSION"
-        },
-        {
-          "field": "errorCode",
-          "annotation": "enum",
-          "enum_type": "chreError"
-        },
-        {
-          "field": "cookie",
-          "annotation": "fixed_value",
-          "value": "0"
-        },
-        {
-          "field": "cookie",
-          "annotation": "rewrite_type",
-          "type_override": "uint32_t"
-        },
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        },
-        {
-          "field": "cells",
-          "annotation": "var_len_array",
-          "length_field": "cellInfoCount"
-        }
-      ]
-    },
-    {
-      "name": "chreWwanCellInfo",
-      "annotations": [
-        {
-          "field": "cellInfoType",
-          "annotation": "enum",
-          "enum_type": "chreWwanCellInfoType"
-        },
-        {
-          "field": "CellInfo",
-          "annotation": "union_variant",
-          "discriminator": "cellInfoType",
-          "mapping": [
-            ["CHRE_WWAN_CELL_INFO_TYPE_GSM", "gsm"],
-            ["CHRE_WWAN_CELL_INFO_TYPE_CDMA", "cdma"],
-            ["CHRE_WWAN_CELL_INFO_TYPE_LTE", "lte"],
-            ["CHRE_WWAN_CELL_INFO_TYPE_WCDMA", "wcdma"],
-            ["CHRE_WWAN_CELL_INFO_TYPE_TD_SCDMA", "tdscdma"],
-            ["CHRE_WWAN_CELL_INFO_TYPE_NR", "nr"]
-          ]
-        },
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    },
-    {
-      "name": "chreWwanCellIdentityGsm",
-      "annotations": [
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    }
-  ],
-  "root_structs": [
-    "chreWwanCellInfoResult"
-  ]
-},
-{
-  "filename": "chre_api/include/chre_api/chre/wifi.h",
-  "includes": [
-    "chre_api/include/chre_api/chre/common.h"
-  ],
-  "output_includes": [
-    "chpp/common/common_types.h",
-    "chre_api/chre/wifi.h"
-  ],
-  "struct_info": [
-    {
-      "name": "chreWifiScanEvent",
-      "annotations": [
-        {
-          "field": "version",
-          "annotation": "fixed_value",
-          "value": "CHRE_WIFI_SCAN_EVENT_VERSION"
-        },
-        {
-          "field": "scannedFreqList",
-          "annotation": "var_len_array",
-          "length_field": "scannedFreqListLen"
-        },
-        {
-          "field": "results",
-          "annotation": "var_len_array",
-          "length_field": "resultCount"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiScanResult",
-      "annotations": [
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiScanParams",
-      "annotations": [
-        {
-          "field": "frequencyList",
-          "annotation": "var_len_array",
-          "length_field": "frequencyListLen"
-        },
-        {
-          "field": "ssidList",
-          "annotation": "var_len_array",
-          "length_field": "ssidListLen"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiRangingEvent",
-      "annotations": [
-        {
-          "field": "version",
-          "annotation": "fixed_value",
-          "value": "CHRE_WIFI_RANGING_EVENT_VERSION"
-        },
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        },
-        {
-          "field": "results",
-          "annotation": "var_len_array",
-          "length_field": "resultCount"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiRangingResult",
-      "annotations": [
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiRangingParams",
-      "annotations": [
-        {
-          "field": "targetList",
-          "annotation": "var_len_array",
-          "length_field": "targetListLen"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiRangingTarget",
-      "annotations": [
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiNanSubscribeConfig",
-      "annotations": [
-        {
-          "field": "subscribeType",
-          "annotation": "enum",
-          "enum_type": "chreWifiNanSubscribeType"
-        },
-        {
-          "field": "service",
-          "annotation": "string"
-        },
-        {
-          "field": "serviceSpecificInfo",
-          "annotation": "var_len_array",
-          "length_field": "serviceSpecificInfoSize"
-        },
-        {
-          "field": "matchFilter",
-          "annotation": "var_len_array",
-          "length_field": "matchFilterLength"
-        }
-      ]
-    },
-    {
-      "name": "chreWifiNanDiscoveryEvent",
-      "annotations": [
-        {
-          "field": "serviceSpecificInfo",
-          "annotation": "var_len_array",
-          "length_field": "serviceSpecificInfoSize"
-        }
-      ]
-    }
-  ],
-  "root_structs": [
-    "chreWifiScanEvent",
-    "chreWifiScanParams",
-    "chreWifiRangingEvent",
-    "chreWifiRangingParams",
-    "chreWifiNanSubscribeConfig",
-    "chreWifiNanDiscoveryEvent",
-    "chreWifiNanSessionLostEvent",
-    "chreWifiNanSessionTerminatedEvent",
-    "chreWifiNanRangingParams"
-  ]
-},
-{
-  "filename": "chre_api/include/chre_api/chre/gnss.h",
-  "includes": [
-    "chre_api/include/chre_api/chre/common.h"
-  ],
-  "output_includes": [
-    "chpp/common/common_types.h",
-    "chre_api/chre/gnss.h"
-  ],
-  "struct_info": [
-    {
-      "name": "chreGnssDataEvent",
-      "annotations": [
-        {
-          "field": "version",
-          "annotation": "fixed_value",
-          "value": "CHRE_GNSS_DATA_EVENT_VERSION"
-        },
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        },
-        {
-          "field": "measurements",
-          "annotation": "var_len_array",
-          "length_field": "measurement_count"
-        }
-      ]
-    },
-    {
-      "name": "chreGnssLocationEvent",
-      "annotations": [
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    },
-    {
-      "name": "chreGnssClock",
-      "annotations": [
-        {
-          "field": "reserved",
-          "annotation": "fixed_value",
-          "value": "0"
-        }
-      ]
-    }
-  ],
-  "root_structs": [
-    "chreGnssDataEvent",
-    "chreGnssLocationEvent"
-  ]
-}]
diff --git a/chpp/api_parser/chre_api_to_chpp.py b/chpp/api_parser/chre_api_to_chpp.py
deleted file mode 100755
index f1bde02..0000000
--- a/chpp/api_parser/chre_api_to_chpp.py
+++ /dev/null
@@ -1,1101 +0,0 @@
-#!/usr/bin/python3
-#
-# Copyright (C) 2020 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import argparse
-import json
-import os.path
-import subprocess
-from collections import defaultdict
-from datetime import datetime
-
-from pyclibrary import CParser
-
-LICENSE_HEADER = """/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-"""
-
-# Paths for output, relative to system/chre
-CHPP_PARSER_INCLUDE_PATH = "chpp/include/chpp/common"
-CHPP_PARSER_SOURCE_PATH = "chpp/common"
-
-
-def system_chre_abs_path():
-    """Gets the absolute path to the system/chre directory containing this script."""
-    script_dir = os.path.dirname(os.path.realpath(__file__))
-    # Assuming we're at system/chre/chpp/api_parser (i.e. up 2 to get to system/chre)
-    chre_project_base_dir = os.path.normpath(script_dir + "/../..")
-    return chre_project_base_dir
-
-
-class CodeGenerator:
-    """Given an ApiParser object, generates a header file with structure definitions in CHPP format.
-    """
-
-    def __init__(self, api, commit_hash):
-        """
-        :param api: ApiParser object
-        """
-        self.api = api
-        self.json = api.json
-        # Turn "chre_api/include/chre_api/chre/wwan.h" into "wwan"
-        self.service_name = self.json['filename'].split('/')[-1].split('.')[0]
-        self.capitalized_service_name = self.service_name[0].upper() + self.service_name[1:]
-        self.commit_hash = commit_hash
-
-    # ----------------------------------------------------------------------------------------------
-    # Header generation methods (plus some methods shared with encoder generation)
-    # ----------------------------------------------------------------------------------------------
-
-    def _autogen_notice(self):
-        out = []
-        out.append("// This file was automatically generated by {}\n".format(
-            os.path.basename(__file__)))
-        out.append("// Date: {} UTC\n".format(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')))
-        out.append("// Source: {} @ commit {}\n\n".format(self.json['filename'], self.commit_hash))
-        out.append("// DO NOT modify this file directly, as those changes will be lost the next\n")
-        out.append("// time the script is executed\n\n")
-        return out
-
-    def _dump_to_file(self, output_filename, contents, dry_run, skip_clang_fomat):
-        """Outputs contents to output_filename, or prints contents if dry_run is True"""
-        if dry_run:
-            print("---- {} ----".format(output_filename))
-            print(contents)
-            print("---- end of {} ----\n".format(output_filename))
-        else:
-            with open(output_filename, 'w') as f:
-                f.write(contents)
-
-            if not skip_clang_fomat:
-                clang_format_path = os.path.normpath(
-                    "../../../../prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format")
-                args = [clang_format_path, '-i', output_filename]
-                result = subprocess.run(args)
-                result.check_returncode()
-
-    def _is_array_type(self, type_info):
-        # If this is an array type, declarators will be a tuple containing a list of
-        # a single int element giving the size of the array
-        return len(type_info.declarators) == 1 and isinstance(type_info.declarators[0], list)
-
-    def _get_array_len(self, type_info):
-        return type_info.declarators[0][0]
-
-    def _get_chpp_type_from_chre(self, chre_type):
-        """Given 'chreWwanCellInfo' returns 'struct ChppWwanCellInfo', etc."""
-        prefix = self._get_struct_or_union_prefix(chre_type)
-
-        # First see if we have an explicit name override (e.g. for anonymous types)
-        for annotation in self.api.annotations[chre_type]["."]:
-            if annotation['annotation'] == "rename_type":
-                return prefix + annotation['type_override']
-
-        # Otherwise, use the existing type name, just replace the "chre" prefix with "Chpp"
-        if chre_type.startswith('chre'):
-            return prefix + 'Chpp' + chre_type[4:]
-        else:
-            raise RuntimeError("Couldn't figure out new type name for {}".format(chre_type))
-
-    def _get_chre_type_with_prefix(self, chre_type):
-        """Given 'chreWwanCellInfo' returns 'struct chreWwanCellInfo', etc."""
-        return self._get_struct_or_union_prefix(chre_type) + chre_type
-
-    def _get_chpp_header_type_from_chre(self, chre_type):
-        """Given 'chreWwanCellInfo' returns 'struct ChppWwanCellInfoWithHeader', etc."""
-        return self._get_chpp_type_from_chre(chre_type) + 'WithHeader'
-
-    def _get_member_comment(self, member_info):
-        for annotation in member_info['annotations']:
-            if annotation['annotation'] == "fixed_value":
-                return "  // Input ignored; always set to {}".format(annotation['value'])
-            elif annotation['annotation'] == "var_len_array":
-                return "  // References {} instances of {}".format(
-                    annotation['length_field'], self._get_member_type(member_info))
-        return ""
-
-    def _get_member_type(self, member_info, underlying_vla_type=False):
-        """Gets the CHPP type specification prefix for a struct/union member.
-
-        :param member_info: a dict element from self.api.structs_and_unions[struct]['members']
-        :param underlying_vla_type: (used only for var-len array types) False to output
-            'struct ChppOffset', and True to output the type that ChppOffset references
-        :return: type specification string that prefixes the field name, e.g. 'uint8_t'
-        """
-        # 4 cases to handle:
-        #   1) Annotation gives explicit type that we should use
-        #   2) Annotation says this is a variable length array (so use ChppOffset if
-        #      underlying_vla_type is False)
-        #   3) This is a struct/union type, so use the renamed (CHPP) type name
-        #   4) Regular type, e.g. uint32_t, so just use the type spec as-is
-        for annotation in member_info['annotations']:
-            if annotation['annotation'] == "rewrite_type":
-                return annotation['type_override']
-            elif not underlying_vla_type and annotation['annotation'] in ["var_len_array", "string"]:
-                return "struct ChppOffset"
-
-        if not underlying_vla_type and len(member_info['type'].declarators) > 0 and \
-                member_info['type'].declarators[0] == "*":
-            # This case should either be handled by rewrite_type (e.g. to uint32_t as
-            # opaque/ignored), or var_len_array
-            raise RuntimeError("Pointer types require annotation\n{}".format(
-                member_info))
-
-        if member_info['is_nested_type']:
-            return self._get_chpp_type_from_chre(member_info['nested_type_name'])
-
-        return member_info['type'].type_spec
-
-    def _get_member_type_suffix(self, member_info):
-        if self._is_array_type(member_info['type']):
-            return "[{}]".format(self._get_array_len(member_info['type']))
-        return ""
-
-    def _get_struct_or_union_prefix(self, chre_type):
-        return 'struct ' if not self.api.structs_and_unions[chre_type]['is_union'] else 'union '
-
-    def _gen_header_includes(self):
-        """Generates #include directives for use in <service>_types.h"""
-        out = ["#include <stdbool.h>\n#include <stddef.h>\n#include <stdint.h>\n\n"]
-
-        includes = ["chpp/app.h", "chpp/macros.h", "chre_api/chre/version.h"]
-        includes.extend(self.json['output_includes'])
-        for incl in sorted(includes):
-            out.append("#include \"{}\"\n".format(incl))
-        out.append("\n")
-        return out
-
-    def _gen_struct_or_union(self, name):
-        """Generates the definition for a single struct/union type"""
-        out = []
-        if not name.startswith('anon'):
-            out.append("//! See {{@link {}}} for details\n".format(name))
-        out.append("{} {{\n".format(self._get_chpp_type_from_chre(name)))
-        for member_info in self.api.structs_and_unions[name]['members']:
-            out.append("  {} {}{};{}\n".format(self._get_member_type(member_info),
-                                               member_info['name'],
-                                               self._get_member_type_suffix(member_info),
-                                               self._get_member_comment(member_info)))
-
-        out.append("} CHPP_PACKED_ATTR;\n\n")
-        return out
-
-    def _gen_header_struct(self, chre_type):
-        """Generates the definition for the type with header (WithHeader)"""
-        out = []
-        out.append("//! CHPP app header plus {}\n".format(
-            self._get_chpp_header_type_from_chre(chre_type)))
-
-        out.append("{} {{\n".format(self._get_chpp_header_type_from_chre(chre_type)))
-        out.append("  struct ChppAppHeader header;\n")
-        out.append("  {} payload;\n".format(self._get_chpp_type_from_chre(chre_type)))
-        out.append("} CHPP_PACKED_ATTR;\n\n")
-
-        return out
-
-    def _gen_structs_and_unions(self):
-        """Generates definitions for all struct/union types required for the root structs."""
-        out = []
-        out.append("CHPP_PACKED_START\n\n")
-
-        sorted_structs = self._sorted_structs(self.json['root_structs'])
-        for type_name in sorted_structs:
-            out.extend(self._gen_struct_or_union(type_name))
-
-        for chre_type in self.json['root_structs']:
-            out.extend(self._gen_header_struct(chre_type))
-
-        out.append("CHPP_PACKED_END\n\n")
-        return out
-
-    def _sorted_structs(self, root_nodes):
-        """Implements a topological sort on self.api.structs_and_unions.
-
-        Elements are ordered by definition dependency, i.e. if A includes a field of type B,
-        then B will appear before A in the returned list.
-        :return: list of keys in self.api.structs_and_unions, sorted by dependency order
-        """
-        result = []
-        visited = set()
-
-        def sort_helper(collection, key):
-            for dep in sorted(collection[key]['dependencies']):
-                if dep not in visited:
-                    visited.add(dep)
-                    sort_helper(collection, dep)
-            result.append(key)
-
-        for node in sorted(root_nodes):
-            sort_helper(self.api.structs_and_unions, node)
-        return result
-
-    # ----------------------------------------------------------------------------------------------
-    # Encoder function generation methods (CHRE --> CHPP)
-    # ----------------------------------------------------------------------------------------------
-
-    def _get_chpp_member_sizeof_call(self, member_info):
-        """Returns invocation used to determine the size of the provided member when encoded.
-
-        Will be either sizeof(<type in CHPP struct>) or a function call if the member contains a VLA
-        :param member_info: a dict element from self.api.structs_and_unions[struct]['members']
-        :return: string
-        """
-        type_name = None
-        if member_info['is_nested_type']:
-            chre_type = member_info['nested_type_name']
-            if self.api.structs_and_unions[chre_type]['has_vla_member']:
-                return "{}(in->{})".format(self._get_chpp_sizeof_function_name(chre_type),
-                                           member_info['name'])
-            else:
-                type_name = self._get_chpp_type_from_chre(chre_type)
-        else:
-            type_name = member_info['type'].type_spec
-        return "sizeof({})".format(type_name)
-
-    def _gen_chpp_sizeof_function(self, chre_type):
-        """Generates a function to determine the encoded size of the CHRE struct, if necessary."""
-        out = []
-
-        # Note that this function *should* work with unions as well, but at the time of writing
-        # it'll only be used with structs, so names, etc. are written with that in mind
-        struct_info = self.api.structs_and_unions[chre_type]
-        if not struct_info['has_vla_member']:
-            # No codegen necessary, just sizeof on the CHPP structure name is sufficient
-            return out
-
-        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(chre_type)
-        parameter_name = core_type_name[0].lower() + core_type_name[1:]
-        chpp_type_name = self._get_chpp_header_type_from_chre(chre_type)
-        out.append("//! @return number of bytes required to represent the given\n"
-                   "//! {} along with the CHPP header as\n"
-                   "//! {}\n"
-                   .format(chre_type, chpp_type_name))
-        out.append("static size_t {}(\n        const {}{} *{}) {{\n"
-                   .format(self._get_chpp_sizeof_function_name(chre_type),
-                           self._get_struct_or_union_prefix(chre_type), chre_type,
-                           parameter_name))
-
-        # sizeof(this struct)
-        out.append("  size_t encodedSize = sizeof({});\n".format(chpp_type_name))
-
-        # Plus count * sizeof(type) for each var-len array included in this struct
-        for member_info in self.api.structs_and_unions[chre_type]['members']:
-            for annotation in member_info['annotations']:
-                if annotation['annotation'] == "var_len_array":
-                    # If the VLA field itself contains a VLA, then we'd need to generate a for
-                    # loop to calculate the size of each element individually - I don't think we
-                    # have any of these in the CHRE API today, so leaving this functionality out.
-                    # Also note that to support that case we'd also want to recursively call this
-                    # function to generate sizeof functions for nested fields.
-                    if member_info['is_nested_type'] and \
-                            self.api.structs_and_unions[member_info['nested_type_name']][
-                                'has_vla_member']:
-                        raise RuntimeError(
-                            "Nested variable-length arrays is not currently supported ({} "
-                            "in {})".format(member_info['name'], chre_type))
-
-                    out.append("  encodedSize += {}->{} * sizeof({});\n".format(
-                        parameter_name, annotation['length_field'],
-                        self._get_member_type(member_info, True)))
-                elif annotation['annotation'] == "string":
-                    out.append("  if ({}->{} != NULL) {{".format(
-                        parameter_name, annotation['field']))
-                    out.append("    encodedSize += strlen({}->{}) + 1;\n".format(
-                        parameter_name, annotation['field']))
-                    out.append("  }\n")
-
-        out.append("  return encodedSize;\n}\n\n")
-        return out
-
-    def _gen_chpp_sizeof_functions(self):
-        """For each root struct, generate necessary functions to determine their encoded size."""
-        out = []
-        for struct in self.json['root_structs']:
-            out.extend(self._gen_chpp_sizeof_function(struct))
-        return out
-
-    def _gen_conversion_includes(self):
-        """Generates #include directives for the conversion source file"""
-        out = ["#include \"chpp/macros.h\"\n"
-               "#include \"chpp/memory.h\"\n"
-               "#include \"chpp/common/{}_types.h\"\n\n".format(self.service_name)]
-        out.append("#include <stddef.h>\n#include <stdint.h>\n#include <string.h>\n\n")
-        return out
-
-    def _get_chpp_sizeof_function_name(self, chre_struct):
-        """Function name used to compute the encoded size of the given struct at runtime"""
-        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(chre_struct)
-        return "chpp{}SizeOf{}FromChre".format(self.capitalized_service_name, core_type_name)
-
-    def _get_encoding_function_name(self, chre_type):
-        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(chre_type)
-        return "chpp{}Convert{}FromChre".format(self.capitalized_service_name, core_type_name)
-
-    def _gen_encoding_function_signature(self, chre_type):
-        out = []
-        out.append("void {}(\n".format(self._get_encoding_function_name(chre_type)))
-        out.append("    const {}{} *in,\n".format(
-            self._get_struct_or_union_prefix(chre_type), chre_type))
-        out.append("    {} *out".format(self._get_chpp_type_from_chre(chre_type)))
-        if self.api.structs_and_unions[chre_type]['has_vla_member']:
-            out.append(",\n")
-            out.append("    uint8_t *payload,\n")
-            out.append("    size_t payloadSize,\n")
-            out.append("    uint16_t *vlaOffset")
-        out.append(")")
-        return out
-
-    def _gen_string_encoding(self, member_info, annotation):
-        out = []
-        # Might want to revisit this if we ever end up supporting NULL strings
-        # in our API. We can assert here since there's currently no API that
-        # does so.
-        member_name = member_info['name']
-        out.append("  if (in->{} != NULL) {{\n".format(member_name))
-        out.append("    size_t strSize = strlen(in->{}) + 1;\n".format(member_name))
-        out.append("    memcpy(&payload[*vlaOffset], in->{}, strSize);\n".format(
-            member_name))
-        out.append("    out->{}.length = (uint16_t)(strSize);\n".format(
-            member_name))
-        out.append("    out->{}.offset = *vlaOffset;\n".format(member_name))
-        out.append("    *vlaOffset += out->{}.length;\n".format(member_name))
-        out.append("  } else {\n")
-        out.append("    out->{}.length = 0;\n".format(member_name))
-        out.append("    out->{}.offset = 0;\n".format(member_name))
-        out.append("  }\n\n")
-
-        return out
-
-    def _gen_vla_encoding(self, member_info, annotation):
-        out = []
-
-        variable_name = member_info['name']
-        chpp_type = self._get_member_type(member_info, True)
-
-        if member_info['is_nested_type']:
-            out.append("\n  {} *{} = ({} *) &payload[*vlaOffset];\n".format(
-                chpp_type, variable_name, chpp_type))
-
-        out.append("  out->{}.length = (uint16_t)(in->{} * {});\n".format(
-            member_info['name'], annotation['length_field'],
-            self._get_chpp_member_sizeof_call(member_info)))
-
-        out.append("  CHPP_ASSERT((size_t)(*vlaOffset + out->{}.length) <= payloadSize);\n".format(
-            member_info['name']))
-
-        out.append("  if (out->{}.length > 0 &&\n"
-                   "      *vlaOffset + out->{}.length <= payloadSize) {{\n".format(
-            member_info['name'], member_info['name']))
-
-        if member_info['is_nested_type']:
-            out.append("    for (size_t i = 0; i < in->{}; i++) {{\n".format(
-                annotation['length_field']))
-            out.append("      {}".format(
-                self._get_assignment_statement_for_field(member_info, in_vla_loop=True)))
-            out.append("    }\n")
-        else:
-            out.append("memcpy(&payload[*vlaOffset], in->{}, in->{} * sizeof({}));\n".format(
-                member_info['name'], annotation['length_field'], chpp_type))
-
-        out.append("    out->{}.offset = *vlaOffset;\n".format(member_info['name']))
-        out.append("    *vlaOffset += out->{}.length;\n".format(member_info['name']))
-
-        out.append("  } else {\n")
-        out.append("    out->{}.offset = 0;\n".format(member_info['name']))
-        out.append("  }\n");
-
-        return out
-
-    # ----------------------------------------------------------------------------------------------
-    # Encoder / decoder function generation methods (CHRE <--> CHPP)
-    # ----------------------------------------------------------------------------------------------
-
-    def _get_assignment_statement_for_field(self, member_info,
-                                            in_vla_loop=False,
-                                            containing_field_name=None,
-                                            decode_mode=False):
-        """Returns a statement to assign the provided member
-
-        :param member_info:
-        :param in_vla_loop: True if we're currently inside a loop and should append [i]
-        :param containing_field_name: Additional member name to use to access the target field, or
-        None; for example the normal case is "out->field = in->field", but if we're generating
-        assignments in the parent conversion function (e.g. as used for union variants), we need to
-        do "out->nested_field.field = in->nested_field.field"
-        :param decode_mode: True converts from CHPP to CHRE. False from CHRE to CHPP
-        :return: assignment statement as a string
-        """
-        array_index = "[i]" if in_vla_loop else ""
-        output_accessor = "" if in_vla_loop else "out->"
-        containing_field = containing_field_name + "." if containing_field_name is not None else ""
-
-        output_variable = "{}{}{}{}".format(output_accessor, containing_field, member_info['name'],
-                                            array_index)
-        input_variable = "in->{}{}{}".format(containing_field, member_info['name'], array_index)
-
-        if decode_mode and in_vla_loop:
-            output_variable = "{}Out{}".format(member_info['name'], array_index)
-            input_variable = "{}In{}".format(member_info['name'], array_index)
-
-        if member_info['is_nested_type']:
-            chre_type = member_info['nested_type_name']
-            has_vla_member = self.api.structs_and_unions[chre_type]['has_vla_member']
-            if decode_mode:
-                # Use decoding function
-                vla_params = ", inSize" if has_vla_member else ""
-                out = "if (!{}(&{}, &{}{})) {{\n".format(
-                    self._get_decoding_function_name(chre_type), input_variable,
-                    output_variable, vla_params)
-                if has_vla_member:
-                    out += "  CHPP_FREE_AND_NULLIFY({}Out);\n".format(member_info['name'])
-                out += "  return false;\n"
-                out += "}\n"
-                return out
-            else:
-                # Use encoding function
-                vla_params = ", payload, payloadSize, vlaOffset" if has_vla_member else ""
-                return "{}(&{}, &{}{});\n".format(
-                    self._get_encoding_function_name(chre_type), input_variable, output_variable,
-                    vla_params)
-        elif self._is_array_type(member_info['type']):
-            # Array of primitive type (e.g. uint32_t[8]) - use memcpy
-            return "memcpy({}, {}, sizeof({}));\n".format(output_variable, input_variable,
-                                                          output_variable)
-        else:
-            # Regular assignment
-            return "{} = {};\n".format(output_variable, input_variable)
-
-    def _gen_union_variant_conversion_code(self, member_info, annotation, decode_mode):
-        """Generates a switch statement to encode the "active"/"used" field within a union.
-
-        Handles cases where a union has multiple types, but there's another peer/adjacent field
-        which tells you which field in the union is to be used. Outputs code like this:
-        switch (in->{discriminator field}) {
-            case {first discriminator value associated with a fields}:
-                {conversion code for the field associated with this discriminator value}
-                ...
-        :param chre_type: CHRE type of the union
-        :param annotation: Reference to JSON annotation data with the discriminator mapping data
-        :param decode_mode: False encodes from CHRE to CHPP. True decodes from CHPP to CHRE
-        :return: list of strings
-        """
-        out = []
-        chre_type = member_info['nested_type_name']
-        struct_info = self.api.structs_and_unions[chre_type]
-
-        # Start off by zeroing out the union field so any padding is set to a consistent value
-        out.append("  memset(&out->{}, 0, sizeof(out->{}));\n".format(member_info['name'],
-                                                                      member_info['name']))
-
-        # Next, generate the switch statement that will copy over the proper values
-        out.append("  switch (in->{}) {{\n".format(annotation['discriminator']))
-        for value, field_name in annotation['mapping']:
-            out.append("    case {}:\n".format(value))
-
-            found = False
-            for nested_member_info in struct_info['members']:
-                if nested_member_info['name'] == field_name:
-                    out.append("      {}".format(
-                        self._get_assignment_statement_for_field(
-                            nested_member_info,
-                            containing_field_name=member_info['name'],
-                            decode_mode=decode_mode)))
-                    found = True
-                    break
-
-            if not found:
-                raise RuntimeError("Invalid mapping - couldn't find target field {} in struct {}"
-                                   .format(field_name, chre_type))
-
-            out.append("      break;\n")
-
-        out.append("    default:\n"
-                   "      CHPP_ASSERT(false);\n"
-                   "  }\n")
-
-        return out
-
-    def _gen_conversion_function(self, chre_type, already_generated, decode_mode):
-        out = []
-
-        struct_info = self.api.structs_and_unions[chre_type]
-        for dependency in sorted(struct_info['dependencies']):
-            if dependency not in already_generated:
-                out.extend(
-                    self._gen_conversion_function(dependency, already_generated, decode_mode))
-
-        # Skip if we've already generated code for this type, or if it's a union (in which case we
-        # handle the assignment in the parent structure to enable support for discrimination of
-        # which field in the union to use)
-        if chre_type in already_generated or struct_info['is_union']:
-            return out
-        already_generated.add(chre_type)
-
-        out.append("static ")
-        if decode_mode:
-            out.extend(self._gen_decoding_function_signature(chre_type))
-        else:
-            out.extend(self._gen_encoding_function_signature(chre_type))
-        out.append(" {\n")
-
-        for member_info in self.api.structs_and_unions[chre_type]['members']:
-            generated_by_annotation = False
-            for annotation in member_info['annotations']:
-                if annotation['annotation'] == "fixed_value":
-                    if self._is_array_type(member_info['type']):
-                        out.append("  memset(&out->{}, {}, sizeof(out->{}));\n".format(
-                            member_info['name'], annotation['value'], member_info['name']))
-                    else:
-                        out.append("  out->{} = {};\n".format(member_info['name'],
-                                                              annotation['value']))
-                    generated_by_annotation = True
-                    break
-                elif annotation['annotation'] == "enum":
-                    # TODO: generate range verification code?
-                    pass
-                elif annotation['annotation'] == "var_len_array":
-                    if decode_mode:
-                        out.extend(self._gen_vla_decoding(member_info, annotation))
-                    else:
-                        out.extend(self._gen_vla_encoding(member_info, annotation))
-                    generated_by_annotation = True
-                    break
-                elif annotation['annotation'] == "string":
-                    if decode_mode:
-                        out.extend(self._gen_string_decoding(member_info, annotation))
-                    else:
-                        out.extend(self._gen_string_encoding(member_info, annotation))
-                    generated_by_annotation = True
-                    break
-                elif annotation['annotation'] == "union_variant":
-                    out.extend(self._gen_union_variant_conversion_code(
-                        member_info, annotation, decode_mode))
-                    generated_by_annotation = True
-                    break
-
-            if not generated_by_annotation:
-                out.append("  {}".format(
-                    self._get_assignment_statement_for_field(member_info, decode_mode=decode_mode)))
-
-        if decode_mode:
-            out.append("\n  return true;\n")
-
-        out.append("}\n\n")
-        return out
-
-    def _gen_conversion_functions(self, decode_mode):
-        out = []
-        already_generated = set()
-        for struct in self.json['root_structs']:
-            out.extend(self._gen_conversion_function(struct, already_generated, decode_mode))
-        return out
-
-    def _strip_prefix_and_service_from_chre_struct_name(self, struct):
-        """Strip 'chre' and service prefix, e.g. 'chreWwanCellResultInfo' -> 'CellResultInfo'"""
-        chre_stripped = struct[4:]
-        upcased_service_name = self.service_name[0].upper() + self.service_name[1:]
-        if not struct.startswith('chre') or not chre_stripped.startswith(upcased_service_name):
-            # If this happens, we need to update the script to handle it. Right we assume struct
-            # naming follows the pattern "chre<Service_name><Thing_name>"
-            raise RuntimeError("Unexpected structure name {}".format(struct))
-
-        return chre_stripped[len(self.service_name):]
-
-    # ----------------------------------------------------------------------------------------------
-    # Memory allocation generation methods
-    # ----------------------------------------------------------------------------------------------
-
-    def _get_chpp_sizeof_call(self, chre_type):
-        """Returns invocation used to determine the size of the provided CHRE struct (with the CHPP
-        app header) after encoding.
-
-        Like _get_chpp_member_sizeof_call(), except for a top-level type assigned to the variable
-        "in" rather than a member within a structure (e.g. a VLA field)
-        :param chre_type: CHRE type name
-        :return: string
-        """
-        if self.api.structs_and_unions[chre_type]['has_vla_member']:
-            return "{}(in)".format(self._get_chpp_sizeof_function_name(chre_type))
-        else:
-            return "sizeof({})".format(self._get_chpp_header_type_from_chre(chre_type))
-
-    def _get_encode_allocation_function_name(self, chre_type):
-        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(chre_type)
-        return "chpp{}{}FromChre".format(self.capitalized_service_name, core_type_name)
-
-    def _gen_encode_allocation_function_signature(self, chre_type, gen_docs=False):
-        out = []
-        if gen_docs:
-            out.append("/**\n"
-                       " * Converts from given CHRE structure to serialized CHPP type.\n"
-                       " *\n"
-                       " * @param in Fully-formed CHRE structure.\n"
-                       " * @param out Upon success, will point to a buffer allocated with "
-                       "chppMalloc().\n"
-                       " * It is the responsibility of the caller to set the values of the CHPP "
-                       "app layer header, and to free the buffer when it is no longer needed via "
-                       "chppFree() or CHPP_FREE_AND_NULLIFY().\n"
-                       " * @param outSize Upon success, will be set to the size of the output "
-                       "buffer, in bytes.\n"
-                       " *\n"
-                       " * @return true on success, false if memory allocation failed.\n"
-                       " */\n")
-        out.append("bool {}(\n".format(self._get_encode_allocation_function_name(chre_type)))
-        out.append("    const {}{} *in,\n".format(
-            self._get_struct_or_union_prefix(chre_type), chre_type))
-        out.append("    {} **out,\n".format(self._get_chpp_header_type_from_chre(chre_type)))
-        out.append("    size_t *outSize)")
-        return out
-
-    def _gen_encode_allocation_function(self, chre_type):
-        out = []
-        out.extend(self._gen_encode_allocation_function_signature(chre_type))
-        out.append(" {\n")
-        out.append("  CHPP_NOT_NULL(out);\n")
-        out.append("  CHPP_NOT_NULL(outSize);\n\n")
-        out.append("  size_t payloadSize = {};\n".format(
-            self._get_chpp_sizeof_call(chre_type)))
-        out.append("  *out = chppMalloc(payloadSize);\n")
-
-        out.append("  if (*out != NULL) {\n")
-
-        struct_info = self.api.structs_and_unions[chre_type]
-        if struct_info['has_vla_member']:
-            out.append("    uint8_t *payload = (uint8_t *) &(*out)->payload;\n")
-            out.append("    uint16_t vlaOffset = sizeof({});\n".format(
-                self._get_chpp_type_from_chre(chre_type)))
-
-        out.append("    {}(in, &(*out)->payload".format(
-            self._get_encoding_function_name(chre_type)))
-        if struct_info['has_vla_member']:
-            out.append(", payload, payloadSize, &vlaOffset")
-        out.append(");\n")
-        out.append("    *outSize = payloadSize;\n")
-        out.append("    return true;\n")
-        out.append("  }\n")
-
-        out.append("  return false;\n}\n\n")
-        return out
-
-    def _gen_encode_allocation_functions(self):
-        out = []
-        for chre_type in self.json['root_structs']:
-            out.extend(self._gen_encode_allocation_function(chre_type))
-        return out
-
-    def _gen_encode_allocation_function_signatures(self):
-        out = []
-        for chre_type in self.json['root_structs']:
-            out.extend(self._gen_encode_allocation_function_signature(chre_type, True))
-            out.append(";\n\n")
-        return out
-
-    # ----------------------------------------------------------------------------------------------
-    # Decoder function generation methods (CHPP --> CHRE)
-    # ----------------------------------------------------------------------------------------------
-
-    def _get_decoding_function_name(self, chre_type):
-        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(chre_type)
-        return "chpp{}Convert{}ToChre".format(self.capitalized_service_name, core_type_name)
-
-    def _gen_decoding_function_signature(self, chre_type):
-        out = []
-        out.append("bool {}(\n".format(self._get_decoding_function_name(chre_type)))
-        out.append("    const {} *in,\n".format(self._get_chpp_type_from_chre(chre_type)))
-        out.append("    {} *out".format(self._get_chre_type_with_prefix(chre_type)))
-        if self.api.structs_and_unions[chre_type]['has_vla_member']:
-            out.append(",\n")
-            out.append("    size_t inSize")
-        out.append(")")
-        return out
-
-    def _gen_string_decoding(self, member_info, annotation):
-        out = []
-        variable_name = member_info['name']
-        out.append("\n")
-        out.append("  if (in->{}.length == 0) {{\n".format(variable_name))
-        out.append("    out->{} = NULL;\n".format(variable_name))
-        out.append("  } else {\n")
-        out.append("    char *{}Out = chppMalloc(in->{}.length);\n".format(
-            variable_name, variable_name))
-        out.append("    if ({}Out == NULL) {{\n".format(variable_name))
-        out.append("      return false;\n")
-        out.append("    }\n\n")
-        out.append("    memcpy({}Out, &((const uint8_t *)in)[in->{}.offset],\n".format(
-            variable_name, variable_name))
-        out.append("      in->{}.length);\n".format(variable_name))
-        out.append("    out->{} = {}Out;\n".format(variable_name, variable_name))
-        out.append("  }\n")
-
-        return out
-
-
-    def _gen_vla_decoding(self, member_info, annotation):
-        out = []
-
-        variable_name = member_info['name']
-        chpp_type = self._get_member_type(member_info, True)
-        if member_info['is_nested_type']:
-            chre_type = self._get_chre_type_with_prefix(member_info['nested_type_name'])
-        else:
-            chre_type = chpp_type
-
-        out.append("\n")
-        out.append("  if (in->{}.length == 0) {{\n".format(variable_name))
-        out.append("    out->{} = NULL;\n".format(variable_name))
-        out.append("  }\n")
-        out.append("  else {\n")
-        out.append("    if (in->{}.offset + in->{}.length > inSize ||\n".format(
-            variable_name, variable_name))
-        out.append("        in->{}.length != in->{} * sizeof({})) {{\n".format(
-            variable_name, annotation['length_field'], chpp_type))
-
-        out.append("      return false;\n")
-        out.append("    }\n\n")
-
-        if member_info['is_nested_type']:
-            out.append("    const {} *{}In =\n".format(chpp_type, variable_name))
-            out.append("        (const {} *) &((const uint8_t *)in)[in->{}.offset];\n\n".format(
-                chpp_type, variable_name))
-
-        out.append("    {} *{}Out = chppMalloc(in->{} * sizeof({}));\n".format(
-            chre_type, variable_name, annotation['length_field'], chre_type))
-        out.append("    if ({}Out == NULL) {{\n".format(variable_name))
-        out.append("      return false;\n")
-        out.append("    }\n\n")
-
-        if member_info['is_nested_type']:
-            out.append("    for (size_t i = 0; i < in->{}; i++) {{\n".format(
-                annotation['length_field'], variable_name))
-            out.append("      {}".format(self._get_assignment_statement_for_field(
-                member_info, in_vla_loop=True, decode_mode=True)))
-            out.append("    }\n")
-        else:
-            out.append("    memcpy({}Out, &((const uint8_t *)in)[in->{}.offset],\n".format(
-                variable_name, variable_name))
-            out.append("      in->{} * sizeof({}));\n".format(
-                annotation['length_field'], chre_type))
-
-        out.append("    out->{} = {}Out;\n".format(variable_name, variable_name))
-        out.append("  }\n\n")
-
-        return out
-
-    def _get_decode_allocation_function_name(self, chre_type):
-        core_type_name = self._strip_prefix_and_service_from_chre_struct_name(chre_type)
-        return "chpp{}{}ToChre".format(self.capitalized_service_name, core_type_name)
-
-    def _gen_decode_allocation_function_signature(self, chre_type, gen_docs=False):
-        out = []
-        if gen_docs:
-            out.append("/**\n"
-                       " * Converts from serialized CHPP structure to a CHRE type.\n"
-                       " *\n"
-                       " * @param in Fully-formed CHPP structure.\n"
-                       " * @param in Size of the CHPP structure in bytes.\n"
-                       " *\n"
-                       " * @return If successful, a pointer to a CHRE structure allocated with "
-                       "chppMalloc(). If unsuccessful, null.\n"
-                       " * It is the responsibility of the caller to free the buffer when it is no "
-                       "longer needed via chppFree() or CHPP_FREE_AND_NULLIFY().\n"
-                       " */\n")
-
-        out.append("{} *{}(\n".format(
-            self._get_chre_type_with_prefix(chre_type),
-            self._get_decode_allocation_function_name(chre_type)))
-        out.append("    const {} *in,\n".format(self._get_chpp_type_from_chre(chre_type)))
-        out.append("    size_t inSize)")
-        return out
-
-    def _gen_decode_allocation_function(self, chre_type):
-        out = []
-
-        out.extend(self._gen_decode_allocation_function_signature(chre_type))
-        out.append(" {\n")
-
-        out.append("  {} *out = NULL;\n\n".format(
-            self._get_chre_type_with_prefix(chre_type)))
-
-        out.append("  if (inSize >= sizeof({})) {{\n".format(
-            self._get_chpp_type_from_chre(chre_type)))
-
-        out.append("    out = chppMalloc(sizeof({}));\n".format(
-            self._get_chre_type_with_prefix(chre_type)))
-        out.append("    if (out != NULL) {\n")
-
-        struct_info = self.api.structs_and_unions[chre_type]
-
-        out.append("      if (!{}(in, out".format(self._get_decoding_function_name(chre_type)))
-        if struct_info['has_vla_member']:
-            out.append(", inSize")
-        out.append(")) {")
-        out.append("        CHPP_FREE_AND_NULLIFY(out);\n")
-        out.append("      }\n")
-
-        out.append("    }\n")
-        out.append("  }\n\n")
-        out.append("  return out;\n")
-        out.append("}\n")
-        return out
-
-    def _gen_decode_allocation_functions(self):
-        out = []
-        for chre_type in self.json['root_structs']:
-            out.extend(self._gen_decode_allocation_function(chre_type))
-        return out
-
-    def _gen_decode_allocation_function_signatures(self):
-        out = []
-        for chre_type in self.json['root_structs']:
-            out.extend(self._gen_decode_allocation_function_signature(chre_type, True))
-            out.append(";\n\n")
-        return out
-
-    # ----------------------------------------------------------------------------------------------
-    # Public methods
-    # ----------------------------------------------------------------------------------------------
-
-    def generate_header_file(self, dry_run=False, skip_clang_format=False):
-        """Creates a C header file for this API and writes it to the file indicated in the JSON."""
-        filename = self.service_name + "_types.h"
-        if not dry_run:
-            print("Generating {} ... ".format(filename), end='', flush=True)
-        output_file = os.path.join(system_chre_abs_path(), CHPP_PARSER_INCLUDE_PATH, filename)
-        header = self.generate_header_string()
-        self._dump_to_file(output_file, header, dry_run, skip_clang_format)
-        if not dry_run:
-            print("done")
-
-    def generate_header_string(self):
-        """Returns a C header with structure definitions for this API."""
-        # To defer concatenation (speed things up), build the file as a list of strings then only
-        # concatenate once at the end
-        out = [LICENSE_HEADER]
-
-        header_guard = "CHPP_{}_TYPES_H_".format(self.service_name.upper())
-
-        out.append("#ifndef {}\n#define {}\n\n".format(header_guard, header_guard))
-        out.extend(self._autogen_notice())
-        out.extend(self._gen_header_includes())
-        out.append("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n")
-        out.extend(self._gen_structs_and_unions())
-
-        out.append("\n// Encoding functions (CHRE --> CHPP)\n\n")
-        out.extend(self._gen_encode_allocation_function_signatures())
-
-        out.append("\n// Decoding functions (CHPP --> CHRE)\n\n")
-        out.extend(self._gen_decode_allocation_function_signatures())
-
-        out.append("#ifdef __cplusplus\n}\n#endif\n\n")
-        out.append("#endif  // {}\n".format(header_guard))
-        return ''.join(out)
-
-    def generate_conversion_file(self, dry_run=False, skip_clang_format=False):
-        """Generates a .c file with functions for encoding CHRE structs into CHPP and vice versa."""
-        filename = self.service_name + "_convert.c"
-        if not dry_run:
-            print("Generating {} ... ".format(filename), end='', flush=True)
-        contents = self.generate_conversion_string()
-        output_file = os.path.join(system_chre_abs_path(), CHPP_PARSER_SOURCE_PATH, filename)
-        self._dump_to_file(output_file, contents, dry_run, skip_clang_format)
-        if not dry_run:
-            print("done")
-
-    def generate_conversion_string(self):
-        """Returns C code for encoding CHRE structs into CHPP and vice versa."""
-        out = [LICENSE_HEADER, "\n"]
-
-        out.extend(self._autogen_notice())
-        out.extend(self._gen_conversion_includes())
-
-        out.append("\n// Encoding (CHRE --> CHPP) size functions\n\n")
-        out.extend(self._gen_chpp_sizeof_functions())
-        out.append("\n// Encoding (CHRE --> CHPP) conversion functions\n\n")
-        out.extend(self._gen_conversion_functions(False))
-        out.append("\n// Encoding (CHRE --> CHPP) top-level functions\n\n")
-        out.extend(self._gen_encode_allocation_functions())
-
-        out.append("\n// Decoding (CHPP --> CHRE) conversion functions\n\n")
-        out.extend(self._gen_conversion_functions(True))
-        out.append("\n// Decoding (CHPP --> CHRE) top-level functions\n\n")
-        out.extend(self._gen_decode_allocation_functions())
-
-        return ''.join(out)
-
-
-class ApiParser:
-    """Given a file-specific set of annotations (extracted from JSON annotations file), parses a
-    single API header file into data structures suitable for use with code generation.
-    """
-
-    def __init__(self, json_obj):
-        """Initialize and parse the API file described in the provided JSON-derived object.
-
-        :param json_obj: Extracted file-specific annotations from JSON
-        """
-        self.json = json_obj
-        self.structs_and_unions = {}
-        self._parse_annotations()
-        self._parse_api()
-
-    def _parse_annotations(self):
-        # Convert annotations list to a more usable data structure: dict keyed by structure name,
-        # containing a dict keyed by field name, containing a list of annotations (as they
-        # appear in the JSON). In other words, we can easily get all of the annotations for the
-        # "version" field in "chreWwanCellInfoResult" via
-        # annotations['chreWwanCellInfoResult']['version']. This is also a defaultdict, so it's safe
-        # to access if there are no annotations for this structure + field; it'll just give you
-        # an empty list in that case.
-        self.annotations = defaultdict(lambda: defaultdict(list))
-        for struct_info in self.json['struct_info']:
-            for annotation in struct_info['annotations']:
-                self.annotations[struct_info['name']][annotation['field']].append(annotation)
-
-    def _files_to_parse(self):
-        """Returns a list of files to supply as input to CParser"""
-        # Input paths for CParser are stored in JSON relative to <android_root>/system/chre
-        # Reformulate these to absolute paths, and add in some default includes that we always
-        # supply
-        chre_project_base_dir = system_chre_abs_path()
-        default_includes = ["chpp/api_parser/parser_defines.h",
-                            "chre_api/include/chre_api/chre/version.h"]
-        files = default_includes + self.json['includes'] + [self.json['filename']]
-        return [os.path.join(chre_project_base_dir, file) for file in files]
-
-    def _parse_structs_and_unions(self):
-        # Starting with the root structures (i.e. those that will appear at the top-level in one
-        # or more CHPP messages), build a data structure containing all of the information we'll
-        # need to emit the CHPP structure definition and conversion code.
-        structs_and_unions_to_parse = self.json['root_structs'].copy()
-        while len(structs_and_unions_to_parse) > 0:
-            type_name = structs_and_unions_to_parse.pop()
-            if type_name in self.structs_and_unions:
-                continue
-
-            entry = {
-                'appears_in': set(),  # Other types this type is nested within
-                'dependencies': set(),  # Types that are nested in this type
-                'has_vla_member': False,  # True if this type or any dependency has a VLA member
-                'members': [],  # Info about each member of this type
-            }
-            if type_name in self.parser.defs['structs']:
-                defs = self.parser.defs['structs'][type_name]
-                entry['is_union'] = False
-            elif type_name in self.parser.defs['unions']:
-                defs = self.parser.defs['unions'][type_name]
-                entry['is_union'] = True
-            else:
-                raise RuntimeError("Couldn't find {} in parsed structs/unions".format(type_name))
-
-            for member_name, member_type, _ in defs['members']:
-                member_info = {
-                    'name': member_name,
-                    'type': member_type,
-                    'annotations': self.annotations[type_name][member_name],
-                    'is_nested_type': False,
-                }
-
-                if member_type.type_spec.startswith('struct ') or \
-                        member_type.type_spec.startswith('union '):
-                    member_info['is_nested_type'] = True
-                    member_type_name = member_type.type_spec.split(' ')[1]
-                    member_info['nested_type_name'] = member_type_name
-                    entry['dependencies'].add(member_type_name)
-                    structs_and_unions_to_parse.append(member_type_name)
-
-                entry['members'].append(member_info)
-
-                # Flip a flag if this structure has at least one variable-length array member, which
-                # means that the encoded size can only be computed at runtime
-                if not entry['has_vla_member']:
-                    for annotation in self.annotations[type_name][member_name]:
-                        if annotation['annotation'] == "var_len_array":
-                            entry['has_vla_member'] = True
-
-            self.structs_and_unions[type_name] = entry
-
-        # Build reverse linkage of dependency chain (i.e. lookup between a type and the other types
-        # it appears in)
-        for type_name, type_info in self.structs_and_unions.items():
-            for dependency in type_info['dependencies']:
-                self.structs_and_unions[dependency]['appears_in'].add(type_name)
-
-        # Bubble up "has_vla_member" to types each type it appears in, i.e. if this flag is set to
-        # True on a leaf node, then all its ancestors should also have the flag set to True
-        for type_name, type_info in self.structs_and_unions.items():
-            if type_info['has_vla_member']:
-                types_to_mark = list(type_info['appears_in'])
-                while len(types_to_mark) > 0:
-                    type_to_mark = types_to_mark.pop()
-                    self.structs_and_unions[type_to_mark]['has_vla_member'] = True
-                    types_to_mark.extend(list(self.structs_and_unions[type_to_mark]['appears_in']))
-
-    def _parse_api(self):
-        file_to_parse = self._files_to_parse()
-        self.parser = CParser(file_to_parse, cache='parser_cache')
-        self._parse_structs_and_unions()
-
-
-def run(args):
-    with open('chre_api_annotations.json') as f:
-        js = json.load(f)
-
-    commit_hash = subprocess.getoutput("git describe --always --long --dirty --exclude '*'")
-    for file in js:
-        if args.file_filter:
-            matched = False
-            for matcher in args.file_filter:
-                if matcher in file['filename']:
-                    matched = True
-                    break
-            if not matched:
-                print("Skipping {} - doesn't match filter(s) {}".format(file['filename'],
-                                                                        args.file_filter))
-                continue
-        print("Parsing {} ... ".format(file['filename']), end='', flush=True)
-        api_parser = ApiParser(file)
-        code_gen = CodeGenerator(api_parser, commit_hash)
-        print("done")
-        code_gen.generate_header_file(args.dry_run, args.skip_clang_format)
-        code_gen.generate_conversion_file(args.dry_run, args.skip_clang_format)
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description='Generate CHPP serialization code from CHRE APIs.')
-    parser.add_argument('-n', dest='dry_run', action='store_true',
-                        help='Print the output instead of writing to a file')
-    parser.add_argument('--skip-clang-format', dest='skip_clang_format', action='store_true',
-                        help='Skip running clang-format on the output files (doesn\'t apply to dry '
-                             'runs)')
-    parser.add_argument('file_filter', nargs='*',
-                        help='Filters the input files (filename field in the JSON) to generate a '
-                             'subset of the typical output, e.g. "wifi" to just generate conversion'
-                             ' routines for wifi.h')
-    args = parser.parse_args()
-    run(args)
diff --git a/chpp/api_parser/requirements.txt b/chpp/api_parser/requirements.txt
deleted file mode 100644
index 9b9ec0a..0000000
--- a/chpp/api_parser/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-future==0.18.2
-pyclibrary==0.1.4
diff --git a/chpp/app.c b/chpp/app.c
index 81aadfd..a25bcdf 100644
--- a/chpp/app.c
+++ b/chpp/app.c
@@ -25,6 +25,7 @@
 
 #include "chpp/clients.h"
 #include "chpp/clients/discovery.h"
+#include "chpp/services.h"
 #ifdef CHPP_CLIENT_ENABLED_LOOPBACK
 #include "chpp/clients/loopback.h"
 #endif
@@ -40,7 +41,7 @@
 #include "chpp/services/loopback.h"
 #include "chpp/services/nonhandle.h"
 #include "chpp/services/timesync.h"
-#include "chre_api/chre/common.h"
+#include "chpp/time.h"
 
 /************************************************
  *  Prototypes
@@ -50,31 +51,28 @@
                                                uint8_t *buf, size_t len);
 static bool chppProcessPredefinedServiceResponse(struct ChppAppState *context,
                                                  uint8_t *buf, size_t len);
-static bool chppProcessPredefinedClientNotification(
-    struct ChppAppState *context, uint8_t *buf, size_t len);
-static bool chppProcessPredefinedServiceNotification(
-    struct ChppAppState *context, uint8_t *buf, size_t len);
 
 static bool chppDatagramLenIsOk(struct ChppAppState *context,
-                                struct ChppAppHeader *rxHeader, size_t len);
-ChppDispatchFunction *chppGetDispatchFunction(struct ChppAppState *context,
-                                              uint8_t handle,
-                                              enum ChppMessageType type);
-ChppNotifierFunction *chppGetClientResetNotifierFunction(
+                                const struct ChppAppHeader *rxHeader,
+                                size_t len);
+static ChppDispatchFunction *chppGetDispatchFunction(
+    struct ChppAppState *context, uint8_t handle, enum ChppMessageType type);
+#ifdef CHPP_CLIENT_ENABLED_DISCOVERY
+static ChppNotifierFunction *chppGetClientResetNotifierFunction(
     struct ChppAppState *context, uint8_t index);
-ChppNotifierFunction *chppGetServiceResetNotifierFunction(
+#endif  // CHPP_CLIENT_ENABLED_DISCOVERY
+static ChppNotifierFunction *chppGetServiceResetNotifierFunction(
     struct ChppAppState *context, uint8_t index);
 static inline const struct ChppService *chppServiceOfHandle(
     struct ChppAppState *appContext, uint8_t handle);
 static inline const struct ChppClient *chppClientOfHandle(
     struct ChppAppState *appContext, uint8_t handle);
-static inline void *chppServiceContextOfHandle(struct ChppAppState *appContext,
-                                               uint8_t handle);
-static inline void *chppClientContextOfHandle(struct ChppAppState *appContext,
-                                              uint8_t handle);
-static void *chppClientServiceContextOfHandle(struct ChppAppState *appContext,
-                                              uint8_t handle,
-                                              enum ChppMessageType type);
+static inline struct ChppEndpointState *chppServiceStateOfHandle(
+    struct ChppAppState *appContext, uint8_t handle);
+static inline struct ChppEndpointState *chppClientStateOfHandle(
+    struct ChppAppState *appContext, uint8_t handle);
+static struct ChppEndpointState *chppClientOrServiceStateOfHandle(
+    struct ChppAppState *appContext, uint8_t handle, enum ChppMessageType type);
 
 static void chppProcessPredefinedHandleDatagram(struct ChppAppState *context,
                                                 uint8_t *buf, size_t len);
@@ -89,7 +87,7 @@
  * Processes a client request that is determined to be for a predefined CHPP
  * service.
  *
- * @param context Maintains status for each app layer instance.
+ * @param context State of the app layer.
  * @param buf Input data. Cannot be null.
  * @param len Length of input data in bytes.
  *
@@ -97,7 +95,7 @@
  */
 static bool chppProcessPredefinedClientRequest(struct ChppAppState *context,
                                                uint8_t *buf, size_t len) {
-  struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
+  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
   bool handleValid = true;
   bool dispatchResult = true;
 
@@ -134,7 +132,7 @@
  * Processes a service response that is determined to be for a predefined CHPP
  * client.
  *
- * @param context Maintains status for each app layer instance.
+ * @param context State of the app layer.
  * @param buf Input data. Cannot be null.
  * @param len Length of input data in bytes.
  *
@@ -142,12 +140,12 @@
  */
 static bool chppProcessPredefinedServiceResponse(struct ChppAppState *context,
                                                  uint8_t *buf, size_t len) {
+  CHPP_DEBUG_NOT_NULL(buf);
   // Possibly unused if compiling without the clients below enabled
   UNUSED_VAR(context);
-  UNUSED_VAR(buf);
   UNUSED_VAR(len);
 
-  struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
+  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
   bool handleValid = true;
   bool dispatchResult = true;
 
@@ -188,55 +186,21 @@
 }
 
 /**
- * Processes a client notification that is determined to be for a predefined
- * CHPP service.
- *
- * @param context Maintains status for each app layer instance.
- * @param buf Input data. Cannot be null.
- * @param len Length of input data in bytes.
- *
- * @return False if handle is invalid. True otherwise.
- */
-static bool chppProcessPredefinedClientNotification(
-    struct ChppAppState *context, uint8_t *buf, size_t len) {
-  UNUSED_VAR(context);
-  UNUSED_VAR(len);
-  UNUSED_VAR(buf);
-  // No predefined services support these.
-  return false;
-}
-
-/**
- * Processes a service notification that is determined to be for a predefined
- * CHPP client.
- *
- * @param context Maintains status for each app layer instance.
- * @param buf Input data. Cannot be null.
- * @param len Length of input data in bytes.
- *
- * @return False if handle is invalid. True otherwise.
- */
-static bool chppProcessPredefinedServiceNotification(
-    struct ChppAppState *context, uint8_t *buf, size_t len) {
-  UNUSED_VAR(context);
-  UNUSED_VAR(len);
-  UNUSED_VAR(buf);
-  // No predefined clients support these.
-  return false;
-}
-
-/**
  * Verifies if the length of a Rx Datagram from the transport layer is
- * sufficient for the associated service.
+ * sufficient for the associated service/client.
  *
- * @param context Maintains status for each app layer instance.
+ * @param context State of the app layer.
  * @param rxHeader The pointer to the datagram RX header.
  * @param len Length of the datagram in bytes.
  *
  * @return true if length is ok.
  */
 static bool chppDatagramLenIsOk(struct ChppAppState *context,
-                                struct ChppAppHeader *rxHeader, size_t len) {
+                                const struct ChppAppHeader *rxHeader,
+                                size_t len) {
+  CHPP_DEBUG_NOT_NULL(context);
+  CHPP_DEBUG_NOT_NULL(rxHeader);
+
   size_t minLen = SIZE_MAX;
   uint8_t handle = rxHeader->handle;
 
@@ -259,6 +223,7 @@
       default:
         // len remains SIZE_MAX
         CHPP_LOGE("Invalid H#%" PRIu8, handle);
+        return false;
     }
 
   } else {  // Negotiated
@@ -267,6 +232,7 @@
 
     switch (messageType) {
       case CHPP_MESSAGE_TYPE_CLIENT_REQUEST:
+      case CHPP_MESSAGE_TYPE_CLIENT_RESPONSE:
       case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION: {
         const struct ChppService *service =
             chppServiceOfHandle(context, handle);
@@ -276,6 +242,7 @@
         break;
       }
       case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
+      case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
       case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
         const struct ChppClient *client = chppClientOfHandle(context, handle);
         if (client != NULL) {
@@ -283,117 +250,128 @@
         }
         break;
       }
-      default: {
-        break;
-      }
-    }
-
-    if (minLen == SIZE_MAX) {
-      CHPP_LOGE("Invalid type=%d or H#%" PRIu8, messageType, handle);
+      default:
+        CHPP_LOGE("Invalid type=%d or H#%" PRIu8, messageType, handle);
+        return false;
     }
   }
 
-  if ((len < minLen) && (minLen != SIZE_MAX)) {
+  if (len < minLen) {
     CHPP_LOGE("Datagram len=%" PRIuSIZE " < %" PRIuSIZE " for H#%" PRIu8, len,
               minLen, handle);
+    return false;
   }
-  return (len >= minLen) && (minLen != SIZE_MAX);
+
+  return true;
 }
 
 /**
  * Returns the dispatch function of a particular negotiated client/service
- * handle and message type. This shall be null if it is unsupported by the
- * service.
+ * handle and message type.
  *
- * @param context Maintains status for each app layer instance.
+ * Returns null if it is unsupported by the service.
+ *
+ * @param context State of the app layer.
  * @param handle Handle number for the client/service.
  * @param type Message type.
  *
  * @return Pointer to a function that dispatches incoming datagrams for any
  * particular client/service.
  */
-ChppDispatchFunction *chppGetDispatchFunction(struct ChppAppState *context,
-                                              uint8_t handle,
-                                              enum ChppMessageType type) {
+static ChppDispatchFunction *chppGetDispatchFunction(
+    struct ChppAppState *context, uint8_t handle, enum ChppMessageType type) {
+  CHPP_DEBUG_NOT_NULL(context);
   // chppDatagramLenIsOk() has already confirmed that the handle # is valid.
   // Therefore, no additional checks are necessary for chppClientOfHandle(),
-  // chppServiceOfHandle(), or chppClientServiceContextOfHandle().
+  // chppServiceOfHandle(), or chppClientOrServiceStateOfHandle().
 
+  // Make sure the client is open before it can receive any message:
   switch (CHPP_APP_GET_MESSAGE_TYPE(type)) {
-    case CHPP_MESSAGE_TYPE_CLIENT_REQUEST: {
-      return chppServiceOfHandle(context, handle)->requestDispatchFunctionPtr;
-    }
-    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE: {
-      struct ChppClientState *clientState =
-          (struct ChppClientState *)chppClientServiceContextOfHandle(
-              context, handle, type);
+    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
+    case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
+    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
+      struct ChppEndpointState *clientState =
+          chppClientStateOfHandle(context, handle);
       if (clientState->openState == CHPP_OPEN_STATE_CLOSED) {
         CHPP_LOGE("RX service response but client closed");
-        break;
+        return NULL;
       }
-      return chppClientOfHandle(context, handle)->responseDispatchFunctionPtr;
+      break;
     }
-    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION: {
+    default:
+      // no check needed on the service side
+      break;
+  }
+
+  switch (CHPP_APP_GET_MESSAGE_TYPE(type)) {
+    case CHPP_MESSAGE_TYPE_CLIENT_REQUEST:
+      return chppServiceOfHandle(context, handle)->requestDispatchFunctionPtr;
+    case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
+      return chppClientOfHandle(context, handle)->responseDispatchFunctionPtr;
+    case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
+      return chppClientOfHandle(context, handle)->requestDispatchFunctionPtr;
+    case CHPP_MESSAGE_TYPE_CLIENT_RESPONSE:
+      return chppServiceOfHandle(context, handle)->responseDispatchFunctionPtr;
+    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION:
       return chppServiceOfHandle(context, handle)
           ->notificationDispatchFunctionPtr;
-    }
-    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
-      struct ChppClientState *clientState =
-          (struct ChppClientState *)chppClientServiceContextOfHandle(
-              context, handle, type);
-      if (clientState->openState == CHPP_OPEN_STATE_CLOSED) {
-        CHPP_LOGE("RX service notification but client closed");
-        break;
-      }
+    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION:
       return chppClientOfHandle(context, handle)
           ->notificationDispatchFunctionPtr;
-    }
   }
 
   return NULL;
 }
 
+#ifdef CHPP_CLIENT_ENABLED_DISCOVERY
 /**
  * Returns the reset notification function pointer of a particular negotiated
- * client. The function pointer will be set to null by clients that do not need
- * or support a reset notification.
+ * client.
  *
- * @param context Maintains status for each app layer instance.
+ * Returns null for clients that do not need or support a reset notification.
+ *
+ * @param context State of the app layer.
  * @param index Index of the registered client.
  *
  * @return Pointer to the reset notification function.
  */
-ChppNotifierFunction *chppGetClientResetNotifierFunction(
+static ChppNotifierFunction *chppGetClientResetNotifierFunction(
     struct ChppAppState *context, uint8_t index) {
+  CHPP_DEBUG_NOT_NULL(context);
   return context->registeredClients[index]->resetNotifierFunctionPtr;
 }
+#endif  // CHPP_CLIENT_ENABLED_DISCOVERY
 
 /**
- * Returns the reset function pointer of a particular registered service. The
- * function pointer will be set to null by services that do not need or support
- * a reset notification.
+ * Returns the reset function pointer of a particular registered service.
  *
- * @param context Maintains status for each app layer instance.
+ * Returns null for services that do not need or support a reset notification.
+ *
+ * @param context State of the app layer.
  * @param index Index of the registered service.
  *
  * @return Pointer to the reset function.
  */
 ChppNotifierFunction *chppGetServiceResetNotifierFunction(
     struct ChppAppState *context, uint8_t index) {
+  CHPP_DEBUG_NOT_NULL(context);
   return context->registeredServices[index]->resetNotifierFunctionPtr;
 }
 
 /**
  * Returns a pointer to the ChppService struct of the service matched to a
- * negotiated handle. Returns null if a service doesn't exist for the handle.
+ * negotiated handle.
  *
- * @param context Maintains status for each app layer instance.
+ * Returns null if a service doesn't exist for the handle.
+ *
+ * @param context State of the app layer.
  * @param handle Handle number.
  *
  * @return Pointer to the ChppService struct of a particular service handle.
  */
 static inline const struct ChppService *chppServiceOfHandle(
     struct ChppAppState *context, uint8_t handle) {
+  CHPP_DEBUG_NOT_NULL(context);
   uint8_t serviceIndex = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
   if (serviceIndex < context->registeredServiceCount) {
     return context->registeredServices[serviceIndex];
@@ -404,15 +382,18 @@
 
 /**
  * Returns a pointer to the ChppClient struct of the client matched to a
- * negotiated handle. Returns null if a client doesn't exist for the handle.
+ * negotiated handle.
  *
- * @param context Maintains status for each app layer instance.
+ * Returns null if a client doesn't exist for the handle.
+ *
+ * @param context State of the app layer.
  * @param handle Handle number.
  *
  * @return Pointer to the ChppClient struct matched to a particular handle.
  */
 static inline const struct ChppClient *chppClientOfHandle(
     struct ChppAppState *context, uint8_t handle) {
+  CHPP_DEBUG_NOT_NULL(context);
   uint8_t serviceIndex = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
   if (serviceIndex < context->discoveredServiceCount) {
     uint8_t clientIndex = context->clientIndexOfServiceIndex[serviceIndex];
@@ -425,71 +406,71 @@
 }
 
 /**
- * Returns a pointer to the service struct of a particular negotiated service
- * handle.
- * It is up to the caller to ensure the handle number is valid.
+ * Returns the service state for a given handle.
  *
- * @param context Maintains status for each app layer instance.
+ * The caller must pass a valid handle.
+ *
+ * @param context State of the app layer.
  * @param handle Handle number for the service.
  *
- * @return Pointer to the context struct of the service.
+ * @return Pointer to a ChppEndpointState.
  */
-static inline void *chppServiceContextOfHandle(struct ChppAppState *context,
-                                               uint8_t handle) {
+static inline struct ChppEndpointState *chppServiceStateOfHandle(
+    struct ChppAppState *context, uint8_t handle) {
+  CHPP_DEBUG_NOT_NULL(context);
   CHPP_DEBUG_ASSERT(CHPP_SERVICE_INDEX_OF_HANDLE(handle) <
                     context->registeredServiceCount);
-  return context
-      ->registeredServiceContexts[CHPP_SERVICE_INDEX_OF_HANDLE(handle)];
+
+  const uint8_t serviceIdx = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
+  return context->registeredServiceStates[serviceIdx];
 }
 
 /**
- * Returns a pointer to the client struct of a particular negotiated client
- * handle.
- * It is up to the caller to ensure the handle number is valid.
+ * Returns a pointer to the client state for a given handle.
  *
- * @param context Maintains status for each app layer instance.
+ * The caller must pass a valid handle.
+ *
+ * @param context State of the app layer.
  * @param handle Handle number for the service.
  *
- * @return Pointer to the ChppService struct of the client.
+ * @return Pointer to the endpoint state.
  */
-static inline void *chppClientContextOfHandle(struct ChppAppState *context,
-                                              uint8_t handle) {
+static inline struct ChppEndpointState *chppClientStateOfHandle(
+    struct ChppAppState *context, uint8_t handle) {
+  CHPP_DEBUG_NOT_NULL(context);
   CHPP_DEBUG_ASSERT(CHPP_SERVICE_INDEX_OF_HANDLE(handle) <
                     context->registeredClientCount);
-  return context
-      ->registeredClientContexts[context->clientIndexOfServiceIndex
-                                     [CHPP_SERVICE_INDEX_OF_HANDLE(handle)]];
+  const uint8_t serviceIdx = CHPP_SERVICE_INDEX_OF_HANDLE(handle);
+  const uint8_t clientIdx = context->clientIndexOfServiceIndex[serviceIdx];
+  return context->registeredClientStates[clientIdx]->context;
 }
 
 /**
- * Returns a pointer to the client/service struct of a particular negotiated
- * client/service handle.
- * It is up to the caller to ensure the handle number is valid.
+ * Returns a pointer to the client or service state for a given handle.
  *
- * @param appContext Maintains status for each app layer instance.
+ * The caller must pass a valid handle.
+ *
+ * @param appContext State of the app layer.
  * @param handle Handle number for the service.
  * @param type Message type (indicates if this is for a client or service).
  *
- * @return Pointer to the client/service struct of the service handle.
+ * @return Pointer to the endpoint state (NULL if wrong type).
  */
-static void *chppClientServiceContextOfHandle(struct ChppAppState *appContext,
-                                              uint8_t handle,
-                                              enum ChppMessageType type) {
+static struct ChppEndpointState *chppClientOrServiceStateOfHandle(
+    struct ChppAppState *appContext, uint8_t handle,
+    enum ChppMessageType type) {
   switch (CHPP_APP_GET_MESSAGE_TYPE(type)) {
     case CHPP_MESSAGE_TYPE_CLIENT_REQUEST:
-    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION: {
-      return chppServiceContextOfHandle(appContext, handle);
-      break;
-    }
+    case CHPP_MESSAGE_TYPE_CLIENT_RESPONSE:
+    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION:
+      return chppServiceStateOfHandle(appContext, handle);
+    case CHPP_MESSAGE_TYPE_SERVICE_REQUEST:
     case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE:
-    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
-      return chppClientContextOfHandle(appContext, handle);
-      break;
-    }
-    default: {
+    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION:
+      return chppClientStateOfHandle(appContext, handle);
+    default:
       CHPP_LOGE("Unknown type=0x%" PRIx8 " (H#%" PRIu8 ")", type, handle);
       return NULL;
-    }
   }
 }
 
@@ -497,38 +478,38 @@
  * Processes a received datagram that is determined to be for a predefined CHPP
  * service. Responds with an error if unsuccessful.
  *
- * @param context Maintains status for each app layer instance.
+ * Predefined requests are only sent by the client side.
+ * Predefined responses are only sent by the service side.
+ *
+ * @param context State of the app layer.
  * @param buf Input data. Cannot be null.
  * @param len Length of input data in bytes.
  */
 static void chppProcessPredefinedHandleDatagram(struct ChppAppState *context,
                                                 uint8_t *buf, size_t len) {
+  CHPP_DEBUG_NOT_NULL(context);
+  CHPP_DEBUG_NOT_NULL(buf);
+
   struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
-  bool success = true;
+  bool success = false;
 
   switch (CHPP_APP_GET_MESSAGE_TYPE(rxHeader->type)) {
     case CHPP_MESSAGE_TYPE_CLIENT_REQUEST: {
       success = chppProcessPredefinedClientRequest(context, buf, len);
       break;
     }
-    case CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION: {
-      success = chppProcessPredefinedClientNotification(context, buf, len);
-      break;
-    }
     case CHPP_MESSAGE_TYPE_SERVICE_RESPONSE: {
       success = chppProcessPredefinedServiceResponse(context, buf, len);
       break;
     }
-    case CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION: {
-      success = chppProcessPredefinedServiceNotification(context, buf, len);
+    default:
+      // Predefined client/services do not use
+      // - notifications,
+      // - service requests / client responses
       break;
-    }
-    default: {
-      success = false;
-    }
   }
 
-  if (success == false) {
+  if (!success) {
     CHPP_LOGE("H#%" PRIu8 " undefined msg type=0x%" PRIx8 " (len=%" PRIuSIZE
               ", ID=%" PRIu8 ")",
               rxHeader->handle, rxHeader->type, len, rxHeader->transaction);
@@ -539,42 +520,54 @@
 
 /**
  * Processes a received datagram that is determined to be for a negotiated CHPP
- * client or service. Responds with an error if unsuccessful.
+ * client or service.
  *
- * @param context Maintains status for each app layer instance.
+ * The datagram is processed by the dispatch function matching the datagram
+ * type. @see ChppService and ChppClient.
+ *
+ * If a request dispatch function returns an error (anything different from
+ * CHPP_APP_ERROR_NONE) then an error response is automatically sent back to the
+ * remote endpoint.
+ *
+ * @param appContext State of the app layer.
  * @param buf Input data. Cannot be null.
  * @param len Length of input data in bytes.
  */
-static void chppProcessNegotiatedHandleDatagram(struct ChppAppState *context,
+static void chppProcessNegotiatedHandleDatagram(struct ChppAppState *appContext,
                                                 uint8_t *buf, size_t len) {
-  struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
+  CHPP_DEBUG_NOT_NULL(appContext);
+  CHPP_DEBUG_NOT_NULL(buf);
+
+  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
   enum ChppMessageType messageType = CHPP_APP_GET_MESSAGE_TYPE(rxHeader->type);
 
-  void *clientServiceContext =
-      chppClientServiceContextOfHandle(context, rxHeader->handle, messageType);
-  if (clientServiceContext == NULL) {
+  // Could be either the client or the service state depending on the message
+  // type.
+  struct ChppEndpointState *endpointState = chppClientOrServiceStateOfHandle(
+      appContext, rxHeader->handle, messageType);
+  if (endpointState == NULL) {
     CHPP_LOGE("H#%" PRIu8 " missing ctx (msg=0x%" PRIx8 " len=%" PRIuSIZE
               ", ID=%" PRIu8 ")",
               rxHeader->handle, rxHeader->type, len, rxHeader->transaction);
-    chppEnqueueTxErrorDatagram(context->transportContext,
+    chppEnqueueTxErrorDatagram(appContext->transportContext,
                                CHPP_TRANSPORT_ERROR_APPLAYER);
     CHPP_DEBUG_ASSERT(false);
     return;
   }
 
   ChppDispatchFunction *dispatchFunc =
-      chppGetDispatchFunction(context, rxHeader->handle, messageType);
+      chppGetDispatchFunction(appContext, rxHeader->handle, messageType);
   if (dispatchFunc == NULL) {
     CHPP_LOGE("H#%" PRIu8 " unsupported msg=0x%" PRIx8 " (len=%" PRIuSIZE
               ", ID=%" PRIu8 ")",
               rxHeader->handle, rxHeader->type, len, rxHeader->transaction);
-    chppEnqueueTxErrorDatagram(context->transportContext,
+    chppEnqueueTxErrorDatagram(appContext->transportContext,
                                CHPP_TRANSPORT_ERROR_APPLAYER);
     return;
   }
 
   // All good. Dispatch datagram and possibly notify a waiting client
-  enum ChppAppErrorCode error = dispatchFunc(clientServiceContext, buf, len);
+  enum ChppAppErrorCode error = dispatchFunc(endpointState->context, buf, len);
 
   if (error != CHPP_APP_ERROR_NONE) {
     CHPP_LOGE("RX dispatch err=0x%" PRIx16 " H#%" PRIu8 " type=0x%" PRIx8
@@ -582,34 +575,30 @@
               error, rxHeader->handle, rxHeader->type, rxHeader->transaction,
               rxHeader->command, len);
 
-    // Only client requests require a dispatch failure response.
-    if (messageType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST) {
+    // Requests require a dispatch failure response.
+    if (messageType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
+        messageType == CHPP_MESSAGE_TYPE_SERVICE_REQUEST) {
       struct ChppAppHeader *response =
-          chppAllocServiceResponseFixed(rxHeader, struct ChppAppHeader);
-      if (response == NULL) {
-        CHPP_LOG_OOM();
-      } else {
+          chppAllocResponseFixed(rxHeader, struct ChppAppHeader);
+      if (response != NULL) {
         response->error = (uint8_t)error;
-        chppEnqueueTxDatagramOrFail(context->transportContext, response,
+        chppEnqueueTxDatagramOrFail(appContext->transportContext, response,
                                     sizeof(*response));
       }
     }
     return;
   }
 
-  if (messageType == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE) {
-    // Datagram is a service response. Check for synchronous operation and
-    // notify waiting client if needed.
-
-    struct ChppClientState *clientState =
-        (struct ChppClientState *)clientServiceContext;
-    chppMutexLock(&clientState->responseMutex);
-    clientState->responseReady = true;
-    CHPP_LOGD(
-        "Finished dispatching a service response. Notifying a potential "
-        "synchronous client");
-    chppConditionVariableSignal(&clientState->responseCondVar);
-    chppMutexUnlock(&clientState->responseMutex);
+  // Datagram is a response.
+  // Check for synchronous operation and notify waiting endpoint if needed.
+  if (messageType == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE ||
+      messageType == CHPP_MESSAGE_TYPE_CLIENT_RESPONSE) {
+    struct ChppSyncResponse *syncResponse = &endpointState->syncResponse;
+    chppMutexLock(&syncResponse->mutex);
+    syncResponse->ready = true;
+    CHPP_LOGD("Finished dispatching a response -> synchronous notification");
+    chppConditionVariableSignal(&syncResponse->condVar);
+    chppMutexUnlock(&syncResponse->mutex);
   }
 }
 
@@ -631,6 +620,7 @@
     struct ChppTransportState *transportContext,
     struct ChppClientServiceSet clientServiceSet) {
   CHPP_NOT_NULL(appContext);
+  CHPP_DEBUG_NOT_NULL(transportContext);
 
   CHPP_LOGD("App Init");
 
@@ -638,7 +628,8 @@
 
   appContext->clientServiceSet = clientServiceSet;
   appContext->transportContext = transportContext;
-  appContext->nextRequestTimeoutNs = CHPP_TIME_MAX;
+  appContext->nextClientRequestTimeoutNs = CHPP_TIME_MAX;
+  appContext->nextServiceRequestTimeoutNs = CHPP_TIME_MAX;
 
   chppPalSystemApiInit(appContext);
 
@@ -670,7 +661,10 @@
 
 void chppAppProcessRxDatagram(struct ChppAppState *context, uint8_t *buf,
                               size_t len) {
-  struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
+  CHPP_DEBUG_NOT_NULL(context);
+  CHPP_DEBUG_NOT_NULL(buf);
+
+  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
 
   if (len == 0) {
     CHPP_DEBUG_ASSERT_LOG(false, "App rx w/ len 0");
@@ -711,6 +705,8 @@
 }
 
 void chppAppProcessReset(struct ChppAppState *context) {
+  CHPP_DEBUG_NOT_NULL(context);
+
 #ifdef CHPP_CLIENT_ENABLED_DISCOVERY
   if (!context->isDiscoveryComplete) {
     chppInitiateDiscovery(context);
@@ -729,7 +725,8 @@
                   (ResetNotifierFunction != NULL));
 
         if (ResetNotifierFunction != NULL) {
-          ResetNotifierFunction(context->registeredClientContexts[clientIndex]);
+          ResetNotifierFunction(
+              context->registeredClientStates[clientIndex]->context);
         }
       }
     }
@@ -745,7 +742,7 @@
               CHPP_SERVICE_HANDLE_OF_INDEX(i), (ResetNotifierFunction != NULL));
 
     if (ResetNotifierFunction != NULL) {
-      ResetNotifierFunction(context->registeredServiceContexts[i]);
+      ResetNotifierFunction(context->registeredServiceStates[i]->context);
     }
   }
 
@@ -786,8 +783,11 @@
 
 uint8_t chppAppShortResponseErrorHandler(uint8_t *buf, size_t len,
                                          const char *responseName) {
+  CHPP_DEBUG_NOT_NULL(buf);
+  CHPP_DEBUG_NOT_NULL(responseName);
+
   CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
-  struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
+  const struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
 
   if (rxHeader->error == CHPP_APP_ERROR_NONE) {
     CHPP_LOGE("%s resp short len=%" PRIuSIZE, responseName, len);
@@ -797,3 +797,422 @@
   CHPP_LOGD("%s resp short len=%" PRIuSIZE, responseName, len);
   return chppAppErrorToChreError(rxHeader->error);
 }
+
+struct ChppAppHeader *chppAllocNotification(uint8_t type, size_t len) {
+  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
+  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION ||
+              type == CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION);
+
+  struct ChppAppHeader *notification = chppMalloc(len);
+  if (notification != NULL) {
+    notification->type = type;
+    notification->handle = CHPP_HANDLE_NONE;
+    notification->transaction = 0;
+    notification->error = CHPP_APP_ERROR_NONE;
+    notification->command = CHPP_APP_COMMAND_NONE;
+  } else {
+    CHPP_LOG_OOM();
+  }
+  return notification;
+}
+
+struct ChppAppHeader *chppAllocRequest(uint8_t type,
+                                       struct ChppEndpointState *endpointState,
+                                       size_t len) {
+  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
+  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
+              type == CHPP_MESSAGE_TYPE_SERVICE_REQUEST);
+  CHPP_DEBUG_NOT_NULL(endpointState);
+
+  struct ChppAppHeader *request = chppMalloc(len);
+  if (request != NULL) {
+    request->handle = endpointState->handle;
+    request->type = type;
+    request->transaction = endpointState->transaction;
+    request->error = CHPP_APP_ERROR_NONE;
+    request->command = CHPP_APP_COMMAND_NONE;
+
+    endpointState->transaction++;
+  } else {
+    CHPP_LOG_OOM();
+  }
+  return request;
+}
+
+struct ChppAppHeader *chppAllocResponse(
+    const struct ChppAppHeader *requestHeader, size_t len) {
+  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
+  CHPP_DEBUG_NOT_NULL(requestHeader);
+  uint8_t type = requestHeader->type;
+  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
+              type == CHPP_MESSAGE_TYPE_SERVICE_REQUEST);
+
+  struct ChppAppHeader *response = chppMalloc(len);
+  if (response != NULL) {
+    *response = *requestHeader;
+    response->type = type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+                         ? CHPP_MESSAGE_TYPE_SERVICE_RESPONSE
+                         : CHPP_MESSAGE_TYPE_CLIENT_RESPONSE;
+    response->error = CHPP_APP_ERROR_NONE;
+  } else {
+    CHPP_LOG_OOM();
+  }
+  return response;
+}
+
+void chppTimestampIncomingRequest(struct ChppIncomingRequestState *inReqState,
+                                  const struct ChppAppHeader *requestHeader) {
+  CHPP_DEBUG_NOT_NULL(inReqState);
+  CHPP_DEBUG_NOT_NULL(requestHeader);
+  if (inReqState->responseTimeNs == CHPP_TIME_NONE &&
+      inReqState->requestTimeNs != CHPP_TIME_NONE) {
+    CHPP_LOGE("RX dupe req t=%" PRIu64,
+              inReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
+  }
+  inReqState->requestTimeNs = chppGetCurrentTimeNs();
+  inReqState->responseTimeNs = CHPP_TIME_NONE;
+  inReqState->transaction = requestHeader->transaction;
+}
+
+void chppTimestampOutgoingRequest(struct ChppAppState *appState,
+                                  struct ChppOutgoingRequestState *outReqState,
+                                  const struct ChppAppHeader *requestHeader,
+                                  uint64_t timeoutNs) {
+  CHPP_DEBUG_NOT_NULL(appState);
+  CHPP_DEBUG_NOT_NULL(outReqState);
+  CHPP_DEBUG_NOT_NULL(requestHeader);
+  enum ChppMessageType msgType = requestHeader->type;
+  enum ChppEndpointType endpointType =
+      msgType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ? CHPP_ENDPOINT_CLIENT
+                                                  : CHPP_ENDPOINT_SERVICE;
+
+  CHPP_ASSERT(msgType == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ||
+              msgType == CHPP_MESSAGE_TYPE_SERVICE_REQUEST);
+
+  if (outReqState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT) {
+    CHPP_LOGE("Dupe req ID=%" PRIu8 " existing ID=%" PRIu8 " from t=%" PRIu64,
+              requestHeader->transaction, outReqState->transaction,
+              outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
+
+    // Clear a possible pending timeout from the previous request
+    outReqState->responseTimeNs = CHPP_TIME_MAX;
+    chppRecalculateNextTimeout(appState, endpointType);
+  }
+
+  outReqState->requestTimeNs = chppGetCurrentTimeNs();
+  outReqState->requestState = CHPP_REQUEST_STATE_REQUEST_SENT;
+  outReqState->transaction = requestHeader->transaction;
+
+  uint64_t *nextRequestTimeoutNs =
+      getNextRequestTimeoutNs(appState, endpointType);
+
+  if (timeoutNs == CHPP_REQUEST_TIMEOUT_INFINITE) {
+    outReqState->responseTimeNs = CHPP_TIME_MAX;
+
+  } else {
+    outReqState->responseTimeNs = timeoutNs + outReqState->requestTimeNs;
+
+    *nextRequestTimeoutNs =
+        MIN(*nextRequestTimeoutNs, outReqState->responseTimeNs);
+  }
+
+  CHPP_LOGD("Timestamp req ID=%" PRIu8 " at t=%" PRIu64 " timeout=%" PRIu64
+            " (requested=%" PRIu64 "), next timeout=%" PRIu64,
+            outReqState->transaction,
+            outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC,
+            outReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
+            timeoutNs / CHPP_NSEC_PER_MSEC,
+            *nextRequestTimeoutNs / CHPP_NSEC_PER_MSEC);
+}
+
+bool chppTimestampIncomingResponse(struct ChppAppState *appState,
+                                   struct ChppOutgoingRequestState *outReqState,
+                                   const struct ChppAppHeader *responseHeader) {
+  CHPP_DEBUG_NOT_NULL(appState);
+  CHPP_DEBUG_NOT_NULL(outReqState);
+  CHPP_DEBUG_NOT_NULL(responseHeader);
+
+  uint8_t type = responseHeader->type;
+
+  CHPP_ASSERT(type == CHPP_MESSAGE_TYPE_CLIENT_RESPONSE ||
+              type == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE);
+
+  bool success = false;
+  uint64_t responseTime = chppGetCurrentTimeNs();
+
+  switch (outReqState->requestState) {
+    case CHPP_REQUEST_STATE_NONE: {
+      CHPP_LOGE("Resp with no req t=%" PRIu64,
+                responseTime / CHPP_NSEC_PER_MSEC);
+      break;
+    }
+
+    case CHPP_REQUEST_STATE_RESPONSE_RCV: {
+      CHPP_LOGE("Extra resp at t=%" PRIu64 " for req t=%" PRIu64,
+                responseTime / CHPP_NSEC_PER_MSEC,
+                outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
+      break;
+    }
+
+    case CHPP_REQUEST_STATE_RESPONSE_TIMEOUT: {
+      CHPP_LOGE("Late resp at t=%" PRIu64 " for req t=%" PRIu64,
+                responseTime / CHPP_NSEC_PER_MSEC,
+                outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
+      break;
+    }
+
+    case CHPP_REQUEST_STATE_REQUEST_SENT: {
+      if (responseHeader->transaction != outReqState->transaction) {
+        CHPP_LOGE("Invalid resp ID=%" PRIu8 " at t=%" PRIu64
+                  " expected=%" PRIu8,
+                  responseHeader->transaction,
+                  responseTime / CHPP_NSEC_PER_MSEC, outReqState->transaction);
+      } else {
+        outReqState->requestState = (responseTime > outReqState->responseTimeNs)
+                                        ? CHPP_REQUEST_STATE_RESPONSE_TIMEOUT
+                                        : CHPP_REQUEST_STATE_RESPONSE_RCV;
+        success = true;
+
+        CHPP_LOGD(
+            "Timestamp resp ID=%" PRIu8 " req t=%" PRIu64 " resp t=%" PRIu64
+            " timeout t=%" PRIu64 " (RTT=%" PRIu64 ", timeout = %s)",
+            outReqState->transaction,
+            outReqState->requestTimeNs / CHPP_NSEC_PER_MSEC,
+            responseTime / CHPP_NSEC_PER_MSEC,
+            outReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
+            (responseTime - outReqState->requestTimeNs) / CHPP_NSEC_PER_MSEC,
+            (responseTime > outReqState->responseTimeNs) ? "yes" : "no");
+      }
+      break;
+    }
+
+    default: {
+      CHPP_DEBUG_ASSERT_LOG(false, "Invalid req state");
+    }
+  }
+
+  if (success) {
+    // When the received request is the next one that was expected
+    // to timeout we need to recompute the timeout considering the
+    // other pending requests.
+    enum ChppEndpointType endpointType =
+        type == CHPP_MESSAGE_TYPE_SERVICE_RESPONSE ? CHPP_ENDPOINT_CLIENT
+                                                   : CHPP_ENDPOINT_SERVICE;
+    if (outReqState->responseTimeNs ==
+        *getNextRequestTimeoutNs(appState, endpointType)) {
+      chppRecalculateNextTimeout(appState, endpointType);
+    }
+    outReqState->responseTimeNs = responseTime;
+  }
+  return success;
+}
+
+uint64_t chppTimestampOutgoingResponse(
+    struct ChppIncomingRequestState *inReqState) {
+  CHPP_DEBUG_NOT_NULL(inReqState);
+
+  uint64_t previousResponseTime = inReqState->responseTimeNs;
+  inReqState->responseTimeNs = chppGetCurrentTimeNs();
+  return previousResponseTime;
+}
+
+bool chppSendTimestampedResponseOrFail(
+    struct ChppAppState *appState, struct ChppIncomingRequestState *inReqState,
+    void *buf, size_t len) {
+  CHPP_DEBUG_NOT_NULL(appState);
+  CHPP_DEBUG_NOT_NULL(inReqState);
+  CHPP_DEBUG_NOT_NULL(buf);
+  uint64_t previousResponseTime = chppTimestampOutgoingResponse(inReqState);
+
+  if (inReqState->requestTimeNs == CHPP_TIME_NONE) {
+    CHPP_LOGE("TX response w/ no req t=%" PRIu64,
+              inReqState->responseTimeNs / CHPP_NSEC_PER_MSEC);
+
+  } else if (previousResponseTime != CHPP_TIME_NONE) {
+    CHPP_LOGW("TX additional response t=%" PRIu64 " for req t=%" PRIu64,
+              inReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
+              inReqState->requestTimeNs / CHPP_NSEC_PER_MSEC);
+
+  } else {
+    CHPP_LOGD("Sending initial response at t=%" PRIu64
+              " for request at t=%" PRIu64 " (RTT=%" PRIu64 ")",
+              inReqState->responseTimeNs / CHPP_NSEC_PER_MSEC,
+              inReqState->requestTimeNs / CHPP_NSEC_PER_MSEC,
+              (inReqState->responseTimeNs - inReqState->requestTimeNs) /
+                  CHPP_NSEC_PER_MSEC);
+  }
+
+  return chppEnqueueTxDatagramOrFail(appState->transportContext, buf, len);
+}
+
+bool chppSendTimestampedRequestOrFail(
+    struct ChppEndpointState *endpointState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs) {
+  CHPP_DEBUG_NOT_NULL(outReqState);
+  CHPP_DEBUG_NOT_NULL(buf);
+  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
+
+  if (timeoutNs < CHPP_TRANSPORT_TX_TIMEOUT_NS) {
+    // The app layer sits above the transport layer.
+    // Request timeout (app layer) should be longer than the transport timeout.
+    CHPP_LOGW("Request timeout (%" PRIu64
+              "ns) should be longer than the transport timeout (%" PRIu64 "ns)",
+              timeoutNs, (uint64_t)CHPP_TRANSPORT_TX_TIMEOUT_NS);
+  }
+
+  chppTimestampOutgoingRequest(endpointState->appContext, outReqState, buf,
+                               timeoutNs);
+  endpointState->syncResponse.ready = false;
+
+  bool success = chppEnqueueTxDatagramOrFail(
+      endpointState->appContext->transportContext, buf, len);
+
+  // Failure to enqueue a TX datagram means that a request was known to be not
+  // transmitted. We explicitly set requestState to be in the NONE state, so
+  // that unintended app layer timeouts do not occur.
+  if (!success) {
+    outReqState->requestState = CHPP_REQUEST_STATE_NONE;
+  }
+
+  return success;
+}
+
+bool chppWaitForResponseWithTimeout(
+    struct ChppSyncResponse *syncResponse,
+    struct ChppOutgoingRequestState *outReqState, uint64_t timeoutNs) {
+  CHPP_DEBUG_NOT_NULL(syncResponse);
+  CHPP_DEBUG_NOT_NULL(outReqState);
+
+  bool result = true;
+
+  chppMutexLock(&syncResponse->mutex);
+
+  while (result && !syncResponse->ready) {
+    result = chppConditionVariableTimedWait(&syncResponse->condVar,
+                                            &syncResponse->mutex, timeoutNs);
+  }
+  if (!syncResponse->ready) {
+    outReqState->requestState = CHPP_REQUEST_STATE_RESPONSE_TIMEOUT;
+    CHPP_LOGE("Response timeout after %" PRIu64 " ms",
+              timeoutNs / CHPP_NSEC_PER_MSEC);
+    result = false;
+  }
+
+  chppMutexUnlock(&syncResponse->mutex);
+
+  return result;
+}
+
+struct ChppEndpointState *getRegisteredEndpointState(
+    struct ChppAppState *appState, uint8_t index, enum ChppEndpointType type) {
+  CHPP_DEBUG_NOT_NULL(appState);
+  CHPP_DEBUG_ASSERT(index < getRegisteredEndpointCount(appState, type));
+
+  return type == CHPP_ENDPOINT_CLIENT
+             ? appState->registeredClientStates[index]
+             : appState->registeredServiceStates[index];
+}
+
+uint16_t getRegisteredEndpointOutReqCount(struct ChppAppState *appState,
+                                          uint8_t index,
+                                          enum ChppEndpointType type) {
+  CHPP_DEBUG_NOT_NULL(appState);
+  CHPP_DEBUG_ASSERT(index < getRegisteredEndpointCount(appState, type));
+
+  return type == CHPP_ENDPOINT_CLIENT
+             ? appState->registeredClients[index]->outReqCount
+             : appState->registeredServices[index]->outReqCount;
+}
+
+uint8_t getRegisteredEndpointCount(struct ChppAppState *appState,
+                                   enum ChppEndpointType type) {
+  return type == CHPP_ENDPOINT_CLIENT ? appState->registeredClientCount
+                                      : appState->registeredServiceCount;
+}
+
+void chppRecalculateNextTimeout(struct ChppAppState *appState,
+                                enum ChppEndpointType type) {
+  CHPP_DEBUG_NOT_NULL(appState);
+
+  uint64_t timeoutNs = CHPP_TIME_MAX;
+
+  const uint8_t endpointCount = getRegisteredEndpointCount(appState, type);
+
+  for (uint8_t endpointIdx = 0; endpointIdx < endpointCount; endpointIdx++) {
+    uint16_t reqCount =
+        getRegisteredEndpointOutReqCount(appState, endpointIdx, type);
+    struct ChppEndpointState *endpointState =
+        getRegisteredEndpointState(appState, endpointIdx, type);
+    struct ChppOutgoingRequestState *reqStates = endpointState->outReqStates;
+    for (uint16_t cmdIdx = 0; cmdIdx < reqCount; cmdIdx++) {
+      struct ChppOutgoingRequestState *reqState = &reqStates[cmdIdx];
+
+      if (reqState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT) {
+        timeoutNs = MIN(timeoutNs, reqState->responseTimeNs);
+      }
+    }
+  }
+
+  CHPP_LOGD("nextReqTimeout=%" PRIu64, timeoutNs / CHPP_NSEC_PER_MSEC);
+
+  if (type == CHPP_ENDPOINT_CLIENT) {
+    appState->nextClientRequestTimeoutNs = timeoutNs;
+  } else {
+    appState->nextServiceRequestTimeoutNs = timeoutNs;
+  }
+}
+
+uint64_t *getNextRequestTimeoutNs(struct ChppAppState *appState,
+                                  enum ChppEndpointType type) {
+  return type == CHPP_ENDPOINT_CLIENT ? &appState->nextClientRequestTimeoutNs
+                                      : &appState->nextServiceRequestTimeoutNs;
+}
+
+void chppCloseOpenRequests(struct ChppEndpointState *endpointState,
+                           enum ChppEndpointType type, bool clearOnly) {
+  CHPP_DEBUG_NOT_NULL(endpointState);
+
+  bool recalcNeeded = false;
+
+  struct ChppAppState *appState = endpointState->appContext;
+  const uint8_t enpointIdx = endpointState->index;
+  const uint16_t cmdCount =
+      getRegisteredEndpointOutReqCount(appState, enpointIdx, type);
+
+  for (uint16_t cmdIdx = 0; cmdIdx < cmdCount; cmdIdx++) {
+    if (endpointState->outReqStates[cmdIdx].requestState ==
+        CHPP_REQUEST_STATE_REQUEST_SENT) {
+      recalcNeeded = true;
+
+      CHPP_LOGE("Closing open req #%" PRIu16 " clear %d", cmdIdx, clearOnly);
+
+      if (clearOnly) {
+        endpointState->outReqStates[cmdIdx].requestState =
+            CHPP_REQUEST_STATE_RESPONSE_TIMEOUT;
+      } else {
+        struct ChppAppHeader *response =
+            chppMalloc(sizeof(struct ChppAppHeader));
+        if (response == NULL) {
+          CHPP_LOG_OOM();
+        } else {
+          // Simulate receiving a timeout response.
+          response->handle = endpointState->handle;
+          response->type = type == CHPP_ENDPOINT_CLIENT
+                               ? CHPP_MESSAGE_TYPE_SERVICE_RESPONSE
+                               : CHPP_MESSAGE_TYPE_CLIENT_RESPONSE;
+          response->transaction =
+              endpointState->outReqStates[cmdIdx].transaction;
+          response->error = CHPP_APP_ERROR_TIMEOUT;
+          response->command = cmdIdx;
+
+          chppAppProcessRxDatagram(appState, (uint8_t *)response,
+                                   sizeof(struct ChppAppHeader));
+        }
+      }
+    }
+  }
+  if (recalcNeeded) {
+    chppRecalculateNextTimeout(appState, type);
+  }
+}
\ No newline at end of file
diff --git a/chpp/clients.c b/chpp/clients.c
index 198e32b..4c3df54 100644
--- a/chpp/clients.c
+++ b/chpp/clients.c
@@ -50,8 +50,8 @@
  *  Prototypes
  ***********************************************/
 
-static bool chppIsClientApiReady(struct ChppClientState *clientState);
-ChppClientDeinitFunction *chppGetClientDeinitFunction(
+static bool chppIsClientApiReady(struct ChppEndpointState *clientState);
+static ChppClientDeinitFunction *chppGetClientDeinitFunction(
     struct ChppAppState *context, uint8_t index);
 
 /************************************************
@@ -65,9 +65,11 @@
  *
  * @param clientState State of the client sending the client request.
  *
- * @return Indicates whetherthe client is ready.
+ * @return Indicates whether the client is ready.
  */
-static bool chppIsClientApiReady(struct ChppClientState *clientState) {
+static bool chppIsClientApiReady(struct ChppEndpointState *clientState) {
+  CHPP_DEBUG_NOT_NULL(clientState);
+
   bool result = false;
 
   if (clientState->initialized) {
@@ -109,8 +111,10 @@
  *
  * @return Pointer to the match notification function.
  */
-ChppClientDeinitFunction *chppGetClientDeinitFunction(
+static ChppClientDeinitFunction *chppGetClientDeinitFunction(
     struct ChppAppState *context, uint8_t index) {
+  CHPP_DEBUG_NOT_NULL(context);
+
   return context->registeredClients[index]->deinitFunctionPtr;
 }
 
@@ -120,6 +124,8 @@
 
 void chppRegisterCommonClients(struct ChppAppState *context) {
   UNUSED_VAR(context);
+  CHPP_DEBUG_NOT_NULL(context);
+
   CHPP_LOGD("Registering Clients");
 
 #ifdef CHPP_CLIENT_ENABLED_WWAN
@@ -143,6 +149,8 @@
 
 void chppDeregisterCommonClients(struct ChppAppState *context) {
   UNUSED_VAR(context);
+  CHPP_DEBUG_NOT_NULL(context);
+
   CHPP_LOGD("Deregistering Clients");
 
 #ifdef CHPP_CLIENT_ENABLED_WWAN
@@ -165,10 +173,14 @@
 }
 
 void chppRegisterClient(struct ChppAppState *appContext, void *clientContext,
-                        struct ChppClientState *clientState,
-                        struct ChppRequestResponseState *rRStates,
+                        struct ChppEndpointState *clientState,
+                        struct ChppOutgoingRequestState *outReqStates,
                         const struct ChppClient *newClient) {
   CHPP_NOT_NULL(newClient);
+  CHPP_DEBUG_NOT_NULL(appContext);
+  CHPP_DEBUG_NOT_NULL(clientContext);
+  CHPP_DEBUG_NOT_NULL(clientState);
+  CHPP_DEBUG_NOT_NULL(newClient);
 
   if (appContext->registeredClientCount >= CHPP_MAX_REGISTERED_CLIENTS) {
     CHPP_LOGE("Max clients registered: %" PRIu8,
@@ -176,13 +188,12 @@
     return;
   }
   clientState->appContext = appContext;
-  clientState->rRStates = rRStates;
+  clientState->outReqStates = outReqStates;
   clientState->index = appContext->registeredClientCount;
-
-  appContext->registeredClientContexts[appContext->registeredClientCount] =
-      clientContext;
+  clientState->context = clientContext;
   appContext->registeredClientStates[appContext->registeredClientCount] =
       clientState;
+
   appContext->registeredClients[appContext->registeredClientCount] = newClient;
 
   char uuidText[CHPP_SERVICE_UUID_STRING_LEN];
@@ -199,6 +210,8 @@
 
 void chppInitBasicClients(struct ChppAppState *context) {
   UNUSED_VAR(context);
+  CHPP_DEBUG_NOT_NULL(context);
+
   CHPP_LOGD("Initializing basic clients");
 
 #ifdef CHPP_CLIENT_ENABLED_LOOPBACK
@@ -216,21 +229,23 @@
 #endif
 }
 
-void chppClientInit(struct ChppClientState *clientState, uint8_t handle) {
+void chppClientInit(struct ChppEndpointState *clientState, uint8_t handle) {
+  CHPP_DEBUG_NOT_NULL(clientState);
   CHPP_ASSERT_LOG(!clientState->initialized,
                   "Client H#%" PRIu8 " already initialized", handle);
 
   if (!clientState->everInitialized) {
     clientState->handle = handle;
-    chppMutexInit(&clientState->responseMutex);
-    chppConditionVariableInit(&clientState->responseCondVar);
+    chppMutexInit(&clientState->syncResponse.mutex);
+    chppConditionVariableInit(&clientState->syncResponse.condVar);
     clientState->everInitialized = true;
   }
 
   clientState->initialized = true;
 }
 
-void chppClientDeinit(struct ChppClientState *clientState) {
+void chppClientDeinit(struct ChppEndpointState *clientState) {
+  CHPP_DEBUG_NOT_NULL(clientState);
   CHPP_ASSERT_LOG(clientState->initialized,
                   "Client H#%" PRIu8 " already deinitialized",
                   clientState->handle);
@@ -240,6 +255,8 @@
 
 void chppDeinitBasicClients(struct ChppAppState *context) {
   UNUSED_VAR(context);
+  CHPP_DEBUG_NOT_NULL(context);
+
   CHPP_LOGD("Deinitializing basic clients");
 
 #ifdef CHPP_CLIENT_ENABLED_LOOPBACK
@@ -258,6 +275,7 @@
 }
 
 void chppDeinitMatchedClients(struct ChppAppState *context) {
+  CHPP_DEBUG_NOT_NULL(context);
   CHPP_LOGD("Deinitializing matched clients");
 
   for (uint8_t i = 0; i < context->discoveredServiceCount; i++) {
@@ -272,211 +290,79 @@
                 (clientDeinitFunction != NULL));
 
       if (clientDeinitFunction != NULL) {
-        clientDeinitFunction(context->registeredClientContexts[clientIndex]);
+        clientDeinitFunction(
+            context->registeredClientStates[clientIndex]->context);
       }
     }
   }
 }
 
 struct ChppAppHeader *chppAllocClientRequest(
-    struct ChppClientState *clientState, size_t len) {
-  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
-
-  struct ChppAppHeader *result = chppMalloc(len);
-  if (result != NULL) {
-    result->handle = clientState->handle;
-    result->type = CHPP_MESSAGE_TYPE_CLIENT_REQUEST;
-    result->transaction = clientState->transaction;
-    result->error = CHPP_APP_ERROR_NONE;
-    result->command = CHPP_APP_COMMAND_NONE;
-
-    clientState->transaction++;
-  }
-  return result;
+    struct ChppEndpointState *clientState, size_t len) {
+  CHPP_DEBUG_NOT_NULL(clientState);
+  return chppAllocRequest(CHPP_MESSAGE_TYPE_CLIENT_REQUEST, clientState, len);
 }
 
 struct ChppAppHeader *chppAllocClientRequestCommand(
-    struct ChppClientState *clientState, uint16_t command) {
-  struct ChppAppHeader *result =
+    struct ChppEndpointState *clientState, uint16_t command) {
+  struct ChppAppHeader *request =
       chppAllocClientRequest(clientState, sizeof(struct ChppAppHeader));
 
-  if (result != NULL) {
-    result->command = command;
+  if (request != NULL) {
+    request->command = command;
   }
-  return result;
+  return request;
 }
 
-void chppClientTimestampRequest(struct ChppClientState *clientState,
-                                struct ChppRequestResponseState *rRState,
-                                struct ChppAppHeader *requestHeader,
-                                uint64_t timeoutNs) {
-  if (rRState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT) {
-    CHPP_LOGE("Dupe req ID=%" PRIu8 " existing ID=%" PRIu8 " from t=%" PRIu64,
-              requestHeader->transaction, rRState->transaction,
-              rRState->requestTimeNs / CHPP_NSEC_PER_MSEC);
+bool chppClientSendTimestampedRequestOrFail(
+    struct ChppEndpointState *clientState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs) {
+  CHPP_DEBUG_NOT_NULL(clientState);
+  CHPP_DEBUG_NOT_NULL(outReqState);
+  CHPP_DEBUG_NOT_NULL(buf);
 
-    // Clear a possible pending timeout from the previous request
-    rRState->responseTimeNs = CHPP_TIME_MAX;
-    chppClientRecalculateNextTimeout(clientState->appContext);
-  }
-
-  rRState->requestTimeNs = chppGetCurrentTimeNs();
-  rRState->requestState = CHPP_REQUEST_STATE_REQUEST_SENT;
-  rRState->transaction = requestHeader->transaction;
-
-  if (timeoutNs == CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE) {
-    rRState->responseTimeNs = CHPP_TIME_MAX;
-
-  } else {
-    rRState->responseTimeNs = timeoutNs + rRState->requestTimeNs;
-
-    clientState->appContext->nextRequestTimeoutNs = MIN(
-        clientState->appContext->nextRequestTimeoutNs, rRState->responseTimeNs);
-  }
-
-  CHPP_LOGD("Timestamp req ID=%" PRIu8 " at t=%" PRIu64 " timeout=%" PRIu64
-            " (requested=%" PRIu64 "), next timeout=%" PRIu64,
-            rRState->transaction, rRState->requestTimeNs / CHPP_NSEC_PER_MSEC,
-            rRState->responseTimeNs / CHPP_NSEC_PER_MSEC,
-            timeoutNs / CHPP_NSEC_PER_MSEC,
-            clientState->appContext->nextRequestTimeoutNs / CHPP_NSEC_PER_MSEC);
-}
-
-bool chppClientTimestampResponse(struct ChppClientState *clientState,
-                                 struct ChppRequestResponseState *rRState,
-                                 const struct ChppAppHeader *responseHeader) {
-  bool success = false;
-  uint64_t responseTime = chppGetCurrentTimeNs();
-
-  switch (rRState->requestState) {
-    case CHPP_REQUEST_STATE_NONE: {
-      CHPP_LOGE("Resp with no req t=%" PRIu64,
-                responseTime / CHPP_NSEC_PER_MSEC);
-      break;
-    }
-
-    case CHPP_REQUEST_STATE_RESPONSE_RCV: {
-      CHPP_LOGE("Extra resp at t=%" PRIu64 " for req t=%" PRIu64,
-                responseTime / CHPP_NSEC_PER_MSEC,
-                rRState->requestTimeNs / CHPP_NSEC_PER_MSEC);
-      break;
-    }
-
-    case CHPP_REQUEST_STATE_RESPONSE_TIMEOUT: {
-      CHPP_LOGE("Late resp at t=%" PRIu64 " for req t=%" PRIu64,
-                responseTime / CHPP_NSEC_PER_MSEC,
-                rRState->requestTimeNs / CHPP_NSEC_PER_MSEC);
-      break;
-    }
-
-    case CHPP_REQUEST_STATE_REQUEST_SENT: {
-      if (responseHeader->transaction != rRState->transaction) {
-        CHPP_LOGE("Invalid resp ID=%" PRIu8 " at t=%" PRIu64
-                  " expected=%" PRIu8,
-                  responseHeader->transaction,
-                  responseTime / CHPP_NSEC_PER_MSEC, rRState->transaction);
-      } else {
-        rRState->requestState = (responseTime > rRState->responseTimeNs)
-                                    ? CHPP_REQUEST_STATE_RESPONSE_TIMEOUT
-                                    : CHPP_REQUEST_STATE_RESPONSE_RCV;
-        success = true;
-
-        CHPP_LOGD(
-            "Timestamp resp ID=%" PRIu8 " req t=%" PRIu64 " resp t=%" PRIu64
-            " timeout t=%" PRIu64 " (RTT=%" PRIu64 ", timeout = %s)",
-            rRState->transaction, rRState->requestTimeNs / CHPP_NSEC_PER_MSEC,
-            responseTime / CHPP_NSEC_PER_MSEC,
-            rRState->responseTimeNs / CHPP_NSEC_PER_MSEC,
-            (responseTime - rRState->requestTimeNs) / CHPP_NSEC_PER_MSEC,
-            (responseTime > rRState->responseTimeNs) ? "yes" : "no");
-      }
-      break;
-    }
-
-    default: {
-      CHPP_DEBUG_ASSERT_LOG(false, "Invalid req state");
-    }
-  }
-
-  if (success) {
-    if (rRState->responseTimeNs ==
-        clientState->appContext->nextRequestTimeoutNs) {
-      // This was the next upcoming timeout
-      chppClientRecalculateNextTimeout(clientState->appContext);
-    }
-    rRState->responseTimeNs = responseTime;
-  }
-  return success;
-}
-
-bool chppSendTimestampedRequestOrFail(struct ChppClientState *clientState,
-                                      struct ChppRequestResponseState *rRState,
-                                      void *buf, size_t len,
-                                      uint64_t timeoutNs) {
-  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
   if (!chppIsClientApiReady(clientState)) {
     CHPP_FREE_AND_NULLIFY(buf);
     return false;
   }
 
-  chppClientTimestampRequest(clientState, rRState, buf, timeoutNs);
-  clientState->responseReady = false;
-
-  bool success = chppEnqueueTxDatagramOrFail(
-      clientState->appContext->transportContext, buf, len);
-
-  // Failure to enqueue a TX datagram means that a request was known to be not
-  // transmitted. We explicitly set requestState to be in the NONE state, so
-  // that unintended app layer timeouts do not occur.
-  if (!success) {
-    rRState->requestState = CHPP_REQUEST_STATE_NONE;
-  }
-
-  return success;
+  return chppSendTimestampedRequestOrFail(clientState, outReqState, buf, len,
+                                          timeoutNs);
 }
 
-bool chppSendTimestampedRequestAndWait(struct ChppClientState *clientState,
-                                       struct ChppRequestResponseState *rRState,
-                                       void *buf, size_t len) {
-  return chppSendTimestampedRequestAndWaitTimeout(
-      clientState, rRState, buf, len, CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT);
+bool chppClientSendTimestampedRequestAndWait(
+    struct ChppEndpointState *clientState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len) {
+  return chppClientSendTimestampedRequestAndWaitTimeout(
+      clientState, outReqState, buf, len, CHPP_REQUEST_TIMEOUT_DEFAULT);
 }
 
-bool chppSendTimestampedRequestAndWaitTimeout(
-    struct ChppClientState *clientState,
-    struct ChppRequestResponseState *rRState, void *buf, size_t len,
+bool chppClientSendTimestampedRequestAndWaitTimeout(
+    struct ChppEndpointState *clientState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
     uint64_t timeoutNs) {
-  bool result = chppSendTimestampedRequestOrFail(
-      clientState, rRState, buf, len, CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE);
+  bool result = chppClientSendTimestampedRequestOrFail(
+      clientState, outReqState, buf, len, CHPP_REQUEST_TIMEOUT_INFINITE);
 
-  if (result) {
-    chppMutexLock(&clientState->responseMutex);
-
-    while (result && !clientState->responseReady) {
-      result = chppConditionVariableTimedWait(&clientState->responseCondVar,
-                                              &clientState->responseMutex,
-                                              timeoutNs);
-    }
-    if (!clientState->responseReady) {
-      rRState->requestState = CHPP_REQUEST_STATE_RESPONSE_TIMEOUT;
-      CHPP_LOGE("Response timeout after %" PRIu64 " ms",
-                timeoutNs / CHPP_NSEC_PER_MSEC);
-      result = false;
-    }
-
-    chppMutexUnlock(&clientState->responseMutex);
+  if (!result) {
+    return false;
   }
 
-  return result;
+  return chppWaitForResponseWithTimeout(&clientState->syncResponse, outReqState,
+                                        timeoutNs);
 }
 
-void chppClientPseudoOpen(struct ChppClientState *clientState) {
+void chppClientPseudoOpen(struct ChppEndpointState *clientState) {
   clientState->pseudoOpen = true;
 }
 
-bool chppClientSendOpenRequest(struct ChppClientState *clientState,
-                               struct ChppRequestResponseState *openRRState,
+bool chppClientSendOpenRequest(struct ChppEndpointState *clientState,
+                               struct ChppOutgoingRequestState *openReqState,
                                uint16_t openCommand, bool blocking) {
+  CHPP_NOT_NULL(clientState);
+  CHPP_NOT_NULL(openReqState);
+
   bool result = false;
   uint8_t priorState = clientState->openState;
 
@@ -488,7 +374,6 @@
       chppAllocClientRequestCommand(clientState, openCommand);
 
   if (request == NULL) {
-    CHPP_LOG_OOM();
     return false;
   }
 
@@ -496,13 +381,13 @@
 
   if (blocking) {
     CHPP_LOGD("Opening service - blocking");
-    result = chppSendTimestampedRequestAndWait(clientState, openRRState,
-                                               request, sizeof(*request));
+    result = chppClientSendTimestampedRequestAndWait(clientState, openReqState,
+                                                     request, sizeof(*request));
   } else {
     CHPP_LOGD("Opening service - non-blocking");
-    result = chppSendTimestampedRequestOrFail(
-        clientState, openRRState, request, sizeof(*request),
-        CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE);
+    result = chppClientSendTimestampedRequestOrFail(
+        clientState, openReqState, request, sizeof(*request),
+        CHPP_REQUEST_TIMEOUT_INFINITE);
   }
 
   if (!result) {
@@ -517,8 +402,11 @@
   return result;
 }
 
-void chppClientProcessOpenResponse(struct ChppClientState *clientState,
+void chppClientProcessOpenResponse(struct ChppEndpointState *clientState,
                                    uint8_t *buf, size_t len) {
+  CHPP_DEBUG_NOT_NULL(clientState);
+  CHPP_DEBUG_NOT_NULL(buf);
+
   UNUSED_VAR(len);  // Necessary depending on assert macro below
   // Assert condition already guaranteed by chppAppProcessRxDatagram() but
   // checking again since this is a public function
@@ -534,63 +422,13 @@
   }
 }
 
-void chppClientRecalculateNextTimeout(struct ChppAppState *context) {
-  context->nextRequestTimeoutNs = CHPP_TIME_MAX;
-
-  for (uint8_t clientIdx = 0; clientIdx < context->registeredClientCount;
-       clientIdx++) {
-    const struct ChppClient *client = context->registeredClients[clientIdx];
-    for (uint16_t cmdIdx = 0; cmdIdx < client->rRStateCount; cmdIdx++) {
-      const struct ChppClientState *state =
-          context->registeredClientStates[clientIdx];
-      struct ChppRequestResponseState *rRState = &state->rRStates[cmdIdx];
-
-      if (rRState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT) {
-        context->nextRequestTimeoutNs =
-            MIN(context->nextRequestTimeoutNs, rRState->responseTimeNs);
-      }
-    }
-  }
-
-  CHPP_LOGD("nextReqTimeout=%" PRIu64,
-            context->nextRequestTimeoutNs / CHPP_NSEC_PER_MSEC);
-}
-
-void chppClientCloseOpenRequests(struct ChppClientState *clientState,
+void chppClientCloseOpenRequests(struct ChppEndpointState *clientState,
                                  const struct ChppClient *client,
                                  bool clearOnly) {
-  bool recalcNeeded = false;
-
-  for (uint16_t cmdIdx = 0; cmdIdx < client->rRStateCount; cmdIdx++) {
-    if (clientState->rRStates[cmdIdx].requestState ==
-        CHPP_REQUEST_STATE_REQUEST_SENT) {
-      recalcNeeded = true;
-
-      CHPP_LOGE("Closing open req #%" PRIu16 " clear %d", cmdIdx, clearOnly);
-
-      if (clearOnly) {
-        clientState->rRStates[cmdIdx].requestState =
-            CHPP_REQUEST_STATE_RESPONSE_TIMEOUT;
-      } else {
-        struct ChppAppHeader *response =
-            chppMalloc(sizeof(struct ChppAppHeader));
-        if (response == NULL) {
-          CHPP_LOG_OOM();
-        } else {
-          response->handle = clientState->handle;
-          response->type = CHPP_MESSAGE_TYPE_SERVICE_RESPONSE;
-          response->transaction = clientState->rRStates[cmdIdx].transaction;
-          response->error = CHPP_APP_ERROR_TIMEOUT;
-          response->command = cmdIdx;
-
-          chppAppProcessRxDatagram(clientState->appContext, (uint8_t *)response,
-                                   sizeof(struct ChppAppHeader));
-        }
-      }
-    }
-  }
-
-  if (recalcNeeded) {
-    chppClientRecalculateNextTimeout(clientState->appContext);
-  }
+  UNUSED_VAR(client);
+  chppCloseOpenRequests(clientState, CHPP_ENDPOINT_CLIENT, clearOnly);
 }
+
+struct ChppAppHeader *chppAllocClientNotification(size_t len) {
+  return chppAllocNotification(CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION, len);
+}
\ No newline at end of file
diff --git a/chpp/clients/discovery.c b/chpp/clients/discovery.c
index e681752..af314e2 100644
--- a/chpp/clients/discovery.c
+++ b/chpp/clients/discovery.c
@@ -22,6 +22,7 @@
 #include <string.h>
 
 #include "chpp/app.h"
+#include "chpp/clients.h"
 #include "chpp/common/discovery.h"
 #include "chpp/log.h"
 #include "chpp/macros.h"
@@ -160,7 +161,7 @@
 
     // Initialize client
     if (!client->initFunctionPtr(
-            appState->registeredClientContexts[clientIndex],
+            appState->registeredClientStates[clientIndex]->context,
             CHPP_SERVICE_HANDLE_OF_INDEX(i), service->version)) {
       CHPP_LOGE("Client v=%" PRIu8 ".%" PRIu8 ".%" PRIu16
                 " rejected init. Service v=%" PRIu8 ".%" PRIu8 ".%" PRIu16,
@@ -199,7 +200,8 @@
                 (matchNotifierFunction != NULL));
 
       if (matchNotifierFunction != NULL) {
-        matchNotifierFunction(appState->registeredClientContexts[clientIndex]);
+        matchNotifierFunction(
+            appState->registeredClientStates[clientIndex]->context);
       }
     }
   }
diff --git a/chpp/clients/gnss.c b/chpp/clients/gnss.c
index ca131d7..f56ce01 100644
--- a/chpp/clients/gnss.c
+++ b/chpp/clients/gnss.c
@@ -62,10 +62,11 @@
  * (RR) functionality.
  */
 struct ChppGnssClientState {
-  struct ChppClientState client;     // GNSS client state
+  struct ChppEndpointState client;   // CHPP client state
   const struct chrePalGnssApi *api;  // GNSS PAL API
 
-  struct ChppRequestResponseState rRState[CHPP_GNSS_CLIENT_REQUEST_MAX + 1];
+  struct ChppOutgoingRequestState
+      outReqStates[CHPP_GNSS_CLIENT_REQUEST_MAX + 1];
 
   uint32_t capabilities;           // Cached GetCapabilities result
   bool requestStateResyncPending;  // requestStateResync() is waiting to be
@@ -109,8 +110,8 @@
     // Service notification dispatch function pointer
     .deinitFunctionPtr = &chppGnssClientDeinit,
 
-    // Number of request-response states in the rRStates array.
-    .rRStateCount = ARRAY_SIZE(gGnssClientContext.rRState),
+    // Number of request-response states in the outReqStates array.
+    .outReqCount = ARRAY_SIZE(gGnssClientContext.outReqStates),
 
     // Min length is the entire header
     .minLength = sizeof(struct ChppAppHeader),
@@ -181,9 +182,10 @@
   if (rxHeader->command > CHPP_GNSS_CLIENT_REQUEST_MAX) {
     error = CHPP_APP_ERROR_INVALID_COMMAND;
 
-  } else if (!chppClientTimestampResponse(
-                 &gnssClientContext->client,
-                 &gnssClientContext->rRState[rxHeader->command], rxHeader)) {
+  } else if (!chppTimestampIncomingResponse(
+                 gnssClientContext->client.appContext,
+                 &gnssClientContext->outReqStates[rxHeader->command],
+                 rxHeader)) {
     error = CHPP_APP_ERROR_UNEXPECTED_RESPONSE;
 
   } else {
@@ -332,7 +334,7 @@
               gnssClientContext->client.openState);
     gnssClientContext->requestStateResyncPending = true;
     chppClientSendOpenRequest(&gGnssClientContext.client,
-                              &gGnssClientContext.rRState[CHPP_GNSS_OPEN],
+                              &gGnssClientContext.outReqStates[CHPP_GNSS_OPEN],
                               CHPP_GNSS_OPEN,
                               /*blocking=*/false);
   }
@@ -350,7 +352,7 @@
   if (gnssClientContext->client.pseudoOpen) {
     CHPP_LOGD("Pseudo-open GNSS client opening");
     chppClientSendOpenRequest(&gGnssClientContext.client,
-                              &gGnssClientContext.rRState[CHPP_GNSS_OPEN],
+                              &gGnssClientContext.outReqStates[CHPP_GNSS_OPEN],
                               CHPP_GNSS_OPEN,
                               /*blocking=*/false);
   }
@@ -589,8 +591,8 @@
  */
 static bool chppGnssClientOpen(const struct chrePalSystemApi *systemApi,
                                const struct chrePalGnssCallbacks *callbacks) {
-  CHPP_DEBUG_ASSERT(systemApi != NULL);
-  CHPP_DEBUG_ASSERT(callbacks != NULL);
+  CHPP_DEBUG_NOT_NULL(systemApi);
+  CHPP_DEBUG_NOT_NULL(callbacks);
 
   bool result = false;
   gSystemApi = systemApi;
@@ -604,7 +606,7 @@
                                      CHPP_GNSS_DISCOVERY_TIMEOUT_MS)) {
       result = chppClientSendOpenRequest(
           &gGnssClientContext.client,
-          &gGnssClientContext.rRState[CHPP_GNSS_OPEN], CHPP_GNSS_OPEN,
+          &gGnssClientContext.outReqStates[CHPP_GNSS_OPEN], CHPP_GNSS_OPEN,
           /*blocking=*/true);
     }
 
@@ -627,9 +629,9 @@
 
   if (request == NULL) {
     CHPP_LOG_OOM();
-  } else if (chppSendTimestampedRequestAndWait(
+  } else if (chppClientSendTimestampedRequestAndWait(
                  &gGnssClientContext.client,
-                 &gGnssClientContext.rRState[CHPP_GNSS_CLOSE], request,
+                 &gGnssClientContext.outReqStates[CHPP_GNSS_CLOSE], request,
                  sizeof(*request))) {
     gGnssClientContext.client.openState = CHPP_OPEN_STATE_CLOSED;
     gGnssClientContext.capabilities = CHRE_GNSS_CAPABILITIES_NONE;
@@ -659,10 +661,10 @@
     if (request == NULL) {
       CHPP_LOG_OOM();
     } else {
-      if (chppSendTimestampedRequestAndWait(
+      if (chppClientSendTimestampedRequestAndWait(
               &gGnssClientContext.client,
-              &gGnssClientContext.rRState[CHPP_GNSS_GET_CAPABILITIES], request,
-              sizeof(*request))) {
+              &gGnssClientContext.outReqStates[CHPP_GNSS_GET_CAPABILITIES],
+              request, sizeof(*request))) {
         // Success. gGnssClientContext.capabilities is now populated
         if (gGnssClientContext.capabilitiesValid) {
           capabilities = gGnssClientContext.capabilities;
@@ -703,9 +705,9 @@
     request->params.minIntervalMs = minIntervalMs;
     request->params.minTimeToNextFixMs = minTimeToNextFixMs;
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gGnssClientContext.client,
-        &gGnssClientContext.rRState[CHPP_GNSS_CONTROL_LOCATION_SESSION],
+        &gGnssClientContext.outReqStates[CHPP_GNSS_CONTROL_LOCATION_SESSION],
         request, sizeof(*request), CHRE_GNSS_ASYNC_RESULT_TIMEOUT_NS);
   }
 
@@ -749,9 +751,9 @@
     request->params.enable = enable;
     request->params.minIntervalMs = minIntervalMs;
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gGnssClientContext.client,
-        &gGnssClientContext.rRState[CHPP_GNSS_CONTROL_MEASUREMENT_SESSION],
+        &gGnssClientContext.outReqStates[CHPP_GNSS_CONTROL_MEASUREMENT_SESSION],
         request, sizeof(*request), CHRE_GNSS_ASYNC_RESULT_TIMEOUT_NS);
   }
 
@@ -795,11 +797,11 @@
     request->header.command = CHPP_GNSS_CONFIGURE_PASSIVE_LOCATION_LISTENER;
     request->params.enable = enable;
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gGnssClientContext.client,
         &gGnssClientContext
-             .rRState[CHPP_GNSS_CONFIGURE_PASSIVE_LOCATION_LISTENER],
-        request, sizeof(*request), CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT);
+             .outReqStates[CHPP_GNSS_CONFIGURE_PASSIVE_LOCATION_LISTENER],
+        request, sizeof(*request), CHPP_REQUEST_TIMEOUT_DEFAULT);
   }
 
   return result;
@@ -812,8 +814,8 @@
 void chppRegisterGnssClient(struct ChppAppState *appContext) {
   memset(&gGnssClientContext, 0, sizeof(gGnssClientContext));
   chppRegisterClient(appContext, (void *)&gGnssClientContext,
-                     &gGnssClientContext.client, gGnssClientContext.rRState,
-                     &kGnssClientConfig);
+                     &gGnssClientContext.client,
+                     gGnssClientContext.outReqStates, &kGnssClientConfig);
 }
 
 void chppDeregisterGnssClient(struct ChppAppState *appContext) {
@@ -822,7 +824,7 @@
   UNUSED_VAR(appContext);
 }
 
-struct ChppClientState *getChppGnssClientState(void) {
+struct ChppEndpointState *getChppGnssClientState(void) {
   return &gGnssClientContext.client;
 }
 
diff --git a/chpp/clients/loopback.c b/chpp/clients/loopback.c
index b8f19e8..c92ae68 100644
--- a/chpp/clients/loopback.c
+++ b/chpp/clients/loopback.c
@@ -42,8 +42,8 @@
  * (RR) functionality.
  */
 struct ChppLoopbackClientState {
-  struct ChppClientState client;                    // Loopback client state
-  struct ChppRequestResponseState runLoopbackTest;  // Loopback test state
+  struct ChppEndpointState client;                  // CHPP client state
+  struct ChppOutgoingRequestState runLoopbackTest;  // Loopback test state
 
   struct ChppLoopbackTestResult testResult;  // Last test result
   const uint8_t *loopbackData;               // Pointer to loopback data
@@ -88,9 +88,9 @@
   CHPP_NOT_NULL(state);
   CHPP_NOT_NULL(state->loopbackData);
 
-  CHPP_ASSERT(
-      chppClientTimestampResponse(&state->client, &state->runLoopbackTest,
-                                  (const struct ChppAppHeader *)response));
+  CHPP_ASSERT(chppTimestampIncomingResponse(
+      state->client.appContext, &state->runLoopbackTest,
+      (const struct ChppAppHeader *)response));
 
   struct ChppLoopbackTestResult *result = &state->testResult;
 
@@ -123,10 +123,10 @@
             result->firstError, result->byteErrors);
 
   // Notify waiting (synchronous) client
-  chppMutexLock(&state->client.responseMutex);
-  state->client.responseReady = true;
-  chppConditionVariableSignal(&state->client.responseCondVar);
-  chppMutexUnlock(&state->client.responseMutex);
+  chppMutexLock(&state->client.syncResponse.mutex);
+  state->client.syncResponse.ready = true;
+  chppConditionVariableSignal(&state->client.syncResponse.condVar);
+  chppMutexUnlock(&state->client.syncResponse.mutex);
 
   return true;
 }
@@ -187,7 +187,7 @@
   state->loopbackData = buf;
   memcpy(&loopbackRequest[CHPP_LOOPBACK_HEADER_LEN], buf, len);
 
-  if (!chppSendTimestampedRequestAndWaitTimeout(
+  if (!chppClientSendTimestampedRequestAndWaitTimeout(
           &state->client, &state->runLoopbackTest, loopbackRequest,
           result->requestLen, 5 * CHPP_NSEC_PER_SEC)) {
     result->error = CHPP_APP_ERROR_UNSPECIFIED;
diff --git a/chpp/clients/timesync.c b/chpp/clients/timesync.c
index de0664b..3026f21 100644
--- a/chpp/clients/timesync.c
+++ b/chpp/clients/timesync.c
@@ -40,8 +40,8 @@
  * (RR) functionality.
  */
 struct ChppTimesyncClientState {
-  struct ChppClientState client;                  // Timesync client state
-  struct ChppRequestResponseState measureOffset;  // Request response state
+  struct ChppEndpointState client;                // CHPP client state
+  struct ChppOutgoingRequestState measureOffset;  // Request response state
 
   struct ChppTimesyncResult timesyncResult;  // Result of measureOffset
 };
@@ -104,8 +104,8 @@
 
   const struct ChppTimesyncResponse *response =
       (const struct ChppTimesyncResponse *)buf;
-  if (chppClientTimestampResponse(&state->client, &state->measureOffset,
-                                  &response->header)) {
+  if (chppTimestampIncomingResponse(state->client.appContext,
+                                    &state->measureOffset, &response->header)) {
     state->timesyncResult.rttNs = state->measureOffset.responseTimeNs -
                                   state->measureOffset.requestTimeNs;
     int64_t offsetNs =
@@ -164,9 +164,9 @@
     state->timesyncResult.error = CHPP_APP_ERROR_OOM;
     CHPP_LOG_OOM();
 
-  } else if (!chppSendTimestampedRequestOrFail(
+  } else if (!chppClientSendTimestampedRequestOrFail(
                  &state->client, &state->measureOffset, request, requestLen,
-                 CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE)) {
+                 CHPP_REQUEST_TIMEOUT_INFINITE)) {
     state->timesyncResult.error = CHPP_APP_ERROR_UNSPECIFIED;
 
   } else {
diff --git a/chpp/clients/wifi.c b/chpp/clients/wifi.c
index b544684..9438a84 100644
--- a/chpp/clients/wifi.c
+++ b/chpp/clients/wifi.c
@@ -75,10 +75,11 @@
  * (RR) functionality.
  */
 struct ChppWifiClientState {
-  struct ChppClientState client;     // WiFi client state
+  struct ChppEndpointState client;   // CHPP client state
   const struct chrePalWifiApi *api;  // WiFi PAL API
 
-  struct ChppRequestResponseState rRState[CHPP_WIFI_CLIENT_REQUEST_MAX + 1];
+  struct ChppOutgoingRequestState
+      outReqStates[CHPP_WIFI_CLIENT_REQUEST_MAX + 1];
 
   uint32_t capabilities;            // Cached GetCapabilities result
   bool scanMonitorEnabled;          // Scan monitoring is enabled
@@ -123,8 +124,8 @@
     // Service notification dispatch function pointer
     .deinitFunctionPtr = &chppWifiClientDeinit,
 
-    // Number of request-response states in the rRStates array.
-    .rRStateCount = ARRAY_SIZE(gWifiClientContext.rRState),
+    // Number of request-response states in the outReqStates array.
+    .outReqCount = ARRAY_SIZE(gWifiClientContext.outReqStates),
 
     // Min length is the entire header
     .minLength = sizeof(struct ChppAppHeader),
@@ -201,9 +202,10 @@
   if (rxHeader->command > CHPP_WIFI_CLIENT_REQUEST_MAX) {
     error = CHPP_APP_ERROR_INVALID_COMMAND;
 
-  } else if (!chppClientTimestampResponse(
-                 &wifiClientContext->client,
-                 &wifiClientContext->rRState[rxHeader->command], rxHeader)) {
+  } else if (!chppTimestampIncomingResponse(
+                 wifiClientContext->client.appContext,
+                 &wifiClientContext->outReqStates[rxHeader->command],
+                 rxHeader)) {
     error = CHPP_APP_ERROR_UNEXPECTED_RESPONSE;
 
   } else {
@@ -380,7 +382,7 @@
     CHPP_LOGI("WiFi client reopening from state=%" PRIu8,
               wifiClientContext->client.openState);
     chppClientSendOpenRequest(&wifiClientContext->client,
-                              &wifiClientContext->rRState[CHPP_WIFI_OPEN],
+                              &wifiClientContext->outReqStates[CHPP_WIFI_OPEN],
                               CHPP_WIFI_OPEN,
                               /*blocking=*/false);
   }
@@ -398,7 +400,7 @@
   if (wifiClientContext->client.pseudoOpen) {
     CHPP_LOGD("Pseudo-open WiFi client opening");
     chppClientSendOpenRequest(&wifiClientContext->client,
-                              &wifiClientContext->rRState[CHPP_WIFI_OPEN],
+                              &wifiClientContext->outReqStates[CHPP_WIFI_OPEN],
                               CHPP_WIFI_OPEN,
                               /*blocking=*/false);
   }
@@ -832,8 +834,8 @@
  */
 static bool chppWifiClientOpen(const struct chrePalSystemApi *systemApi,
                                const struct chrePalWifiCallbacks *callbacks) {
-  CHPP_DEBUG_ASSERT(systemApi != NULL);
-  CHPP_DEBUG_ASSERT(callbacks != NULL);
+  CHPP_DEBUG_NOT_NULL(systemApi);
+  CHPP_DEBUG_NOT_NULL(callbacks);
 
   bool result = false;
   gSystemApi = systemApi;
@@ -847,7 +849,7 @@
                                      CHPP_WIFI_DISCOVERY_TIMEOUT_MS)) {
       result = chppClientSendOpenRequest(
           &gWifiClientContext.client,
-          &gWifiClientContext.rRState[CHPP_WIFI_OPEN], CHPP_WIFI_OPEN,
+          &gWifiClientContext.outReqStates[CHPP_WIFI_OPEN], CHPP_WIFI_OPEN,
           /*blocking=*/true);
     }
 
@@ -870,9 +872,9 @@
 
   if (request == NULL) {
     CHPP_LOG_OOM();
-  } else if (chppSendTimestampedRequestAndWait(
+  } else if (chppClientSendTimestampedRequestAndWait(
                  &gWifiClientContext.client,
-                 &gWifiClientContext.rRState[CHPP_WIFI_CLOSE], request,
+                 &gWifiClientContext.outReqStates[CHPP_WIFI_CLOSE], request,
                  sizeof(*request))) {
     gWifiClientContext.client.openState = CHPP_OPEN_STATE_CLOSED;
     gWifiClientContext.capabilities = CHRE_WIFI_CAPABILITIES_NONE;
@@ -902,10 +904,10 @@
     if (request == NULL) {
       CHPP_LOG_OOM();
     } else {
-      if (chppSendTimestampedRequestAndWait(
+      if (chppClientSendTimestampedRequestAndWait(
               &gWifiClientContext.client,
-              &gWifiClientContext.rRState[CHPP_WIFI_GET_CAPABILITIES], request,
-              sizeof(*request))) {
+              &gWifiClientContext.outReqStates[CHPP_WIFI_GET_CAPABILITIES],
+              request, sizeof(*request))) {
         // Success. gWifiClientContext.capabilities is now populated
         if (gWifiClientContext.capabilitiesValid) {
           capabilities = gWifiClientContext.capabilities;
@@ -938,12 +940,14 @@
     request->header.command = CHPP_WIFI_CONFIGURE_SCAN_MONITOR_ASYNC;
     request->params.enable = enable;
     request->params.cookie =
-        &gWifiClientContext.rRState[CHPP_WIFI_CONFIGURE_SCAN_MONITOR_ASYNC];
+        &gWifiClientContext
+             .outReqStates[CHPP_WIFI_CONFIGURE_SCAN_MONITOR_ASYNC];
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gWifiClientContext.client,
-        &gWifiClientContext.rRState[CHPP_WIFI_CONFIGURE_SCAN_MONITOR_ASYNC],
-        request, sizeof(*request), CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT);
+        &gWifiClientContext
+             .outReqStates[CHPP_WIFI_CONFIGURE_SCAN_MONITOR_ASYNC],
+        request, sizeof(*request), CHPP_REQUEST_TIMEOUT_DEFAULT);
   }
 
   return result;
@@ -976,9 +980,9 @@
         CHRE_WIFI_SCAN_RESULT_TIMEOUT_NS > CHPP_WIFI_SCAN_RESULT_TIMEOUT_NS,
         "Chpp wifi scan timeout needs to be smaller than CHRE wifi scan "
         "timeout");
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gWifiClientContext.client,
-        &gWifiClientContext.rRState[CHPP_WIFI_REQUEST_SCAN_ASYNC], request,
+        &gWifiClientContext.outReqStates[CHPP_WIFI_REQUEST_SCAN_ASYNC], request,
         requestLen, CHPP_WIFI_SCAN_RESULT_TIMEOUT_NS);
   }
 
@@ -1027,10 +1031,10 @@
     request->header.error = CHPP_APP_ERROR_NONE;
     request->header.command = CHPP_WIFI_REQUEST_RANGING_ASYNC;
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gWifiClientContext.client,
-        &gWifiClientContext.rRState[CHPP_WIFI_REQUEST_RANGING_ASYNC], request,
-        requestLen, CHRE_WIFI_RANGING_RESULT_TIMEOUT_NS);
+        &gWifiClientContext.outReqStates[CHPP_WIFI_REQUEST_RANGING_ASYNC],
+        request, requestLen, CHRE_WIFI_RANGING_RESULT_TIMEOUT_NS);
   }
 
   return result;
@@ -1075,9 +1079,9 @@
     request->header.error = CHPP_APP_ERROR_NONE;
     request->header.command = CHPP_WIFI_REQUEST_NAN_SUB;
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gWifiClientContext.client,
-        &gWifiClientContext.rRState[CHPP_WIFI_REQUEST_NAN_SUB], request,
+        &gWifiClientContext.outReqStates[CHPP_WIFI_REQUEST_NAN_SUB], request,
         requestLen, CHRE_ASYNC_RESULT_TIMEOUT_NS);
   }
   return result;
@@ -1106,10 +1110,10 @@
     request->header.error = CHPP_APP_ERROR_NONE;
     request->subscriptionId = subscriptionId;
 
-    result = chppSendTimestampedRequestAndWait(
+    result = chppClientSendTimestampedRequestAndWait(
         &gWifiClientContext.client,
-        &gWifiClientContext.rRState[CHPP_WIFI_REQUEST_NAN_SUB_CANCEL], request,
-        sizeof(*request));
+        &gWifiClientContext.outReqStates[CHPP_WIFI_REQUEST_NAN_SUB_CANCEL],
+        request, sizeof(*request));
   }
   return result;
 }
@@ -1152,9 +1156,9 @@
     request->header.transaction = gWifiClientContext.client.transaction++;
     request->header.error = CHPP_APP_ERROR_NONE;
 
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gWifiClientContext.client,
-        &gWifiClientContext.rRState[CHPP_WIFI_REQUEST_NAN_RANGING_ASYNC],
+        &gWifiClientContext.outReqStates[CHPP_WIFI_REQUEST_NAN_RANGING_ASYNC],
         request, requestLen, CHRE_ASYNC_RESULT_TIMEOUT_NS);
   }
   return result;
@@ -1174,8 +1178,8 @@
 void chppRegisterWifiClient(struct ChppAppState *appContext) {
   memset(&gWifiClientContext, 0, sizeof(gWifiClientContext));
   chppRegisterClient(appContext, (void *)&gWifiClientContext,
-                     &gWifiClientContext.client, gWifiClientContext.rRState,
-                     &kWifiClientConfig);
+                     &gWifiClientContext.client,
+                     gWifiClientContext.outReqStates, &kWifiClientConfig);
 }
 
 void chppDeregisterWifiClient(struct ChppAppState *appContext) {
@@ -1184,7 +1188,7 @@
   UNUSED_VAR(appContext);
 }
 
-struct ChppClientState *getChppWifiClientState(void) {
+struct ChppEndpointState *getChppWifiClientState(void) {
   return &gWifiClientContext.client;
 }
 
diff --git a/chpp/clients/wwan.c b/chpp/clients/wwan.c
index 98aea88..1ac8aa3 100644
--- a/chpp/clients/wwan.c
+++ b/chpp/clients/wwan.c
@@ -64,10 +64,11 @@
  * (RR) functionality.
  */
 struct ChppWwanClientState {
-  struct ChppClientState client;     // WWAN client state
+  struct ChppEndpointState client;   // CHPP client state
   const struct chrePalWwanApi *api;  // WWAN PAL API
 
-  struct ChppRequestResponseState rRState[CHPP_WWAN_CLIENT_REQUEST_MAX + 1];
+  struct ChppOutgoingRequestState
+      outReqStates[CHPP_WWAN_CLIENT_REQUEST_MAX + 1];
 
   uint32_t capabilities;   // Cached GetCapabilities result
   bool capabilitiesValid;  // Flag to indicate if the capabilities result
@@ -109,8 +110,8 @@
     // Service notification dispatch function pointer
     .deinitFunctionPtr = &chppWwanClientDeinit,
 
-    // Number of request-response states in the rRStates array.
-    .rRStateCount = ARRAY_SIZE(gWwanClientContext.rRState),
+    // Number of request-response states in the outReqStates array.
+    .outReqCount = ARRAY_SIZE(gWwanClientContext.outReqStates),
 
     // Min length is the entire header
     .minLength = sizeof(struct ChppAppHeader),
@@ -163,9 +164,10 @@
   if (rxHeader->command > CHPP_WWAN_CLIENT_REQUEST_MAX) {
     error = CHPP_APP_ERROR_INVALID_COMMAND;
 
-  } else if (!chppClientTimestampResponse(
-                 &wwanClientContext->client,
-                 &wwanClientContext->rRState[rxHeader->command], rxHeader)) {
+  } else if (!chppTimestampIncomingResponse(
+                 wwanClientContext->client.appContext,
+                 &wwanClientContext->outReqStates[rxHeader->command],
+                 rxHeader)) {
     error = CHPP_APP_ERROR_UNEXPECTED_RESPONSE;
 
   } else {
@@ -252,7 +254,7 @@
     CHPP_LOGI("WWAN client reopening from state=%" PRIu8,
               wwanClientContext->client.openState);
     chppClientSendOpenRequest(&wwanClientContext->client,
-                              &wwanClientContext->rRState[CHPP_WWAN_OPEN],
+                              &wwanClientContext->outReqStates[CHPP_WWAN_OPEN],
                               CHPP_WWAN_OPEN,
                               /*blocking=*/false);
   }
@@ -270,7 +272,7 @@
   if (wwanClientContext->client.pseudoOpen) {
     CHPP_LOGD("Pseudo-open WWAN client opening");
     chppClientSendOpenRequest(&wwanClientContext->client,
-                              &wwanClientContext->rRState[CHPP_WWAN_OPEN],
+                              &wwanClientContext->outReqStates[CHPP_WWAN_OPEN],
                               CHPP_WWAN_OPEN,
                               /*blocking=*/false);
   }
@@ -400,8 +402,8 @@
  */
 static bool chppWwanClientOpen(const struct chrePalSystemApi *systemApi,
                                const struct chrePalWwanCallbacks *callbacks) {
-  CHPP_DEBUG_ASSERT(systemApi != NULL);
-  CHPP_DEBUG_ASSERT(callbacks != NULL);
+  CHPP_DEBUG_NOT_NULL(systemApi);
+  CHPP_DEBUG_NOT_NULL(callbacks);
 
   bool result = false;
   gSystemApi = systemApi;
@@ -416,7 +418,7 @@
                                      CHPP_WWAN_DISCOVERY_TIMEOUT_MS)) {
       result = chppClientSendOpenRequest(
           &gWwanClientContext.client,
-          &gWwanClientContext.rRState[CHPP_WWAN_OPEN], CHPP_WWAN_OPEN,
+          &gWwanClientContext.outReqStates[CHPP_WWAN_OPEN], CHPP_WWAN_OPEN,
           /*blocking=*/true);
     }
 
@@ -439,9 +441,9 @@
 
   if (request == NULL) {
     CHPP_LOG_OOM();
-  } else if (chppSendTimestampedRequestAndWait(
+  } else if (chppClientSendTimestampedRequestAndWait(
                  &gWwanClientContext.client,
-                 &gWwanClientContext.rRState[CHPP_WWAN_CLOSE], request,
+                 &gWwanClientContext.outReqStates[CHPP_WWAN_CLOSE], request,
                  sizeof(*request))) {
     gWwanClientContext.client.openState = CHPP_OPEN_STATE_CLOSED;
     gWwanClientContext.capabilities = CHRE_WWAN_CAPABILITIES_NONE;
@@ -471,10 +473,10 @@
     if (request == NULL) {
       CHPP_LOG_OOM();
     } else {
-      if (chppSendTimestampedRequestAndWait(
+      if (chppClientSendTimestampedRequestAndWait(
               &gWwanClientContext.client,
-              &gWwanClientContext.rRState[CHPP_WWAN_GET_CAPABILITIES], request,
-              sizeof(*request))) {
+              &gWwanClientContext.outReqStates[CHPP_WWAN_GET_CAPABILITIES],
+              request, sizeof(*request))) {
         // Success. gWwanClientContext.capabilities is now populated
         if (gWwanClientContext.capabilitiesValid) {
           capabilities = gWwanClientContext.capabilities;
@@ -502,10 +504,10 @@
   if (request == NULL) {
     CHPP_LOG_OOM();
   } else {
-    result = chppSendTimestampedRequestOrFail(
+    result = chppClientSendTimestampedRequestOrFail(
         &gWwanClientContext.client,
-        &gWwanClientContext.rRState[CHPP_WWAN_GET_CELLINFO_ASYNC], request,
-        sizeof(*request), CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT);
+        &gWwanClientContext.outReqStates[CHPP_WWAN_GET_CELLINFO_ASYNC], request,
+        sizeof(*request), CHPP_REQUEST_TIMEOUT_DEFAULT);
   }
 
   return result;
@@ -531,8 +533,8 @@
 void chppRegisterWwanClient(struct ChppAppState *appContext) {
   memset(&gWwanClientContext, 0, sizeof(gWwanClientContext));
   chppRegisterClient(appContext, (void *)&gWwanClientContext,
-                     &gWwanClientContext.client, gWwanClientContext.rRState,
-                     &kWwanClientConfig);
+                     &gWwanClientContext.client,
+                     gWwanClientContext.outReqStates, &kWwanClientConfig);
 }
 
 void chppDeregisterWwanClient(struct ChppAppState *appContext) {
@@ -541,7 +543,7 @@
   UNUSED_VAR(appContext);
 }
 
-struct ChppClientState *getChppWwanClientState(void) {
+struct ChppEndpointState *getChppWwanClientState(void) {
   return &gWwanClientContext.client;
 }
 
diff --git a/chpp/include/chpp/app.h b/chpp/include/chpp/app.h
index 67bf54d..eda11fa 100644
--- a/chpp/include/chpp/app.h
+++ b/chpp/include/chpp/app.h
@@ -24,6 +24,7 @@
 #include "chpp/condition_variable.h"
 #include "chpp/macros.h"
 #include "chpp/transport.h"
+#include "chre_api/chre/common.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -34,6 +35,32 @@
  ***********************************************/
 
 /**
+ * Allocates a variable-length response message of a specific type.
+ *
+ * @param requestHeader request header, as per chppAllocResponse().
+ * @param type Type of response which includes an arrayed member.
+ * @param count number of items in the array of arrayField.
+ * @param arrayField The arrayed member field.
+ *
+ * @return Pointer to allocated memory.
+ */
+#define chppAllocResponseTypedArray(requestHeader, type, count, arrayField) \
+  (type *)chppAllocResponse(                                                \
+      requestHeader,                                                        \
+      sizeof(type) + (count)*sizeof_member(type, arrayField[0]))
+
+/**
+ * Allocates a response message of a specific type and its corresponding length.
+ *
+ * @param requestHeader request header, as per chppAllocResponse().
+ * @param type Type of response.
+ *
+ * @return Pointer to allocated memory.
+ */
+#define chppAllocResponseFixed(requestHeader, type) \
+  (type *)chppAllocResponse(requestHeader, sizeof(type))
+
+/**
  * Maximum number of services that can be registered by CHPP (not including
  * predefined services), if not defined by the build system.
  */
@@ -58,6 +85,25 @@
   MAX(CHPP_MAX_REGISTERED_SERVICES, CHPP_MAX_REGISTERED_CLIENTS)
 #endif
 
+#define CHPP_REQUEST_TIMEOUT_INFINITE CHPP_TIME_MAX
+
+#if defined(CHPP_REQUEST_TIMEOUT_DEFAULT) && \
+    defined(CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT)
+// Build systems should prefer to only set CHPP_REQUEST_TIMEOUT_DEFAULT
+#error Can not set both CHPP_REQUEST_TIMEOUT_DEFAULT and CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT
+#endif
+
+// For backwards compatibility with vendor build systems
+#ifdef CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT
+#define CHPP_REQUEST_TIMEOUT_DEFAULT CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT
+#undef CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT
+#endif
+
+// If not customized in the build, we default to CHRE expectations
+#ifndef CHPP_REQUEST_TIMEOUT_DEFAULT
+#define CHPP_REQUEST_TIMEOUT_DEFAULT CHRE_ASYNC_RESULT_TIMEOUT_NS
+#endif
+
 /**
  * Default value for reserved fields.
  */
@@ -74,6 +120,14 @@
 #define CHPP_APP_COMMAND_NONE 0
 
 /**
+ * Type of endpoint (either client or service)
+ */
+enum ChppEndpointType {
+  CHPP_ENDPOINT_CLIENT = 0,
+  CHPP_ENDPOINT_SERVICE = 1,
+};
+
+/**
  * Handle Numbers in ChppAppHeader
  */
 enum ChppHandleNumber {
@@ -114,6 +168,13 @@
 
   //! Notification from service. Client shall not respond.
   CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION = 3,
+
+  //! Request from service. Needs response from client.
+  CHPP_MESSAGE_TYPE_SERVICE_REQUEST = 4,
+
+  //! Response from client (with the same Command and Transaction ID as the
+  //! service request).
+  CHPP_MESSAGE_TYPE_CLIENT_RESPONSE = 5,
 };
 
 /**
@@ -189,7 +250,10 @@
 CHPP_PACKED_END
 
 /**
- * Function type that dispatches incoming datagrams for any client or service
+ * Function type that dispatches incoming datagrams for any client or service.
+ *
+ * The buffer is freed shortly after the function returns.
+ * User code must make a copy for later processing if needed.
  */
 typedef enum ChppAppErrorCode(ChppDispatchFunction)(void *context, uint8_t *buf,
                                                     size_t len);
@@ -229,6 +293,17 @@
 #define CHPP_SERVICE_NAME_MAX_LEN (15 + 1)
 
 /**
+ * Support for sync response.
+ *
+ * @see chppClientSendTimestampedRequestAndWaitTimeout.
+ */
+struct ChppSyncResponse {
+  struct ChppMutex mutex;
+  struct ChppConditionVariable condVar;
+  bool ready;
+};
+
+/**
  * CHPP definition of a service descriptor as sent over the wire.
  */
 CHPP_PACKED_START
@@ -252,19 +327,31 @@
   //! Service Descriptor as sent over the wire.
   struct ChppServiceDescriptor descriptor;
 
-  //! Pointer to the function that is used to notify the service if CHPP is
-  //! reset.
+  //! Notifies the service if CHPP is reset.
   ChppNotifierFunction *resetNotifierFunctionPtr;
 
-  //! Pointer to the function that dispatches incoming client requests for the
-  //! service.
+  //! Dispatches incoming client requests.
+  //! When an error is returned by the dispatch function it is logged and an
+  //! error response is automatically sent to the remote endpoint.
   ChppDispatchFunction *requestDispatchFunctionPtr;
 
-  //! Pointer to the function that dispatches incoming client notifications for
-  //! the service.
+  //! Dispatches incoming client notifications.
+  //! Errors returned by the dispatch function are logged.
   ChppDispatchFunction *notificationDispatchFunctionPtr;
 
-  //! Minimum valid length of datagrams for the service.
+  //! Dispatches incoming client responses.
+  //! Errors returned by the dispatch function are logged.
+  ChppDispatchFunction *responseDispatchFunctionPtr;
+
+  //! Number of outgoing requests supported by this service.
+  //! ChppAppHeader.command must be in the range [0, outReqCount - 1]
+  //! ChppEndpointState.outReqStates must contains that many elements.
+  uint16_t outReqCount;
+
+  //! Minimum valid length of datagrams for the service:
+  //! - client requests
+  //! - client notifications
+  //! - client responses
   size_t minLength;
 };
 
@@ -287,38 +374,45 @@
   //! Client descriptor.
   struct ChppClientDescriptor descriptor;
 
-  //! Pointer to the function that is used to notify the client if CHPP is
-  //! reset.
+  //! Notifies the client if CHPP is reset.
   ChppNotifierFunction *resetNotifierFunctionPtr;
 
-  //! Pointer to the function that is used to notify the client if CHPP is
-  //! matched to a service.
+  //! Notifies the client if CHPP is matched to a service.
   ChppNotifierFunction *matchNotifierFunctionPtr;
 
-  //! Pointer to the function that dispatches incoming service responses for the
-  //! client.
+  //! Dispatches incoming service responses.
   //! Service responses are only dispatched to clients that have been opened or
-  //! are in the process of being (re)opened. @see ChppOpenState
+  //! are in the process of being (re)opened. @see ChppOpenState.
+  //! Errors returned by the dispatch function are logged.
   ChppDispatchFunction *responseDispatchFunctionPtr;
 
-  //! Pointer to the function that dispatches incoming service notifications for
-  //! the client.
+  //! Dispatches incoming service notifications.
   //! Service notifications are only dispatched to clients that have been
   //! opened. @see ChppOpenState
+  //! Errors returned by the dispatch function are logged.
   ChppDispatchFunction *notificationDispatchFunctionPtr;
 
-  //! Pointer to the function that initializes the client (after it is matched
-  //! with a service at discovery) and assigns it its handle number.
+  //! Dispatches incoming service requests.
+  //! When an error is returned by the dispatch function it is logged and an
+  //! error response is automatically sent to the remote endpoint.
+  ChppDispatchFunction *requestDispatchFunctionPtr;
+
+  //! Initializes the client (after it is matched with a service at discovery)
+  //! and assigns it its handle number.
   ChppClientInitFunction *initFunctionPtr;
 
-  //! Pointer to the function that deinitializes the client.
+  //! Deinitializes the client.
   ChppClientDeinitFunction *deinitFunctionPtr;
 
-  //! Number of request-response states in the rRStates array. This is a
-  //! uint16_t to match the uint16_t command in struct ChppAppHeader.
-  uint16_t rRStateCount;
+  //! Number of outgoing requests supported by this client.
+  //! ChppAppHeader.command must be in the range [0, outReqCount - 1]
+  //! ChppEndpointState.outReqStates must contains that many elements.
+  uint16_t outReqCount;
 
-  //! Minimum valid length of datagrams for the service.
+  //! Minimum valid length of datagrams for the service:
+  //! - service responses
+  //! - service notifications
+  //! - service requests
   size_t minLength;
 };
 
@@ -326,31 +420,54 @@
  * Request status for clients.
  */
 enum ChppRequestState {
-  CHPP_REQUEST_STATE_NONE = 0,              // No request sent ever
-  CHPP_REQUEST_STATE_REQUEST_SENT = 1,      // Sent but no response yet
-  CHPP_REQUEST_STATE_RESPONSE_RCV = 2,      // Sent and response received
-  CHPP_REQUEST_STATE_RESPONSE_TIMEOUT = 3,  // Timeout. Responded as need be
+  CHPP_REQUEST_STATE_NONE = 0,              //!< No request sent ever
+  CHPP_REQUEST_STATE_REQUEST_SENT = 1,      //!< Sent, waiting for a response
+  CHPP_REQUEST_STATE_RESPONSE_RCV = 2,      //!< Sent and response received
+  CHPP_REQUEST_STATE_RESPONSE_TIMEOUT = 3,  //!< Timeout. Responded as need be
 };
 
 /**
- * Maintains the basic state for each request/response functionality of a
- * client or service.
- * Any number of these may be included in the (context) status variable of a
- * client or service (one per every every request/response functionality).
+ * State of each outgoing request and their response.
+ *
+ * There must be as many ChppOutgoingRequestState in the client or service state
+ * (ChppEndpointState) as the number of commands they support.
  */
-struct ChppRequestResponseState {
+struct ChppOutgoingRequestState {
   uint64_t requestTimeNs;  // Time of the last request
-  uint64_t
-      responseTimeNs;  // If requestState is CHPP_REQUEST_STATE_REQUEST_SENT,
-                       // indicates the timeout time for the request
-                       // If requestState is CHPP_REQUEST_STATE_RESPONSE_RCV,
-                       // indicates when the response was received
-
+  // When requestState is CHPP_REQUEST_STATE_REQUEST_SENT,
+  // indicates the timeout time for the request.
+  // When requestState is CHPP_REQUEST_STATE_RESPONSE_RCV,
+  // indicates when the response was received.
+  uint64_t responseTimeNs;
   uint8_t requestState;  // From enum ChppRequestState
   uint8_t transaction;   // Transaction ID for the last request/response
 };
 
 /**
+ * State of each incoming request and their response.
+ *
+ * There must be as many ChppIncomingRequestState in the client or service state
+ * as the number of commands supported by the other side (corresponding service
+ * for a client and corresponding client for a service).
+ *
+ * Contrary to ChppOutgoingRequestState those are not part of
+ * CChppEndpointState. They must be stored to and retrieved from the context
+ * passed to chppRegisterClient / chppRegisterService.
+ *
+ * Note: while ChppIncomingRequestState and ChppOutgoingRequestState have the
+ * same layout, we want the types to be distinct to be enforced at compile time.
+ * Using a typedef would make both types equivalent.
+ *
+ * @see ChppOutgoingRequestState for field details.
+ */
+struct ChppIncomingRequestState {
+  uint64_t requestTimeNs;
+  uint64_t responseTimeNs;
+  uint8_t requestState;
+  uint8_t transaction;
+};
+
+/**
  * Enabled clients and services.
  */
 struct ChppClientServiceSet {
@@ -366,6 +483,36 @@
 struct ChppLoopbackClientState;
 struct ChppTimesyncClientState;
 
+/**
+ * CHPP state of a client or a service.
+ *
+ * This is the CHPP internal client/service state.
+ * Their private state is store in the context field.
+ */
+struct ChppEndpointState {
+  struct ChppAppState *appContext;  // Pointer to app layer context
+
+  // State for the outgoing requests.
+  // It must accommodate Chpp{Client,Service}.outReqCount elements.
+  // It also tracks corresponding incoming responses.
+  // NULL when outReqCount = 0.
+  struct ChppOutgoingRequestState *outReqStates;
+
+  void *context;  //!< Private state of the endpoint.
+
+  struct ChppSyncResponse syncResponse;
+
+  uint8_t index;        //!< Index (in ChppAppState lists).
+  uint8_t handle;       //!< Handle used to match client and service.
+  uint8_t transaction;  //!< Next Transaction ID to be used.
+
+  uint8_t openState;  //!< see enum ChppOpenState
+
+  bool pseudoOpen : 1;       //!< Client to be opened upon a reset
+  bool initialized : 1;      //!< Client is initialized
+  bool everInitialized : 1;  //!< Client sync primitives initialized
+};
+
 struct ChppAppState {
   struct ChppTransportState *transportContext;  // Pointing to transport context
 
@@ -375,18 +522,19 @@
 
   const struct ChppService *registeredServices[CHPP_MAX_REGISTERED_SERVICES];
 
-  void *registeredServiceContexts[CHPP_MAX_REGISTERED_SERVICES];
+  struct ChppEndpointState
+      *registeredServiceStates[CHPP_MAX_REGISTERED_SERVICES];
 
   uint8_t registeredClientCount;  // Number of clients currently registered
 
   const struct ChppClient *registeredClients[CHPP_MAX_REGISTERED_CLIENTS];
 
-  const struct ChppClientState
-      *registeredClientStates[CHPP_MAX_REGISTERED_CLIENTS];
+  struct ChppEndpointState *registeredClientStates[CHPP_MAX_REGISTERED_CLIENTS];
 
-  void *registeredClientContexts[CHPP_MAX_REGISTERED_CLIENTS];
-
-  uint64_t nextRequestTimeoutNs;
+  // When the first outstanding request sent from the client timeouts.
+  uint64_t nextClientRequestTimeoutNs;
+  // When the first outstanding request sent from the service timeouts.
+  uint64_t nextServiceRequestTimeoutNs;
 
   uint8_t
       clientIndexOfServiceIndex[CHPP_MAX_DISCOVERED_SERVICES];  // Lookup table
@@ -509,6 +657,256 @@
 uint8_t chppAppShortResponseErrorHandler(uint8_t *buf, size_t len,
                                          const char *responseName);
 
+/**
+ * Allocates a notification of a specified length.
+ *
+ * This function is internal. Instead use either
+ * - chppAllocClientNotification
+ * - or chppAllocServiceNotification
+ *
+ * The caller must initialize at least the handle and command fields of the
+ * ChppAppHeader.
+ *
+ * @param type CHPP_MESSAGE_TYPE_CLIENT_NOTIFICATION or
+ *        CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION.
+ * @param len Length of the notification (including header) in bytes. Note
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
+ *
+ * @return Pointer to allocated memory.
+ */
+struct ChppAppHeader *chppAllocNotification(uint8_t type, size_t len);
+
+/**
+ * Allocates a request message.
+ *
+ * This function is internal. Instead use either:
+ * - chppAllocClientRequest
+ * - or chppAllocServiceRequest
+ *
+ * @param type CHPP_MESSAGE_TYPE_CLIENT_REQUEST or
+ *        CHPP_MESSAGE_TYPE_SERVICE_REQUEST.
+ * @param endpointState State of the endpoint.
+ * @param len Length of the response message (including header) in bytes. Note
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
+ *
+ * @return Pointer to allocated memory.
+ */
+struct ChppAppHeader *chppAllocRequest(uint8_t type,
+                                       struct ChppEndpointState *endpointState,
+                                       size_t len);
+
+/**
+ * Allocates a response message of a specified length, populating the (app
+ * layer) response header according to the provided request (app layer) header.
+ *
+ * This function can be used to allocate either client or service response.
+ *
+ * @param requestHeader request header.
+ * @param len Length of the response message (including header) in bytes. Note
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
+ *
+ * @return Pointer to allocated memory.
+ */
+struct ChppAppHeader *chppAllocResponse(
+    const struct ChppAppHeader *requestHeader, size_t len);
+
+/**
+ * This function shall be called for all incoming requests in order to
+ * A) Timestamp them, and
+ * B) Save their Transaction ID
+ *
+ * This function prints an error message if a duplicate request is received
+ * while outstanding request is still pending without a response.
+ *
+ * @param inReqState State of the request/response.
+ * @param requestHeader Request header.
+ */
+void chppTimestampIncomingRequest(struct ChppIncomingRequestState *inReqState,
+                                  const struct ChppAppHeader *requestHeader);
+
+/**
+ * This function shall be called for all outgoing requests in order to
+ * A) Timestamp them, and
+ * B) Save their Transaction ID
+ *
+ * This function prints an error message if a duplicate request is sent
+ * while outstanding request is still pending without a response.
+ *
+ * @param appState App layer state.
+ * @param outReqState state of the request/response.
+ * @param requestHeader Client request header.
+ * @param timeoutNs The timeout.
+ */
+void chppTimestampOutgoingRequest(struct ChppAppState *appState,
+                                  struct ChppOutgoingRequestState *outReqState,
+                                  const struct ChppAppHeader *requestHeader,
+                                  uint64_t timeoutNs);
+
+/**
+ * This function shall be called for incoming responses to a request in
+ * order to
+ * A) Verify the correct transaction ID
+ * B) Timestamp them, and
+ * C) Mark them as fulfilled
+ *
+ * This function prints an error message if a response is received without an
+ * outstanding request.
+ *
+ * @param appState App layer state.
+ * @param outReqState state of the request/response.
+ * @param requestHeader Request header.
+ *
+ * @return false if there is an error. true otherwise.
+ */
+bool chppTimestampIncomingResponse(struct ChppAppState *appState,
+                                   struct ChppOutgoingRequestState *outReqState,
+                                   const struct ChppAppHeader *responseHeader);
+
+/**
+ * This function shall be called for the outgoing response to a request in order
+ * to:
+ * A) Timestamp them, and
+ * B) Mark them as fulfilled part of the request/response's
+ *    ChppOutgoingRequestState struct.
+ *
+ * For most responses, it is expected that chppSendTimestampedResponseOrFail()
+ * shall be used to both timestamp and send the response in one shot.
+ *
+ * @param inReqState State of the request/response.
+ * @return The last response time (CHPP_TIME_NONE for the first response).
+ */
+uint64_t chppTimestampOutgoingResponse(
+    struct ChppIncomingRequestState *inReqState);
+
+/**
+ * Timestamps a response using chppTimestampOutgoingResponse() and enqueues it
+ * using chppEnqueueTxDatagramOrFail().
+ *
+ * Refer to their respective documentation for details.
+ *
+ * This function logs an error message if a response is attempted without an
+ * outstanding request.
+ *
+ * @param appState App layer state.
+ * @param inReqState State of the request/response.
+ * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
+ * @param len Datagram length in bytes.
+ *
+ * @return whether the datagram was successfully enqueued. false means that the
+ *         queue was full and the payload discarded.
+ */
+bool chppSendTimestampedResponseOrFail(
+    struct ChppAppState *appState, struct ChppIncomingRequestState *inReqState,
+    void *buf, size_t len);
+
+/**
+ * Timestamps and enqueues a request.
+ *
+ * This function is internal. User either:
+ * - chppClientSendTimestampedRequestOrFail
+ * - or chppServiceSendTimestampedRequestOrFail
+ *
+ * Note that the ownership of buf is taken from the caller when this method is
+ * invoked.
+ *
+ * @param endpointState state of the endpoint.
+ * @param outReqState state of the request/response.
+ * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
+ * @param len Datagram length in bytes.
+ * @param timeoutNs Time in nanoseconds before a timeout response is generated.
+ *        Zero means no timeout response.
+ *
+ * @return True informs the sender that the datagram was successfully enqueued.
+ *         False informs the sender that the queue was full and the payload
+ *         discarded.
+ */
+bool chppSendTimestampedRequestOrFail(
+    struct ChppEndpointState *endpointState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs);
+
+/**
+ * Wait for a response to be received.
+ *
+ * @param syncResponse sync primitives.
+ * @param outReqState state of the request/response.
+ * @param timeoutNs Time in nanoseconds before a timeout response is generated.
+ */
+bool chppWaitForResponseWithTimeout(
+    struct ChppSyncResponse *syncResponse,
+    struct ChppOutgoingRequestState *outReqState, uint64_t timeoutNs);
+
+/**
+ * Returns the state of a registered endpoint.
+ *
+ * @param appState State of the app layer.
+ * @param index Index of the client or service.
+ * @param type Type of the endpoint to return.
+ * @return state of the client or service.
+ */
+struct ChppEndpointState *getRegisteredEndpointState(
+    struct ChppAppState *appState, uint8_t index, enum ChppEndpointType type);
+
+/**
+ * Returns the number of possible outgoing requests.
+ *
+ * @param appState State of the app layer.
+ * @param index Index of the client or service.
+ * @param type Type of the endpoint to return.
+ * @return The number of possible outgoing requests.
+ */
+uint16_t getRegisteredEndpointOutReqCount(struct ChppAppState *appState,
+                                          uint8_t index,
+                                          enum ChppEndpointType type);
+
+/**
+ * Returns the number of registered endpoints of the given type.
+ *
+ * @param appState State of the app layer.
+ * @param type Type of the endpoint to return.
+ * @return The number of endpoints.
+ */
+uint8_t getRegisteredEndpointCount(struct ChppAppState *appState,
+                                   enum ChppEndpointType type);
+
+/**
+ * Recalculates the next upcoming request timeout.
+ *
+ * The timeout is updated in the app layer state.
+ *
+ * @param appState State of the app layer.
+ * @param type Type of the endpoint.
+ */
+void chppRecalculateNextTimeout(struct ChppAppState *appState,
+                                enum ChppEndpointType type);
+
+/**
+ * Returns a pointer to the next request timeout for the given endpoint type.
+ *
+ * @param appState State of the app layer.
+ * @param type Type of the endpoint.
+ * @return Pointer to the timeout in nanoseconds.
+ */
+uint64_t *getNextRequestTimeoutNs(struct ChppAppState *appState,
+                                  enum ChppEndpointType type);
+
+/**
+ * Closes any remaining open requests by simulating a timeout.
+ *
+ * This function is used when an endpoint is reset.
+ *
+ * @param endpointState State of the endpoint.
+ * @param type The type of the endpoint.
+ * @param clearOnly If true, indicates that a timeout response shouldn't be
+ *        sent. This must only be set if the requests are being cleared as
+ *        part of the closing.
+ */
+void chppCloseOpenRequests(struct ChppEndpointState *endpointState,
+                           enum ChppEndpointType type, bool clearOnly);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/chpp/include/chpp/clients.h b/chpp/include/chpp/clients.h
index f48686f..fe2dfe7 100644
--- a/chpp/include/chpp/clients.h
+++ b/chpp/include/chpp/clients.h
@@ -25,7 +25,6 @@
 #include "chpp/condition_variable.h"
 #include "chpp/macros.h"
 #include "chpp/mutex.h"
-#include "chre_api/chre/common.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -36,10 +35,10 @@
  ***********************************************/
 
 /**
- * Uses chppAllocClientRequest() to allocate a client request message of a
- * specific type and its corresponding length.
+ * Allocates a client request message of a specific type and its corresponding
+ * length.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  * @param type Type of response.
  *
  * @return Pointer to allocated memory
@@ -48,10 +47,9 @@
   (type *)chppAllocClientRequest(clientState, sizeof(type))
 
 /**
- * Uses chppAllocClientRequest() to allocate a variable-length client request
- * message of a specific type.
+ * Allocates a variable-length client request message of a specific type.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  * @param type Type of response which includes an arrayed member.
  * @param count number of items in the array of arrayField.
  * @param arrayField The arrayed member field.
@@ -63,27 +61,27 @@
       clientState, sizeof(type) + (count)*sizeof_member(type, arrayField[0]))
 
 /**
- * Maintains the basic state of a client.
- * This is expected to be included once in the (context) status variable of
- * each client.
+ * Allocates a variable-length notification of a specific type.
+ *
+ * @param type Type of notification which includes an arrayed member.
+ * @param count number of items in the array of arrayField.
+ * @param arrayField The arrayed member field.
+ *
+ * @return Pointer to allocated memory
  */
-struct ChppClientState {
-  struct ChppAppState *appContext;  // Pointer to app layer context
-  struct ChppRequestResponseState
-      *rRStates;        // Pointer to array of request-response states, if any
-  uint8_t index;        // Index of this client
-  uint8_t handle;       // Handle number for this client
-  uint8_t transaction;  // Next Transaction ID to be used
+#define chppAllocClientNotificationTypedArray(type, count, arrayField) \
+  (type *)chppAllocClientNotification(                                 \
+      sizeof(type) + (count)*sizeof_member(type, arrayField[0]))
 
-  uint8_t openState;         // As defined in enum ChppOpenState
-  bool pseudoOpen : 1;       // Client to be opened upon a reset
-  bool initialized : 1;      // Is initialized
-  bool everInitialized : 1;  // Synchronization primitives initialized
-
-  bool responseReady : 1;  // For sync. request/responses
-  struct ChppMutex responseMutex;
-  struct ChppConditionVariable responseCondVar;
-};
+/**
+ * Allocates a notification of a specific type and its corresponding length.
+ *
+ * @param type Type of notification.
+ *
+ * @return Pointer to allocated memory
+ */
+#define chppAllocClientNotificationFixed(type) \
+  (type *)chppAllocClientNotification(sizeof(type))
 
 #ifdef CHPP_CLIENT_ENABLED_CHRE_WWAN
 #define CHPP_CLIENT_ENABLED_WWAN
@@ -105,12 +103,6 @@
 #define CHPP_CLIENT_ENABLED
 #endif
 
-#define CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE CHPP_TIME_MAX
-
-#ifndef CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT
-#define CHPP_CLIENT_REQUEST_TIMEOUT_DEFAULT CHRE_ASYNC_RESULT_TIMEOUT_NS
-#endif
-
 // Default timeout for discovery completion.
 #ifndef CHPP_DISCOVERY_DEFAULT_TIMEOUT_MS
 #define CHPP_DISCOVERY_DEFAULT_TIMEOUT_MS UINT64_C(10000)  // 10s
@@ -149,19 +141,27 @@
  * When a match succeeds, the client's initialization function (pointer) is
  * called, assigning them their handle number.
  *
+ * outReqStates must point to an array of ChppOutgoingRequestState with
+ * ChppEndpointState.outReqCount elements. It must be NULL when the client
+ * does not send requests (ChppEndpointState.outReqCount = 0).
+ *
+ * inReqStates must point to an array of ChppIncomingRequestState with
+ * as many elements as the corresponding service can send. It must be NULL when
+ * the service does not send requests (ChppEndpointState.outReqCount = 0).
+ *
  * Note that the maximum number of clients that can be registered on a platform
  * can specified as CHPP_MAX_REGISTERED_CLIENTS by the initialization code.
  * Otherwise, a default value will be used.
  *
  * @param appContext State of the app layer.
  * @param clientContext State of the client instance.
- * @param clientState State variable of the client.
- * @param rRStates Pointer to array of request-response states, if any.
+ * @param clientState State of the client.
+ * @param outReqStates List of outgoing request states.
  * @param newClient The client to be registered on this platform.
  */
 void chppRegisterClient(struct ChppAppState *appContext, void *clientContext,
-                        struct ChppClientState *clientState,
-                        struct ChppRequestResponseState *rRStates,
+                        struct ChppEndpointState *clientState,
+                        struct ChppOutgoingRequestState *outReqStates,
                         const struct ChppClient *newClient);
 
 /**
@@ -175,17 +175,17 @@
  * Initializes a client. This function must be called when a client is matched
  * with a service during discovery to provides its handle number.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  * @param handle Handle number for this client.
  */
-void chppClientInit(struct ChppClientState *clientState, uint8_t handle);
+void chppClientInit(struct ChppEndpointState *clientState, uint8_t handle);
 
 /**
  * Deinitializes a client.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  */
-void chppClientDeinit(struct ChppClientState *clientState);
+void chppClientDeinit(struct ChppEndpointState *clientState);
 
 /**
  * Deinitializes basic clients.
@@ -202,140 +202,102 @@
 void chppDeinitMatchedClients(struct ChppAppState *context);
 
 /**
- * Allocates a client request message of a specified length, populating the
- * (app layer) client request header, including the sequence ID. The
- * next-sequence ID stored in the client state variable is subsequently
- * incremented.
+ * Allocates a client request message of a specified length.
  *
- * It is expected that for most use cases, the chppAllocClientRequestFixed()
- * or chppAllocClientRequestTypedArray() macros shall be used rather than
- * calling this function directly.
+ * It populates the request header, including the transaction number which is
+ * then incremented.
  *
- * @param clientState State variable of the client.
+ * For most use cases, the chppAllocClientRequestFixed() or
+ * chppAllocClientRequestTypedArray() macros shall be preferred.
+ *
+ * @param clientState State of the client.
  * @param len Length of the response message (including header) in bytes. Note
- * that the specified length must be at least equal to the lendth of the app
- * layer header.
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
  *
  * @return Pointer to allocated memory
  */
 struct ChppAppHeader *chppAllocClientRequest(
-    struct ChppClientState *clientState, size_t len);
+    struct ChppEndpointState *clientState, size_t len);
 
 /**
- * Uses chppAllocClientRequest() to allocate a specific client request command
- * without any additional payload.
+ * Allocates a specific client request command without any additional payload.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  * @param command Type of response.
  *
  * @return Pointer to allocated memory
  */
 struct ChppAppHeader *chppAllocClientRequestCommand(
-    struct ChppClientState *clientState, uint16_t command);
+    struct ChppEndpointState *clientState, uint16_t command);
 
 /**
- * This function shall be called for all outgoing client requests in order to
- * A) Timestamp them, and
- * B) Save their Transaction ID
- * as part of the request/response's ChppRequestResponseState struct.
- *
- * This function prints an error message if a duplicate request is sent
- * while outstanding request is still pending without a response.
- *
- * @param clientState State of the client sending the client request.
- * @param transactionId The transaction ID to use when loading the app.
- * @param rRState Maintains the basic state for each request/response
- * functionality of a client.
- * @param requestHeader Client request header.
- */
-void chppClientTimestampRequest(struct ChppClientState *clientState,
-                                struct ChppRequestResponseState *rRState,
-                                struct ChppAppHeader *requestHeader,
-                                uint64_t timeoutNs);
-
-/**
- * This function shall be called for incoming responses to a client request in
- * order to
- * A) Verify the correct transaction ID
- * B) Timestamp them, and
- * C) Mark them as fulfilled
- * D) TODO: check for timeout
- *
- * This function prints an error message if a response is received without an
- * outstanding request.
- *
- * @param clientState State of the client sending the client request.
- * @param rRState Maintains the basic state for each request/response
- * functionality of a client.
- * @param requestHeader Client request header.
- *
- * @return false if there is an error. True otherwise.
- */
-bool chppClientTimestampResponse(struct ChppClientState *clientState,
-                                 struct ChppRequestResponseState *rRState,
-                                 const struct ChppAppHeader *responseHeader);
-
-/**
- * Timestamps a client request using chppClientTimestampResponse() and enqueues
- * it using chppEnqueueTxDatagramOrFail().
- *
- * Refer to their respective documentation for details.
+ * Timestamps and enqueues a request.
  *
  * Note that the ownership of buf is taken from the caller when this method is
  * invoked.
  *
- * @param clientState State of the client sending the client request.
- * @param rRState Maintains the basic state for each request/response
- * functionality of a client.
+ * @param clientState State of the client sending the request.
+ * @param outReqState State of the request/response
  * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
  * @param len Datagram length in bytes.
  * @param timeoutNs Time in nanoseconds before a timeout response is generated.
- * Zero means no timeout response.
+ *        Zero means no timeout response.
  *
  * @return True informs the sender that the datagram was successfully enqueued.
- * False informs the sender that the queue was full and the payload discarded.
+ *         False informs the sender that the queue was full and the payload
+ *         discarded.
  */
-bool chppSendTimestampedRequestOrFail(struct ChppClientState *clientState,
-                                      struct ChppRequestResponseState *rRState,
-                                      void *buf, size_t len,
-                                      uint64_t timeoutNs);
+bool chppClientSendTimestampedRequestOrFail(
+    struct ChppEndpointState *clientState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs);
 
 /**
- * Similar to chppSendTimestampedRequestOrFail() but blocks execution until a
- * response is received. Used for synchronous requests.
+ * Similar to chppClientSendTimestampedRequestOrFail() but blocks execution
+ * until a response is received. Used for synchronous requests.
  *
  * In order to use this function, clientState->responseNotifier must have been
  * initialized using chppNotifierInit() upon initialization of the client.
  *
- * @param clientState State of the client sending the client request.
- * @param rRState Maintains the basic state for each request/response
- * functionality of a client.
+ * @param clientState State of the client sending the request.
+ * @param outReqState State of the request/response.
  * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
  * @param len Datagram length in bytes.
  *
  * @return True informs the sender that the datagram was successfully enqueued.
- * False informs the sender that the payload was discarded because either the
- * queue was full, or the request timed out.
+ *         False informs the sender that the payload was discarded because
+ *         either the queue was full, or the request timed out.
  */
-bool chppSendTimestampedRequestAndWait(struct ChppClientState *clientState,
-                                       struct ChppRequestResponseState *rRState,
-                                       void *buf, size_t len);
+bool chppClientSendTimestampedRequestAndWait(
+    struct ChppEndpointState *clientState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len);
 
 /**
- * Same as chppSendTimestampedRequestAndWait() but with a specified timeout.
+ * Same as chppClientSendTimestampedRequestAndWait() but with a specified
+ * timeout.
+ *
+ * @param clientState State of the client sending the request.
+ * @param outReqState State of the request/response.
+ * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
+ * @param len Datagram length in bytes.
+ *
+ * @return True informs the sender that the datagram was successfully enqueued.
+ *         False informs the sender that the payload was discarded because
+ *         either the queue was full, or the request timed out.
  */
-bool chppSendTimestampedRequestAndWaitTimeout(
-    struct ChppClientState *clientState,
-    struct ChppRequestResponseState *rRState, void *buf, size_t len,
+bool chppClientSendTimestampedRequestAndWaitTimeout(
+    struct ChppEndpointState *clientState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
     uint64_t timeoutNs);
 
 /**
  * Marks a closed client as pseudo-open, so that it would be opened upon a
  * reset.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  */
-void chppClientPseudoOpen(struct ChppClientState *clientState);
+void chppClientPseudoOpen(struct ChppEndpointState *clientState);
 
 /**
  * Sends a client request for the open command in a blocking or non-blocking
@@ -343,46 +305,59 @@
  * A non-blocking open is used to for reopening a service after a reset or for
  * opening a pseudo-open service.
  *
- * @param clientState State variable of the client.
- * @param openRRState Request/response state for the open command.
+ * @param clientState State of the client.
+ * @param openReqState State of the request/response for the open command.
  * @param openCommand Open command to be sent.
  * @param blocking Indicates a blocking (vs. non-blocking) open request.
  *
  * @return Indicates success or failure.
  */
-bool chppClientSendOpenRequest(struct ChppClientState *clientState,
-                               struct ChppRequestResponseState *openRRState,
+bool chppClientSendOpenRequest(struct ChppEndpointState *clientState,
+                               struct ChppOutgoingRequestState *openReqState,
                                uint16_t openCommand, bool blocking);
 
 /**
  * Processes a service response for the open command.
  *
- * @param clientState State variable of the client.
+ * @param clientState State of the client.
  */
-void chppClientProcessOpenResponse(struct ChppClientState *clientState,
+void chppClientProcessOpenResponse(struct ChppEndpointState *clientState,
                                    uint8_t *buf, size_t len);
 
 /**
- * Recalculates the next upcoming client request timeout time.
- *
- * @param context State of the app layer.
- */
-void chppClientRecalculateNextTimeout(struct ChppAppState *context);
+ * Closes any remaining open requests by simulating a timeout.
 
-/**
- * Closes any remaining open requests for a given client by sending a timeout.
  * This function is used when a client is reset.
  *
- * @param clientState State variable of the client.
- * @param client The client for whech to clear out open requests.
+ * @param clientState State of the client.
+ * @param client The client for which to clear out open requests.
  * @param clearOnly If true, indicates that a timeout response shouldn't be
- *     sent to the client. This must only be set if the requests are being
- *     cleared as part of the client closing.
+ *        sent. This must only be set if the requests are being cleared as part
+ *        of the client closing.
  */
-void chppClientCloseOpenRequests(struct ChppClientState *clientState,
+void chppClientCloseOpenRequests(struct ChppEndpointState *clientState,
                                  const struct ChppClient *client,
                                  bool clearOnly);
 
+/**
+ * Allocates a client notification of a specified length.
+ *
+ * It is expected that for most use cases, the
+ * chppAllocClientNotificationFixed() or
+ * chppAllocClientNotificationTypedArray() macros shall be used rather than
+ * calling this function directly.
+ *
+ * The caller must initialize at least the handle and command fields of the
+ * ChppAppHeader.
+ *
+ * @param len Length of the notification (including header) in bytes. Note
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
+ *
+ * @return Pointer to allocated memory.
+ */
+struct ChppAppHeader *chppAllocClientNotification(size_t len);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/chpp/include/chpp/clients/gnss.h b/chpp/include/chpp/clients/gnss.h
index 74f5022..a26dcbe 100644
--- a/chpp/include/chpp/clients/gnss.h
+++ b/chpp/include/chpp/clients/gnss.h
@@ -50,9 +50,9 @@
 void chppDeregisterGnssClient(struct ChppAppState *appContext);
 
 /**
- * @return The ChppClientState pointer to the GNSS client.
+ * @return The ChppEndpointState pointer to the GNSS client.
  */
-struct ChppClientState *getChppGnssClientState(void);
+struct ChppEndpointState *getChppGnssClientState(void);
 
 #ifndef CHPP_CLIENT_ENABLED_CHRE_GNSS
 /**
diff --git a/chpp/include/chpp/clients/wifi.h b/chpp/include/chpp/clients/wifi.h
index e0384cd..f750c76 100644
--- a/chpp/include/chpp/clients/wifi.h
+++ b/chpp/include/chpp/clients/wifi.h
@@ -50,9 +50,9 @@
 void chppDeregisterWifiClient(struct ChppAppState *appContext);
 
 /**
- * @return The ChppClientState pointer to the WiFi client.
+ * @return The ChppEndpointState pointer to the WiFi client.
  */
-struct ChppClientState *getChppWifiClientState(void);
+struct ChppEndpointState *getChppWifiClientState(void);
 
 #ifndef CHPP_CLIENT_ENABLED_CHRE_WIFI
 /**
diff --git a/chpp/include/chpp/clients/wwan.h b/chpp/include/chpp/clients/wwan.h
index 3b10f2a..41a680c 100644
--- a/chpp/include/chpp/clients/wwan.h
+++ b/chpp/include/chpp/clients/wwan.h
@@ -50,9 +50,9 @@
 void chppDeregisterWwanClient(struct ChppAppState *appContext);
 
 /**
- * @return The ChppClientState pointer to the WWAN client.
+ * @return The ChppEndpointState pointer to the WWAN client.
  */
-struct ChppClientState *getChppWwanClientState(void);
+struct ChppEndpointState *getChppWwanClientState(void);
 
 #ifndef CHPP_CLIENT_ENABLED_CHRE_WWAN
 /**
diff --git a/chpp/include/chpp/notifier.h b/chpp/include/chpp/notifier.h
index 2624b47..de0e6bd 100644
--- a/chpp/include/chpp/notifier.h
+++ b/chpp/include/chpp/notifier.h
@@ -96,6 +96,8 @@
  * multiple events to be handled simultaneously in chppNotifierTimedWait().
  *
  * @param notifier Points to the ChppNotifier being used.
+ * @param signal The value where each bit represents a different event.
+ *               As such the value 0 will not notify any event.
  */
 static void chppNotifierSignal(struct ChppNotifier *notifier, uint32_t signal);
 
diff --git a/chpp/include/chpp/services.h b/chpp/include/chpp/services.h
index ddd5a93..a89af3c 100644
--- a/chpp/include/chpp/services.h
+++ b/chpp/include/chpp/services.h
@@ -38,21 +38,47 @@
 #endif
 
 /**
- * Uses chppAllocServiceNotification() to allocate a variable-length response
- * message of a specific type.
+ * Allocates a service request message of a specific type and its corresponding
+ * length.
+ *
+ * @param serviceState State of the service.
+ * @param type Type of response.
+ *
+ * @return Pointer to allocated memory.
+ */
+#define chppAllocServiceRequestFixed(serviceState, type) \
+  (type *)chppAllocServiceRequest(serviceState, sizeof(type))
+
+/**
+ * Allocate a variable-length service request message of a specific type.
+ *
+ * @param serviceState State of the service.
+ * @param type Type of response which includes an arrayed member.
+ * @param count number of items in the array of arrayField.
+ * @param arrayField The arrayed member field.
+ *
+ * @return Pointer to allocated memory.
+ */
+#define chppAllocServiceRequestTypedArray(serviceState, type, count, \
+                                          arrayField)                \
+  (type *)chppAllocServiceRequest(                                   \
+      serviceState, sizeof(type) + (count)*sizeof_member(type, arrayField[0]))
+
+/**
+ * Allocates a variable-length notification of a specific type.
  *
  * @param type Type of notification which includes an arrayed member.
  * @param count number of items in the array of arrayField.
  * @param arrayField The arrayed member field.
  *
- * @return Pointer to allocated memory
+ * @return Pointer to allocated memory.
  */
 #define chppAllocServiceNotificationTypedArray(type, count, arrayField) \
   (type *)chppAllocServiceNotification(                                 \
       sizeof(type) + (count)*sizeof_member(type, arrayField[0]))
 
 /**
- * Uses chppAllocServiceNotification() to allocate a response message of a
+ * Uses chppAllocServiceNotification() to allocate a response notification of a
  * specific type and its corresponding length.
  *
  * @param type Type of notification.
@@ -62,48 +88,6 @@
 #define chppAllocServiceNotificationFixed(type) \
   (type *)chppAllocServiceNotification(sizeof(type))
 
-/**
- * Uses chppAllocServiceResponse() to allocate a variable-length response
- * message of a specific type.
- *
- * @param requestHeader client request header, as per
- * chppAllocServiceResponse().
- * @param type Type of response which includes an arrayed member.
- * @param count number of items in the array of arrayField.
- * @param arrayField The arrayed member field.
- *
- * @return Pointer to allocated memory
- */
-#define chppAllocServiceResponseTypedArray(requestHeader, type, count, \
-                                           arrayField)                 \
-  (type *)chppAllocServiceResponse(                                    \
-      requestHeader,                                                   \
-      sizeof(type) + (count)*sizeof_member(type, arrayField[0]))
-
-/**
- * Uses chppAllocServiceResponse() to allocate a response message of a specific
- * type and its corresponding length.
- *
- * @param requestHeader client request header, as per
- * chppAllocServiceResponse().
- * @param type Type of response.
- *
- * @return Pointer to allocated memory
- */
-#define chppAllocServiceResponseFixed(requestHeader, type) \
-  (type *)chppAllocServiceResponse(requestHeader, sizeof(type))
-
-/**
- * Maintains the basic state of a service.
- * This is expected to be included in the state of each service.
- */
-struct ChppServiceState {
-  struct ChppAppState *appContext;  // Pointer to app layer context
-  uint8_t handle;                   // Handle number for this service
-
-  uint8_t openState;  // As defined in enum ChppOpenState
-};
-
 /************************************************
  *  Public functions
  ***********************************************/
@@ -132,17 +116,27 @@
  * server (if any), i.e. except those that are registered through
  * chppRegisterCommonServices().
  *
+ * outReqStates must point to an array of ChppOutgoingRequestState with
+ * ChppEndpointState.outReqCount elements. It must be NULL when the service
+ * does not send requests (ChppEndpointState.outReqCount = 0).
+ *
+ * inReqStates must point to an array of ChppIncomingRequestState with
+ * as many elements as the corresponding client can send. It must be NULL when
+ * the client does not send requests (ChppEndpointState.outReqCount = 0).
+ *
  * Note that the maximum number of services that can be registered on a platform
  * can specified as CHPP_MAX_REGISTERED_SERVICES by the initialization code.
  * Otherwise, a default value will be used.
  *
  * @param appContext State of the app layer.
  * @param serviceContext State of the service instance.
- * @param serviceState State variable of the client.
+ * @param serviceState State of the client.
+ * @param outReqStates List of outgoing request states.
  * @param newService The service to be registered on this platform.
  */
 void chppRegisterService(struct ChppAppState *appContext, void *serviceContext,
-                         struct ChppServiceState *serviceState,
+                         struct ChppEndpointState *serviceState,
+                         struct ChppOutgoingRequestState *outReqStates,
                          const struct ChppService *newService);
 
 /**
@@ -153,83 +147,118 @@
  * chppAllocServiceNotificationTypedArray() macros shall be used rather than
  * calling this function directly.
  *
+ * The caller must initialize at least the handle and command fields of the
+ * ChppAppHeader.
+ *
  * @param len Length of the notification (including header) in bytes. Note
- * that the specified length must be at least equal to the length of the app
- * layer header.
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
  *
  * @return Pointer to allocated memory
  */
 struct ChppAppHeader *chppAllocServiceNotification(size_t len);
 
 /**
- * Allocates a service response message of a specified length, populating the
- * (app layer) service response header according to the provided client request
- * (app layer) header.
+ * Allocates a service request message of a specified length.
  *
- * It is expected that for most use cases, the chppAllocServiceResponseFixed()
- * or chppAllocServiceResponseTypedArray() macros shall be used rather than
- * calling this function directly.
+ * It populates the request header, including the transaction number which is
+ * then incremented.
  *
- * @param requestHeader Client request header.
+ * For most use cases, the chppAllocServiceRequestFixed() or
+ * chppAllocServiceRequestTypedArray() macros shall be preferred.
+ *
+ * @param serviceState State of the service.
  * @param len Length of the response message (including header) in bytes. Note
- * that the specified length must be at least equal to the length of the app
- * layer header.
+ *        that the specified length must be at least equal to the length of the
+ *        app layer header.
  *
  * @return Pointer to allocated memory
  */
-struct ChppAppHeader *chppAllocServiceResponse(
-    const struct ChppAppHeader *requestHeader, size_t len);
+struct ChppAppHeader *chppAllocServiceRequest(
+    struct ChppEndpointState *serviceState, size_t len);
 
 /**
- * This function shall be called for all incoming client requests in order to
- * A) Timestamp them, and
- * B) Save their Transaction ID
- * as part of the request/response's ChppRequestResponseState struct.
+ * Allocates a specific service request command without any additional payload.
  *
- * This function prints an error message if a duplicate request is received
- * while outstanding request is still pending without a response.
+ * @param serviceState State of the service.
+ * @param command Type of response.
  *
- * @param rRStateState State of the current request/response.
- * @param requestHeader Client request header.
+ * @return Pointer to allocated memory
  */
-void chppServiceTimestampRequest(struct ChppRequestResponseState *rRState,
-                                 struct ChppAppHeader *requestHeader);
+struct ChppAppHeader *chppAllocServiceRequestCommand(
+    struct ChppEndpointState *serviceState, uint16_t command);
 
 /**
- * This function shall be called for the final service response to a client
- * request in order to
- * A) Timestamp them, and
- * B) Mark them as fulfilled
- * part of the request/response's ChppRequestResponseState struct.
+ * Timestamps and enqueues a request.
  *
- * For most responses, it is expected that chppSendTimestampedResponseOrFail()
- * shall be used to both timestamp and send the response in one shot.
+ * Note that the ownership of buf is taken from the caller when this method is
+ * invoked.
  *
- * @param rRState State of the current request/response.
- * @return The last response time (CHPP_TIME_NONE for the first response).
+ * @param serviceState State of the service sending the request.
+ * @param outReqState State of the request/response.
+ * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
+ * @param len Datagram length in bytes.
+ * @param timeoutNs Time in nanoseconds before a timeout response is generated.
+ *        Zero means no timeout response.
+ *
+ * @return True informs the sender that the datagram was successfully enqueued.
+ *         False informs the sender that the queue was full and the payload
+ *         discarded.
  */
-uint64_t chppServiceTimestampResponse(struct ChppRequestResponseState *rRState);
+bool chppServiceSendTimestampedRequestOrFail(
+    struct ChppEndpointState *serviceState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs);
 
 /**
- * Timestamps a service response using chppServiceTimestampResponse() and
- * enqueues it using chppEnqueueTxDatagramOrFail().
+ * Similar to chppServiceSendTimestampedRequestOrFail() but blocks execution
+ * until a response is received. Used for synchronous requests.
  *
- * Refer to their respective documentation for details.
- *
- * This function logs an error message if a response is attempted without an
- * outstanding request.
- *
- * @param serviceState State of the service sending the response service.
- * @param rRState State of the current request/response.
+ * @param serviceState State of the service sending the request.
+ * @param outReqState State of the request/response.
  * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
  * @param len Datagram length in bytes.
  *
  * @return True informs the sender that the datagram was successfully enqueued.
- * False informs the sender that the queue was full and the payload discarded.
+ *         False informs the sender that the payload was discarded because
+ *         either the queue was full, or the request timed out.
  */
-bool chppSendTimestampedResponseOrFail(struct ChppServiceState *serviceState,
-                                       struct ChppRequestResponseState *rRState,
-                                       void *buf, size_t len);
+bool chppServiceSendTimestampedRequestAndWait(
+    struct ChppEndpointState *serviceState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len);
+
+/**
+ * Same as chppClientSendTimestampedRequestAndWait() but with a specified
+ * timeout.
+ *
+ * @param serviceState State of the service sending the request.
+ * @param outReqState State of the request/response.
+ * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
+ * @param len Datagram length in bytes.
+ *
+ * @return True informs the sender that the datagram was successfully enqueued.
+ *         False informs the sender that the payload was discarded because
+ *         either the queue was full, or the request timed out.
+ */
+bool chppServiceSendTimestampedRequestAndWaitTimeout(
+    struct ChppEndpointState *serviceState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs);
+
+/**
+ * Closes any remaining open requests by simulating a timeout.
+ *
+ * This function is used when a service is reset.
+ *
+ * @param serviceState State of the service.
+ * @param service The service for which to clear out open requests.
+ * @param clearOnly If true, indicates that a timeout response shouldn't be
+ *        sent. This must only be set if the requests are being cleared as
+ *        part of the closing.
+ */
+void chppServiceCloseOpenRequests(struct ChppEndpointState *serviceState,
+                                  const struct ChppService *service,
+                                  bool clearOnly);
 
 #ifdef __cplusplus
 }
diff --git a/chpp/include/chpp/transport.h b/chpp/include/chpp/transport.h
index 19c701c..f25b321 100644
--- a/chpp/include/chpp/transport.h
+++ b/chpp/include/chpp/transport.h
@@ -295,7 +295,7 @@
   uint16_t reserved3;
 } CHPP_PACKED_ATTR;
 CHPP_PACKED_END
-// LINT.ThenChange(chpp/test/packet_util.cpp)
+// LINT.ThenChange(../../../chpp/test/packet_util.cpp)
 
 struct ChppRxStatus {
   //! Current receiving state, as described in ChppRxState.
diff --git a/chpp/services.c b/chpp/services.c
index 3445524..0420bfe 100644
--- a/chpp/services.c
+++ b/chpp/services.c
@@ -23,7 +23,9 @@
 
 #include "chpp/app.h"
 #include "chpp/log.h"
+#include "chpp/macros.h"
 #include "chpp/memory.h"
+#include "chpp/mutex.h"
 #ifdef CHPP_SERVICE_ENABLED_GNSS
 #include "chpp/services/gnss.h"
 #endif
@@ -33,7 +35,6 @@
 #ifdef CHPP_SERVICE_ENABLED_WWAN
 #include "chpp/services/wwan.h"
 #endif
-#include "chpp/time.h"
 #include "chpp/transport.h"
 
 /************************************************
@@ -41,6 +42,7 @@
  ***********************************************/
 
 void chppRegisterCommonServices(struct ChppAppState *context) {
+  CHPP_DEBUG_NOT_NULL(context);
   UNUSED_VAR(context);
 
 #ifdef CHPP_SERVICE_ENABLED_WWAN
@@ -63,6 +65,7 @@
 }
 
 void chppDeregisterCommonServices(struct ChppAppState *context) {
+  CHPP_DEBUG_NOT_NULL(context);
   UNUSED_VAR(context);
 
 #ifdef CHPP_SERVICE_ENABLED_WWAN
@@ -85,7 +88,8 @@
 }
 
 void chppRegisterService(struct ChppAppState *appContext, void *serviceContext,
-                         struct ChppServiceState *serviceState,
+                         struct ChppEndpointState *serviceState,
+                         struct ChppOutgoingRequestState *outReqStates,
                          const struct ChppService *newService) {
   CHPP_DEBUG_NOT_NULL(appContext);
   CHPP_DEBUG_NOT_NULL(serviceContext);
@@ -96,6 +100,8 @@
 
   serviceState->openState = CHPP_OPEN_STATE_CLOSED;
   serviceState->appContext = appContext;
+  serviceState->outReqStates = outReqStates;
+  serviceState->context = serviceContext;
 
   if (numServices >= CHPP_MAX_REGISTERED_SERVICES) {
     CHPP_LOGE("Max services registered: # %" PRIu8, numServices);
@@ -103,12 +109,16 @@
     return;
   }
 
+  serviceState->index = numServices;
   serviceState->handle = CHPP_SERVICE_HANDLE_OF_INDEX(numServices);
 
   appContext->registeredServices[numServices] = newService;
-  appContext->registeredServiceContexts[numServices] = serviceContext;
+  appContext->registeredServiceStates[numServices] = serviceState;
   appContext->registeredServiceCount++;
 
+  chppMutexInit(&serviceState->syncResponse.mutex);
+  chppConditionVariableInit(&serviceState->syncResponse.condVar);
+
   char uuidText[CHPP_SERVICE_UUID_STRING_LEN];
   chppUuidToStr(newService->descriptor.uuid, uuidText);
   CHPP_LOGD("Registered service # %" PRIu8
@@ -119,79 +129,64 @@
             uuidText, newService->descriptor.version.major,
             newService->descriptor.version.minor,
             newService->descriptor.version.patch, newService->minLength);
-
-  return;
 }
 
 struct ChppAppHeader *chppAllocServiceNotification(size_t len) {
-  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
+  return chppAllocNotification(CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION, len);
+}
 
-  struct ChppAppHeader *result = chppMalloc(len);
-  if (result) {
-    result->handle = CHPP_HANDLE_NONE;
-    result->type = CHPP_MESSAGE_TYPE_SERVICE_NOTIFICATION;
-    result->transaction = 0;
-    result->error = CHPP_APP_ERROR_NONE;
-    result->command = CHPP_APP_COMMAND_NONE;
+struct ChppAppHeader *chppAllocServiceRequest(
+    struct ChppEndpointState *serviceState, size_t len) {
+  CHPP_DEBUG_NOT_NULL(serviceState);
+  return chppAllocRequest(CHPP_MESSAGE_TYPE_SERVICE_REQUEST, serviceState, len);
+}
+
+struct ChppAppHeader *chppAllocServiceRequestCommand(
+    struct ChppEndpointState *serviceState, uint16_t command) {
+  struct ChppAppHeader *request =
+      chppAllocServiceRequest(serviceState, sizeof(struct ChppAppHeader));
+
+  if (request != NULL) {
+    request->command = command;
   }
-  return result;
+  return request;
 }
 
-struct ChppAppHeader *chppAllocServiceResponse(
-    const struct ChppAppHeader *requestHeader, size_t len) {
-  CHPP_ASSERT(len >= sizeof(struct ChppAppHeader));
-
-  struct ChppAppHeader *result = chppMalloc(len);
-  if (result) {
-    *result = *requestHeader;
-    result->type = CHPP_MESSAGE_TYPE_SERVICE_RESPONSE;
-    result->error = CHPP_APP_ERROR_NONE;
-  }
-  return result;
+bool chppServiceSendTimestampedRequestOrFail(
+    struct ChppEndpointState *serviceState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs) {
+  return chppSendTimestampedRequestOrFail(serviceState, outReqState, buf, len,
+                                          timeoutNs);
 }
 
-void chppServiceTimestampRequest(struct ChppRequestResponseState *rRState,
-                                 struct ChppAppHeader *requestHeader) {
-  if (rRState->responseTimeNs == CHPP_TIME_NONE &&
-      rRState->requestTimeNs != CHPP_TIME_NONE) {
-    CHPP_LOGE("RX dupe req t=%" PRIu64,
-              rRState->requestTimeNs / CHPP_NSEC_PER_MSEC);
-  }
-  rRState->requestTimeNs = chppGetCurrentTimeNs();
-  rRState->responseTimeNs = CHPP_TIME_NONE;
-  rRState->transaction = requestHeader->transaction;
+bool chppServiceSendTimestampedRequestAndWait(
+    struct ChppEndpointState *serviceState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len) {
+  return chppServiceSendTimestampedRequestAndWaitTimeout(
+      serviceState, outReqState, buf, len, CHPP_REQUEST_TIMEOUT_DEFAULT);
 }
 
-uint64_t chppServiceTimestampResponse(
-    struct ChppRequestResponseState *rRState) {
-  uint64_t previousResponseTime = rRState->responseTimeNs;
-  rRState->responseTimeNs = chppGetCurrentTimeNs();
-  return previousResponseTime;
-}
+bool chppServiceSendTimestampedRequestAndWaitTimeout(
+    struct ChppEndpointState *serviceState,
+    struct ChppOutgoingRequestState *outReqState, void *buf, size_t len,
+    uint64_t timeoutNs) {
+  CHPP_DEBUG_NOT_NULL(serviceState);
 
-bool chppSendTimestampedResponseOrFail(struct ChppServiceState *serviceState,
-                                       struct ChppRequestResponseState *rRState,
-                                       void *buf, size_t len) {
-  uint64_t previousResponseTime = chppServiceTimestampResponse(rRState);
+  bool result = chppServiceSendTimestampedRequestOrFail(
+      serviceState, outReqState, buf, len, CHPP_REQUEST_TIMEOUT_INFINITE);
 
-  if (rRState->requestTimeNs == CHPP_TIME_NONE) {
-    CHPP_LOGE("TX response w/ no req t=%" PRIu64,
-              rRState->responseTimeNs / CHPP_NSEC_PER_MSEC);
-
-  } else if (previousResponseTime != CHPP_TIME_NONE) {
-    CHPP_LOGW("TX additional response t=%" PRIu64 " for req t=%" PRIu64,
-              rRState->responseTimeNs / CHPP_NSEC_PER_MSEC,
-              rRState->requestTimeNs / CHPP_NSEC_PER_MSEC);
-
-  } else {
-    CHPP_LOGD("Sending initial response at t=%" PRIu64
-              " for request at t=%" PRIu64 " (RTT=%" PRIu64 ")",
-              rRState->responseTimeNs / CHPP_NSEC_PER_MSEC,
-              rRState->requestTimeNs / CHPP_NSEC_PER_MSEC,
-              (rRState->responseTimeNs - rRState->requestTimeNs) /
-                  CHPP_NSEC_PER_MSEC);
+  if (!result) {
+    return false;
   }
 
-  return chppEnqueueTxDatagramOrFail(serviceState->appContext->transportContext,
-                                     buf, len);
+  return chppWaitForResponseWithTimeout(&serviceState->syncResponse,
+                                        outReqState, timeoutNs);
 }
+
+void chppServiceCloseOpenRequests(struct ChppEndpointState *serviceState,
+                                  const struct ChppService *service,
+                                  bool clearOnly) {
+  UNUSED_VAR(service);
+  chppCloseOpenRequests(serviceState, CHPP_ENDPOINT_SERVICE, clearOnly);
+}
\ No newline at end of file
diff --git a/chpp/services/discovery.c b/chpp/services/discovery.c
index 4ed5491..2712a64 100644
--- a/chpp/services/discovery.c
+++ b/chpp/services/discovery.c
@@ -51,9 +51,9 @@
       sizeof(struct ChppAppHeader) +
       context->registeredServiceCount * sizeof(struct ChppServiceDescriptor);
 
-  struct ChppDiscoveryResponse *response = chppAllocServiceResponseTypedArray(
-      requestHeader, struct ChppDiscoveryResponse,
-      context->registeredServiceCount, services);
+  struct ChppDiscoveryResponse *response =
+      chppAllocResponseTypedArray(requestHeader, struct ChppDiscoveryResponse,
+                                  context->registeredServiceCount, services);
 
   if (response == NULL) {
     CHPP_LOG_OOM();
diff --git a/chpp/services/gnss.c b/chpp/services/gnss.c
index 33f2ca4..57bdf24 100644
--- a/chpp/services/gnss.c
+++ b/chpp/services/gnss.c
@@ -72,18 +72,18 @@
  * (RR) functionality.
  */
 struct ChppGnssServiceState {
-  struct ChppServiceState service;   // GNSS service state
+  struct ChppEndpointState service;  // CHPP service state
   const struct chrePalGnssApi *api;  // GNSS PAL API
 
   // Based on chre/pal/gnss.h and chrePalGnssApi
-  struct ChppRequestResponseState open;             // Service init state
-  struct ChppRequestResponseState close;            // Service deinit state
-  struct ChppRequestResponseState getCapabilities;  // Get Capabilities state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState open;             // Service init state
+  struct ChppIncomingRequestState close;            // Service deinit state
+  struct ChppIncomingRequestState getCapabilities;  // Get Capabilities state
+  struct ChppIncomingRequestState
       controlLocationSession;  // Control Location measurement state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState
       controlMeasurementSession;  // Control Raw GNSS measurement state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState
       configurePassiveLocationListener;  // Configure Passive location receiving
                                          // state
 };
@@ -153,51 +153,51 @@
 
   struct ChppGnssServiceState *gnssServiceContext =
       (struct ChppGnssServiceState *)serviceContext;
-  struct ChppRequestResponseState *rRState = NULL;
+  struct ChppIncomingRequestState *inReqState = NULL;
   enum ChppAppErrorCode error = CHPP_APP_ERROR_NONE;
   bool dispatched = true;
 
   switch (rxHeader->command) {
     case CHPP_GNSS_OPEN: {
-      rRState = &gnssServiceContext->open;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &gnssServiceContext->open;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppGnssServiceOpen(gnssServiceContext, rxHeader);
       break;
     }
 
     case CHPP_GNSS_CLOSE: {
-      rRState = &gnssServiceContext->close;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &gnssServiceContext->close;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppGnssServiceClose(gnssServiceContext, rxHeader);
       break;
     }
 
     case CHPP_GNSS_GET_CAPABILITIES: {
-      rRState = &gnssServiceContext->getCapabilities;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &gnssServiceContext->getCapabilities;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppGnssServiceGetCapabilities(gnssServiceContext, rxHeader);
       break;
     }
 
     case CHPP_GNSS_CONTROL_LOCATION_SESSION: {
-      rRState = &gnssServiceContext->controlLocationSession;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &gnssServiceContext->controlLocationSession;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppGnssServiceControlLocationSession(gnssServiceContext,
                                                     rxHeader, buf, len);
       break;
     }
 
     case CHPP_GNSS_CONTROL_MEASUREMENT_SESSION: {
-      rRState = &gnssServiceContext->controlMeasurementSession;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &gnssServiceContext->controlMeasurementSession;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppGnssServiceControlMeasurementSession(gnssServiceContext,
                                                        rxHeader, buf, len);
       break;
     }
 
     case CHPP_GNSS_CONFIGURE_PASSIVE_LOCATION_LISTENER: {
-      rRState = &gnssServiceContext->configurePassiveLocationListener;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &gnssServiceContext->configurePassiveLocationListener;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppGnssServiceConfigurePassiveLocationListener(
           gnssServiceContext, rxHeader, buf, len);
       break;
@@ -212,8 +212,8 @@
 
   if (dispatched == true && error != CHPP_APP_ERROR_NONE) {
     // Request was dispatched but an error was returned. Close out
-    // chppServiceTimestampRequest()
-    chppServiceTimestampResponse(rRState);
+    // chppTimestampIncomingRequest()
+    chppTimestampOutgoingResponse(inReqState);
   }
 
   return error;
@@ -258,14 +258,14 @@
     gnssServiceContext->service.openState = CHPP_OPEN_STATE_OPENED;
 
     struct ChppAppHeader *response =
-        chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+        chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
     size_t responseLen = sizeof(*response);
 
     if (response == NULL) {
       CHPP_LOG_OOM();
       error = CHPP_APP_ERROR_OOM;
     } else {
-      chppSendTimestampedResponseOrFail(&gnssServiceContext->service,
+      chppSendTimestampedResponseOrFail(gnssServiceContext->service.appContext,
                                         &gnssServiceContext->open, response,
                                         responseLen);
     }
@@ -293,14 +293,14 @@
   CHPP_LOGD("GNSS service closed");
 
   struct ChppAppHeader *response =
-      chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+      chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
     CHPP_LOG_OOM();
     error = CHPP_APP_ERROR_OOM;
   } else {
-    chppSendTimestampedResponseOrFail(&gnssServiceContext->service,
+    chppSendTimestampedResponseOrFail(gnssServiceContext->service.appContext,
                                       &gnssServiceContext->close, response,
                                       responseLen);
   }
@@ -340,9 +340,8 @@
     struct ChppAppHeader *requestHeader) {
   enum ChppAppErrorCode error = CHPP_APP_ERROR_NONE;
 
-  struct ChppGnssGetCapabilitiesResponse *response =
-      chppAllocServiceResponseFixed(requestHeader,
-                                    struct ChppGnssGetCapabilitiesResponse);
+  struct ChppGnssGetCapabilitiesResponse *response = chppAllocResponseFixed(
+      requestHeader, struct ChppGnssGetCapabilitiesResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
@@ -354,7 +353,7 @@
     CHPP_LOGD("chppGnssServiceGetCapabilities returning 0x%" PRIx32
               ", %" PRIuSIZE " bytes",
               response->params.capabilities, responseLen);
-    chppSendTimestampedResponseOrFail(&gnssServiceContext->service,
+    chppSendTimestampedResponseOrFail(gnssServiceContext->service.appContext,
                                       &gnssServiceContext->getCapabilities,
                                       response, responseLen);
   }
@@ -472,7 +471,7 @@
 
     } else {
       struct ChppAppHeader *response =
-          chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+          chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
       size_t responseLen = sizeof(*response);
 
       if (response == NULL) {
@@ -480,7 +479,7 @@
         error = CHPP_APP_ERROR_OOM;
       } else {
         chppSendTimestampedResponseOrFail(
-            &gnssServiceContext->service,
+            gnssServiceContext->service.appContext,
             &gnssServiceContext->configurePassiveLocationListener, response,
             responseLen);
       }
@@ -527,8 +526,8 @@
   };
 
   struct ChppGnssControlLocationSessionResponse *response =
-      chppAllocServiceResponseFixed(
-          &requestHeader, struct ChppGnssControlLocationSessionResponse);
+      chppAllocResponseFixed(&requestHeader,
+                             struct ChppGnssControlLocationSessionResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
@@ -540,7 +539,7 @@
     response->errorCode = errorCode;
 
     chppSendTimestampedResponseOrFail(
-        &gGnssServiceContext.service,
+        gGnssServiceContext.service.appContext,
         &gGnssServiceContext.controlLocationSession, response, responseLen);
   }
 }
@@ -599,8 +598,8 @@
   };
 
   struct ChppGnssControlMeasurementSessionResponse *response =
-      chppAllocServiceResponseFixed(
-          &requestHeader, struct ChppGnssControlMeasurementSessionResponse);
+      chppAllocResponseFixed(&requestHeader,
+                             struct ChppGnssControlMeasurementSessionResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
@@ -612,7 +611,7 @@
     response->errorCode = errorCode;
 
     chppSendTimestampedResponseOrFail(
-        &gGnssServiceContext.service,
+        gGnssServiceContext.service.appContext,
         &gGnssServiceContext.controlMeasurementSession, response, responseLen);
   }
 }
@@ -670,7 +669,8 @@
 
   } else {
     chppRegisterService(appContext, (void *)&gGnssServiceContext,
-                        &gGnssServiceContext.service, &kGnssServiceConfig);
+                        &gGnssServiceContext.service, NULL /*outReqState*/,
+                        &kGnssServiceConfig);
     CHPP_DEBUG_ASSERT(gGnssServiceContext.service.handle);
   }
 }
diff --git a/chpp/services/timesync.c b/chpp/services/timesync.c
index c81187a..b7ad625 100644
--- a/chpp/services/timesync.c
+++ b/chpp/services/timesync.c
@@ -39,7 +39,7 @@
 static void chppTimesyncGetTime(struct ChppAppState *context,
                                 const struct ChppAppHeader *requestHeader) {
   struct ChppTimesyncResponse *response =
-      chppAllocServiceResponseFixed(requestHeader, struct ChppTimesyncResponse);
+      chppAllocResponseFixed(requestHeader, struct ChppTimesyncResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
diff --git a/chpp/services/wifi.c b/chpp/services/wifi.c
index 966e77f..090e599 100644
--- a/chpp/services/wifi.c
+++ b/chpp/services/wifi.c
@@ -73,22 +73,22 @@
  * (RR) functionality.
  */
 struct ChppWifiServiceState {
-  struct ChppServiceState service;   // WiFi service state
+  struct ChppEndpointState service;  // CHPP service state
   const struct chrePalWifiApi *api;  // WiFi PAL API
 
   // Based on chre/pal/wifi.h and chrePalWifiApi
-  struct ChppRequestResponseState open;             // Service init state
-  struct ChppRequestResponseState close;            // Service deinit state
-  struct ChppRequestResponseState getCapabilities;  // Get Capabilities state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState open;             // Service init state
+  struct ChppIncomingRequestState close;            // Service deinit state
+  struct ChppIncomingRequestState getCapabilities;  // Get Capabilities state
+  struct ChppIncomingRequestState
       configureScanMonitorAsync;  // Configure scan monitor state
-  struct ChppRequestResponseState requestScanAsync;     // Request scan state
-  struct ChppRequestResponseState requestRangingAsync;  // Request ranging state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState requestScanAsync;     // Request scan state
+  struct ChppIncomingRequestState requestRangingAsync;  // Request ranging state
+  struct ChppIncomingRequestState
       requestNanSubscribe;  // Request Nan Subscription state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState
       requestNanSubscribeCancel;  // Request Nan Subscription cancelation state
-  struct ChppRequestResponseState
+  struct ChppIncomingRequestState
       requestNanRangingAsync;  // Request NAN ranging state
 };
 
@@ -174,75 +174,75 @@
 
   struct ChppWifiServiceState *wifiServiceContext =
       (struct ChppWifiServiceState *)serviceContext;
-  struct ChppRequestResponseState *rRState = NULL;
+  struct ChppIncomingRequestState *inReqState = NULL;
   enum ChppAppErrorCode error = CHPP_APP_ERROR_NONE;
   bool dispatched = true;
 
   switch (rxHeader->command) {
     case CHPP_WIFI_OPEN: {
-      rRState = &wifiServiceContext->open;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->open;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceOpen(wifiServiceContext, rxHeader);
       break;
     }
 
     case CHPP_WIFI_CLOSE: {
-      rRState = &wifiServiceContext->close;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->close;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceClose(wifiServiceContext, rxHeader);
       break;
     }
 
     case CHPP_WIFI_GET_CAPABILITIES: {
-      rRState = &wifiServiceContext->getCapabilities;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->getCapabilities;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceGetCapabilities(wifiServiceContext, rxHeader);
       break;
     }
 
     case CHPP_WIFI_CONFIGURE_SCAN_MONITOR_ASYNC: {
-      rRState = &wifiServiceContext->configureScanMonitorAsync;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->configureScanMonitorAsync;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceConfigureScanMonitorAsync(wifiServiceContext,
                                                        rxHeader, buf, len);
       break;
     }
 
     case CHPP_WIFI_REQUEST_SCAN_ASYNC: {
-      rRState = &wifiServiceContext->requestScanAsync;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->requestScanAsync;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceRequestScanAsync(wifiServiceContext, rxHeader, buf,
                                               len);
       break;
     }
 
     case CHPP_WIFI_REQUEST_RANGING_ASYNC: {
-      rRState = &wifiServiceContext->requestRangingAsync;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->requestRangingAsync;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceRequestRangingAsync(wifiServiceContext, rxHeader,
                                                  buf, len);
       break;
     }
 
     case CHPP_WIFI_REQUEST_NAN_SUB: {
-      rRState = &wifiServiceContext->requestNanSubscribe;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->requestNanSubscribe;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceRequestNanSubscribe(wifiServiceContext, rxHeader,
                                                  buf, len);
       break;
     }
 
     case CHPP_WIFI_REQUEST_NAN_SUB_CANCEL: {
-      rRState = &wifiServiceContext->requestNanSubscribeCancel;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->requestNanSubscribeCancel;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceRequestNanSubscribeCancel(wifiServiceContext,
                                                        rxHeader, buf, len);
       break;
     };
 
     case CHPP_WIFI_REQUEST_NAN_RANGING_ASYNC: {
-      rRState = &wifiServiceContext->requestNanRangingAsync;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wifiServiceContext->requestNanRangingAsync;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWifiServiceRequestNanRanging(wifiServiceContext, rxHeader,
                                                buf, len);
       break;
@@ -257,8 +257,8 @@
 
   if (dispatched == true && error != CHPP_APP_ERROR_NONE) {
     // Request was dispatched but an error was returned. Close out
-    // chppServiceTimestampRequest()
-    chppServiceTimestampResponse(rRState);
+    // chppTimestampIncomingRequest()
+    chppTimestampOutgoingResponse(inReqState);
   }
 
   return error;
@@ -307,14 +307,14 @@
     wifiServiceContext->service.openState = CHPP_OPEN_STATE_OPENED;
 
     struct ChppAppHeader *response =
-        chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+        chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
     size_t responseLen = sizeof(*response);
 
     if (response == NULL) {
       CHPP_LOG_OOM();
       error = CHPP_APP_ERROR_OOM;
     } else {
-      chppSendTimestampedResponseOrFail(&wifiServiceContext->service,
+      chppSendTimestampedResponseOrFail(wifiServiceContext->service.appContext,
                                         &wifiServiceContext->open, response,
                                         responseLen);
     }
@@ -342,14 +342,14 @@
   CHPP_LOGD("WiFi service closed");
 
   struct ChppAppHeader *response =
-      chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+      chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
     CHPP_LOG_OOM();
     error = CHPP_APP_ERROR_OOM;
   } else {
-    chppSendTimestampedResponseOrFail(&wifiServiceContext->service,
+    chppSendTimestampedResponseOrFail(wifiServiceContext->service.appContext,
                                       &wifiServiceContext->close, response,
                                       responseLen);
   }
@@ -388,9 +388,8 @@
     struct ChppAppHeader *requestHeader) {
   enum ChppAppErrorCode error = CHPP_APP_ERROR_NONE;
 
-  struct ChppWifiGetCapabilitiesResponse *response =
-      chppAllocServiceResponseFixed(requestHeader,
-                                    struct ChppWifiGetCapabilitiesResponse);
+  struct ChppWifiGetCapabilitiesResponse *response = chppAllocResponseFixed(
+      requestHeader, struct ChppWifiGetCapabilitiesResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
@@ -402,7 +401,7 @@
     CHPP_LOGD("chppWifiServiceGetCapabilities returning 0x%" PRIx32
               ", %" PRIuSIZE " bytes",
               response->params.capabilities, responseLen);
-    chppSendTimestampedResponseOrFail(&wifiServiceContext->service,
+    chppSendTimestampedResponseOrFail(wifiServiceContext->service.appContext,
                                       &wifiServiceContext->getCapabilities,
                                       response, responseLen);
   }
@@ -532,7 +531,7 @@
 
     } else {
       struct ChppAppHeader *response =
-          chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+          chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
       size_t responseLen = sizeof(*response);
 
       if (response == NULL) {
@@ -540,7 +539,7 @@
         error = CHPP_APP_ERROR_OOM;
       } else {
         chppSendTimestampedResponseOrFail(
-            &wifiServiceContext->service,
+            wifiServiceContext->service.appContext,
             &wifiServiceContext->requestRangingAsync, response, responseLen);
       }
     }
@@ -575,7 +574,7 @@
 
     } else {
       struct ChppAppHeader *response =
-          chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+          chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
       size_t responseLen = sizeof(*response);
 
       if (response == NULL) {
@@ -583,7 +582,7 @@
         error = CHPP_APP_ERROR_OOM;
       } else {
         chppSendTimestampedResponseOrFail(
-            &wifiServiceContext->service,
+            wifiServiceContext->service.appContext,
             &wifiServiceContext->requestNanSubscribe, response, responseLen);
       }
     }
@@ -610,7 +609,7 @@
 
     } else {
       struct ChppAppHeader *response =
-          chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+          chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
       size_t responseLen = sizeof(*response);
 
       if (response == NULL) {
@@ -618,7 +617,7 @@
         error = CHPP_APP_ERROR_OOM;
       } else {
         chppSendTimestampedResponseOrFail(
-            &wifiServiceContext->service,
+            wifiServiceContext->service.appContext,
             &wifiServiceContext->requestNanSubscribeCancel, response,
             responseLen);
       }
@@ -647,7 +646,7 @@
 
     } else {
       struct ChppAppHeader *response =
-          chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+          chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
       size_t responseLen = sizeof(*response);
 
       if (response == NULL) {
@@ -655,7 +654,7 @@
         error = CHPP_APP_ERROR_OOM;
       } else {
         chppSendTimestampedResponseOrFail(
-            &wifiServiceContext->service,
+            wifiServiceContext->service.appContext,
             &wifiServiceContext->requestNanRangingAsync, response, responseLen);
       }
     }
@@ -680,8 +679,8 @@
   };
 
   struct ChppWifiConfigureScanMonitorAsyncResponse *response =
-      chppAllocServiceResponseFixed(
-          &requestHeader, struct ChppWifiConfigureScanMonitorAsyncResponse);
+      chppAllocResponseFixed(&requestHeader,
+                             struct ChppWifiConfigureScanMonitorAsyncResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
@@ -693,7 +692,7 @@
     response->params.errorCode = errorCode;
 
     chppSendTimestampedResponseOrFail(
-        &gWifiServiceContext.service,
+        gWifiServiceContext.service.appContext,
         &gWifiServiceContext.configureScanMonitorAsync, response, responseLen);
   }
 }
@@ -713,7 +712,7 @@
       .command = CHPP_WIFI_REQUEST_SCAN_ASYNC,
   };
 
-  struct ChppWifiRequestScanResponse *response = chppAllocServiceResponseFixed(
+  struct ChppWifiRequestScanResponse *response = chppAllocResponseFixed(
       &requestHeader, struct ChppWifiRequestScanResponse);
   size_t responseLen = sizeof(*response);
 
@@ -725,7 +724,7 @@
     response->params.pending = pending;
     response->params.errorCode = errorCode;
 
-    chppSendTimestampedResponseOrFail(&gWifiServiceContext.service,
+    chppSendTimestampedResponseOrFail(gWifiServiceContext.service.appContext,
                                       &gWifiServiceContext.requestScanAsync,
                                       response, responseLen);
   }
@@ -1020,7 +1019,8 @@
 
   } else {
     chppRegisterService(appContext, (void *)&gWifiServiceContext,
-                        &gWifiServiceContext.service, &kWifiServiceConfig);
+                        &gWifiServiceContext.service, NULL /*outReqStates*/,
+                        &kWifiServiceConfig);
     CHPP_DEBUG_ASSERT(gWifiServiceContext.service.handle);
   }
 }
diff --git a/chpp/services/wwan.c b/chpp/services/wwan.c
index bd28403..7f173c8 100644
--- a/chpp/services/wwan.c
+++ b/chpp/services/wwan.c
@@ -74,13 +74,13 @@
  * (RR) functionality.
  */
 struct ChppWwanServiceState {
-  struct ChppServiceState service;   // WWAN service state
+  struct ChppEndpointState service;  // CHPP service state
   const struct chrePalWwanApi *api;  // WWAN PAL API
 
-  struct ChppRequestResponseState open;              // Service init state
-  struct ChppRequestResponseState close;             // Service deinit state
-  struct ChppRequestResponseState getCapabilities;   // Get Capabilities state
-  struct ChppRequestResponseState getCellInfoAsync;  // Get CellInfo Async state
+  struct ChppIncomingRequestState open;              // Service init state
+  struct ChppIncomingRequestState close;             // Service deinit state
+  struct ChppIncomingRequestState getCapabilities;   // Get Capabilities state
+  struct ChppIncomingRequestState getCellInfoAsync;  // Get CellInfo Async state
 };
 
 // Note: This global definition of gWwanServiceContext supports only one
@@ -138,7 +138,7 @@
   struct ChppAppHeader *rxHeader = (struct ChppAppHeader *)buf;
   struct ChppWwanServiceState *wwanServiceContext =
       (struct ChppWwanServiceState *)serviceContext;
-  struct ChppRequestResponseState *rRState = NULL;
+  struct ChppIncomingRequestState *inReqState = NULL;
   enum ChppAppErrorCode error = CHPP_APP_ERROR_NONE;
   bool dispatched = true;
 
@@ -146,29 +146,29 @@
 
   switch (rxHeader->command) {
     case CHPP_WWAN_OPEN: {
-      rRState = &wwanServiceContext->open;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wwanServiceContext->open;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWwanServiceOpen(wwanServiceContext, rxHeader);
       break;
     }
 
     case CHPP_WWAN_CLOSE: {
-      rRState = &wwanServiceContext->close;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wwanServiceContext->close;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWwanServiceClose(wwanServiceContext, rxHeader);
       break;
     }
 
     case CHPP_WWAN_GET_CAPABILITIES: {
-      rRState = &wwanServiceContext->getCapabilities;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wwanServiceContext->getCapabilities;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWwanServiceGetCapabilities(wwanServiceContext, rxHeader);
       break;
     }
 
     case CHPP_WWAN_GET_CELLINFO_ASYNC: {
-      rRState = &wwanServiceContext->getCellInfoAsync;
-      chppServiceTimestampRequest(rRState, rxHeader);
+      inReqState = &wwanServiceContext->getCellInfoAsync;
+      chppTimestampIncomingRequest(inReqState, rxHeader);
       error = chppWwanServiceGetCellInfoAsync(wwanServiceContext, rxHeader);
       break;
     }
@@ -182,8 +182,8 @@
 
   if (dispatched == true && error != CHPP_APP_ERROR_NONE) {
     // Request was dispatched but an error was returned. Close out
-    // chppServiceTimestampRequest()
-    chppServiceTimestampResponse(rRState);
+    // chppTimestampIncomingRequest()
+    chppTimestampOutgoingResponse(inReqState);
   }
 
   return error;
@@ -222,14 +222,14 @@
     wwanServiceContext->service.openState = CHPP_OPEN_STATE_OPENED;
 
     struct ChppAppHeader *response =
-        chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+        chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
     size_t responseLen = sizeof(*response);
 
     if (response == NULL) {
       CHPP_LOG_OOM();
       error = CHPP_APP_ERROR_OOM;
     } else {
-      chppSendTimestampedResponseOrFail(&wwanServiceContext->service,
+      chppSendTimestampedResponseOrFail(wwanServiceContext->service.appContext,
                                         &wwanServiceContext->open, response,
                                         responseLen);
     }
@@ -257,14 +257,14 @@
   CHPP_LOGD("WWAN service closed");
 
   struct ChppAppHeader *response =
-      chppAllocServiceResponseFixed(requestHeader, struct ChppAppHeader);
+      chppAllocResponseFixed(requestHeader, struct ChppAppHeader);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
     CHPP_LOG_OOM();
     error = CHPP_APP_ERROR_OOM;
   } else {
-    chppSendTimestampedResponseOrFail(&wwanServiceContext->service,
+    chppSendTimestampedResponseOrFail(wwanServiceContext->service.appContext,
                                       &wwanServiceContext->close, response,
                                       responseLen);
   }
@@ -304,9 +304,8 @@
     struct ChppAppHeader *requestHeader) {
   enum ChppAppErrorCode error = CHPP_APP_ERROR_NONE;
 
-  struct ChppWwanGetCapabilitiesResponse *response =
-      chppAllocServiceResponseFixed(requestHeader,
-                                    struct ChppWwanGetCapabilitiesResponse);
+  struct ChppWwanGetCapabilitiesResponse *response = chppAllocResponseFixed(
+      requestHeader, struct ChppWwanGetCapabilitiesResponse);
   size_t responseLen = sizeof(*response);
 
   if (response == NULL) {
@@ -318,7 +317,7 @@
     CHPP_LOGD("chppWwanServiceGetCapabilities returning 0x%" PRIx32
               ", %" PRIuSIZE " bytes",
               response->params.capabilities, responseLen);
-    chppSendTimestampedResponseOrFail(&wwanServiceContext->service,
+    chppSendTimestampedResponseOrFail(wwanServiceContext->service.appContext,
                                       &wwanServiceContext->getCapabilities,
                                       response, responseLen);
   }
@@ -366,17 +365,17 @@
 static void chppWwanServiceCellInfoResultCallback(
     struct chreWwanCellInfoResult *result) {
   // Recover state
-  struct ChppRequestResponseState *rRState =
+  struct ChppIncomingRequestState *inReqState =
       &gWwanServiceContext.getCellInfoAsync;
   struct ChppWwanServiceState *wwanServiceContext =
-      container_of(rRState, struct ChppWwanServiceState, getCellInfoAsync);
+      container_of(inReqState, struct ChppWwanServiceState, getCellInfoAsync);
 
   // Craft response per parser script
   struct ChppWwanCellInfoResultWithHeader *response = NULL;
   size_t responseLen = 0;
   if (!chppWwanCellInfoResultFromChre(result, &response, &responseLen)) {
     CHPP_LOGE("CellInfo conversion failed (OOM?) ID=%" PRIu8,
-              rRState->transaction);
+              inReqState->transaction);
 
     response = chppMalloc(sizeof(struct ChppAppHeader));
     if (response == NULL) {
@@ -389,14 +388,14 @@
   if (response != NULL) {
     response->header.handle = wwanServiceContext->service.handle;
     response->header.type = CHPP_MESSAGE_TYPE_SERVICE_RESPONSE;
-    response->header.transaction = rRState->transaction;
+    response->header.transaction = inReqState->transaction;
     response->header.error = (responseLen > sizeof(struct ChppAppHeader))
                                  ? CHPP_APP_ERROR_NONE
                                  : CHPP_APP_ERROR_CONVERSION_FAILED;
     response->header.command = CHPP_WWAN_GET_CELLINFO_ASYNC;
 
-    chppSendTimestampedResponseOrFail(&wwanServiceContext->service, rRState,
-                                      response, responseLen);
+    chppSendTimestampedResponseOrFail(wwanServiceContext->service.appContext,
+                                      inReqState, response, responseLen);
   }
 
   gWwanServiceContext.api->releaseCellInfoResult(result);
@@ -415,7 +414,8 @@
 
   } else {
     chppRegisterService(appContext, (void *)&gWwanServiceContext,
-                        &gWwanServiceContext.service, &kWwanServiceConfig);
+                        &gWwanServiceContext.service, NULL /*outReqStates*/,
+                        &kWwanServiceConfig);
     CHPP_DEBUG_ASSERT(gWwanServiceContext.service.handle);
   }
 }
diff --git a/chpp/test/app_discovery_test.cpp b/chpp/test/app_discovery_test.cpp
new file mode 100644
index 0000000..51ab800
--- /dev/null
+++ b/chpp/test/app_discovery_test.cpp
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2023 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 <gtest/gtest.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <cstring>
+#include <thread>
+
+#include "chpp/app.h"
+#include "chpp/clients.h"
+#include "chpp/clients/discovery.h"
+#include "chpp/macros.h"
+#include "chpp/platform/platform_link.h"
+#include "chpp/platform/utils.h"
+#include "chpp/services.h"
+#include "chpp/transport.h"
+
+namespace chre {
+
+namespace {
+
+constexpr uint64_t kResetWaitTimeMs = 5000;
+constexpr uint64_t kDiscoveryWaitTimeMs = 5000;
+
+void *workThread(void *transportState) {
+  ChppTransportState *state = static_cast<ChppTransportState *>(transportState);
+
+  auto linkContext =
+      static_cast<struct ChppLinuxLinkState *>(state->linkContext);
+
+  pthread_setname_np(pthread_self(), linkContext->workThreadName);
+
+  chppWorkThreadStart(state);
+
+  return nullptr;
+}
+
+#define TEST_UUID                                                           \
+  {                                                                         \
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
+        0x00, 0x00, 0x00, 0x12                                              \
+  }
+
+constexpr uint16_t kNumCommands = 1;
+
+struct ClientState {
+  struct ChppEndpointState chppClientState;
+  struct ChppOutgoingRequestState outReqStates[kNumCommands];
+  bool resetNotified;
+  bool matchNotified;
+};
+
+void clientNotifyReset(void *clientState);
+void clientNotifyMatch(void *clientState);
+bool clientInit(void *clientState, uint8_t handle,
+                struct ChppVersion serviceVersion);
+void clientDeinit(void *clientState);
+
+constexpr struct ChppClient kClient = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .resetNotifierFunctionPtr = &clientNotifyReset,
+    .matchNotifierFunctionPtr = &clientNotifyMatch,
+    .responseDispatchFunctionPtr = nullptr,
+    .notificationDispatchFunctionPtr = nullptr,
+    .initFunctionPtr = &clientInit,
+    .deinitFunctionPtr = &clientDeinit,
+    .outReqCount = kNumCommands,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+void clientNotifyReset(void *clientState) {
+  auto state = static_cast<struct ClientState *>(clientState);
+  state->resetNotified = true;
+}
+
+void clientNotifyMatch(void *clientState) {
+  auto state = static_cast<struct ClientState *>(clientState);
+  state->matchNotified = true;
+}
+
+bool clientInit(void *clientState, uint8_t handle,
+                struct ChppVersion serviceVersion) {
+  UNUSED_VAR(serviceVersion);
+  auto state = static_cast<struct ClientState *>(clientState);
+  state->chppClientState.openState = CHPP_OPEN_STATE_OPENED;
+  chppClientInit(&state->chppClientState, handle);
+  return true;
+}
+
+void clientDeinit(void *clientState) {
+  auto state = static_cast<struct ClientState *>(clientState);
+  chppClientDeinit(&state->chppClientState);
+  state->chppClientState.openState = CHPP_OPEN_STATE_CLOSED;
+}
+
+// Service
+struct ServiceState {
+  struct ChppEndpointState chppServiceState;
+  struct ChppIncomingRequestState inReqStates[kNumCommands];
+  bool resetNotified;
+};
+
+void serviceNotifyReset(void *serviceState) {
+  auto state = static_cast<struct ServiceState *>(serviceState);
+  state->resetNotified = true;
+}
+
+const struct ChppService kService = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.name = "Test",
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .resetNotifierFunctionPtr = &serviceNotifyReset,
+    .requestDispatchFunctionPtr = nullptr,
+    .notificationDispatchFunctionPtr = nullptr,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+// Test Clients/Services discovery and matching.
+class AppDiscoveryTest : public testing::Test {
+ protected:
+  void SetUp() {
+    chppClearTotalAllocBytes();
+    memset(&mClientLinkContext, 0, sizeof(mClientLinkContext));
+    memset(&mServiceLinkContext, 0, sizeof(mServiceLinkContext));
+
+    mServiceLinkContext.linkThreadName = "Host Link";
+    mServiceLinkContext.workThreadName = "Host worker";
+    mServiceLinkContext.isLinkActive = true;
+    mServiceLinkContext.remoteLinkState = &mClientLinkContext;
+    mServiceLinkContext.rxInRemoteEndpointWorker = false;
+
+    mClientLinkContext.linkThreadName = "CHRE Link";
+    mClientLinkContext.workThreadName = "CHRE worker";
+    mClientLinkContext.isLinkActive = true;
+    mClientLinkContext.remoteLinkState = &mServiceLinkContext;
+    mClientLinkContext.rxInRemoteEndpointWorker = false;
+
+    // No default clients/services.
+    struct ChppClientServiceSet set;
+    memset(&set, 0, sizeof(set));
+
+    const struct ChppLinkApi *linkApi = getLinuxLinkApi();
+
+    // Init client side.
+    chppTransportInit(&mClientTransportContext, &mClientAppContext,
+                      &mClientLinkContext, linkApi);
+    chppAppInitWithClientServiceSet(&mClientAppContext,
+                                    &mClientTransportContext, set);
+
+    // Init service side.
+    chppTransportInit(&mServiceTransportContext, &mServiceAppContext,
+                      &mServiceLinkContext, linkApi);
+    chppAppInitWithClientServiceSet(&mServiceAppContext,
+                                    &mServiceTransportContext, set);
+  }
+
+  void TearDown() {
+    chppWorkThreadStop(&mClientTransportContext);
+    chppWorkThreadStop(&mServiceTransportContext);
+    pthread_join(mClientWorkThread, NULL);
+    pthread_join(mServiceWorkThread, NULL);
+
+    // Deinit client side.
+    chppAppDeinit(&mClientAppContext);
+    chppTransportDeinit(&mClientTransportContext);
+
+    // Deinit service side.
+    chppAppDeinit(&mServiceAppContext);
+    chppTransportDeinit(&mServiceTransportContext);
+
+    EXPECT_EQ(chppGetTotalAllocBytes(), 0);
+  }
+
+  // Client side.
+  ChppLinuxLinkState mClientLinkContext = {};
+  ChppTransportState mClientTransportContext = {};
+  ChppAppState mClientAppContext = {};
+  pthread_t mClientWorkThread;
+  ClientState mClientState;
+
+  // Service side
+  ChppLinuxLinkState mServiceLinkContext = {};
+  ChppTransportState mServiceTransportContext = {};
+  ChppAppState mServiceAppContext = {};
+  pthread_t mServiceWorkThread;
+  ServiceState mServiceState;
+};
+
+TEST_F(AppDiscoveryTest, workWhenThereIsNoService) {
+  // Register the client
+  memset(&mClientState, 0, sizeof(mClientState));
+  chppRegisterClient(&mClientAppContext, &mClientState,
+                     &mClientState.chppClientState,
+                     &mClientState.outReqStates[0], &kClient);
+
+  pthread_create(&mClientWorkThread, NULL, workThread,
+                 &mClientTransportContext);
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(450));
+
+  // Start the service thread (no service registered).
+  pthread_create(&mServiceWorkThread, NULL, workThread,
+                 &mServiceTransportContext);
+
+  mClientLinkContext.linkEstablished = true;
+  mServiceLinkContext.linkEstablished = true;
+
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mClientTransportContext,
+                                                kResetWaitTimeMs));
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mServiceTransportContext,
+                                                kResetWaitTimeMs));
+
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mClientAppContext, kDiscoveryWaitTimeMs));
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mServiceAppContext, kDiscoveryWaitTimeMs));
+
+  EXPECT_FALSE(mClientState.resetNotified);
+  EXPECT_FALSE(mClientState.matchNotified);
+  EXPECT_EQ(mClientAppContext.discoveredServiceCount, 0);
+  EXPECT_EQ(mClientAppContext.matchedClientCount, 0);
+  EXPECT_EQ(mServiceAppContext.discoveredServiceCount, 0);
+  EXPECT_EQ(mServiceAppContext.matchedClientCount, 0);
+}
+
+TEST_F(AppDiscoveryTest, servicesShouldBeDiscovered) {
+  // Start the client thread (no client registered).
+  pthread_create(&mClientWorkThread, NULL, workThread,
+                 &mClientTransportContext);
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(450));
+
+  // Register the service
+  memset(&mServiceState, 0, sizeof(mServiceState));
+  chppRegisterService(&mServiceAppContext, &mServiceState,
+                      &mServiceState.chppServiceState, NULL /*outReqStates*/,
+                      &kService);
+
+  pthread_create(&mServiceWorkThread, NULL, workThread,
+                 &mServiceTransportContext);
+
+  mClientLinkContext.linkEstablished = true;
+  mServiceLinkContext.linkEstablished = true;
+
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mClientTransportContext,
+                                                kResetWaitTimeMs));
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mServiceTransportContext,
+                                                kResetWaitTimeMs));
+
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mClientAppContext, kDiscoveryWaitTimeMs));
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mServiceAppContext, kDiscoveryWaitTimeMs));
+
+  EXPECT_FALSE(mClientState.resetNotified);
+  EXPECT_TRUE(mServiceState.resetNotified);
+  EXPECT_FALSE(mClientState.matchNotified);
+  EXPECT_EQ(mClientAppContext.discoveredServiceCount, 1);
+  EXPECT_EQ(mClientAppContext.matchedClientCount, 0);
+  EXPECT_EQ(mServiceAppContext.discoveredServiceCount, 0);
+  EXPECT_EQ(mServiceAppContext.matchedClientCount, 0);
+}
+
+TEST_F(AppDiscoveryTest, discoveredServiceShouldBeMatchedWithClients) {
+  // Register the client
+  memset(&mClientState, 0, sizeof(mClientState));
+  chppRegisterClient(&mClientAppContext, &mClientState,
+                     &mClientState.chppClientState,
+                     &mClientState.outReqStates[0], &kClient);
+
+  pthread_create(&mClientWorkThread, NULL, workThread,
+                 &mClientTransportContext);
+
+  std::this_thread::sleep_for(std::chrono::milliseconds(450));
+
+  // Register the service
+  memset(&mServiceState, 0, sizeof(mServiceState));
+  chppRegisterService(&mServiceAppContext, &mServiceState,
+                      &mServiceState.chppServiceState, NULL /*outReqStates*/,
+                      &kService);
+
+  pthread_create(&mServiceWorkThread, NULL, workThread,
+                 &mServiceTransportContext);
+
+  mClientLinkContext.linkEstablished = true;
+  mServiceLinkContext.linkEstablished = true;
+
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mClientTransportContext,
+                                                kResetWaitTimeMs));
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mServiceTransportContext,
+                                                kResetWaitTimeMs));
+
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mClientAppContext, kDiscoveryWaitTimeMs));
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mServiceAppContext, kDiscoveryWaitTimeMs));
+
+  EXPECT_FALSE(mClientState.resetNotified);
+  EXPECT_TRUE(mServiceState.resetNotified);
+  EXPECT_TRUE(mClientState.matchNotified);
+  EXPECT_EQ(mClientAppContext.discoveredServiceCount, 1);
+  EXPECT_EQ(mClientAppContext.matchedClientCount, 1);
+  EXPECT_TRUE(mClientState.chppClientState.initialized);
+  EXPECT_EQ(mServiceAppContext.discoveredServiceCount, 0);
+  EXPECT_EQ(mServiceAppContext.matchedClientCount, 0);
+}
+
+}  // namespace
+
+}  // namespace chre
\ No newline at end of file
diff --git a/chpp/test/app_notification_test.cpp b/chpp/test/app_notification_test.cpp
new file mode 100644
index 0000000..6f1f557
--- /dev/null
+++ b/chpp/test/app_notification_test.cpp
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2023 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 <gtest/gtest.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <cstring>
+#include <thread>
+
+#include "chpp/app.h"
+#include "chpp/clients.h"
+#include "chpp/clients/discovery.h"
+#include "chpp/macros.h"
+#include "chpp/notifier.h"
+#include "chpp/platform/platform_link.h"
+#include "chpp/platform/utils.h"
+#include "chpp/services.h"
+#include "chpp/transport.h"
+#include "chre/util/enum.h"
+#include "chre/util/time.h"
+
+namespace chre {
+
+namespace {
+
+constexpr uint64_t kResetWaitTimeMs = 5000;
+constexpr uint64_t kDiscoveryWaitTimeMs = 5000;
+
+void *workThread(void *transportState) {
+  ChppTransportState *state = static_cast<ChppTransportState *>(transportState);
+
+  auto linkContext =
+      static_cast<struct ChppLinuxLinkState *>(state->linkContext);
+
+  pthread_setname_np(pthread_self(), linkContext->workThreadName);
+
+  chppWorkThreadStart(state);
+
+  return nullptr;
+}
+
+#define TEST_UUID                                                           \
+  {                                                                         \
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
+        0x00, 0x00, 0x00, 0x12                                              \
+  }
+
+enum class Commands : uint16_t {
+  kServiceNotification,
+  kClientNotification,
+};
+
+constexpr uint16_t kNumCommands = 1;
+
+struct ClientState {
+  struct ChppEndpointState chppClientState;
+  struct ChppOutgoingRequestState outReqStates[kNumCommands];
+  bool serviceNotificationStatus;
+  struct ChppNotifier notifier;
+};
+
+bool clientInit(void *clientState, uint8_t handle,
+                struct ChppVersion serviceVersion);
+void clientDeinit(void *clientState);
+enum ChppAppErrorCode clientDispatchNotification(void *clientState,
+                                                 uint8_t *buf, size_t len);
+constexpr struct ChppClient kClient = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .resetNotifierFunctionPtr = nullptr,
+    .matchNotifierFunctionPtr = nullptr,
+    .responseDispatchFunctionPtr = nullptr,
+    .notificationDispatchFunctionPtr = &clientDispatchNotification,
+    .initFunctionPtr = &clientInit,
+    .deinitFunctionPtr = &clientDeinit,
+    .outReqCount = kNumCommands,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+// Called when a notification from a service is received.
+enum ChppAppErrorCode clientDispatchNotification(void *clientState,
+                                                 uint8_t *buf, size_t len) {
+  auto state = static_cast<struct ClientState *>(clientState);
+
+  // The response is composed of the app header only.
+  if (len != sizeof(ChppAppHeader)) {
+    return CHPP_APP_ERROR_NONE;
+  }
+
+  auto notification = reinterpret_cast<struct ChppAppHeader *>(buf);
+
+  switch (notification->command) {
+    case asBaseType(Commands::kServiceNotification):
+      state->serviceNotificationStatus =
+          notification->error == CHPP_APP_ERROR_NONE;
+      chppNotifierSignal(&state->notifier, 1 /*signal*/);
+      return CHPP_APP_ERROR_NONE;
+
+    default:
+      return CHPP_APP_ERROR_NONE;
+  }
+}
+
+bool clientInit(void *clientState, uint8_t handle,
+                struct ChppVersion serviceVersion) {
+  UNUSED_VAR(serviceVersion);
+  auto state = static_cast<struct ClientState *>(clientState);
+  state->chppClientState.openState = CHPP_OPEN_STATE_OPENED;
+  chppClientInit(&state->chppClientState, handle);
+  return true;
+}
+
+void clientDeinit(void *clientState) {
+  auto state = static_cast<struct ClientState *>(clientState);
+  chppClientDeinit(&state->chppClientState);
+  state->chppClientState.openState = CHPP_OPEN_STATE_CLOSED;
+}
+
+// Service
+struct ServiceState {
+  struct ChppEndpointState chppServiceState;
+  struct ChppIncomingRequestState inReqStates[kNumCommands];
+  bool clientNotificationStatus;
+  struct ChppNotifier notifier;
+};
+
+// Called when a notification from a client is received.
+enum ChppAppErrorCode serviceDispatchNotification(void *serviceState,
+                                                  uint8_t *buf, size_t len) {
+  auto state = static_cast<struct ServiceState *>(serviceState);
+
+  // The response is composed of the app header only.
+  if (len != sizeof(ChppAppHeader)) {
+    return CHPP_APP_ERROR_NONE;
+  }
+
+  auto notification = reinterpret_cast<struct ChppAppHeader *>(buf);
+
+  switch (notification->command) {
+    case asBaseType(Commands::kClientNotification):
+      state->clientNotificationStatus =
+          notification->error == CHPP_APP_ERROR_NONE;
+      chppNotifierSignal(&state->notifier, 1 /*signal*/);
+      return CHPP_APP_ERROR_NONE;
+
+    default:
+      return CHPP_APP_ERROR_NONE;
+  }
+}
+
+const struct ChppService kService = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.name = "Test",
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .resetNotifierFunctionPtr = nullptr,
+    .requestDispatchFunctionPtr = nullptr,
+    .notificationDispatchFunctionPtr = &serviceDispatchNotification,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+// Test notifications.
+class AppNotificationTest : public testing::Test {
+ protected:
+  void SetUp() {
+    chppClearTotalAllocBytes();
+    chppNotifierInit(&mClientState.notifier);
+    chppNotifierInit(&mServiceState.notifier);
+    memset(&mClientLinkContext, 0, sizeof(mClientLinkContext));
+    memset(&mServiceLinkContext, 0, sizeof(mServiceLinkContext));
+
+    mServiceLinkContext.linkThreadName = "Host Link";
+    mServiceLinkContext.workThreadName = "Host worker";
+    mServiceLinkContext.isLinkActive = true;
+    mServiceLinkContext.remoteLinkState = &mClientLinkContext;
+    mServiceLinkContext.rxInRemoteEndpointWorker = false;
+
+    mClientLinkContext.linkThreadName = "CHRE Link";
+    mClientLinkContext.workThreadName = "CHRE worker";
+    mClientLinkContext.isLinkActive = true;
+    mClientLinkContext.remoteLinkState = &mServiceLinkContext;
+    mClientLinkContext.rxInRemoteEndpointWorker = false;
+
+    // No default clients/services.
+    struct ChppClientServiceSet set;
+    memset(&set, 0, sizeof(set));
+
+    const struct ChppLinkApi *linkApi = getLinuxLinkApi();
+
+    // Init client side.
+    chppTransportInit(&mClientTransportContext, &mClientAppContext,
+                      &mClientLinkContext, linkApi);
+    chppAppInitWithClientServiceSet(&mClientAppContext,
+                                    &mClientTransportContext, set);
+
+    // Init service side.
+    chppTransportInit(&mServiceTransportContext, &mServiceAppContext,
+                      &mServiceLinkContext, linkApi);
+    chppAppInitWithClientServiceSet(&mServiceAppContext,
+                                    &mServiceTransportContext, set);
+
+    BringUpClient();
+    std::this_thread::sleep_for(std::chrono::milliseconds(450));
+    BringUpService();
+    mClientLinkContext.linkEstablished = true;
+    mServiceLinkContext.linkEstablished = true;
+
+    EXPECT_TRUE(chppTransportWaitForResetComplete(&mClientTransportContext,
+                                                  kResetWaitTimeMs));
+    EXPECT_TRUE(chppTransportWaitForResetComplete(&mServiceTransportContext,
+                                                  kResetWaitTimeMs));
+    EXPECT_TRUE(
+        chppWaitForDiscoveryComplete(&mClientAppContext, kDiscoveryWaitTimeMs));
+    EXPECT_TRUE(chppWaitForDiscoveryComplete(&mServiceAppContext,
+                                             kDiscoveryWaitTimeMs));
+  }
+
+  void BringUpClient() {
+    memset(&mClientState, 0, sizeof(mClientState));
+    chppRegisterClient(&mClientAppContext, &mClientState,
+                       &mClientState.chppClientState,
+                       &mClientState.outReqStates[0], &kClient);
+
+    pthread_create(&mClientWorkThread, NULL, workThread,
+                   &mClientTransportContext);
+  }
+
+  void BringUpService() {
+    memset(&mServiceState, 0, sizeof(mServiceState));
+    chppRegisterService(&mServiceAppContext, &mServiceState,
+                        &mServiceState.chppServiceState, NULL /*outReqStates*/,
+                        &kService);
+
+    pthread_create(&mServiceWorkThread, NULL, workThread,
+                   &mServiceTransportContext);
+  }
+
+  void TearDown() {
+    chppNotifierDeinit(&mClientState.notifier);
+    chppNotifierDeinit(&mServiceState.notifier);
+    chppWorkThreadStop(&mClientTransportContext);
+    chppWorkThreadStop(&mServiceTransportContext);
+    pthread_join(mClientWorkThread, NULL);
+    pthread_join(mServiceWorkThread, NULL);
+
+    // Deinit client side.
+    chppAppDeinit(&mClientAppContext);
+    chppTransportDeinit(&mClientTransportContext);
+
+    // Deinit service side.
+    chppAppDeinit(&mServiceAppContext);
+    chppTransportDeinit(&mServiceTransportContext);
+
+    EXPECT_EQ(chppGetTotalAllocBytes(), 0);
+  }
+
+  // Client side.
+  ChppLinuxLinkState mClientLinkContext = {};
+  ChppTransportState mClientTransportContext = {};
+  ChppAppState mClientAppContext = {};
+  pthread_t mClientWorkThread;
+  ClientState mClientState;
+
+  // Service side
+  ChppLinuxLinkState mServiceLinkContext = {};
+  ChppTransportState mServiceTransportContext = {};
+  ChppAppState mServiceAppContext = {};
+  pthread_t mServiceWorkThread;
+  ServiceState mServiceState;
+};
+
+TEST_F(AppNotificationTest, serviceSendANotificationToClient) {
+  // Send a notification.
+  constexpr size_t notificationLen = sizeof(struct ChppAppHeader);
+
+  struct ChppAppHeader *notification =
+      chppAllocServiceNotification(notificationLen);
+  ASSERT_NE(notification, nullptr);
+  notification->command = asBaseType(Commands::kServiceNotification);
+  notification->handle = mServiceState.chppServiceState.handle;
+
+  mClientState.serviceNotificationStatus = false;
+
+  EXPECT_TRUE(chppEnqueueTxDatagramOrFail(&mServiceTransportContext,
+                                          notification, notificationLen));
+
+  chppNotifierWait(&mClientState.notifier);
+
+  EXPECT_TRUE(mClientState.serviceNotificationStatus);
+}
+
+TEST_F(AppNotificationTest, clientSendANotificationToService) {
+  // Send a notification.
+  constexpr size_t notificationLen = sizeof(struct ChppAppHeader);
+
+  struct ChppAppHeader *notification =
+      chppAllocClientNotification(notificationLen);
+  ASSERT_NE(notification, nullptr);
+  notification->command = asBaseType(Commands::kClientNotification);
+  notification->handle = mClientState.chppClientState.handle;
+
+  mServiceState.clientNotificationStatus = false;
+
+  EXPECT_TRUE(chppEnqueueTxDatagramOrFail(&mClientTransportContext,
+                                          notification, notificationLen));
+
+  chppNotifierWait(&mServiceState.notifier);
+
+  EXPECT_TRUE(mServiceState.clientNotificationStatus);
+}
+
+}  // namespace
+
+}  // namespace chre
\ No newline at end of file
diff --git a/chpp/test/app_req_resp_test.cpp b/chpp/test/app_req_resp_test.cpp
new file mode 100644
index 0000000..0bcf057
--- /dev/null
+++ b/chpp/test/app_req_resp_test.cpp
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2023 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 <gtest/gtest.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <cstring>
+#include <thread>
+
+#include "chpp/app.h"
+#include "chpp/clients.h"
+#include "chpp/clients/discovery.h"
+#include "chpp/macros.h"
+#include "chpp/notifier.h"
+#include "chpp/platform/platform_link.h"
+#include "chpp/platform/utils.h"
+#include "chpp/services.h"
+#include "chpp/transport.h"
+#include "chre/util/enum.h"
+#include "chre/util/time.h"
+
+namespace chre {
+namespace {
+
+constexpr uint64_t kResetWaitTimeMs = 5000;
+constexpr uint64_t kDiscoveryWaitTimeMs = 5000;
+
+void *workThread(void *transportState) {
+  ChppTransportState *state = static_cast<ChppTransportState *>(transportState);
+
+  auto linkContext =
+      static_cast<struct ChppLinuxLinkState *>(state->linkContext);
+
+  pthread_setname_np(pthread_self(), linkContext->workThreadName);
+
+  chppWorkThreadStart(state);
+
+  return nullptr;
+}
+
+#define TEST_UUID                                                           \
+  {                                                                         \
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
+        0x00, 0x00, 0x00, 0x12                                              \
+  }
+
+enum class Commands : uint16_t {
+  kOk,
+  kError,
+  kTimeout,
+  // Number of request, must stay last
+  kNumCommands,
+};
+
+constexpr uint16_t kNumCommands = asBaseType(Commands::kNumCommands);
+
+// Common code for the client and the service.
+
+struct CommonState {
+  bool okResponseStatus;
+  bool errorResponseStatus;
+  bool timeoutResponseStatus;
+  struct ChppNotifier notifier;
+};
+
+enum ChppAppErrorCode dispatchResponse(
+    struct ChppAppState *appState,
+    struct ChppOutgoingRequestState *outReqStates, struct CommonState *common,
+    struct ChppAppHeader *response, size_t len) {
+  // The response is composed of the app header only.
+  if (len != sizeof(ChppAppHeader)) {
+    return CHPP_APP_ERROR_NONE;
+  }
+
+  switch (response->command) {
+    case asBaseType(Commands::kOk):
+      // The response for the kOk command should have a CHPP_APP_ERROR_NONE
+      // error.
+      common->okResponseStatus = chppTimestampIncomingResponse(
+          appState, &outReqStates[asBaseType(Commands::kOk)], response);
+
+      common->okResponseStatus &= response->error == CHPP_APP_ERROR_NONE;
+      return CHPP_APP_ERROR_NONE;
+
+    case asBaseType(Commands::kError):
+      // The response for the kError command should have a
+      // CHPP_APP_ERROR_UNSPECIFIED error.
+      common->errorResponseStatus = chppTimestampIncomingResponse(
+          appState, &outReqStates[asBaseType(Commands::kError)], response);
+
+      common->errorResponseStatus &=
+          response->error == CHPP_APP_ERROR_UNSPECIFIED;
+      return CHPP_APP_ERROR_NONE;
+
+    case asBaseType(Commands::kTimeout):
+      // The response for the kTimeout command should have a
+      // CHPP_APP_ERROR_TIMEOUT error. That response is generated by the app
+      // layer.
+      common->timeoutResponseStatus = chppTimestampIncomingResponse(
+          appState, &outReqStates[asBaseType(Commands::kTimeout)], response);
+
+      common->timeoutResponseStatus &=
+          response->error == CHPP_APP_ERROR_TIMEOUT;
+      chppNotifierSignal(&common->notifier, 1 /*signal*/);
+      return CHPP_APP_ERROR_NONE;
+
+    default:
+      return CHPP_APP_ERROR_NONE;
+  }
+}
+
+enum ChppAppErrorCode dispatchRequest(
+    struct ChppAppState *appState, struct ChppIncomingRequestState *inReqStates,
+    struct ChppAppHeader *request, size_t len) {
+  // The request is composed of the app header only.
+  if (len != sizeof(ChppAppHeader)) {
+    return CHPP_APP_ERROR_NONE;
+  }
+
+  switch (request->command) {
+    case asBaseType(Commands::kOk): {
+      // Return a response for the kOk command.
+      chppTimestampIncomingRequest(&inReqStates[asBaseType(Commands::kOk)],
+                                   request);
+
+      struct ChppAppHeader *response =
+          chppAllocResponse(request, sizeof(ChppAppHeader));
+
+      chppSendTimestampedResponseOrFail(appState,
+                                        &inReqStates[asBaseType(Commands::kOk)],
+                                        response, sizeof(ChppAppHeader));
+      return CHPP_APP_ERROR_NONE;
+    }
+    case asBaseType(Commands::kError): {
+      // Return a response with a CHPP_APP_ERROR_UNSPECIFIED error on kError
+      // command.
+      return CHPP_APP_ERROR_UNSPECIFIED;
+    }
+
+    case asBaseType(Commands::kTimeout): {
+      // Do not send a response on kTimeout for the remote endpoint to timeout.
+      chppTimestampIncomingRequest(&inReqStates[asBaseType(Commands::kError)],
+                                   request);
+
+      return CHPP_APP_ERROR_NONE;
+    }
+
+    default:
+      return CHPP_APP_ERROR_NONE;
+  }
+}
+
+// Client specific code.
+struct ClientState {
+  struct ChppEndpointState chppClientState;
+  struct ChppOutgoingRequestState outReqStates[kNumCommands];
+  struct ChppIncomingRequestState inReqStates[kNumCommands];
+  struct CommonState common;
+};
+
+bool clientInit(void *clientState, uint8_t handle,
+                struct ChppVersion serviceVersion);
+void clientDeinit(void *clientState);
+enum ChppAppErrorCode clientDispatchResponse(void *clientState, uint8_t *buf,
+                                             size_t len);
+enum ChppAppErrorCode clientDispatchRequest(void *clientState, uint8_t *buf,
+                                            size_t len);
+
+constexpr struct ChppClient kClient = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .resetNotifierFunctionPtr = nullptr,
+    .matchNotifierFunctionPtr = nullptr,
+    .responseDispatchFunctionPtr = &clientDispatchResponse,
+    .notificationDispatchFunctionPtr = nullptr,
+    .requestDispatchFunctionPtr = &clientDispatchRequest,
+    .initFunctionPtr = &clientInit,
+    .deinitFunctionPtr = &clientDeinit,
+    .outReqCount = kNumCommands,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+// Called when a response is received from the service.
+enum ChppAppErrorCode clientDispatchResponse(void *clientState, uint8_t *buf,
+                                             size_t len) {
+  CHPP_NOT_NULL(clientState);
+
+  auto state = static_cast<struct ClientState *>(clientState);
+
+  return dispatchResponse(state->chppClientState.appContext,
+                          state->outReqStates, &state->common,
+                          reinterpret_cast<struct ChppAppHeader *>(buf), len);
+}
+
+// Called when a request is received from the service.
+enum ChppAppErrorCode clientDispatchRequest(void *clientState, uint8_t *buf,
+                                            size_t len) {
+  auto request = reinterpret_cast<struct ChppAppHeader *>(buf);
+  auto state = static_cast<struct ClientState *>(clientState);
+
+  return dispatchRequest(state->chppClientState.appContext, state->inReqStates,
+                         request, len);
+}
+
+bool clientInit(void *clientState, uint8_t handle,
+                struct ChppVersion serviceVersion) {
+  UNUSED_VAR(serviceVersion);
+  auto state = static_cast<struct ClientState *>(clientState);
+  state->chppClientState.openState = CHPP_OPEN_STATE_OPENED;
+  chppClientInit(&state->chppClientState, handle);
+  return true;
+}
+
+void clientDeinit(void *clientState) {
+  auto state = static_cast<struct ClientState *>(clientState);
+  chppClientDeinit(&state->chppClientState);
+  state->chppClientState.openState = CHPP_OPEN_STATE_CLOSED;
+}
+
+// Service specific code.
+
+struct ServiceState {
+  struct ChppEndpointState chppServiceState;
+  struct ChppOutgoingRequestState outReqStates[kNumCommands];
+  struct ChppIncomingRequestState inReqStates[kNumCommands];
+  struct CommonState common;
+};
+
+// Called when a request is received from the client.
+enum ChppAppErrorCode serviceDispatchRequest(void *serviceState, uint8_t *buf,
+                                             size_t len) {
+  auto request = reinterpret_cast<struct ChppAppHeader *>(buf);
+  auto state = static_cast<struct ServiceState *>(serviceState);
+
+  return dispatchRequest(state->chppServiceState.appContext, state->inReqStates,
+                         request, len);
+}
+
+// Called when a response is received from the client.
+enum ChppAppErrorCode serviceDispatchResponse(void *serviceState, uint8_t *buf,
+                                              size_t len) {
+  CHPP_NOT_NULL(serviceState);
+
+  auto state = static_cast<struct ServiceState *>(serviceState);
+
+  return dispatchResponse(state->chppServiceState.appContext,
+                          state->outReqStates, &state->common,
+                          reinterpret_cast<struct ChppAppHeader *>(buf), len);
+}
+
+const struct ChppService kService = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.name = "Test",
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .resetNotifierFunctionPtr = nullptr,
+    .requestDispatchFunctionPtr = &serviceDispatchRequest,
+    .notificationDispatchFunctionPtr = nullptr,
+    .responseDispatchFunctionPtr = &serviceDispatchResponse,
+    .outReqCount = kNumCommands,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+/**
+ * Test requests and responses.
+ *
+ * The test parameter is:
+ * - CHPP_MESSAGE_TYPE_CLIENT_REQUEST for client side requests
+ * - CHPP_MESSAGE_TYPE_SERVICE_REQUEST for service side requests
+ */
+class AppReqRespParamTest : public testing::TestWithParam<ChppMessageType> {
+ protected:
+  void SetUp() {
+    chppClearTotalAllocBytes();
+    chppNotifierInit(&mClientState.common.notifier);
+    chppNotifierInit(&mServiceState.common.notifier);
+    memset(&mClientLinkState, 0, sizeof(mClientLinkState));
+    memset(&mServiceLinkState, 0, sizeof(mServiceLinkState));
+
+    mServiceLinkState.linkThreadName = "Service Link";
+    mServiceLinkState.workThreadName = "Service worker";
+    mServiceLinkState.isLinkActive = true;
+    mServiceLinkState.remoteLinkState = &mClientLinkState;
+    mServiceLinkState.rxInRemoteEndpointWorker = false;
+
+    mClientLinkState.linkThreadName = "Client Link";
+    mClientLinkState.workThreadName = "Client worker";
+    mClientLinkState.isLinkActive = true;
+    mClientLinkState.remoteLinkState = &mServiceLinkState;
+    mClientLinkState.rxInRemoteEndpointWorker = false;
+
+    // No default clients/services.
+    struct ChppClientServiceSet set;
+    memset(&set, 0, sizeof(set));
+
+    const struct ChppLinkApi *linkApi = getLinuxLinkApi();
+
+    // Init client side.
+    chppTransportInit(&mClientTransportState, &mClientAppState,
+                      &mClientLinkState, linkApi);
+    chppAppInitWithClientServiceSet(&mClientAppState, &mClientTransportState,
+                                    set);
+
+    // Init service side.
+    chppTransportInit(&mServiceTransportState, &mServiceAppState,
+                      &mServiceLinkState, linkApi);
+    chppAppInitWithClientServiceSet(&mServiceAppState, &mServiceTransportState,
+                                    set);
+
+    BringUpClient();
+    std::this_thread::sleep_for(std::chrono::milliseconds(450));
+    BringUpService();
+    mClientLinkState.linkEstablished = true;
+    mServiceLinkState.linkEstablished = true;
+
+    EXPECT_TRUE(chppTransportWaitForResetComplete(&mClientTransportState,
+                                                  kResetWaitTimeMs));
+    EXPECT_TRUE(chppTransportWaitForResetComplete(&mServiceTransportState,
+                                                  kResetWaitTimeMs));
+
+    EXPECT_TRUE(
+        chppWaitForDiscoveryComplete(&mClientAppState, kDiscoveryWaitTimeMs));
+    EXPECT_TRUE(
+        chppWaitForDiscoveryComplete(&mServiceAppState, kDiscoveryWaitTimeMs));
+  }
+
+  void BringUpClient() {
+    memset(&mClientState, 0, sizeof(mClientState));
+    chppRegisterClient(&mClientAppState, &mClientState,
+                       &mClientState.chppClientState,
+                       &mClientState.outReqStates[0], &kClient);
+
+    pthread_create(&mClientWorkThread, NULL, workThread,
+                   &mClientTransportState);
+  }
+
+  void BringUpService() {
+    memset(&mServiceState, 0, sizeof(mServiceState));
+    chppRegisterService(&mServiceAppState, &mServiceState,
+                        &mServiceState.chppServiceState,
+                        &mServiceState.outReqStates[0], &kService);
+
+    pthread_create(&mServiceWorkThread, NULL, workThread,
+                   &mServiceTransportState);
+  }
+
+  void TearDown() {
+    chppNotifierDeinit(&mClientState.common.notifier);
+    chppNotifierDeinit(&mServiceState.common.notifier);
+    chppWorkThreadStop(&mClientTransportState);
+    chppWorkThreadStop(&mServiceTransportState);
+    pthread_join(mClientWorkThread, NULL);
+    pthread_join(mServiceWorkThread, NULL);
+
+    // Deinit client side.
+    chppAppDeinit(&mClientAppState);
+    chppTransportDeinit(&mClientTransportState);
+
+    // Deinit service side.
+    chppAppDeinit(&mServiceAppState);
+    chppTransportDeinit(&mServiceTransportState);
+
+    EXPECT_EQ(chppGetTotalAllocBytes(), 0);
+  }
+
+  struct ChppAppHeader *AllocRequestCommand(Commands command) {
+    return GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+               ? chppAllocClientRequestCommand(&mClientState.chppClientState,
+                                               asBaseType(command))
+               : chppAllocServiceRequestCommand(&mServiceState.chppServiceState,
+                                                asBaseType(command));
+  }
+
+  struct CommonState *GetCommonState() {
+    return GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+               ? &mClientState.common
+               : &mServiceState.common;
+  }
+
+  bool SendTimestampedRequestAndWait(struct ChppAppHeader *request) {
+    constexpr size_t len = sizeof(struct ChppAppHeader);
+    if (request->type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST) {
+      return chppClientSendTimestampedRequestAndWait(
+          &mClientState.chppClientState,
+          &mClientState.outReqStates[request->command], request, len);
+    }
+
+    return chppServiceSendTimestampedRequestAndWait(
+        &mServiceState.chppServiceState,
+        &mServiceState.outReqStates[request->command], request, len);
+  }
+
+  bool SendTimestampedRequestOrFail(struct ChppAppHeader *request,
+                                    uint64_t timeoutNs) {
+    constexpr size_t len = sizeof(struct ChppAppHeader);
+    if (request->type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST) {
+      return chppClientSendTimestampedRequestOrFail(
+          &mClientState.chppClientState,
+          &mClientState.outReqStates[request->command], request, len,
+          timeoutNs);
+    }
+
+    return chppServiceSendTimestampedRequestOrFail(
+        &mServiceState.chppServiceState,
+        &mServiceState.outReqStates[request->command], request, len, timeoutNs);
+  }
+
+  // Client side.
+  ChppLinuxLinkState mClientLinkState = {};
+  ChppTransportState mClientTransportState = {};
+  ChppAppState mClientAppState = {};
+  ClientState mClientState;
+  pthread_t mClientWorkThread;
+
+  // Service side
+  ChppLinuxLinkState mServiceLinkState = {};
+  ChppTransportState mServiceTransportState = {};
+  ChppAppState mServiceAppState = {};
+  ServiceState mServiceState = {};
+  pthread_t mServiceWorkThread;
+};
+
+TEST_P(AppReqRespParamTest, sendsRequestAndReceiveResponse) {
+  struct ChppAppHeader *request = AllocRequestCommand(Commands::kOk);
+  ASSERT_NE(request, nullptr);
+
+  GetCommonState()->okResponseStatus = false;
+
+  EXPECT_TRUE(SendTimestampedRequestAndWait(request));
+
+  EXPECT_TRUE(GetCommonState()->okResponseStatus);
+}
+
+TEST_P(AppReqRespParamTest, sendsRequestAndReceiveErrorResponse) {
+  struct ChppAppHeader *request = AllocRequestCommand(Commands::kError);
+  ASSERT_NE(request, nullptr);
+
+  GetCommonState()->errorResponseStatus = false;
+
+  EXPECT_TRUE(SendTimestampedRequestAndWait(request));
+
+  EXPECT_TRUE(GetCommonState()->errorResponseStatus);
+}
+
+TEST_P(AppReqRespParamTest, sendsRequestAndReceiveTimeoutResponse) {
+  struct ChppAppHeader *request = AllocRequestCommand(Commands::kTimeout);
+  ASSERT_NE(request, nullptr);
+
+  GetCommonState()->timeoutResponseStatus = false;
+
+  EXPECT_TRUE(
+      SendTimestampedRequestOrFail(request, 10 * kOneMicrosecondInNanoseconds));
+
+  chppNotifierWait(&GetCommonState()->notifier);
+
+  EXPECT_TRUE(GetCommonState()->timeoutResponseStatus);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    AppReqRespTest, AppReqRespParamTest,
+    testing::Values(CHPP_MESSAGE_TYPE_CLIENT_REQUEST,
+                    CHPP_MESSAGE_TYPE_SERVICE_REQUEST),
+    [](const testing::TestParamInfo<AppReqRespParamTest::ParamType> &info) {
+      return info.param == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ? "ClientRequests"
+                                                            : "ServiceRequests";
+    });
+
+}  // namespace
+}  // namespace chre
\ No newline at end of file
diff --git a/chpp/test/app_test.cpp b/chpp/test/app_test.cpp
index 84d826c..fdc2f4b 100644
--- a/chpp/test/app_test.cpp
+++ b/chpp/test/app_test.cpp
@@ -142,8 +142,12 @@
 }
 
 TEST_F(ChppAppTest, Timesync) {
-  constexpr uint64_t kMaxRtt = 2 * CHPP_NSEC_PER_MSEC;    // in ms
-  constexpr int64_t kMaxOffset = 1 * CHPP_NSEC_PER_MSEC;  // in ms
+  // Upper bound for the RTT (response received - request sent).
+  constexpr uint64_t kMaxRttNs = 20 * CHPP_NSEC_PER_MSEC;
+  // The offset is defined as (Time when the service sent the response) - (Time
+  // when the client got the response).
+  // We use half the RTT as the upper bound.
+  constexpr int64_t kMaxOffsetNs = kMaxRttNs / 2;
 
   CHPP_LOGI("Starting timesync test...");
 
@@ -154,11 +158,11 @@
   EXPECT_EQ(chppTimesyncGetResult(&mClientAppContext)->error,
             CHPP_APP_ERROR_NONE);
 
-  EXPECT_LT(chppTimesyncGetResult(&mClientAppContext)->rttNs, kMaxRtt);
+  EXPECT_LT(chppTimesyncGetResult(&mClientAppContext)->rttNs, kMaxRttNs);
   EXPECT_NE(chppTimesyncGetResult(&mClientAppContext)->rttNs, 0);
 
-  EXPECT_LT(chppTimesyncGetResult(&mClientAppContext)->offsetNs, kMaxOffset);
-  EXPECT_GT(chppTimesyncGetResult(&mClientAppContext)->offsetNs, -kMaxOffset);
+  EXPECT_LT(chppTimesyncGetResult(&mClientAppContext)->offsetNs, kMaxOffsetNs);
+  EXPECT_GT(chppTimesyncGetResult(&mClientAppContext)->offsetNs, -kMaxOffsetNs);
   EXPECT_NE(chppTimesyncGetResult(&mClientAppContext)->offsetNs, 0);
 }
 
diff --git a/chpp/test/app_test_base.cpp b/chpp/test/app_test_base.cpp
index 907b15e..1a9e3fc 100644
--- a/chpp/test/app_test_base.cpp
+++ b/chpp/test/app_test_base.cpp
@@ -97,11 +97,17 @@
   mClientLinkContext.linkEstablished = true;
   mServiceLinkContext.linkEstablished = true;
 
-  constexpr uint64_t kResetWaitTimeMs = 1500;
-  chppTransportWaitForResetComplete(&mClientTransportContext, kResetWaitTimeMs);
+  constexpr uint64_t kResetWaitTimeMs = 5000;
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mClientTransportContext,
+                                                kResetWaitTimeMs));
+  EXPECT_TRUE(chppTransportWaitForResetComplete(&mServiceTransportContext,
+                                                kResetWaitTimeMs));
 
   constexpr uint64_t kDiscoveryWaitTimeMs = 5000;
-  chppWaitForDiscoveryComplete(&mClientAppContext, kDiscoveryWaitTimeMs);
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mClientAppContext, kDiscoveryWaitTimeMs));
+  EXPECT_TRUE(
+      chppWaitForDiscoveryComplete(&mServiceAppContext, kDiscoveryWaitTimeMs));
 }
 
 void AppTestBase::TearDown() {
diff --git a/chpp/test/app_timeout_test.cpp b/chpp/test/app_timeout_test.cpp
new file mode 100644
index 0000000..8f155f2
--- /dev/null
+++ b/chpp/test/app_timeout_test.cpp
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2021 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 "app_timeout_test.h"
+
+#include <gtest/gtest.h>
+#include <string.h>
+#include <cstdint>
+#include <thread>
+
+#include "chpp/app.h"
+#include "chpp/clients.h"
+#include "chpp/clients/gnss.h"
+#include "chpp/clients/wifi.h"
+#include "chpp/clients/wwan.h"
+#include "chpp/macros.h"
+#include "chpp/memory.h"
+#include "chpp/platform/platform_link.h"
+#include "chpp/platform/utils.h"
+#include "chpp/services.h"
+#include "chpp/time.h"
+#include "chpp/transport.h"
+#include "chre/pal/wwan.h"
+
+namespace chre {
+namespace {
+
+#define TEST_UUID                                                           \
+  {                                                                         \
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
+        0x00, 0x00, 0x00, 0x12                                              \
+  }
+
+// Number of requests supported by the client and the service.
+constexpr uint16_t kNumCommands = 3;
+
+struct ClientState {
+  struct ChppEndpointState chppClientState;
+  struct ChppOutgoingRequestState outReqStates[kNumCommands];
+};
+
+constexpr struct ChppClient kClient = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .outReqCount = kNumCommands,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+struct ServiceState {
+  struct ChppEndpointState chppServiceState;
+  struct ChppOutgoingRequestState outReqStates[kNumCommands];
+};
+
+const struct ChppService kService = {
+    .descriptor.uuid = TEST_UUID,
+    .descriptor.name = "Test",
+    .descriptor.version.major = 1,
+    .descriptor.version.minor = 0,
+    .descriptor.version.patch = 0,
+    .outReqCount = kNumCommands,
+    .minLength = sizeof(struct ChppAppHeader),
+};
+
+void ValidateClientStateAndReqState(struct ChppEndpointState *clientState,
+                                    const struct ChppAppHeader *request) {
+  ASSERT_NE(clientState, nullptr);
+  const uint8_t clientIdx = clientState->index;
+
+  ASSERT_NE(clientState->appContext, nullptr);
+  ASSERT_NE(clientState->appContext->registeredClients, nullptr);
+  ASSERT_NE(clientState->appContext->registeredClients[clientIdx], nullptr);
+  ASSERT_LT(request->command,
+            clientState->appContext->registeredClients[clientIdx]->outReqCount);
+  ASSERT_NE(clientState->appContext->registeredClientStates[clientIdx],
+            nullptr);
+  ASSERT_NE(
+      clientState->appContext->registeredClientStates[clientIdx]->outReqStates,
+      nullptr);
+  ASSERT_NE(clientState->appContext->registeredClientStates[clientIdx]->context,
+            nullptr);
+}
+
+void ValidateServiceStateAndReqState(struct ChppEndpointState *serviceState,
+                                     const struct ChppAppHeader *request) {
+  ASSERT_NE(serviceState, nullptr);
+  const uint8_t serviceIdx = serviceState->index;
+
+  ASSERT_NE(serviceState->appContext, nullptr);
+  ASSERT_NE(serviceState->appContext->registeredServices, nullptr);
+  ASSERT_NE(serviceState->appContext->registeredServices[serviceIdx], nullptr);
+  ASSERT_LT(
+      request->command,
+      serviceState->appContext->registeredServices[serviceIdx]->outReqCount);
+  ASSERT_NE(serviceState->appContext->registeredServiceStates[serviceIdx],
+            nullptr);
+  ASSERT_NE(serviceState->appContext->registeredServiceStates[serviceIdx]
+                ->outReqStates,
+            nullptr);
+  ASSERT_NE(
+      serviceState->appContext->registeredServiceStates[serviceIdx]->context,
+      nullptr);
+}
+
+void validateTimeout(uint64_t timeoutTimeNs, uint64_t expectedTimeNs) {
+  constexpr uint64_t kJitterNs = 10 * CHPP_NSEC_PER_MSEC;
+
+  if (expectedTimeNs == CHPP_TIME_MAX) {
+    EXPECT_EQ(timeoutTimeNs, expectedTimeNs);
+  } else {
+    EXPECT_GE(timeoutTimeNs, expectedTimeNs);
+    EXPECT_LE(timeoutTimeNs, expectedTimeNs + kJitterNs);
+  }
+}
+
+void validateTimeoutResponse(const struct ChppAppHeader *request,
+                             const struct ChppAppHeader *response) {
+  ASSERT_NE(request, nullptr);
+  ASSERT_NE(response, nullptr);
+
+  EXPECT_EQ(response->handle, request->handle);
+
+  EXPECT_EQ(response->type, request->type == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+                                ? CHPP_MESSAGE_TYPE_SERVICE_RESPONSE
+                                : CHPP_MESSAGE_TYPE_CLIENT_RESPONSE);
+  EXPECT_EQ(response->transaction, request->transaction);
+  EXPECT_EQ(response->error, CHPP_APP_ERROR_TIMEOUT);
+  EXPECT_EQ(response->command, request->command);
+}
+
+/**
+ * Test timeout for client and service side requests.
+ *
+ * The test parameter is:
+ * - CHPP_MESSAGE_TYPE_CLIENT_REQUEST for client side requests
+ * - CHPP_MESSAGE_TYPE_SERVICE_REQUEST for service side requests
+ */
+class TimeoutParamTest : public testing::TestWithParam<ChppMessageType> {
+ protected:
+  void SetUp() override {
+    chppClearTotalAllocBytes();
+
+    memset(&mClientLinkContext, 0, sizeof(mClientLinkContext));
+    memset(&mServiceLinkContext, 0, sizeof(mServiceLinkContext));
+
+    mServiceLinkContext.isLinkActive = true;
+    mServiceLinkContext.remoteLinkState = &mClientLinkContext;
+    mServiceLinkContext.rxInRemoteEndpointWorker = false;
+
+    mClientLinkContext.isLinkActive = true;
+    mClientLinkContext.remoteLinkState = &mServiceLinkContext;
+    mClientLinkContext.rxInRemoteEndpointWorker = false;
+
+    // No default clients/services.
+    struct ChppClientServiceSet set;
+    memset(&set, 0, sizeof(set));
+
+    const struct ChppLinkApi *linkApi = getLinuxLinkApi();
+
+    // Init client side.
+    chppTransportInit(&mClientTransportContext, &mClientAppContext,
+                      &mClientLinkContext, linkApi);
+    mClientTransportContext.resetState = CHPP_RESET_STATE_NONE;
+    chppAppInitWithClientServiceSet(&mClientAppContext,
+                                    &mClientTransportContext, set);
+
+    // Init service side.
+    chppTransportInit(&mServiceTransportContext, &mServiceAppContext,
+                      &mServiceLinkContext, linkApi);
+    mServiceTransportContext.resetState = CHPP_RESET_STATE_NONE;
+    chppAppInitWithClientServiceSet(&mServiceAppContext,
+                                    &mServiceTransportContext, set);
+
+    // Bring up the client
+    memset(&mClientState, 0, sizeof(mClientState));
+    chppRegisterClient(&mClientAppContext, &mClientState,
+                       &mClientState.chppClientState,
+                       &mClientState.outReqStates[0], &kClient);
+
+    // Bring up the service
+    memset(&mServiceState, 0, sizeof(mServiceState));
+    chppRegisterService(&mServiceAppContext, &mServiceState,
+                        &mServiceState.chppServiceState,
+                        &mServiceState.outReqStates[0], &kService);
+
+    mClientLinkContext.linkEstablished = true;
+    mServiceLinkContext.linkEstablished = true;
+
+    chppClientInit(&mClientState.chppClientState,
+                   CHPP_HANDLE_NEGOTIATED_RANGE_START);
+  }
+
+  void TearDown() override {
+    chppAppDeinit(&mClientAppContext);
+    chppTransportDeinit(&mClientTransportContext);
+    chppClientDeinit(&mClientState.chppClientState);
+
+    chppAppDeinit(&mServiceAppContext);
+    chppTransportDeinit(&mServiceTransportContext);
+
+    EXPECT_EQ(chppGetTotalAllocBytes(), 0);
+  }
+
+  struct ChppAppHeader *AllocRequestCommand(uint16_t command) {
+    return GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+               ? chppAllocClientRequestCommand(&mClientState.chppClientState,
+                                               command)
+               : chppAllocServiceRequestCommand(&mServiceState.chppServiceState,
+                                                command);
+  }
+
+  void TimestampOutgoingRequest(struct ChppAppHeader *request,
+                                uint64_t timeoutNs) {
+    CHPP_NOT_NULL(request);
+
+    const uint16_t command = request->command;
+
+    if (GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST) {
+      chppTimestampOutgoingRequest(&mClientAppContext,
+                                   &mClientState.outReqStates[command], request,
+                                   timeoutNs);
+    } else {
+      chppTimestampOutgoingRequest(&mServiceAppContext,
+                                   &mServiceState.outReqStates[command],
+                                   request, timeoutNs);
+    }
+  }
+
+  bool TimestampIncomingResponse(struct ChppAppHeader *response) {
+    CHPP_NOT_NULL(response);
+
+    const uint16_t command = response->command;
+
+    if (GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST) {
+      return chppTimestampIncomingResponse(
+          &mClientAppContext, &mClientState.outReqStates[command], response);
+    }
+    return chppTimestampIncomingResponse(
+        &mServiceAppContext, &mServiceState.outReqStates[command], response);
+  }
+
+  uint64_t GetNextRequestTimeoutNs(void) {
+    return GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+               ? mClientAppContext.nextClientRequestTimeoutNs
+               : mServiceAppContext.nextServiceRequestTimeoutNs;
+  }
+
+  struct ChppAppHeader *GetTimeoutResponse(void) {
+    return GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST
+               ? chppTransportGetRequestTimeoutResponse(
+                     &mClientTransportContext, CHPP_ENDPOINT_CLIENT)
+               : chppTransportGetRequestTimeoutResponse(
+                     &mServiceTransportContext, CHPP_ENDPOINT_SERVICE);
+  }
+
+  void ValidateRequestState(struct ChppAppHeader *request) {
+    CHPP_NOT_NULL(request);
+    if (GetParam() == CHPP_MESSAGE_TYPE_CLIENT_REQUEST) {
+      ValidateClientStateAndReqState(&mClientState.chppClientState, request);
+    } else {
+      ValidateServiceStateAndReqState(&mServiceState.chppServiceState, request);
+    }
+  }
+
+  void RegisterAndValidateRequestForTimeout(struct ChppAppHeader *request,
+                                            uint64_t kTimeoutNs,
+                                            uint64_t expectedTimeNs) {
+    CHPP_NOT_NULL(request);
+    ValidateRequestState(request);
+    TimestampOutgoingRequest(request, kTimeoutNs);
+
+    validateTimeout(GetNextRequestTimeoutNs(), expectedTimeNs);
+  }
+
+  void RegisterAndValidateResponseForTimeout(struct ChppAppHeader *request,
+                                             uint64_t expectedTimeNs) {
+    CHPP_NOT_NULL(request);
+
+    struct ChppAppHeader *response =
+        chppAllocResponse(request, sizeof(*request));
+
+    ValidateRequestState(request);
+    TimestampIncomingResponse(response);
+
+    validateTimeout(GetNextRequestTimeoutNs(), expectedTimeNs);
+
+    chppFree(response);
+  }
+
+  // Client side.
+  ChppLinuxLinkState mClientLinkContext = {};
+  ChppTransportState mClientTransportContext = {};
+  ChppAppState mClientAppContext = {};
+  ClientState mClientState;
+
+  // Service side
+  ChppLinuxLinkState mServiceLinkContext = {};
+  ChppTransportState mServiceTransportContext = {};
+  ChppAppState mServiceAppContext = {};
+  ServiceState mServiceState;
+};
+
+// Simulates a request and a response.
+// There should be no error as the timeout is infinite.
+TEST_P(TimeoutParamTest, RequestResponseTimestampValid) {
+  struct ChppAppHeader *request = AllocRequestCommand(0 /* command */);
+  ASSERT_NE(request, nullptr);
+  TimestampOutgoingRequest(request, CHPP_REQUEST_TIMEOUT_INFINITE);
+
+  struct ChppAppHeader *response = chppAllocResponse(request, sizeof(*request));
+  EXPECT_TRUE(TimestampIncomingResponse(response));
+
+  chppFree(request);
+  chppFree(response);
+}
+
+// Simulates a single request with 2 responses.
+TEST_P(TimeoutParamTest, RequestResponseTimestampDuplicate) {
+  struct ChppAppHeader *request = AllocRequestCommand(0 /* command */);
+  ASSERT_NE(request, nullptr);
+  TimestampOutgoingRequest(request, CHPP_REQUEST_TIMEOUT_INFINITE);
+
+  struct ChppAppHeader *response = chppAllocResponse(request, sizeof(*request));
+
+  // The first response has no error.
+  EXPECT_TRUE(TimestampIncomingResponse(response));
+
+  // The second response errors as one response has already been received.
+  EXPECT_FALSE(TimestampIncomingResponse(response));
+
+  chppFree(request);
+  chppFree(response);
+}
+
+// Simulates a response to a request that has not been timestamped.
+TEST_P(TimeoutParamTest, RequestResponseTimestampInvalidId) {
+  constexpr uint16_t command = 0;
+
+  struct ChppAppHeader *request1 = AllocRequestCommand(command);
+  ASSERT_NE(request1, nullptr);
+  TimestampOutgoingRequest(request1, CHPP_REQUEST_TIMEOUT_INFINITE);
+
+  struct ChppAppHeader *request2 = AllocRequestCommand(command);
+  ASSERT_NE(request2, nullptr);
+
+  // We expect a response for req but get a response for newReq.
+  // That is an error (the transaction does not match).
+  struct ChppAppHeader *response =
+      chppAllocResponse(request2, sizeof(*request1));
+  EXPECT_FALSE(TimestampIncomingResponse(response));
+
+  chppFree(request1);
+  chppFree(request2);
+  chppFree(response);
+}
+
+// Make sure the request does not timeout right away.
+TEST_P(TimeoutParamTest, RequestTimeoutAddRemoveSingle) {
+  EXPECT_EQ(GetNextRequestTimeoutNs(), CHPP_TIME_MAX);
+
+  struct ChppAppHeader *request = AllocRequestCommand(1 /* command */);
+  ASSERT_NE(request, nullptr);
+
+  const uint64_t timeNs = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeoutNs = 1000 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request, kTimeoutNs,
+                                       timeNs + kTimeoutNs);
+
+  // Timeout is not expired yet.
+  EXPECT_EQ(GetTimeoutResponse(), nullptr);
+
+  RegisterAndValidateResponseForTimeout(request, CHPP_TIME_MAX);
+
+  chppFree(request);
+}
+
+TEST_P(TimeoutParamTest, RequestTimeoutAddRemoveMultiple) {
+  EXPECT_EQ(GetNextRequestTimeoutNs(), CHPP_TIME_MAX);
+
+  struct ChppAppHeader *request1 = AllocRequestCommand(0 /* command */);
+  struct ChppAppHeader *request2 = AllocRequestCommand(1 /* command */);
+  struct ChppAppHeader *request3 = AllocRequestCommand(2 /* command */);
+  ASSERT_NE(request1, nullptr);
+  ASSERT_NE(request2, nullptr);
+  ASSERT_NE(request3, nullptr);
+
+  // kTimeout1Ns is the smallest so it will be the first timeout to expire
+  // for all the requests.
+  const uint64_t time1Ns = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeout1Ns = 2000 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request1, kTimeout1Ns,
+                                       time1Ns + kTimeout1Ns);
+
+  const uint64_t time2Ns = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeout2Ns = 4000 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request2, kTimeout2Ns,
+                                       time1Ns + kTimeout1Ns);
+
+  const uint64_t time3Ns = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeout3Ns = 3000 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request3, kTimeout3Ns,
+                                       time1Ns + kTimeout1Ns);
+
+  RegisterAndValidateResponseForTimeout(request1, time3Ns + kTimeout3Ns);
+
+  // Timeout is not expired yet.
+  EXPECT_EQ(GetTimeoutResponse(), nullptr);
+
+  // kTimeout4Ns is now the smallest timeout.
+  const uint64_t time4Ns = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeout4Ns = 1000 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request1, kTimeout4Ns,
+                                       time4Ns + kTimeout4Ns);
+
+  RegisterAndValidateResponseForTimeout(request1, time3Ns + kTimeout3Ns);
+
+  RegisterAndValidateResponseForTimeout(request3, time2Ns + kTimeout2Ns);
+
+  RegisterAndValidateResponseForTimeout(request2, CHPP_TIME_MAX);
+
+  EXPECT_EQ(GetTimeoutResponse(), nullptr);
+
+  chppFree(request1);
+  chppFree(request2);
+  chppFree(request3);
+}
+
+TEST_P(TimeoutParamTest, DuplicateRequestTimeoutResponse) {
+  // Sleep padding to make sure we timeout.
+  constexpr auto kTimeoutPadding = std::chrono::milliseconds(50);
+
+  EXPECT_EQ(GetNextRequestTimeoutNs(), CHPP_TIME_MAX);
+
+  struct ChppAppHeader *request = AllocRequestCommand(1 /* command */);
+  ASSERT_NE(request, nullptr);
+
+  // Send the first request.
+  constexpr uint64_t kTimeout1Ns = 20 * CHPP_NSEC_PER_MSEC;
+  const uint64_t kShouldTimeout1AtNs = chppGetCurrentTimeNs() + kTimeout1Ns;
+  RegisterAndValidateRequestForTimeout(request, kTimeout1Ns,
+                                       kShouldTimeout1AtNs);
+
+  // Override with a new request.
+  constexpr uint64_t kTimeout2Ns = 400 * CHPP_NSEC_PER_MSEC;
+  const uint64_t kShouldTimeout2AtNs = chppGetCurrentTimeNs() + kTimeout2Ns;
+  RegisterAndValidateRequestForTimeout(request, kTimeout2Ns,
+                                       kShouldTimeout2AtNs);
+
+  std::this_thread::sleep_for(
+      std::chrono::nanoseconds(kShouldTimeout1AtNs - chppGetCurrentTimeNs()) +
+      kTimeoutPadding);
+  // First request would have timed out but superseded by second request.
+  EXPECT_GT(GetNextRequestTimeoutNs(), chppGetCurrentTimeNs());
+
+  std::this_thread::sleep_for(
+      std::chrono::nanoseconds(kShouldTimeout2AtNs - chppGetCurrentTimeNs()) +
+      kTimeoutPadding);
+  // Second request should have timed out - so we get a response.
+  EXPECT_LT(GetNextRequestTimeoutNs(), chppGetCurrentTimeNs());
+
+  struct ChppAppHeader *response = GetTimeoutResponse();
+  ASSERT_NE(response, nullptr);
+  validateTimeoutResponse(request, response);
+  chppFree(response);
+
+  RegisterAndValidateResponseForTimeout(request, CHPP_TIME_MAX);
+  EXPECT_EQ(GetTimeoutResponse(), nullptr);
+
+  chppFree(request);
+}
+
+TEST_P(TimeoutParamTest, RequestTimeoutResponse) {
+  EXPECT_EQ(GetNextRequestTimeoutNs(), CHPP_TIME_MAX);
+
+  struct ChppAppHeader *request1 = AllocRequestCommand(1 /* command */);
+  struct ChppAppHeader *request2 = AllocRequestCommand(2 /* command */);
+  ASSERT_NE(request1, nullptr);
+  ASSERT_NE(request2, nullptr);
+
+  const uint64_t time1Ns = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeout1Ns = 200 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request1, kTimeout1Ns,
+                                       time1Ns + kTimeout1Ns);
+
+  std::this_thread::sleep_for(std::chrono::nanoseconds(kTimeout1Ns));
+  ASSERT_LT(GetNextRequestTimeoutNs(), chppGetCurrentTimeNs());
+
+  // No response in time, we then get a timeout response.
+  struct ChppAppHeader *response = GetTimeoutResponse();
+  validateTimeoutResponse(request1, response);
+  chppFree(response);
+
+  RegisterAndValidateResponseForTimeout(request1, CHPP_TIME_MAX);
+  // No other request in timeout.
+  EXPECT_EQ(GetTimeoutResponse(), nullptr);
+
+  // Simulate a new timeout and make sure we have a timeout response.
+  const uint64_t time2Ns = chppGetCurrentTimeNs();
+  constexpr uint64_t kTimeout2Ns = 200 * CHPP_NSEC_PER_MSEC;
+  RegisterAndValidateRequestForTimeout(request2, kTimeout2Ns,
+                                       time2Ns + kTimeout2Ns);
+
+  std::this_thread::sleep_for(std::chrono::nanoseconds(kTimeout2Ns));
+  ASSERT_LT(GetNextRequestTimeoutNs(), chppGetCurrentTimeNs());
+
+  response = GetTimeoutResponse();
+  validateTimeoutResponse(request2, response);
+  chppFree(response);
+
+  chppFree(request1);
+  chppFree(request2);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    TimeoutTest, TimeoutParamTest,
+    testing::Values(CHPP_MESSAGE_TYPE_CLIENT_REQUEST,
+                    CHPP_MESSAGE_TYPE_SERVICE_REQUEST),
+    [](const testing::TestParamInfo<TimeoutParamTest::ParamType> &info) {
+      return info.param == CHPP_MESSAGE_TYPE_CLIENT_REQUEST ? "ClientRequests"
+                                                            : "ServiceRequests";
+    });
+
+}  // namespace
+}  // namespace chre
\ No newline at end of file
diff --git a/chpp/test/clients_test.h b/chpp/test/app_timeout_test.h
similarity index 87%
rename from chpp/test/clients_test.h
rename to chpp/test/app_timeout_test.h
index d7caa32..2b41d1e 100644
--- a/chpp/test/clients_test.h
+++ b/chpp/test/app_timeout_test.h
@@ -28,8 +28,8 @@
  *  Functions necessary for unit testing
  ***********************************************/
 
-struct ChppAppHeader *chppTransportGetClientRequestTimeoutResponse(
-    struct ChppTransportState *context);
+struct ChppAppHeader *chppTransportGetRequestTimeoutResponse(
+    struct ChppTransportState *context, enum ChppEndpointType type);
 
 #ifdef __cplusplus
 }
diff --git a/chpp/test/clients_test.cpp b/chpp/test/clients_test.cpp
deleted file mode 100644
index 5f69ec8..0000000
--- a/chpp/test/clients_test.cpp
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * Copyright (C) 2021 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 "clients_test.h"
-
-#include <gtest/gtest.h>
-
-#include <string.h>
-#include <thread>
-
-#include "chpp/app.h"
-#include "chpp/clients.h"
-#include "chpp/clients/gnss.h"
-#include "chpp/clients/wifi.h"
-#include "chpp/clients/wwan.h"
-#include "chpp/macros.h"
-#include "chpp/memory.h"
-#include "chpp/platform/platform_link.h"
-#include "chpp/platform/utils.h"
-#include "chpp/services.h"
-#include "chpp/time.h"
-#include "chpp/transport.h"
-#include "chre/pal/wwan.h"
-
-class ClientsTest : public testing::Test {
- protected:
-  void SetUp() override {
-    chppClearTotalAllocBytes();
-
-    memset(&mAppContext, 0, sizeof(mAppContext));
-    memset(&mTransportContext, 0, sizeof(mTransportContext));
-    memset(&mLinkContext, 0, sizeof(mLinkContext));
-    mLinkContext.linkEstablished = true;
-
-    chppTransportInit(&mTransportContext, &mAppContext, &mLinkContext,
-                      getLinuxLinkApi());
-    chppAppInit(&mAppContext, &mTransportContext);
-    mClientState =
-        (struct ChppClientState *)mAppContext.registeredClientContexts[0];
-    chppClientInit(mClientState, CHPP_HANDLE_NEGOTIATED_RANGE_START);
-
-    mTransportContext.resetState = CHPP_RESET_STATE_NONE;
-  }
-
-  void TearDown() override {
-    chppAppDeinit(&mAppContext);
-    chppTransportDeinit(&mTransportContext);
-
-    EXPECT_EQ(chppGetTotalAllocBytes(), 0);
-  }
-
-  struct ChppTransportState mTransportContext;
-  struct ChppAppState mAppContext;
-  struct ChppLinuxLinkState mLinkContext;
-  struct ChppClientState *mClientState;
-  struct ChppRequestResponseState mRRState;
-};
-
-void getClientRRStateInputCheck(struct ChppClientState *clientState,
-                                struct ChppAppHeader *header) {
-  ASSERT_TRUE(clientState != NULL);
-  uint8_t clientIdx = clientState->index;
-
-  ASSERT_TRUE(clientState->appContext != NULL);
-  ASSERT_TRUE(clientState->appContext->registeredClients != NULL);
-  ASSERT_TRUE(clientState->appContext->registeredClients[clientIdx] != NULL);
-  ASSERT_TRUE(
-      clientState->appContext->registeredClientStates[clientIdx]->rRStates !=
-      NULL);
-  ASSERT_LT(
-      header->command,
-      clientState->appContext->registeredClients[clientIdx]->rRStateCount);
-}
-
-struct ChppRequestResponseState *getClientRRState(
-    struct ChppClientState *clientState, struct ChppAppHeader *header) {
-  getClientRRStateInputCheck(clientState, header);
-
-  uint8_t clientIdx = clientState->index;
-  return &(clientState->appContext->registeredClientStates[clientIdx]
-               ->rRStates[header->command]);
-}
-
-void isTimeoutAsExpected(uint64_t timeoutTimeNs, uint64_t expectedTimeNs) {
-  uint64_t kJitterNs = 10 * CHPP_NSEC_PER_MSEC;
-
-  if (expectedTimeNs == CHPP_TIME_MAX) {
-    EXPECT_EQ(timeoutTimeNs, expectedTimeNs);
-  } else {
-    EXPECT_GE(timeoutTimeNs, expectedTimeNs);
-    EXPECT_LE(timeoutTimeNs, expectedTimeNs + kJitterNs);
-  }
-}
-
-void registerAndValidateRequestForTimeout(struct ChppClientState *clientState,
-                                          struct ChppAppHeader *header,
-                                          uint64_t timeoutNs,
-                                          uint64_t expectedTimeNs) {
-  struct ChppRequestResponseState *rRState =
-      getClientRRState(clientState, header);
-  chppClientTimestampRequest(clientState, rRState, header, timeoutNs);
-
-  isTimeoutAsExpected(clientState->appContext->nextRequestTimeoutNs,
-                      expectedTimeNs);
-}
-
-void registerAndValidateResponseForTimeout(struct ChppClientState *clientState,
-                                           const struct ChppAppHeader *header,
-                                           uint64_t expectedTimeNs) {
-  ASSERT_FALSE(clientState == NULL);
-  uint8_t clientIdx = clientState->index;
-
-  ASSERT_FALSE(clientState->appContext == NULL);
-  ASSERT_FALSE(clientState->appContext->registeredClients == NULL);
-  ASSERT_FALSE(clientState->appContext->registeredClients[clientIdx] == NULL);
-  ASSERT_FALSE(
-      clientState->appContext->registeredClientStates[clientIdx]->rRStates ==
-      NULL);
-  ASSERT_LT(
-      header->command,
-      clientState->appContext->registeredClients[clientIdx]->rRStateCount);
-
-  struct ChppRequestResponseState *rRState =
-      &(clientState->appContext->registeredClientStates[clientIdx]
-            ->rRStates[header->command]);
-  chppClientTimestampResponse(clientState, rRState, header);
-
-  isTimeoutAsExpected(clientState->appContext->nextRequestTimeoutNs,
-                      expectedTimeNs);
-}
-
-void validateTimeoutResponse(const struct ChppAppHeader *request,
-                             const struct ChppAppHeader *response) {
-  ASSERT_TRUE(request != NULL);
-  ASSERT_TRUE(response != NULL);
-
-  EXPECT_EQ(response->handle, request->handle);
-  EXPECT_EQ(response->type, CHPP_MESSAGE_TYPE_SERVICE_RESPONSE);
-  EXPECT_EQ(response->transaction, request->transaction);
-  EXPECT_EQ(response->error, CHPP_APP_ERROR_TIMEOUT);
-  EXPECT_EQ(response->command, request->command);
-}
-
-TEST_F(ClientsTest, RequestResponseTimestampValid) {
-  struct ChppAppHeader *reqHeader =
-      chppAllocClientRequestCommand(mClientState, 0 /* command */);
-  chppClientTimestampRequest(mClientState, &mRRState, reqHeader,
-                             CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE);
-
-  struct ChppAppHeader *respHeader =
-      chppAllocServiceResponse(reqHeader, sizeof(*reqHeader));
-  ASSERT_TRUE(chppClientTimestampResponse(mClientState, &mRRState, respHeader));
-
-  chppFree(reqHeader);
-  chppFree(respHeader);
-}
-
-TEST_F(ClientsTest, RequestResponseTimestampDuplicate) {
-  struct ChppAppHeader *reqHeader =
-      chppAllocClientRequestCommand(mClientState, 0 /* command */);
-  chppClientTimestampRequest(mClientState, &mRRState, reqHeader,
-                             CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE);
-
-  struct ChppAppHeader *respHeader =
-      chppAllocServiceResponse(reqHeader, sizeof(*reqHeader));
-  ASSERT_TRUE(chppClientTimestampResponse(mClientState, &mRRState, respHeader));
-  ASSERT_FALSE(
-      chppClientTimestampResponse(mClientState, &mRRState, respHeader));
-
-  chppFree(reqHeader);
-  chppFree(respHeader);
-}
-
-TEST_F(ClientsTest, RequestResponseTimestampInvalidId) {
-  struct ChppAppHeader *reqHeader =
-      chppAllocClientRequestCommand(mClientState, 0 /* command */);
-  chppClientTimestampRequest(mClientState, &mRRState, reqHeader,
-                             CHPP_CLIENT_REQUEST_TIMEOUT_INFINITE);
-
-  struct ChppAppHeader *newReqHeader =
-      chppAllocClientRequestCommand(mClientState, 0 /* command */);
-  struct ChppAppHeader *respHeader =
-      chppAllocServiceResponse(newReqHeader, sizeof(*reqHeader));
-  ASSERT_FALSE(
-      chppClientTimestampResponse(mClientState, &mRRState, respHeader));
-
-  chppFree(reqHeader);
-  chppFree(newReqHeader);
-  chppFree(respHeader);
-}
-
-TEST_F(ClientsTest, RequestTimeoutAddRemoveSingle) {
-  EXPECT_EQ(mAppContext.nextRequestTimeoutNs, CHPP_TIME_MAX);
-
-  struct ChppAppHeader *reqHeader =
-      chppAllocClientRequestCommand(mClientState, 1 /* command */);
-
-  uint64_t time = chppGetCurrentTimeNs();
-  uint64_t timeout = 1000 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader, timeout,
-                                       time + timeout);
-
-  EXPECT_TRUE(
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext) == NULL);
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader, CHPP_TIME_MAX);
-
-  chppFree(reqHeader);
-}
-
-TEST_F(ClientsTest, RequestTimeoutAddRemoveMultiple) {
-  struct ChppAppHeader *reqHeader1 =
-      chppAllocClientRequestCommand(mClientState, 0 /* command */);
-  struct ChppAppHeader *reqHeader2 =
-      chppAllocClientRequestCommand(mClientState, 1 /* command */);
-  struct ChppAppHeader *reqHeader3 =
-      chppAllocClientRequestCommand(mClientState, 2 /* command */);
-
-  EXPECT_EQ(mAppContext.nextRequestTimeoutNs, CHPP_TIME_MAX);
-
-  uint64_t time1 = chppGetCurrentTimeNs();
-  uint64_t timeout1 = 2000 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader1, timeout1,
-                                       time1 + timeout1);
-
-  uint64_t time2 = chppGetCurrentTimeNs();
-  uint64_t timeout2 = 4000 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader2, timeout2,
-                                       time1 + timeout1);
-
-  uint64_t time3 = chppGetCurrentTimeNs();
-  uint64_t timeout3 = 3000 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader3, timeout3,
-                                       time1 + timeout1);
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader1,
-                                        time3 + timeout3);
-
-  EXPECT_TRUE(
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext) == NULL);
-
-  uint64_t time4 = chppGetCurrentTimeNs();
-  uint64_t timeout4 = 1000 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader1, timeout4,
-                                       time4 + timeout4);
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader1,
-                                        time3 + timeout3);
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader3,
-                                        time2 + timeout2);
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader2,
-                                        CHPP_TIME_MAX);
-
-  EXPECT_TRUE(
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext) == NULL);
-
-  chppFree(reqHeader1);
-  chppFree(reqHeader2);
-  chppFree(reqHeader3);
-}
-
-TEST_F(ClientsTest, DuplicateRequestTimeoutResponse) {
-  EXPECT_EQ(mAppContext.nextRequestTimeoutNs, CHPP_TIME_MAX);
-
-  struct ChppAppHeader *reqHeader =
-      chppAllocClientRequestCommand(mClientState, 1 /* command */);
-
-  uint64_t time1 = chppGetCurrentTimeNs();
-  uint64_t timeout1 = 200 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader, timeout1,
-                                       time1 + timeout1);
-
-  std::this_thread::sleep_for(std::chrono::nanoseconds(timeout1 / 2));
-
-  uint64_t time2 = chppGetCurrentTimeNs();
-  uint64_t timeout2 = 200 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader, timeout2,
-                                       time2 + timeout2);
-
-  std::this_thread::sleep_for(
-      std::chrono::nanoseconds(timeout1 + time1 - chppGetCurrentTimeNs()));
-  // First request would have timed out but superseded by second request
-  ASSERT_GT(mAppContext.nextRequestTimeoutNs, chppGetCurrentTimeNs());
-
-  std::this_thread::sleep_for(
-      std::chrono::nanoseconds(timeout2 + time2 - chppGetCurrentTimeNs()));
-  // Second request should have timed out
-  ASSERT_LT(mAppContext.nextRequestTimeoutNs, chppGetCurrentTimeNs());
-
-  struct ChppAppHeader *response =
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext);
-  validateTimeoutResponse(reqHeader, response);
-  if (response != NULL) {
-    chppFree(response);
-  }
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader, CHPP_TIME_MAX);
-  EXPECT_TRUE(
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext) == NULL);
-
-  chppFree(reqHeader);
-}
-
-TEST_F(ClientsTest, RequestTimeoutResponse) {
-  EXPECT_EQ(mAppContext.nextRequestTimeoutNs, CHPP_TIME_MAX);
-
-  struct ChppAppHeader *reqHeader1 =
-      chppAllocClientRequestCommand(mClientState, 1 /* command */);
-  struct ChppAppHeader *reqHeader2 =
-      chppAllocClientRequestCommand(mClientState, 2 /* command */);
-
-  uint64_t time1 = chppGetCurrentTimeNs();
-  uint64_t timeout1 = 200 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader1, timeout1,
-                                       time1 + timeout1);
-
-  std::this_thread::sleep_for(std::chrono::nanoseconds(timeout1));
-  ASSERT_LT(mAppContext.nextRequestTimeoutNs, chppGetCurrentTimeNs());
-
-  struct ChppAppHeader *response =
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext);
-  validateTimeoutResponse(reqHeader1, response);
-  if (response != NULL) {
-    chppFree(response);
-  }
-
-  registerAndValidateResponseForTimeout(mClientState, reqHeader1,
-                                        CHPP_TIME_MAX);
-  EXPECT_TRUE(
-      chppTransportGetClientRequestTimeoutResponse(&mTransportContext) == NULL);
-
-  uint64_t time2 = chppGetCurrentTimeNs();
-  uint64_t timeout2 = 200 * CHPP_NSEC_PER_MSEC;
-  registerAndValidateRequestForTimeout(mClientState, reqHeader2, timeout2,
-                                       time2 + timeout2);
-
-  std::this_thread::sleep_for(std::chrono::nanoseconds(timeout2));
-  ASSERT_LT(mAppContext.nextRequestTimeoutNs, chppGetCurrentTimeNs());
-
-  response = chppTransportGetClientRequestTimeoutResponse(&mTransportContext);
-  validateTimeoutResponse(reqHeader2, response);
-  if (response != NULL) {
-    chppFree(response);
-  }
-
-  chppFree(reqHeader1);
-  chppFree(reqHeader2);
-}
\ No newline at end of file
diff --git a/chpp/test/fake_link_sync_test.cpp b/chpp/test/fake_link_sync_test.cpp
index c4ec2a1..f926dd6 100644
--- a/chpp/test/fake_link_sync_test.cpp
+++ b/chpp/test/fake_link_sync_test.cpp
@@ -16,6 +16,7 @@
 
 #include <gtest/gtest.h>
 
+#include <string.h>
 #include <cstdint>
 #include <iostream>
 #include <thread>
@@ -87,6 +88,7 @@
 class FakeLinkSyncTests : public testing::Test {
  protected:
   void SetUp() override {
+    memset(&mLinkContext, 0, sizeof(mLinkContext));
     chppTransportInit(&mTransportContext, &mAppContext, &mLinkContext,
                       &gLinkApi);
     chppAppInitWithClientServiceSet(&mAppContext, &mTransportContext,
@@ -96,19 +98,24 @@
     mWorkThread = std::thread(chppWorkThreadStart, &mTransportContext);
 
     // Proceed to the initialized state by performing the CHPP 3-way handshake
+    CHPP_LOGI("Send a RESET packet");
     ASSERT_TRUE(mFakeLink->waitForTxPacket());
     std::vector<uint8_t> resetPkt = mFakeLink->popTxPacket();
     ASSERT_TRUE(comparePacket(resetPkt, generateResetPacket()))
         << "Full packet: " << asResetPacket(resetPkt);
 
+    CHPP_LOGI("Receive a RESET ACK packet");
     ChppResetPacket resetAck = generateResetAckPacket();
     chppRxDataCb(&mTransportContext, reinterpret_cast<uint8_t *>(&resetAck),
                  sizeof(resetAck));
 
+    // chppProcessResetAck() results in sending a no error packet.
+    CHPP_LOGI("Send CHPP_TRANSPORT_ERROR_NONE packet");
     ASSERT_TRUE(mFakeLink->waitForTxPacket());
     std::vector<uint8_t> ackPkt = mFakeLink->popTxPacket();
     ASSERT_TRUE(comparePacket(ackPkt, generateEmptyPacket()))
         << "Full packet: " << asChpp(ackPkt);
+    CHPP_LOGI("CHPP handshake complete");
   }
 
   void TearDown() override {
@@ -127,7 +134,7 @@
 
   ChppTransportState mTransportContext = {};
   ChppAppState mAppContext = {};
-  ChppTestLinkState mLinkContext = {};
+  ChppTestLinkState mLinkContext;
   FakeLink *mFakeLink;
   std::thread mWorkThread;
 };
@@ -139,6 +146,7 @@
 
   std::vector<uint8_t> pkt1 = mFakeLink->popTxPacket();
 
+  // Not calling chppRxDataCb() will result in a timeout.
   // Ideally, to speed up the test, we'd have a mechanism to trigger
   // chppNotifierWait() to return immediately, to simulate timeout
   ASSERT_TRUE(mFakeLink->waitForTxPacket());
diff --git a/chpp/test/transport_test.cpp b/chpp/test/transport_test.cpp
index 2ef0bed..f1defb3 100644
--- a/chpp/test/transport_test.cpp
+++ b/chpp/test/transport_test.cpp
@@ -83,7 +83,7 @@
     mTransportContext.resetState = CHPP_RESET_STATE_NONE;
 
     // Make sure CHPP has a correct count of the number of registered services
-    // on this platform, (in this case, 1,) as registered in the function
+    // on this platform as registered in the function
     // chppRegisterCommonServices().
     ASSERT_EQ(mAppContext.registeredServiceCount, kServiceCount);
   }
@@ -108,7 +108,7 @@
   // Start sending data out.
   cycleSendThread();
   // Wait for data to be received and processed.
-  std::this_thread::sleep_for(std::chrono::milliseconds(20));
+  std::this_thread::sleep_for(std::chrono::milliseconds(50));
 
   // Should have reset loc and length for next packet / datagram
   EXPECT_EQ(transportContext->rxStatus.locInDatagram, 0);
diff --git a/chpp/transport.c b/chpp/transport.c
index 14001cf..53687c0 100644
--- a/chpp/transport.c
+++ b/chpp/transport.c
@@ -31,6 +31,7 @@
 #include "chpp/macros.h"
 #include "chpp/memory.h"
 #include "chpp/platform/platform_link.h"
+#include "chpp/services.h"
 #include "chpp/time.h"
 
 /************************************************
@@ -73,6 +74,7 @@
     struct ChppTransportState *context);
 static void chppAddPayload(struct ChppTransportState *context);
 static void chppAddFooter(struct ChppTransportState *context);
+// Can not be static (used in tests).
 size_t chppDequeueTxDatagram(struct ChppTransportState *context);
 static void chppClearTxDatagramQueue(struct ChppTransportState *context);
 static void chppTransportDoWork(struct ChppTransportState *context);
@@ -81,35 +83,53 @@
 static const char *chppGetPacketAttrStr(uint8_t packetCode);
 static bool chppEnqueueTxDatagram(struct ChppTransportState *context,
                                   uint8_t packetCode, void *buf, size_t len);
-enum ChppLinkErrorCode chppSendPendingPacket(
+static enum ChppLinkErrorCode chppSendPendingPacket(
     struct ChppTransportState *context);
 
 static void chppResetTransportContext(struct ChppTransportState *context);
 static void chppReset(struct ChppTransportState *context,
                       enum ChppTransportPacketAttributes resetType,
                       enum ChppTransportErrorCode error);
-#ifdef CHPP_CLIENT_ENABLED
-struct ChppAppHeader *chppTransportGetClientRequestTimeoutResponse(
-    struct ChppTransportState *context);
-#endif
+struct ChppAppHeader *chppTransportGetRequestTimeoutResponse(
+    struct ChppTransportState *context, enum ChppEndpointType type);
+static const char *chppGetRxStatusLabel(enum ChppRxState state);
+static void chppWorkHandleTimeout(struct ChppTransportState *context);
 
 /************************************************
  *  Private Functions
  ***********************************************/
 
+/** Returns a string representation of the passed ChppRxState */
+static const char *chppGetRxStatusLabel(enum ChppRxState state) {
+  switch (state) {
+    case CHPP_STATE_PREAMBLE:
+      return "PREAMBLE (0)";
+    case CHPP_STATE_HEADER:
+      return "HEADER (1)";
+    case CHPP_STATE_PAYLOAD:
+      return "PAYLOAD (2)";
+    case CHPP_STATE_FOOTER:
+      return "FOOTER (3)";
+  }
+
+  return "invalid";
+}
+
 /**
  * Called any time the Rx state needs to be changed. Ensures that the location
  * counter among that state (rxStatus.locInState) is also reset at the same
  * time.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param newState Next Rx state.
  */
 static void chppSetRxState(struct ChppTransportState *context,
                            enum ChppRxState newState) {
-  CHPP_LOGD("Changing RX transport state from %" PRIu8 " to %" PRIu8
-            " after %" PRIuSIZE " bytes",
-            context->rxStatus.state, newState, context->rxStatus.locInState);
+  CHPP_LOGD(
+      "Changing RX transport state from %s to %s"
+      " after %" PRIuSIZE " bytes",
+      chppGetRxStatusLabel(context->rxStatus.state),
+      chppGetRxStatusLabel(newState), context->rxStatus.locInState);
   context->rxStatus.locInState = 0;
   context->rxStatus.state = newState;
 }
@@ -122,7 +142,7 @@
  * Any future backwards-incompatible versions of CHPP Transport will use a
  * different preamble.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param buf Input data.
  * @param len Length of input data in bytes.
  *
@@ -169,7 +189,7 @@
  * stream.
  * Moves the Rx state to CHPP_STATE_PAYLOAD afterwards.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param buf Input data.
  * @param len Length of input data in bytes.
  *
@@ -235,7 +255,7 @@
  * by the header, from the incoming data stream.
  * Moves the Rx state to CHPP_STATE_FOOTER afterwards.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param buf Input data
  * @param len Length of input data in bytes
  *
@@ -264,7 +284,7 @@
  * stream. Checks checksum, triggering the correct response (ACK / NACK).
  * Moves the Rx state to CHPP_STATE_PREAMBLE afterwards.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param buf Input data.
  * @param len Length of input data in bytes.
  *
@@ -361,7 +381,7 @@
  * Discards of an incomplete Rx packet during receive (e.g. due to a timeout or
  * bad checksum).
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppAbortRxPacket(struct ChppTransportState *context) {
   size_t undoLen = 0;
@@ -422,7 +442,7 @@
 /**
  * Processes a request that is determined to be for a transport-layer loopback.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 #ifdef CHPP_SERVICE_ENABLED_TRANSPORT_LOOPBACK
 static void chppProcessTransportLoopbackRequest(
@@ -467,7 +487,7 @@
 /**
  * Processes a response that is determined to be for a transport-layer loopback.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 #ifdef CHPP_CLIENT_ENABLED_TRANSPORT_LOOPBACK
 static void chppProcessTransportLoopbackResponse(
@@ -504,7 +524,7 @@
 /**
  * Method to invoke when the reset sequence is completed.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppSetResetComplete(struct ChppTransportState *context) {
   context->resetState = CHPP_RESET_STATE_NONE;
@@ -516,7 +536,7 @@
  * An incoming reset-ack packet indicates that a reset is complete at the other
  * end of the CHPP link.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppProcessResetAck(struct ChppTransportState *context) {
   if (context->resetState == CHPP_RESET_STATE_NONE) {
@@ -564,7 +584,7 @@
 /**
  * Process a received, checksum-validated packet.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppProcessRxPacket(struct ChppTransportState *context) {
   uint64_t now = chppGetCurrentTimeNs();
@@ -611,7 +631,7 @@
  * Process the payload of a validated payload-bearing packet and send out the
  * ACK.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppProcessRxPayload(struct ChppTransportState *context) {
   context->rxStatus.expectedSeq++;  // chppProcessRxPacket() already confirms
@@ -657,7 +677,7 @@
  * layer to inform the transport layer using chppDatagramProcessDoneCb() once it
  * is done with the buffer so it is freed.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppClearRxDatagram(struct ChppTransportState *context) {
   context->rxStatus.locInDatagram = 0;
@@ -668,7 +688,7 @@
 /**
  * Validates the checksum of an incoming packet.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  *
  * @return True if and only if the checksum is correct.
  */
@@ -696,7 +716,7 @@
  * Performs consistency checks on received packet header to determine if it is
  * obviously corrupt / invalid / duplicate / out-of-order.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  *
  * @return True if and only if header passes checks
  */
@@ -722,7 +742,7 @@
  * Registers a received ACK. If an outgoing datagram is fully ACKed, it is
  * popped from the TX queue.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppRegisterRxAck(struct ChppTransportState *context) {
   uint8_t rxAckSeq = context->rxHeader.ackSeq;
@@ -789,7 +809,7 @@
  * would only need to send an ACK for the last (correct) packet, hence we only
  * need a queue length of one here.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param packetCode Error code and packet attributes to be sent.
  */
 static void chppEnqueueTxPacket(struct ChppTransportState *context,
@@ -820,7 +840,7 @@
 /**
  * Adds the packet header to link tx buffer.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  *
  * @return Pointer to the added packet header.
  */
@@ -844,7 +864,7 @@
 /**
  * Adds the packet payload to link tx buffer.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppAddPayload(struct ChppTransportState *context) {
   uint8_t *linkTxBuffer = context->linkApi->getTxBuffer(context->linkContext);
@@ -884,7 +904,7 @@
 /**
  * Adds a footer (containing the checksum) to a packet.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppAddFooter(struct ChppTransportState *context) {
   struct ChppTransportFooter footer;
@@ -906,7 +926,7 @@
  * Dequeues the datagram at the front of the datagram tx queue, if any, and
  * frees the payload. Returns the number of remaining datagrams in the queue.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @return Number of remaining datagrams in queue.
  */
 size_t chppDequeueTxDatagram(struct ChppTransportState *context) {
@@ -939,7 +959,7 @@
 /**
  * Flushes the Tx datagram queue of any pending packets.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppClearTxDatagramQueue(struct ChppTransportState *context) {
   while (context->txDatagramQueue.pending > 0) {
@@ -959,7 +979,7 @@
  * Repeat payload: If we haven't received an ACK yet for our previous payload,
  * i.e. we have registered an explicit or implicit NACK.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppTransportDoWork(struct ChppTransportState *context) {
   bool havePacketForLinkLayer = false;
@@ -1021,10 +1041,11 @@
   } else {
     CHPP_LOGW(
         "DoWork nothing to send. hasPackets=%d, linkBusy=%d, pending=%" PRIu8
-        ", RX ACK=%" PRIu8 ", TX seq=%" PRIu8 ", RX state=%" PRIu8,
+        ", RX ACK=%" PRIu8 ", TX seq=%" PRIu8 ", RX state=%s",
         context->txStatus.hasPacketsToSend, context->txStatus.linkBusy,
         context->txDatagramQueue.pending, context->rxStatus.receivedAckSeq,
-        context->txStatus.sentSeq, context->rxStatus.state);
+        context->txStatus.sentSeq,
+        chppGetRxStatusLabel(context->rxStatus.state));
   }
 
   chppMutexUnlock(&context->mutex);
@@ -1049,7 +1070,7 @@
 #ifdef CHPP_CLIENT_ENABLED
   {  // create a scope to declare timeoutResponse (C89).
     struct ChppAppHeader *timeoutResponse =
-        chppTransportGetClientRequestTimeoutResponse(context);
+        chppTransportGetRequestTimeoutResponse(context, CHPP_ENDPOINT_CLIENT);
 
     if (timeoutResponse != NULL) {
       CHPP_LOGE("Response timeout H#%" PRIu8 " cmd=%" PRIu16 " ID=%" PRIu8,
@@ -1060,13 +1081,27 @@
     }
   }
 #endif  // CHPP_CLIENT_ENABLED
+#ifdef CHPP_SERVICE_ENABLED
+  {  // create a scope to declare timeoutResponse (C89).
+    struct ChppAppHeader *timeoutResponse =
+        chppTransportGetRequestTimeoutResponse(context, CHPP_ENDPOINT_SERVICE);
+
+    if (timeoutResponse != NULL) {
+      CHPP_LOGE("Response timeout H#%" PRIu8 " cmd=%" PRIu16 " ID=%" PRIu8,
+                timeoutResponse->handle, timeoutResponse->command,
+                timeoutResponse->transaction);
+      chppAppProcessRxDatagram(context->appContext, (uint8_t *)timeoutResponse,
+                               sizeof(struct ChppAppHeader));
+    }
+  }
+#endif  // CHPP_SERVICE_ENABLED
 }
 
 /**
  * Appends data from a buffer of length len to a link tx buffer, updating its
  * length.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param buf Input data to be copied from.
  * @param len Length of input data in bytes.
  */
@@ -1109,7 +1144,7 @@
  * If enqueueing is unsuccessful, it is up to the caller to decide when or if
  * to free the payload and/or resend it later.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  * @param packetCode Error code and packet attributes to be sent.
  * @param buf Datagram payload allocated through chppMalloc. Cannot be null.
  * @param len Datagram length in bytes.
@@ -1171,11 +1206,11 @@
  * Sends the pending outgoing packet over to the link
  * layer using Send() and updates the last Tx packet time.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  *
  * @return Result of Send().
  */
-enum ChppLinkErrorCode chppSendPendingPacket(
+static enum ChppLinkErrorCode chppSendPendingPacket(
     struct ChppTransportState *context) {
   enum ChppLinkErrorCode error =
       context->linkApi->send(context->linkContext, context->linkBufferSize);
@@ -1188,7 +1223,7 @@
 /**
  * Resets the transport state, maintaining the link layer parameters.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
  */
 static void chppResetTransportContext(struct ChppTransportState *context) {
   memset(&context->rxStatus, 0, sizeof(struct ChppRxStatus));
@@ -1212,7 +1247,7 @@
  * This function retains and restores the platform-specific values of
  * transportContext.linkContext.
  *
- * @param transportContext Maintains state for each transport layer instance.
+ * @param transportContext State of the transport layer.
  * @param resetType Type of reset to send after resetting CHPP (reset vs.
  * reset-ack), as defined in the ChppTransportPacketAttributes struct.
  * @param error Provides the error that led to the reset.
@@ -1266,68 +1301,74 @@
 }
 
 /**
- * Checks for a timed out client request and generates a timeout response if a
- * client request timeout has occurred.
+ * Checks for a timed out request and generates a timeout response if a timeout
+ * has occurred.
  *
- * @param context Maintains state for each transport layer instance.
+ * @param context State of the transport layer.
+ * @param type The type of the endpoint.
  * @return App layer response header if a timeout has occurred. Null otherwise.
  */
-#ifdef CHPP_CLIENT_ENABLED
-struct ChppAppHeader *chppTransportGetClientRequestTimeoutResponse(
-    struct ChppTransportState *context) {
+struct ChppAppHeader *chppTransportGetRequestTimeoutResponse(
+    struct ChppTransportState *context, enum ChppEndpointType type) {
+  CHPP_DEBUG_NOT_NULL(context);
+
+  struct ChppAppState *appState = context->appContext;
   struct ChppAppHeader *response = NULL;
 
-  bool timeoutClientFound = false;
-  uint8_t timedOutClient;
+  bool timeoutEndpointFound = false;
+  uint8_t timedOutEndpointIdx;
   uint16_t timedOutCmd;
 
   chppMutexLock(&context->mutex);
 
-  if (context->appContext->nextRequestTimeoutNs <= chppGetCurrentTimeNs()) {
+  if (*getNextRequestTimeoutNs(appState, type) <= chppGetCurrentTimeNs()) {
     // Determine which request has timed out
+    const uint8_t endpointCount = getRegisteredEndpointCount(appState, type);
+    uint64_t firstTimeout = CHPP_TIME_MAX;
 
-    uint64_t lowestTimeout = CHPP_TIME_MAX;
-    for (uint8_t clientIdx = 0;
-         clientIdx < context->appContext->registeredClientCount; clientIdx++) {
-      for (uint16_t cmdIdx = 0;
-           cmdIdx <
-           context->appContext->registeredClients[clientIdx]->rRStateCount;
-           cmdIdx++) {
-        struct ChppRequestResponseState *rRState =
-            &context->appContext->registeredClientStates[clientIdx]
-                 ->rRStates[cmdIdx];
+    for (uint8_t endpointIdx = 0; endpointIdx < endpointCount; endpointIdx++) {
+      const uint16_t cmdCount =
+          getRegisteredEndpointOutReqCount(appState, endpointIdx, type);
+      const struct ChppEndpointState *endpointState =
+          getRegisteredEndpointState(appState, endpointIdx, type);
+      const struct ChppOutgoingRequestState *reqStates =
+          &endpointState->outReqStates[0];
+      for (uint16_t cmdIdx = 0; cmdIdx < cmdCount; cmdIdx++) {
+        const struct ChppOutgoingRequestState *reqState = &reqStates[cmdIdx];
 
-        if (rRState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT &&
-            rRState->responseTimeNs != CHPP_TIME_NONE &&
-            rRState->responseTimeNs < lowestTimeout) {
-          lowestTimeout = rRState->responseTimeNs;
-          timedOutClient = clientIdx;
+        if (reqState->requestState == CHPP_REQUEST_STATE_REQUEST_SENT &&
+            reqState->responseTimeNs != CHPP_TIME_NONE &&
+            reqState->responseTimeNs < firstTimeout) {
+          firstTimeout = reqState->responseTimeNs;
+          timedOutEndpointIdx = endpointIdx;
           timedOutCmd = cmdIdx;
-          timeoutClientFound = true;
+          timeoutEndpointFound = true;
         }
       }
     }
 
-    if (!timeoutClientFound) {
-      CHPP_LOGE("Timeout at %" PRIu64 " but no client",
-                context->appContext->nextRequestTimeoutNs / CHPP_NSEC_PER_MSEC);
-      chppClientRecalculateNextTimeout(context->appContext);
+    if (!timeoutEndpointFound) {
+      CHPP_LOGE("Timeout at %" PRIu64 " but no endpoint",
+                *getNextRequestTimeoutNs(appState, type) / CHPP_NSEC_PER_MSEC);
+      chppRecalculateNextTimeout(appState, CHPP_ENDPOINT_CLIENT);
     }
   }
 
-  if (timeoutClientFound) {
-    CHPP_LOGE("Client=%" PRIu8 " cmd=%" PRIu16 " timed out", timedOutClient,
-              timedOutCmd);
+  if (timeoutEndpointFound) {
+    CHPP_LOGE("Endpoint=%" PRIu8 " cmd=%" PRIu16 " timed out",
+              timedOutEndpointIdx, timedOutCmd);
     response = chppMalloc(sizeof(struct ChppAppHeader));
     if (response == NULL) {
       CHPP_LOG_OOM();
     } else {
-      response->handle = CHPP_SERVICE_HANDLE_OF_INDEX(timedOutClient);
-      response->type = CHPP_MESSAGE_TYPE_SERVICE_RESPONSE;
+      const struct ChppEndpointState *endpointState =
+          getRegisteredEndpointState(appState, timedOutEndpointIdx, type);
+      response->handle = endpointState->handle;
+      response->type = type == CHPP_ENDPOINT_CLIENT
+                           ? CHPP_MESSAGE_TYPE_SERVICE_RESPONSE
+                           : CHPP_MESSAGE_TYPE_CLIENT_RESPONSE;
       response->transaction =
-          context->appContext->registeredClientStates[timedOutClient]
-              ->rRStates[timedOutCmd]
-              .transaction;
+          endpointState->outReqStates[timedOutCmd].transaction;
       response->error = CHPP_APP_ERROR_TIMEOUT;
       response->command = timedOutCmd;
     }
@@ -1337,7 +1378,6 @@
 
   return response;
 }
-#endif
 
 /************************************************
  *  Public Functions
@@ -1437,8 +1477,8 @@
   }
   chppMutexUnlock(&context->mutex);
 
-  CHPP_LOGD("RX %" PRIuSIZE " bytes: state=%" PRIu8, len,
-            context->rxStatus.state);
+  CHPP_LOGD("RX %" PRIuSIZE " bytes: state=%s", len,
+            chppGetRxStatusLabel(context->rxStatus.state));
   uint64_t now = chppGetCurrentTimeNs();
   context->rxStatus.lastDataTimeMs = (uint32_t)(now / CHPP_NSEC_PER_MSEC);
   context->rxStatus.numTotalDataBytes += len;
@@ -1485,10 +1525,9 @@
 void chppRxPacketCompleteCb(struct ChppTransportState *context) {
   chppMutexLock(&context->mutex);
   if (context->rxStatus.state != CHPP_STATE_PREAMBLE) {
-    CHPP_LOGE("RX pkt ended early: state=%" PRIu8 " seq=%" PRIu8
-              " len=%" PRIu16,
-              context->rxStatus.state, context->rxHeader.seq,
-              context->rxHeader.length);
+    CHPP_LOGE("RX pkt ended early: state=%s seq=%" PRIu8 " len=%" PRIu16,
+              chppGetRxStatusLabel(context->rxStatus.state),
+              context->rxHeader.seq, context->rxHeader.length);
     chppAbortRxPacket(context);
     chppEnqueueTxPacket(context, CHPP_TRANSPORT_ERROR_HEADER);  // NACK
   }
@@ -1548,7 +1587,12 @@
 uint64_t chppTransportGetTimeUntilNextDoWorkNs(
     struct ChppTransportState *context) {
   uint64_t currentTime = chppGetCurrentTimeNs();
-  uint64_t nextDoWorkTime = context->appContext->nextRequestTimeoutNs;
+  // This function is called in the context of the transport worker thread.
+  // As we do not know if the transport is used in the context of a service
+  // or a client, we use the min of both timeouts.
+  uint64_t nextDoWorkTime =
+      MIN(context->appContext->nextClientRequestTimeoutNs,
+          context->appContext->nextServiceRequestTimeoutNs);
 
   if (context->txStatus.hasPacketsToSend ||
       context->resetState == CHPP_RESET_STATE_RESETTING) {
@@ -1606,40 +1650,18 @@
     CHPP_LOGD("CHPP Work Thread terminated");
   } else {
     continueProcessing = true;
-    if (signals & CHPP_TRANSPORT_SIGNAL_EVENT) {
-      chppTransportDoWork(context);
-    }
-
     if (signals == 0) {
-      // Triggered by timeout
-
-      if (chppGetCurrentTimeNs() - context->txStatus.lastTxTimeNs >=
-          CHPP_TRANSPORT_TX_TIMEOUT_NS) {
-        CHPP_LOGE("ACK timeout. Tx t=%" PRIu64,
-                  context->txStatus.lastTxTimeNs / CHPP_NSEC_PER_MSEC);
+      // Triggered by timeout.
+      chppWorkHandleTimeout(context);
+    } else {
+      if (signals & CHPP_TRANSPORT_SIGNAL_EVENT) {
         chppTransportDoWork(context);
       }
-
-      if ((context->resetState == CHPP_RESET_STATE_RESETTING) &&
-          (chppGetCurrentTimeNs() - context->resetTimeNs >=
-           CHPP_TRANSPORT_RESET_TIMEOUT_NS)) {
-        if (context->resetCount + 1 < CHPP_TRANSPORT_MAX_RESET) {
-          CHPP_LOGE("RESET-ACK timeout; retrying");
-          context->resetCount++;
-          chppReset(context, CHPP_TRANSPORT_ATTR_RESET,
-                    CHPP_TRANSPORT_ERROR_TIMEOUT);
-        } else {
-          CHPP_LOGE("RESET-ACK timeout; giving up");
-          context->resetState = CHPP_RESET_STATE_PERMANENT_FAILURE;
-          chppClearTxDatagramQueue(context);
-        }
+      if (signals & CHPP_TRANSPORT_SIGNAL_PLATFORM_MASK) {
+        context->linkApi->doWork(context->linkContext,
+                                 signals & CHPP_TRANSPORT_SIGNAL_PLATFORM_MASK);
       }
     }
-
-    if (signals & CHPP_TRANSPORT_SIGNAL_PLATFORM_MASK) {
-      context->linkApi->doWork(context->linkContext,
-                               signals & CHPP_TRANSPORT_SIGNAL_PLATFORM_MASK);
-    }
   }
 
 #ifdef CHPP_ENABLE_WORK_MONITOR
@@ -1649,6 +1671,55 @@
   return continueProcessing;
 }
 
+/**
+ * Handle timeouts in the worker thread.
+ *
+ * Timeouts occurs when either:
+ * 1. There are packets to send and last packet send was more than
+ *    CHPP_TRANSPORT_TX_TIMEOUT_NS ago
+ * 2. We haven't received a response to a request in time
+ * 3. We haven't received the reset ACK
+ *
+ * For 1 and 2, chppTransportDoWork should be called to respectively
+ * - Transmit the packet
+ * - Send a timeout response
+ */
+static void chppWorkHandleTimeout(struct ChppTransportState *context) {
+  const uint64_t currentTimeNs = chppGetCurrentTimeNs();
+  const bool isTxTimeout = currentTimeNs - context->txStatus.lastTxTimeNs >=
+                           CHPP_TRANSPORT_TX_TIMEOUT_NS;
+
+  // Call chppTransportDoWork for both TX and request timeouts.
+  if (isTxTimeout) {
+    CHPP_LOGE("ACK timeout. Tx t=%" PRIu64,
+              context->txStatus.lastTxTimeNs / CHPP_NSEC_PER_MSEC);
+    chppTransportDoWork(context);
+  } else {
+    const uint64_t requestTimeoutNs =
+        MIN(context->appContext->nextClientRequestTimeoutNs,
+            context->appContext->nextServiceRequestTimeoutNs);
+    const bool isRequestTimeout = requestTimeoutNs <= currentTimeNs;
+    if (isRequestTimeout) {
+      chppTransportDoWork(context);
+    }
+  }
+
+  if ((context->resetState == CHPP_RESET_STATE_RESETTING) &&
+      (currentTimeNs - context->resetTimeNs >=
+       CHPP_TRANSPORT_RESET_TIMEOUT_NS)) {
+    if (context->resetCount + 1 < CHPP_TRANSPORT_MAX_RESET) {
+      CHPP_LOGE("RESET-ACK timeout; retrying");
+      context->resetCount++;
+      chppReset(context, CHPP_TRANSPORT_ATTR_RESET,
+                CHPP_TRANSPORT_ERROR_TIMEOUT);
+    } else {
+      CHPP_LOGE("RESET-ACK timeout; giving up");
+      context->resetState = CHPP_RESET_STATE_PERMANENT_FAILURE;
+      chppClearTxDatagramQueue(context);
+    }
+  }
+}
+
 void chppWorkThreadStop(struct ChppTransportState *context) {
   chppNotifierSignal(&context->notifier, CHPP_TRANSPORT_SIGNAL_EXIT);
 }
diff --git a/chre_api/archive_chre_api.sh b/chre_api/archive_chre_api.sh
index b6c36a5..2d23107 100755
--- a/chre_api/archive_chre_api.sh
+++ b/chre_api/archive_chre_api.sh
@@ -3,18 +3,28 @@
 # Quit if any command produces an error.
 set -e
 
-# Parse variables
-MAJOR_VERSION=$1
-: ${MAJOR_VERSION:?
-    "You must specify the major version of the API to be archived."
-    "Usage ./archive_chre_api.sh <major_version> <minor_version>"}
+VERSION_FILE=include/chre_api/chre/version.h
 
-MINOR_VERSION=$2
-: ${MINOR_VERSION:?
-    "You must specify the minor version of the API to be archived."
-    "Usage ./archive_chre_api.sh <major_version> <minor_version>"}
+# Get the latest API version
+CURRENT_VERSION=$(grep -E "^\#define CHRE_API_VERSION CHRE_API_VERSION_[0-9]+" $VERSION_FILE)
+MAJOR_VERSION=$(echo $CURRENT_VERSION | cut -d "_" -f 6)
+MINOR_VERSION=$(echo $CURRENT_VERSION | cut -d "_" -f 7)
+ARCHIVE_DIRECTORY=v${MAJOR_VERSION}_${MINOR_VERSION}
+mkdir legacy/$ARCHIVE_DIRECTORY
+cp -r include/chre_api/* legacy/$ARCHIVE_DIRECTORY
 
-DIRECTORY=v${MAJOR_VERSION}_${MINOR_VERSION}
+ARCHIVED_VERSION=$(grep -n "^\#define CHRE_API_VERSION_${MAJOR_VERSION}_${MINOR_VERSION}" $VERSION_FILE)
+LINE_NUMBER=$(($(echo $ARCHIVED_VERSION | cut -d ":" -f 1) + 2))
+ARCHIVED_VERSION=$(echo $ARCHIVED_VERSION | cut -d ":" -f 2)
 
-mkdir legacy/$DIRECTORY
-cp -r include/chre_api/* legacy/$DIRECTORY
+HEX_VERSION=$(echo $(echo $(echo $ARCHIVED_VERSION | cut -d "(" -f 2) | cut -d ")" -f 1) | cut -d "x" -f 2)
+HEX_VERSION=$((16#$HEX_VERSION))
+BITSHIFT=$(($MINOR_VERSION << 16))
+HEX_VERSION=$(($HEX_VERSION - $BITSHIFT))
+MINOR_VERSION=$(($MINOR_VERSION + 1))
+BITSHIFT=$(($MINOR_VERSION<< 16));
+HEX_VERSION=$(($HEX_VERSION + $BITSHIFT))
+HEX_VERSION=$(printf "%x" $HEX_VERSION)
+
+sed -i "${LINE_NUMBER}i#define CHRE_API_VERSION_${MAJOR_VERSION}_${MINOR_VERSION} UINT32_C(0x0${HEX_VERSION})\n" $VERSION_FILE
+sed -i "s/${CURRENT_VERSION}/\#define CHRE_API_VERSION CHRE_API_VERSION_${MAJOR_VERSION}_${MINOR_VERSION}/g" $VERSION_FILE
diff --git a/chre_api/include/chre_api/chre/audio.h b/chre_api/include/chre_api/chre/audio.h
index e8ec960..085329e 100644
--- a/chre_api/include/chre_api/chre/audio.h
+++ b/chre_api/include/chre_api/chre/audio.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_AUDIO_H_
 #define _CHRE_AUDIO_H_
 
diff --git a/chre_api/include/chre_api/chre/ble.h b/chre_api/include/chre_api/chre/ble.h
index 9f04964..701fbc2 100644
--- a/chre_api/include/chre_api/chre/ble.h
+++ b/chre_api/include/chre_api/chre/ble.h
@@ -13,6 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef CHRE_BLE_H_
 #define CHRE_BLE_H_
 
@@ -89,6 +93,11 @@
 //! CHRE BLE supports RSSI filters
 #define CHRE_BLE_FILTER_CAPABILITIES_RSSI UINT32_C(1 << 1)
 
+//! CHRE BLE supports Broadcaster Address filters (Corresponding HCI OCF:
+//! 0x0157, Sub-command: 0x02)
+//! @since v1.9
+#define CHRE_BLE_FILTER_CAPABILITIES_BROADCASTER_ADDRESS UINT32_C(1 << 2)
+
 //! CHRE BLE supports Manufacturer Data filters (Corresponding HCI OCF: 0x0157,
 //! Sub-command: 0x06)
 //! @since v1.8
@@ -306,11 +315,6 @@
  */
 enum chreBleAdType {
   //! Service Data with 16-bit UUID
-  //! @deprecated as of v1.8
-  //! TODO(b/285207430): Remove this enum once CHRE has been updated.
-  CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 = 0x16,
-
-  //! Service Data with 16-bit UUID
   //! @since v1.8 CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 was renamed
   //! CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE to reflect that nanoapps
   //! compiled against v1.8+ should use OTA format for service data filters.
@@ -322,42 +326,34 @@
 };
 
 /**
- * Generic scan filters definition based on AD Type, mask, and values. The
- * maximum data length is limited to the maximum possible legacy advertisement
- * payload data length (29 bytes).
- *
- * The filter is matched when
- *   data & dataMask == advData & dataMask
- * where advData is the advertisement packet data for the specified AD type.
+ * Generic filters are used to filter for the presence of AD structures in the
+ * data field of LE Extended Advertising Report events (ref: BT Core Spec v5.3,
+ * Vol 3, Part E, Section 11).
  *
  * The CHRE generic filter structure represents a generic filter on an AD Type
  * as defined in the Bluetooth spec Assigned Numbers, Generic Access Profile
  * (ref: https://www.bluetooth.com/specifications/assigned-numbers/). This
- * generic structure is used by the Advertising Packet Content Filter
- * (APCF) HCI generic AD type sub-command 0x09 (ref:
- * https://source.android.com/devices/bluetooth/hci_requirements#le_apcf_command).
+ * generic structure is used by the Android HCI Advertising Packet Content
+ * Filter (APCF) AD Type sub-command 0x09 (ref:
+ * https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command-ad_type_sub_cmd).
+ *
+ * The filter is matched when an advertisement event contains an AD structure in
+ * its data field that matches the following criteria:
+ *   AdStructure.type == type
+ *   AdStructure.data & dataMask == data & dataMask
+ *
+ * The maximum data length is limited to the maximum possible legacy
+ * advertisement payload data length (29 bytes). The data and dataMask must be
+ * in OTA format. For each zero bit of the dataMask, the corresponding
+ * data bit must also be zero.
  *
  * Note that the CHRE implementation may not support every kind of filter that
  * can be represented by this structure. Use chreBleGetFilterCapabilities() to
  * discover supported filtering capabilities at runtime.
  *
- * @since v1.8 The data and dataMask must be in OTA format. The nanoapp support
- * library will handle converting the data and dataMask values to the correct
- * format if a pre v1.8 version of CHRE is running.
- *
- * NOTE: CHRE versions 1.6 and 1.7 expect the 2-byte UUID prefix in
- * CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 to be given as big-endian. This
- * was corrected in v1.8 to match the OTA format and Bluetooth specification,
- * which uses little-endian. This enum has been removed and replaced with
- * CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE to ensure that nanoapps
- * compiled against v1.8 and later specify their UUID filter using
- * little-endian. Nanoapps compiled against v1.7 or earlier should continue to
- * use big-endian, as CHRE must provide cross-version compatibility for all
- * possible version combinations.
- *
  * Example 1: To filter on a 16 bit service data UUID of 0xFE2C, the following
  * settings would be used:
- *   type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16
+ *   type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE
  *   len = 2
  *   data = {0x2C, 0xFE}
  *   dataMask = {0xFF, 0xFF}
@@ -390,25 +386,43 @@
 };
 
 /**
- * CHRE Bluetooth LE scan filters are based on a combination of an RSSI
- * threshold and generic scan filters as defined by AD Type, mask, and values.
+ * Broadcaster address filters are used to filter by the address field of the LE
+ * Extended Advertising Report event which is defined in the BT Core Spec v5.3,
+ * Vol 4, Part E, Section 7.7.65.13.
  *
- * CHRE-provided filters are implemented in a best-effort manner, depending on
- * HW capabilities of the system and available resources. Therefore, provided
- * scan results may be a superset of the specified filters. Nanoapps should try
- * to take advantage of CHRE scan filters as much as possible, but must design
- * their logic as to not depend on CHRE filtering.
+ * The CHRE broadcaster address filter structure is modeled after the
+ * Advertising Packet Content Filter (APCF) HCI broadcaster address sub-command
+ * 0x02 (ref:
+ * https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command-broadcast_address_sub_cmd).
  *
- * The syntax of CHRE scan filter definitions are based on the Android
- * Advertising Packet Content Filter (APCF) HCI requirement subtype 0x08
- * ref:
- * https://source.android.com/devices/bluetooth/hci_requirements#le_apcf_command-set_filtering_parameters_sub_cmd
- * and AD Types as defined in the Bluetooth spec Assigned Numbers, Generic
- * Access Profile
- * ref: https://www.bluetooth.com/specifications/assigned-numbers/
+ * The CHRE broadcaster address filter does not filter by address type at this
+ * time. If a nanoapp wants to filter for a particular address type, it must
+ * check the addressType field of the chreBleAdvertisingReport.
  *
- * Even though the scan filters are defined in a generic manner, CHRE Bluetooth
- * is expected to initially support only a limited set of AD Types.
+ * NOTE: The broadcasterAddress (6-byte) must be in OTA format.
+ *
+ * The filter is matched when an advertisement even meets the following criteria:
+ *   broadcasterAddress == chreBleAdvertisingReport.address.
+ *
+ * Example: To filter on the address (01:02:03:AB:CD:EF), the following
+ * settings would be used:
+ *   broadcasterAddress = {0xEF, 0xCD, 0xAB, 0x03, 0x02, 0x01}
+ *
+ * @since v1.9
+ */
+struct chreBleBroadcasterAddressFilter {
+  //! 6-byte Broadcaster address
+  uint8_t broadcasterAddress[CHRE_BLE_ADDRESS_LEN];
+};
+
+/**
+ * CHRE Bluetooth LE scan filters.
+ *
+ * @see chreBleScanFilterV1_9 for further details.
+ *
+ * @deprecated as of v1.9 due to the addition of the
+ * chreBleBroadcasterAddressFilter. New code should use chreBleScanFilterV1_9
+ * instead of this struct. This struct will be removed in a future version.
  */
 struct chreBleScanFilter {
   //! RSSI threshold filter (Corresponding HCI OCF: 0x0157, Sub: 0x01), where
@@ -428,6 +442,60 @@
 };
 
 /**
+ * CHRE Bluetooth LE scan filters are based on a combination of an RSSI
+ * threshold, generic filters, and broadcaster address filters.
+ *
+ * When multiple filters are specified, rssiThreshold is combined with the other
+ * filters via functional AND, and the other filters are all combined as
+ * functional OR. In other words, an advertisement matches the filter if:
+ *   rssi >= rssiThreshold
+ *   AND (matchAny(genericFilters) OR matchAny(broadcasterAddressFilters))
+ *
+ * CHRE-provided filters are implemented in a best-effort manner, depending on
+ * HW capabilities of the system and available resources. Therefore, provided
+ * scan results may be a superset of the specified filters. Nanoapps should try
+ * to take advantage of CHRE scan filters as much as possible, but must design
+ * their logic as to not depend on CHRE filtering.
+ *
+ * The syntax of CHRE scan filter definition is modeled after a combination of
+ * multiple Android HCI Advertising Packet Content Filter (APCF) sub commands
+ * including the RSSI threshold from the set filtering parameters sub command
+ * (ref:
+ * https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command-set_filtering_parameters_sub_cmd).
+ * @see chreBleGenericFilter and chreBleBroadcasterAddressFilter for details
+ * about other APCF sub commands referenced.
+ *
+ * @since v1.9
+ */
+struct chreBleScanFilterV1_9 {
+  //! RSSI threshold filter (Corresponding HCI OCF: 0x0157, Sub: 0x01), where
+  //! advertisements with RSSI values below this threshold may be disregarded.
+  //! An rssiThreshold value of CHRE_BLE_RSSI_THRESHOLD_NONE indicates no RSSI
+  //! filtering.
+  int8_t rssiThreshold;
+
+  //! Number of generic filters provided in the scanFilters array. A
+  //! genericFilterCount value of 0 indicates no generic filters.
+  uint8_t genericFilterCount;
+
+  //! Pointer to an array of generic filters. If the array contains more than
+  //! one entry, advertisements matching any of the entries will be returned
+  //! (functional OR). This is expected to be null if genericFilterCount is 0.
+  const struct chreBleGenericFilter *genericFilters;
+
+  //! Number of broadcaster address filters provided in the
+  //! broadcasterAddressFilters array. A broadcasterAddressFilterCount value
+  //! of 0 indicates no broadcaster address filters.
+  uint8_t broadcasterAddressFilterCount;
+
+  //! Pointer to an array of broadcaster address filters. If the array contains
+  //! more than one entry, advertisements matching any of the entries will be
+  //! returned (functional OR). This is expected to be null if
+  //! broadcasterAddressFilterCount is 0.
+  const struct chreBleBroadcasterAddressFilter *broadcasterAddressFilters;
+};
+
+/**
  * CHRE BLE advertising address type is based on the BT Core Spec v5.2, Vol 4,
  * Part E, Section 7.7.65.13, LE Extended Advertising Report event,
  * Address_Type.
@@ -685,6 +753,19 @@
 /**
  * Start Bluetooth LE (BLE) scanning on CHRE.
  *
+ * @see chreBleStartScanAsyncV1_9 for further details.
+ *
+ * @deprecated as of v1.9 due to the addition of the chreBleScanFilterV1_9
+ * struct and a cookie parameter. New code should use
+ * chreBleStartScanAsyncV1_9() instead of this function. This function will be
+ * removed in a future version.
+ */
+bool chreBleStartScanAsync(enum chreBleScanMode mode, uint32_t reportDelayMs,
+                           const struct chreBleScanFilter *filter);
+
+/**
+ * Start Bluetooth LE (BLE) scanning on CHRE.
+ *
  * The result of the operation will be delivered asynchronously via the CHRE
  * event CHRE_EVENT_BLE_ASYNC_RESULT.
  *
@@ -716,8 +797,8 @@
  * Legacy-only: false
  * PHY type: PHY_LE_ALL_SUPPORTED
  *
- * For v1.8 and greater, a CHRE_EVENT_BLE_SCAN_STATUS_CHANGE will be generated
- * if the values in chreBleScanStatus changes as a result of this call.
+ * A CHRE_EVENT_BLE_SCAN_STATUS_CHANGE will be generated if the values in
+ * chreBleScanStatus changes as a result of this call.
  *
  * @param mode Scanning mode selected among enum chreBleScanMode
  * @param reportDelayMs Maximum requested batching delay in ms. 0 indicates no
@@ -727,24 +808,43 @@
  *               defined by struct chreBleScanFilter. The ownership of filter
  *               and its nested elements remains with the caller, and the caller
  *               may release it as soon as chreBleStartScanAsync() returns.
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *               sent as a response to this request.
  *
  * @return True to indicate that the request was accepted. False otherwise.
  *
- * @since v1.6
+ * @since v1.9
  */
-bool chreBleStartScanAsync(enum chreBleScanMode mode, uint32_t reportDelayMs,
-                           const struct chreBleScanFilter *filter);
+bool chreBleStartScanAsyncV1_9(enum chreBleScanMode mode,
+                               uint32_t reportDelayMs,
+                               const struct chreBleScanFilterV1_9 *filter,
+                               const void *cookie);
+
+/**
+ * Stops a CHRE BLE scan.
+ *
+ * @see chreBleStopScanAsyncV1_9 for further details.
+ *
+ * @deprecated as of v1.9 due to the addition of the cookie parameter. New code
+ * should use chreBleStopScanAsyncV1_9() instead of this function. This function
+ * will be removed in a future version.
+ */
+bool chreBleStopScanAsync(void);
+
 /**
  * Stops a CHRE BLE scan.
  *
  * The result of the operation will be delivered asynchronously via the CHRE
  * event CHRE_EVENT_BLE_ASYNC_RESULT.
  *
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *               sent as a response to this request.
+ *
  * @return True to indicate that the request was accepted. False otherwise.
  *
- * @since v1.6
+ * @since v1.9
  */
-bool chreBleStopScanAsync(void);
+bool chreBleStopScanAsyncV1_9(const void *cookie);
 
 /**
  * Requests to immediately deliver batched scan results. The nanoapp must
diff --git a/chre_api/include/chre_api/chre/common.h b/chre_api/include/chre_api/chre/common.h
index 9e3378e..3b270af 100644
--- a/chre_api/include/chre_api/chre/common.h
+++ b/chre_api/include/chre_api/chre/common.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_COMMON_H_
 #define _CHRE_COMMON_H_
 
@@ -121,7 +124,7 @@
     //!< Do not exceed this value when adding new error codes
     CHRE_ERROR_LAST = UINT8_MAX,
 };
-// LINT.ThenChange(core/include/chre/core/api_manager_common.h)
+// LINT.ThenChange(../../../../core/include/chre/core/api_manager_common.h)
 
 /**
  * Generic data structure to indicate the result of an asynchronous operation.
@@ -178,7 +181,8 @@
  * @since v1.8
  */
 struct chreBatchCompleteEvent {
-    //! Indicates the type of event (of type CHRE_EVENT_TYPE_*) that was batched.
+    //! Indicates the type of event (of type CHRE_EVENT_TYPE_*) that was
+    //! batched.
     uint16_t eventType;
 
     //! Reserved for future use, set to 0
diff --git a/chre_api/include/chre_api/chre/event.h b/chre_api/include/chre_api/chre/event.h
index 04b2d0d..f91ef06 100644
--- a/chre_api/include/chre_api/chre/event.h
+++ b/chre_api/include/chre_api/chre/event.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_EVENT_H_
 #define _CHRE_EVENT_H_
 
@@ -508,19 +511,19 @@
  */
 
 //! The host endpoint is part of the Android system framework.
-#define CHRE_HOST_ENDPOINT_TYPE_FRAMEWORK UINT8_C(0)
+#define CHRE_HOST_ENDPOINT_TYPE_FRAMEWORK UINT8_C(0x00)
 
 //! The host endpoint is an Android app.
-#define CHRE_HOST_ENDPOINT_TYPE_APP UINT8_C(1)
+#define CHRE_HOST_ENDPOINT_TYPE_APP UINT8_C(0x01)
 
 //! The host endpoint is an Android native program.
-#define CHRE_HOST_ENDPOINT_TYPE_NATIVE UINT8_C(2)
+#define CHRE_HOST_ENDPOINT_TYPE_NATIVE UINT8_C(0x02)
 
 //! Values in the range [CHRE_HOST_ENDPOINT_TYPE_VENDOR_START,
 //! CHRE_HOST_ENDPOINT_TYPE_VENDOR_END] can be a custom defined host endpoint
 //! type for platform-specific vendor use.
-#define CHRE_HOST_ENDPOINT_TYPE_VENDOR_START UINT8_C(128)
-#define CHRE_HOST_ENDPOINT_TYPE_VENDOR_END UINT8_C(255)
+#define CHRE_HOST_ENDPOINT_TYPE_VENDOR_START UINT8_C(0x80)
+#define CHRE_HOST_ENDPOINT_TYPE_VENDOR_END UINT8_C(0xFF)
 
 /** @} */
 
diff --git a/chre_api/include/chre_api/chre/gnss.h b/chre_api/include/chre_api/chre/gnss.h
index 79a8f46..a326e85 100644
--- a/chre_api/include/chre_api/chre/gnss.h
+++ b/chre_api/include/chre_api/chre/gnss.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_GNSS_H_
 #define _CHRE_GNSS_H_
 
diff --git a/chre_api/include/chre_api/chre/nanoapp.h b/chre_api/include/chre_api/chre/nanoapp.h
index da199ee..3a1c362 100644
--- a/chre_api/include/chre_api/chre/nanoapp.h
+++ b/chre_api/include/chre_api/chre/nanoapp.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_NANOAPP_H_
 #define _CHRE_NANOAPP_H_
 
diff --git a/chre_api/include/chre_api/chre/re.h b/chre_api/include/chre_api/chre/re.h
index 20a69b6..1efa5d3 100644
--- a/chre_api/include/chre_api/chre/re.h
+++ b/chre_api/include/chre_api/chre/re.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_RE_H_
 #define _CHRE_RE_H_
 
@@ -363,7 +366,7 @@
  * @return Never.  This method does not return, as the CHRE stops nanoapp
  *    execution immediately.
  */
-void chreAbort(uint32_t abortCode);
+void chreAbort(uint32_t abortCode) CHRE_NO_RETURN;
 
 /**
  * Allocate a given number of bytes from the system heap.
diff --git a/chre_api/include/chre_api/chre/sensor.h b/chre_api/include/chre_api/chre/sensor.h
index 4166374..67d07f8 100644
--- a/chre_api/include/chre_api/chre/sensor.h
+++ b/chre_api/include/chre_api/chre/sensor.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_SENSOR_H_
 #define _CHRE_SENSOR_H_
 
diff --git a/chre_api/include/chre_api/chre/sensor_types.h b/chre_api/include/chre_api/chre/sensor_types.h
index 63b495b..0f55efc 100644
--- a/chre_api/include/chre_api/chre/sensor_types.h
+++ b/chre_api/include/chre_api/chre/sensor_types.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_SENSOR_TYPES_H_
 #define _CHRE_SENSOR_TYPES_H_
 
@@ -52,7 +55,7 @@
  *
  * @since v1.2
  */
-#define CHRE_SENSOR_TYPE_VENDOR_START  UINT8_C(192)
+#define CHRE_SENSOR_TYPE_VENDOR_START  UINT8_C(0xC0)
 
 /**
  * Accelerometer.
@@ -65,7 +68,7 @@
  *
  * @see chreConfigureSensorBiasEvents
  */
-#define CHRE_SENSOR_TYPE_ACCELEROMETER  UINT8_C(1)
+#define CHRE_SENSOR_TYPE_ACCELEROMETER  UINT8_C(0x01)
 
 /**
  * Instantaneous motion detection.
@@ -78,7 +81,7 @@
  * to SENSOR_TYPE_MOTION_DETECT, but this triggers instantly upon any
  * motion, instead of waiting for a period of continuous motion.
  */
-#define CHRE_SENSOR_TYPE_INSTANT_MOTION_DETECT  UINT8_C(2)
+#define CHRE_SENSOR_TYPE_INSTANT_MOTION_DETECT  UINT8_C(0x02)
 
 /**
  * Stationary detection.
@@ -87,7 +90,7 @@
  *
  * This is a one-shot sensor.
  */
-#define CHRE_SENSOR_TYPE_STATIONARY_DETECT  UINT8_C(3)
+#define CHRE_SENSOR_TYPE_STATIONARY_DETECT  UINT8_C(0x03)
 
 /**
  * Gyroscope.
@@ -100,7 +103,7 @@
  *
  * @see chreConfigureSensorBiasEvents
  */
-#define CHRE_SENSOR_TYPE_GYROSCOPE  UINT8_C(6)
+#define CHRE_SENSOR_TYPE_GYROSCOPE  UINT8_C(0x06)
 
 /**
  * Uncalibrated gyroscope.
@@ -110,7 +113,7 @@
  * Note that the UNCALIBRATED_GYROSCOPE_DATA must be factory calibrated data,
  * but not runtime calibrated.
  */
-#define CHRE_SENSOR_TYPE_UNCALIBRATED_GYROSCOPE  UINT8_C(7)
+#define CHRE_SENSOR_TYPE_UNCALIBRATED_GYROSCOPE  UINT8_C(0x07)
 
 /**
  * Magnetometer.
@@ -123,7 +126,7 @@
  *
  * @see chreConfigureSensorBiasEvents
  */
-#define CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD  UINT8_C(8)
+#define CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD  UINT8_C(0x08)
 
 /**
  * Uncalibrated magnetometer.
@@ -133,14 +136,14 @@
  * Note that the UNCALIBRATED_GEOMAGNETIC_FIELD_DATA must be factory calibrated
  * data, but not runtime calibrated.
  */
-#define CHRE_SENSOR_TYPE_UNCALIBRATED_GEOMAGNETIC_FIELD  UINT8_C(9)
+#define CHRE_SENSOR_TYPE_UNCALIBRATED_GEOMAGNETIC_FIELD  UINT8_C(0x09)
 
 /**
  * Barometric pressure sensor.
  *
  * Generates: CHRE_EVENT_SENSOR_PRESSURE_DATA
  */
-#define CHRE_SENSOR_TYPE_PRESSURE  UINT8_C(10)
+#define CHRE_SENSOR_TYPE_PRESSURE  UINT8_C(0x0A)
 
 /**
  * Ambient light sensor.
@@ -149,7 +152,7 @@
  *
  * This is an on-change sensor.
  */
-#define CHRE_SENSOR_TYPE_LIGHT  UINT8_C(12)
+#define CHRE_SENSOR_TYPE_LIGHT  UINT8_C(0x0C)
 
 /**
  * Proximity detection.
@@ -158,7 +161,7 @@
  *
  * This is an on-change sensor.
  */
-#define CHRE_SENSOR_TYPE_PROXIMITY  UINT8_C(13)
+#define CHRE_SENSOR_TYPE_PROXIMITY  UINT8_C(0x0D)
 
 /**
  * Step detection.
@@ -167,7 +170,7 @@
  *
  * @since v1.3
  */
-#define CHRE_SENSOR_TYPE_STEP_DETECT  UINT8_C(23)
+#define CHRE_SENSOR_TYPE_STEP_DETECT  UINT8_C(0x17)
 
 /**
  * Step counter.
@@ -181,7 +184,7 @@
  *
  * @since v1.5
  */
-#define CHRE_SENSOR_TYPE_STEP_COUNTER UINT8_C(24)
+#define CHRE_SENSOR_TYPE_STEP_COUNTER UINT8_C(0x18)
 
 /**
  * Hinge angle sensor.
@@ -197,7 +200,7 @@
  *
  * @since v1.5
  */
-#define CHRE_SENSOR_TYPE_HINGE_ANGLE UINT8_C(36)
+#define CHRE_SENSOR_TYPE_HINGE_ANGLE UINT8_C(0x24)
 
 /**
  * Uncalibrated accelerometer.
@@ -207,28 +210,28 @@
  * Note that the UNCALIBRATED_ACCELEROMETER_DATA must be factory calibrated
  * data, but not runtime calibrated.
  */
-#define CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER  UINT8_C(55)
+#define CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER  UINT8_C(0x37)
 
 /**
  * Accelerometer temperature.
  *
  * Generates: CHRE_EVENT_SENSOR_ACCELEROMETER_TEMPERATURE_DATA
  */
-#define CHRE_SENSOR_TYPE_ACCELEROMETER_TEMPERATURE  UINT8_C(56)
+#define CHRE_SENSOR_TYPE_ACCELEROMETER_TEMPERATURE  UINT8_C(0x38)
 
 /**
  * Gyroscope temperature.
  *
  * Generates: CHRE_EVENT_SENSOR_GYROSCOPE_TEMPERATURE_DATA
  */
-#define CHRE_SENSOR_TYPE_GYROSCOPE_TEMPERATURE  UINT8_C(57)
+#define CHRE_SENSOR_TYPE_GYROSCOPE_TEMPERATURE  UINT8_C(0x39)
 
 /**
  * Magnetometer temperature.
  *
  * Generates: CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_TEMPERATURE_DATA
  */
-#define CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD_TEMPERATURE  UINT8_C(58)
+#define CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD_TEMPERATURE  UINT8_C(0x3A)
 
 #if CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD_TEMPERATURE >= CHRE_SENSOR_TYPE_VENDOR_START
 #error Too many sensor types
@@ -252,12 +255,12 @@
  * @{
  */
 
-#define CHRE_SENSOR_ACCURACY_UNKNOWN       UINT8_C(0)
-#define CHRE_SENSOR_ACCURACY_UNRELIABLE    UINT8_C(1)
-#define CHRE_SENSOR_ACCURACY_LOW           UINT8_C(2)
-#define CHRE_SENSOR_ACCURACY_MEDIUM        UINT8_C(3)
-#define CHRE_SENSOR_ACCURACY_HIGH          UINT8_C(4)
-#define CHRE_SENSOR_ACCURACY_VENDOR_START  UINT8_C(192)
+#define CHRE_SENSOR_ACCURACY_UNKNOWN       UINT8_C(0x00)
+#define CHRE_SENSOR_ACCURACY_UNRELIABLE    UINT8_C(0x01)
+#define CHRE_SENSOR_ACCURACY_LOW           UINT8_C(0x02)
+#define CHRE_SENSOR_ACCURACY_MEDIUM        UINT8_C(0x03)
+#define CHRE_SENSOR_ACCURACY_HIGH          UINT8_C(0x04)
+#define CHRE_SENSOR_ACCURACY_VENDOR_START  UINT8_C(0xC0)
 #define CHRE_SENSOR_ACCURACY_VENDOR_END    UINT8_MAX
 
 /** @} */
diff --git a/chre_api/include/chre_api/chre/toolchain.h b/chre_api/include/chre_api/chre/toolchain.h
index c2b8722..05b6fb9 100644
--- a/chre_api/include/chre_api/chre/toolchain.h
+++ b/chre_api/include/chre_api/chre/toolchain.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef CHRE_TOOLCHAIN_H_
 #define CHRE_TOOLCHAIN_H_
 
@@ -28,6 +31,9 @@
 #define CHRE_DEPRECATED(message) \
   __attribute__((deprecated(message)))
 
+// Indicates that the function does not return (i.e. abort).
+#define CHRE_NO_RETURN __attribute__((noreturn))
+
 // Enable printf-style compiler warnings for mismatched format string and args
 #define CHRE_PRINTF_ATTR(formatPos, argStart) \
   __attribute__((format(printf, formatPos, argStart)))
diff --git a/chre_api/include/chre_api/chre/user_settings.h b/chre_api/include/chre_api/chre/user_settings.h
index c40be90..a13290d 100644
--- a/chre_api/include/chre_api/chre/user_settings.h
+++ b/chre_api/include/chre_api/chre/user_settings.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_USER_SETTINGS_H_
 #define _CHRE_USER_SETTINGS_H_
 
diff --git a/chre_api/include/chre_api/chre/version.h b/chre_api/include/chre_api/chre/version.h
index 6872fdd..30087ed 100644
--- a/chre_api/include/chre_api/chre/version.h
+++ b/chre_api/include/chre_api/chre/version.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_VERSION_H_
 #define _CHRE_VERSION_H_
 
@@ -135,14 +138,28 @@
 /**
  * Value for version 1.8 of the Context Hub Runtime Environment API interface.
  *
- * This version of the CHRE API is shipped with the Android U release.
+ * This version of the CHRE API is shipped with the Android U release. It adds
+ * support for filtering by manufacturer data in BLE scans, reading the RSSI
+ * value of a BLE connection, allowing the nanoapp to check BLE scan status,
+ * allowing the nanoapp to specify which RPC services it supports, and
+ * delivering batch complete events for batched BLE scans.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_8 UINT32_C(0x01080000)
+
+/**
+ * Value for version 1.9 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API is shipped with a post-launch update to the
+ * Android U release. It adds the BLE Broadcaster Address filter.
  *
  * @note This version of the CHRE API has not been finalized yet, and is
  * currently considered a preview that is subject to change.
  *
  * @see CHRE_API_VERSION
  */
-#define CHRE_API_VERSION_1_8 UINT32_C(0x01080000)
+#define CHRE_API_VERSION_1_9 UINT32_C(0x01090000)
 
 /**
  * Major and Minor Version of this Context Hub Runtime Environment API.
@@ -161,7 +178,7 @@
  * Note that version numbers can always be numerically compared with
  * expected results, so 1.0.0 < 1.0.4 < 1.1.0 < 2.0.300 < 3.5.0.
  */
-#define CHRE_API_VERSION CHRE_API_VERSION_1_8
+#define CHRE_API_VERSION CHRE_API_VERSION_1_9
 
 /**
  * Utility macro to extract only the API major version of a composite CHRE
diff --git a/chre_api/include/chre_api/chre/wifi.h b/chre_api/include/chre_api/chre/wifi.h
index b4bda9c..048e7ae 100644
--- a/chre_api/include/chre_api/chre/wifi.h
+++ b/chre_api/include/chre_api/chre/wifi.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_WIFI_H_
 #define _CHRE_WIFI_H_
 
diff --git a/chre_api/include/chre_api/chre/wwan.h b/chre_api/include/chre_api/chre/wwan.h
index 51cf5f9..ac55f4d 100644
--- a/chre_api/include/chre_api/chre/wwan.h
+++ b/chre_api/include/chre_api/chre/wwan.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// IWYU pragma: private, include "chre_api/chre.h"
+// IWYU pragma: friend chre/.*\.h
+
 #ifndef _CHRE_WWAN_H_
 #define _CHRE_WWAN_H_
 
diff --git a/chre_api/legacy/v1_8/chre.h b/chre_api/legacy/v1_8/chre.h
new file mode 100644
index 0000000..9b87d08
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre.h
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_H_
+#define _CHRE_H_
+
+/**
+ * @file
+ * This header file includes all the headers which combine to fully define the
+ * interface for the Context Hub Runtime Environment (CHRE).  This interface is
+ * of interest to both implementers of CHREs and authors of nanoapps.  The API
+ * documentation attempts to address concerns of both.
+ *
+ * See individual header files for API details, and general comments below
+ * for overall platform information.
+ */
+
+#include <chre/audio.h>
+#include <chre/ble.h>
+#include <chre/common.h>
+#include <chre/event.h>
+#include <chre/gnss.h>
+#include <chre/nanoapp.h>
+#include <chre/re.h>
+#include <chre/sensor.h>
+#include <chre/toolchain.h>
+#include <chre/user_settings.h>
+#include <chre/version.h>
+#include <chre/wifi.h>
+#include <chre/wwan.h>
+
+/**
+ * @mainpage
+ * CHRE is the Context Hub Runtime Environment.  CHRE is used in Android to run
+ * contextual applications, called nanoapps, in a low-power processing domain
+ * other than the applications processor that runs Android itself.  The CHRE
+ * API, documented herein, is the common interface exposed to nanoapps for any
+ * compatible CHRE implementation.  The CHRE API provides the ability for
+ * creating nanoapps that are code-compatible across different CHRE
+ * implementations and underlying platforms. Refer to the following sections for
+ * a discussion on some important details of CHRE that aren't explicitly exposed
+ * in the API itself.
+ *
+ * @section entry_points Entry points
+ *
+ * The following entry points are used to bind a nanoapp to the CHRE system, and
+ * all three must be implemented by any nanoapp (see chre/nanoapp.h):
+ * - nanoappStart: initialization
+ * - nanoappHandleEvent: hook for event-driven processing
+ * - nanoappEnd: graceful teardown
+ *
+ * The CHRE implementation must also ensure that it performs these functions
+ * prior to invoking nanoappStart, or after nanoappEnd returns:
+ * - bss section zeroed out (prior to nanoappStart)
+ * - static variables initialized (prior to nanoappStart)
+ * - global C++ constructors called (prior to nanoappStart)
+ * - global C++ destructors called (after nanoappEnd)
+ *
+ * @section threading Threading model
+ *
+ * A CHRE implementation is free to choose among many different
+ * threading models, including a single-threaded system or a multi-threaded
+ * system with preemption.  The current platform definition is agnostic to this
+ * underlying choice.  However, the CHRE implementation must ensure that time
+ * spent executing within a nanoapp does not significantly degrade or otherwise
+ * interfere with other functions of the system in which CHRE is implemented,
+ * especially latency-sensitive tasks such as sensor event delivery to the AP.
+ * In other words, it must ensure that these functions can either occur in
+ * parallel or preempt a nanoapp's execution.  The current version of the API
+ * does not specify whether the implementation allows for CPU sharing between
+ * nanoapps on a more granular level than the handling of individual events [1].
+ * In any case, event ordering from the perspective of an individual nanoapp
+ * must be FIFO, but the CHRE implementation may choose to violate total
+ * ordering of events across all nanoapps to achieve more fair resource sharing,
+ * but this is not required.
+ *
+ * This version of the CHRE API does require that all nanoapps are treated as
+ * non-reentrant, meaning that only one instance of program flow can be inside
+ * an individual nanoapp at any given time.  That is, any of the functions of
+ * the nanoapp, including the entry points and all other callbacks, cannot be
+ * invoked if a previous invocation to the same or any other function in the
+ * nanoapp has not completed yet.
+ *
+ * For example, if a nanoapp is currently in nanoappHandleEvent(), the CHRE is
+ * not allowed to call nanoappHandleEvent() again, or to call a memory freeing
+ * callback.  Similarly, if a nanoapp is currently in a memory freeing
+ * callback, the CHRE is not allowed to call nanoappHandleEvent(), or invoke
+ * another memory freeing callback.
+ *
+ * There are two exceptions to this rule: If an invocation of chreSendEvent()
+ * fails (returns 'false'), it is allowed to immediately invoke the memory
+ * freeing callback passed into that function.  This is a rare case, and one
+ * where otherwise a CHRE implementation is likely to leak memory. Similarly,
+ * chreSendMessageToHost() is allowed to invoke the memory freeing callback
+ * directly, whether it returns 'true' or 'false'.  This is because the CHRE
+ * implementation may copy the message data to its own buffer, and therefore
+ * wouldn't need the nanoapp-supplied buffer after chreSendMessageToHost()
+ * returns.
+ *
+ * For a nanoapp author, this means no thought needs to be given to
+ * synchronization issues with global objects, as they will, by definition,
+ * only be accessed by a single thread at once.
+ *
+ * [1]: Note to CHRE implementers: A future version of the CHRE platform may
+ * require multi-threading with preemption.  This is mentioned as a heads up,
+ * and to allow implementors deciding between implementation approaches to
+ * make the most informed choice.
+ *
+ * @section timing Timing
+ *
+ * Nanoapps should expect to be running on a highly constrained system, with
+ * little memory and little CPU.  Any single nanoapp should expect to
+ * be one of several nanoapps on the system, which also share the CPU with the
+ * CHRE and possibly other services as well.
+ *
+ * Thus, a nanoapp needs to be efficient in its memory and CPU usage.
+ * Also, as noted in the Threading Model section, a CHRE implementation may
+ * be single threaded.  As a result, all methods invoked in a nanoapp
+ * (like nanoappStart, nanoappHandleEvent, memory free callbacks, etc.)
+ * must run "quickly".  "Quickly" is difficult to define, as there is a
+ * diversity of Context Hub hardware.  Nanoapp authors are strongly recommended
+ * to limit their application to consuming no more than 1 second of CPU time
+ * prior to returning control to the CHRE implementation.  A CHRE implementation
+ * may consider a nanoapp as unresponsive if it spends more time than this to
+ * process a single event, and take corrective action.
+ *
+ * A nanoapp may have the need to occasionally perform a large block of
+ * calculations that exceeds the 1 second guidance.  The recommended approach in
+ * this case is to split up the large block of calculations into smaller
+ * batches.  In one call into the nanoapp, the nanoapp can perform the first
+ * batch, and then set a timer or send an event (chreSendEvent()) to itself
+ * indicating which batch should be done next. This will allow the nanoapp to
+ * perform the entire calculation over time, without monopolizing system
+ * resources.
+ *
+ * @section floats Floating point support
+ *
+ * The C type 'float' is used in this API, and thus a CHRE implementation
+ * is required to support 'float's.
+ *
+ * Support of the C types 'double' and 'long double' is optional for a
+ * CHRE implementation.  Note that if a CHRE decides to support them, unlike
+ * 'float' support, there is no requirement that this support is particularly
+ * efficient.  So nanoapp authors should be aware this may be inefficient.
+ *
+ * If a CHRE implementation chooses not to support 'double' or
+ * 'long double', then the build toolchain setup provided needs to set
+ * the preprocessor define CHRE_NO_DOUBLE_SUPPORT.
+ *
+ * @section compat CHRE and Nanoapp compatibility
+ *
+ * CHRE implementations must make affordances to maintain binary compatibility
+ * across minor revisions of the API version (e.g. v1.1 to v1.2).  This applies
+ * to both running a nanoapp compiled for a newer version of the API on a CHRE
+ * implementation built against an older version (backwards compatibility), and
+ * vice versa (forwards compatibility).  API changes that are acceptable in
+ * minor version changes that may require special measures to ensure binary
+ * compatibility include: addition of new functions; addition of arguments to
+ * existing functions when the default value used for nanoapps compiled against
+ * the old version is well-defined and does not affect existing functionality;
+ * and addition of fields to existing structures, even when this induces a
+ * binary layout change (this should be made rare via judicious use of reserved
+ * fields).  API changes that must only occur alongside a major version change
+ * and are therefore not compatible include: removal of any function, argument,
+ * field in a data structure, or mandatory functional behavior that a nanoapp
+ * may depend on; any change in the interpretation of an existing data structure
+ * field that alters the way it was defined previously (changing the units of a
+ * field would fall under this, but appropriating a previously reserved field
+ * for some new functionality would not); and any change in functionality or
+ * expected behavior that conflicts with the previous definition.
+ *
+ * Note that the CHRE API only specifies the software interface between a
+ * nanoapp and the CHRE system - the binary interface (ABI) between nanoapp and
+ * CHRE is necessarily implementation-dependent.  Therefore, the recommended
+ * approach to accomplish binary compatibility is to build a Nanoapp Support
+ * Library (NSL) that is specific to the CHRE implementation into the nanoapp
+ * binary, and use it to handle ABI details in a way that ensures compatibility.
+ * In addition, to accomplish forwards compatibility, the CHRE implementation is
+ * expected to recognize the CHRE API version that a nanoapp is targeting and
+ * engage compatibility behaviors where necessary.
+ *
+ * By definition, major API version changes (e.g. v1.1 to v2.0) break
+ * compatibility.  Therefore, a CHRE implementation must not attempt to load a
+ * nanoapp that is targeting a newer major API version.
+ */
+
+#endif  /* _CHRE_H_ */
+
diff --git a/chre_api/legacy/v1_8/chre/audio.h b/chre_api/legacy/v1_8/chre/audio.h
new file mode 100644
index 0000000..e8ec960
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/audio.h
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2017 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 _CHRE_AUDIO_H_
+#define _CHRE_AUDIO_H_
+
+/**
+ * @file
+ * The API for requesting audio in the Context Hub Runtime Environment.
+ *
+ * This includes the definition of audio data structures and the ability to
+ * request audio streams.
+ */
+
+#include <chre/event.h>
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The current compatibility version of the chreAudioDataEvent structure.
+ */
+#define CHRE_AUDIO_DATA_EVENT_VERSION  UINT8_C(1)
+
+/**
+ * Produce an event ID in the block of IDs reserved for audio
+ * @param offset Index into audio event ID block; valid range [0,15]
+ */
+#define CHRE_AUDIO_EVENT_ID(offset)  (CHRE_EVENT_AUDIO_FIRST_EVENT + (offset))
+
+/**
+ * nanoappHandleEvent argument: struct chreAudioSourceStatusEvent
+ *
+ * Indicates a change in the format and/or rate of audio data provided to a
+ * nanoapp.
+ */
+#define CHRE_EVENT_AUDIO_SAMPLING_CHANGE  CHRE_AUDIO_EVENT_ID(0)
+
+/**
+ * nanoappHandleEvent argument: struct chreAudioDataEvent
+ *
+ * Provides a buffer of audio data to a nanoapp.
+ */
+#define CHRE_EVENT_AUDIO_DATA  CHRE_AUDIO_EVENT_ID(1)
+
+/**
+ * The maximum size of the name of an audio source including the
+ * null-terminator.
+ */
+#define CHRE_AUDIO_SOURCE_NAME_MAX_SIZE  (40)
+
+/**
+ * Helper values for sample rates.
+ *
+ * @defgroup CHRE_AUDIO_SAMPLE_RATES
+ * @{
+ */
+
+//! 16kHz Audio Sample Data
+#define CHRE_AUDIO_SAMPLE_RATE_16KHZ  (16000)
+
+/** @} */
+
+/**
+ * Formats for audio that can be provided to a nanoapp.
+ */
+enum chreAudioDataFormat {
+  /**
+   * Unsigned, 8-bit u-Law encoded data as specified by ITU-T G.711.
+   */
+  CHRE_AUDIO_DATA_FORMAT_8_BIT_U_LAW = 0,
+
+  /**
+   * Signed, 16-bit linear PCM data. Endianness must be native to the local
+   * processor.
+   */
+  CHRE_AUDIO_DATA_FORMAT_16_BIT_SIGNED_PCM = 1,
+};
+
+/**
+ * A description of an audio source available to a nanoapp.
+ *
+ * This provides a description of an audio source with a name and a
+ * description of the format of the provided audio data.
+ */
+struct chreAudioSource {
+  /**
+   * A human readable name for this audio source. This is a C-style,
+   * null-terminated string. The length must be less than or equal to
+   * CHRE_AUDIO_SOURCE_NAME_MAX_SIZE bytes (including the null-terminator) and
+   * is expected to describe the source of the audio in US English. All
+   * characters must be printable (i.e.: isprint would return true for all
+   * characters in the name for the EN-US locale). The typical use of this field
+   * is for a nanoapp to log the name of the audio source that it is using.
+   *
+   * Example: "Camcorder Microphone"
+   */
+  const char *name;
+
+  /**
+   * The sampling rate in hertz of this mode. This value is rounded to the
+   * nearest integer. Typical values might include 16000, 44100 and 44800.
+   *
+   * If the requested audio source is preempted by another feature of the system
+   * (e.g. hotword), a gap may occur in received audio data. This is indicated
+   * to the client by posting a CHRE_EVENT_AUDIO_SAMPLING_CHANGE event. The
+   * nanoapp will then receive another CHRE_EVENT_AUDIO_SAMPLING_CHANGE event
+   * once the audio source is available again.
+   */
+  uint32_t sampleRate;
+
+  /**
+   * The minimum amount of time that this audio source can be buffered, in
+   * nanoseconds. Audio data is delivered to nanoapps in buffers. This specifies
+   * the minimum amount of data that can be delivered to a nanoapp without
+   * losing data. A request for a buffer that is smaller than this will fail.
+   */
+  uint64_t minBufferDuration;
+
+  /**
+   * The maximum amount of time that this audio source can be buffered, in
+   * nanoseconds. Audio data is delivered to nanoapps in buffers. This specifies
+   * the maximum amount of data that can be stored by the system in one event
+   * without losing data. A request for a buffer that is larger than this will
+   * fail.
+   */
+  uint64_t maxBufferDuration;
+
+  /**
+   * The format for data provided to the nanoapp. This will be assigned to one
+   * of the enum chreAudioDataFormat values.
+   */
+  uint8_t format;
+};
+
+/**
+ * The current status of an audio source.
+ */
+struct chreAudioSourceStatus {
+  /**
+   * Set to true if the audio source is currently enabled by this nanoapp. If
+   * this struct is provided by a CHRE_EVENT_AUDIO_SAMPLING_CHANGE event, it
+   * must necessarily be set to true because sampling change events are only
+   * sent for sources which this nanoapp has actively subscribed to. If this
+   * struct is obtained from the chreAudioGetStatus API, it may be set to true
+   * or false depending on if audio is currently enabled.
+   */
+  bool enabled;
+
+  /**
+   * Set to true if the audio source is currently suspended and no audio data
+   * will be received from this source.
+   */
+  bool suspended;
+};
+
+/**
+ * The nanoappHandleEvent argument for CHRE_EVENT_AUDIO_SAMPLING_CHANGE.
+ */
+struct chreAudioSourceStatusEvent {
+  /**
+   * The audio source which has completed a status change.
+   */
+  uint32_t handle;
+
+  /**
+   * The status of this audio source.
+   */
+  struct chreAudioSourceStatus status;
+};
+
+/**
+ * The nanoappHandleEvent argument for CHRE_EVENT_AUDIO_DATA.
+ *
+ * One example of the sequence of events for a nanoapp to receive audio data is:
+ *
+ * 1. CHRE_EVENT_AUDIO_SAMPLING_CHANGE - Indicates that audio data is not
+ *                                       suspended.
+ * 2. CHRE_EVENT_AUDIO_DATA - One buffer of audio samples. Potentially repeated.
+ * 3. CHRE_EVENT_AUDIO_SAMPLING_CHANGE - Indicates that audio data has suspended
+ *                                       which indicates a gap in the audio.
+ * 4. CHRE_EVENT_AUDIO_SAMPLING_CHANGE - Indicates that audio data has resumed
+ *                                       and that audio data may be delivered
+ *                                       again if enough samples are buffered.
+ * 5. CHRE_EVENT_AUDIO_DATA - One buffer of audio samples. Potentially repeated.
+ *                            The nanoapp must tolerate a gap in the timestamps.
+ *
+ * This process repeats for as long as an active request is made for an audio
+ * source. A CHRE_EVENT_AUDIO_SAMPLING_CHANGE does not guarantee that the next
+ * event will be a CHRE_EVENT_AUDIO_DATA event when suspended is set to false.
+ * It may happen that the audio source is suspended before a complete buffer can
+ * be captured. This will cause another CHRE_EVENT_AUDIO_SAMPLING_CHANGE event
+ * to be dispatched with suspended set to true before a buffer is delivered.
+ *
+ * Audio events must be delivered to a nanoapp in order.
+ */
+struct chreAudioDataEvent {
+  /**
+   * Indicates the version of the structure, for compatibility purposes. Clients
+   * do not normally need to worry about this field; the CHRE implementation
+   * guarantees that the client only receives the structure version it expects.
+   */
+  uint8_t version;
+
+  /**
+   * Additional bytes reserved for future use; must be set to 0.
+   */
+  uint8_t reserved[3];
+
+  /**
+   * The handle for which this audio data originated from.
+   */
+  uint32_t handle;
+
+  /**
+   * The base timestamp for this buffer of audio data, from the same time base
+   * as chreGetTime() (in nanoseconds). The audio API does not provide
+   * timestamps for each audio sample. This timestamp corresponds to the first
+   * sample of the buffer. Even though the value is expressed in nanoseconds,
+   * there is an expectation that the sample clock may drift and nanosecond
+   * level accuracy may not be possible. The goal is to be as accurate as
+   * possible within reasonable limitations of a given system.
+   */
+  uint64_t timestamp;
+
+  /**
+   * The sample rate for this buffer of data in hertz, rounded to the nearest
+   * integer. Fractional sampling rates are not supported. Typical values might
+   * include 16000, 44100 and 48000.
+   */
+  uint32_t sampleRate;
+
+  /**
+   * The number of samples provided with this buffer.
+   */
+  uint32_t sampleCount;
+
+  /**
+   * The format of this audio data. This enumeration and union of pointers below
+   * form a tagged struct. The consumer of this API must use this enum to
+   * determine which samples pointer below to dereference. This will be assigned
+   * to one of the enum chreAudioDataFormat values.
+   */
+  uint8_t format;
+
+  /**
+   * A union of pointers to various formats of sample data. These correspond to
+   * the valid chreAudioDataFormat values.
+   */
+  union {
+    const uint8_t *samplesULaw8;
+    const int16_t *samplesS16;
+  };
+};
+
+/**
+ * Retrieves information about an audio source supported by the current CHRE
+ * implementation. The source returned by the runtime must not change for the
+ * entire lifecycle of the Nanoapp and hot-pluggable audio sources are not
+ * supported.
+ *
+ * A simple example of iterating all available audio sources is provided here:
+ *
+ * struct chreAudioSource audioSource;
+ * for (uint32_t i = 0; chreAudioGetSource(i, &audioSource); i++) {
+ *     chreLog(CHRE_LOG_INFO, "Found audio source: %s", audioSource.name);
+ * }
+ *
+ * Handles provided to this API must be a stable value for the entire duration
+ * of a nanoapp. Handles for all audio sources must be zero-indexed and
+ * contiguous. The following are examples of handles that could be provided to
+ * this API:
+ *
+ *   Valid: 0
+ *   Valid: 0, 1, 2, 3
+ * Invalid: 1, 2, 3
+ * Invalid: 0, 2
+ *
+ * @param handle The handle for an audio source to obtain details for. The
+ *     range of acceptable handles must be zero-indexed and contiguous.
+ * @param audioSource A struct to populate with details of the audio source.
+ * @return true if the query was successful, false if the provided handle is
+ *     invalid or the supplied audioSource is NULL.
+ *
+ * @since v1.2
+ */
+bool chreAudioGetSource(uint32_t handle, struct chreAudioSource *audioSource);
+
+/**
+ * Nanoapps must define CHRE_NANOAPP_USES_AUDIO somewhere in their build
+ * system (e.g. Makefile) if the nanoapp needs to use the following audio APIs.
+ * In addition to allowing access to these APIs, defining this macro will also
+ * ensure CHRE enforces that all host clients this nanoapp talks to have the
+ * required Android permissions needed to listen to audio data by adding
+ * metadata to the nanoapp.
+ */
+#if defined(CHRE_NANOAPP_USES_AUDIO) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+/**
+ * Configures delivery of audio data to the current nanoapp. Note that this may
+ * not fully disable the audio source if it is used by other clients in the
+ * system but it will halt data delivery to the nanoapp.
+ *
+ * The bufferDuration and deliveryInterval parameters as described below are
+ * used together to determine both how much and how often to deliver data to a
+ * nanoapp, respectively. A nanoapp will always be provided the requested
+ * amount of data at the requested interval, even if another nanoapp in CHRE
+ * requests larger/more frequent buffers or smaller/less frequent buffers.
+ * These two buffering parameters allow describing the duty cycle of captured
+ * audio data. If a nanoapp wishes to receive all available audio data, it will
+ * specify a bufferDuration and deliveryInterval that are equal. A 50% duty
+ * cycle would be achieved by specifying a deliveryInterval that is double the
+ * value of the bufferDuration provided. These parameters allow the audio
+ * subsystem to operate at less than 100% duty cycle and permits use of
+ * incomplete audio data without periodic reconfiguration of the source.
+ *
+ * Two examples are illustrated below:
+ *
+ * Target duty cycle: 50%
+ * bufferDuration:    2
+ * deliveryInterval:  4
+ *
+ * Time       0   1   2   3   4   5   6   7
+ * Batch                  A               B
+ * Sample    --  --  a1  a2  --  --  b1  b2
+ * Duration          [    ]          [    ]
+ * Interval  [            ]  [            ]
+ *
+ *
+ * Target duty cycle: 100%
+ * bufferDuration:    4
+ * deliveryInterval:  4
+ *
+ * Time       0   1   2   3   4   5   6   7
+ * Batch                  A               B
+ * Sample    a1  a2  a3  a4  b1  b2  b3  b4
+ * Duration  [            ]  [            ]
+ * Interval  [            ]  [            ]
+ *
+ *
+ * This is expected to reduce power overall.
+ *
+ * The first audio buffer supplied to the nanoapp may contain data captured
+ * prior to the request. This could happen if the microphone was already enabled
+ * and reading into a buffer prior to the nanoapp requesting audio data for
+ * itself. The nanoapp must tolerate this.
+ *
+ * It is important to note that multiple logical audio sources (e.g. different
+ * sample rate, format, etc.) may map to one physical audio source. It is
+ * possible for a nanoapp to request audio data from more than one logical
+ * source at a time. Audio data may be suspended for either the current or other
+ * requests. The CHRE_EVENT_AUDIO_SAMPLING_CHANGE will be posted to all clients
+ * if such a change occurs. It is also possible for the request to succeed and
+ * all audio sources are serviced simultaneously. This is implementation defined
+ * but at least one audio source must function correctly if it is advertised,
+ * under normal conditions (e.g. not required for some other system function,
+ * such as hotword).
+ *
+ * @param handle The handle for this audio source. The handle for the desired
+ *     audio source can be determined using chreAudioGetSource().
+ * @param enable true if enabling the source, false otherwise. When passed as
+ *     false, the bufferDuration and deliveryInterval parameters are ignored.
+ * @param bufferDuration The amount of time to capture audio samples from this
+ *     audio source, in nanoseconds per delivery interval. This value must be
+ *     in the range of minBufferDuration/maxBufferDuration for this source or
+ *     the request will fail. The number of samples captured per buffer will be
+ *     derived from the sample rate of the source and the requested duration and
+ *     rounded down to the nearest sample boundary.
+ * @param deliveryInterval Desired time between each CHRE_EVENT_AUDIO_DATA
+ *     event. This allows specifying the complete duty cycle of a request
+ *     for audio data, in nanoseconds. This value must be greater than or equal
+ *     to bufferDuration or the request will fail due to an invalid
+ *     configuration.
+ * @return true if the configuration was successful, false if invalid parameters
+ *     were provided (non-existent handle, invalid buffering configuration).
+ *
+ * @since v1.2
+ * @note Requires audio permission
+ */
+bool chreAudioConfigureSource(uint32_t handle, bool enable,
+                              uint64_t bufferDuration,
+                              uint64_t deliveryInterval);
+
+/**
+ * Gets the current chreAudioSourceStatus struct for a given audio handle.
+ *
+ * @param handle The handle for the audio source to query. The provided handle
+ *     is obtained from a chreAudioSource which is requested from the
+ *     chreAudioGetSource API.
+ * @param status The current status of the supplied audio source.
+ * @return true if the provided handle is valid and the status was obtained
+ *     successfully, false if the handle was invalid or status is NULL.
+ *
+ * @since v1.2
+ * @note Requires audio permission
+ */
+bool chreAudioGetStatus(uint32_t handle, struct chreAudioSourceStatus *status);
+
+#else  /* defined(CHRE_NANOAPP_USES_AUDIO) || !defined(CHRE_IS_NANOAPP_BUILD) */
+#define CHRE_AUDIO_PERM_ERROR_STRING \
+    "CHRE_NANOAPP_USES_AUDIO must be defined when building this nanoapp in " \
+    "order to refer to "
+#define chreAudioConfigureSource(...) \
+    CHRE_BUILD_ERROR(CHRE_AUDIO_PERM_ERROR_STRING "chreAudioConfigureSource")
+#define chreAudioGetStatus(...) \
+    CHRE_BUILD_ERROR(CHRE_AUDIO_PERM_ERROR_STRING "chreAudioGetStatus")
+#endif  /* defined(CHRE_NANOAPP_USES_AUDIO) || !defined(CHRE_IS_NANOAPP_BUILD) */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_AUDIO_H_ */
diff --git a/chre_api/legacy/v1_8/chre/ble.h b/chre_api/legacy/v1_8/chre/ble.h
new file mode 100644
index 0000000..9f04964
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/ble.h
@@ -0,0 +1,851 @@
+/*
+ * Copyright (C) 2021 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 CHRE_BLE_H_
+#define CHRE_BLE_H_
+
+/**
+ * @file
+ * CHRE BLE (Bluetooth Low Energy, Bluetooth LE) API.
+ * The CHRE BLE API currently supports BLE scanning features.
+ *
+ * The features in the CHRE BLE API are a subset and adaptation of Android
+ * capabilities as described in the Android BLE API and HCI requirements.
+ * ref:
+ * https://developer.android.com/guide/topics/connectivity/bluetooth/ble-overview
+ * ref: https://source.android.com/devices/bluetooth/hci_requirements
+ */
+
+#include <chre/common.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The set of flags returned by chreBleGetCapabilities().
+ *
+ * @defgroup CHRE_BLE_CAPABILITIES
+ * @{
+ */
+//! No BLE APIs are supported
+#define CHRE_BLE_CAPABILITIES_NONE UINT32_C(0)
+
+//! CHRE supports BLE scanning
+#define CHRE_BLE_CAPABILITIES_SCAN UINT32_C(1 << 0)
+
+//! CHRE BLE supports batching of scan results, either through Android-specific
+//! HCI (OCF: 0x156), or by the CHRE framework, internally.
+//! @since v1.7 Platforms with this capability must also support flushing scan
+//! results during a batched scan.
+#define CHRE_BLE_CAPABILITIES_SCAN_RESULT_BATCHING UINT32_C(1 << 1)
+
+//! CHRE BLE scan supports best-effort hardware filtering. If filtering is
+//! available, chreBleGetFilterCapabilities() returns a bitmap indicating the
+//! specific filtering capabilities that are supported.
+//! To differentiate best-effort vs. no filtering, the following requirement
+//! must be met for this flag:
+//! If only one nanoapp is requesting BLE scans and there are no BLE scans from
+//! the AP, only filtered results will be provided to the nanoapp.
+#define CHRE_BLE_CAPABILITIES_SCAN_FILTER_BEST_EFFORT UINT32_C(1 << 2)
+
+//! CHRE BLE supports reading the RSSI of a specified LE-ACL connection handle.
+#define CHRE_BLE_CAPABILITIES_READ_RSSI UINT32_C(1 << 3)
+/** @} */
+
+/**
+ * The set of flags returned by chreBleGetFilterCapabilities().
+ *
+ * The representative bit for each filtering capability is based on the sub-OCF
+ * of the Android filtering HCI vendor-specific command (LE_APCF_Command, OCF:
+ * 0x0157) for that particular filtering capability, as found in
+ * https://source.android.com/devices/bluetooth/hci_requirements
+ *
+ * For example, the Service Data filter has a sub-command of 0x7; hence
+ * the filtering capability is indicated by (1 << 0x7).
+ *
+ * @defgroup CHRE_BLE_FILTER_CAPABILITIES
+ * @{
+ */
+//! No CHRE BLE filters are supported
+#define CHRE_BLE_FILTER_CAPABILITIES_NONE UINT32_C(0)
+
+//! CHRE BLE supports RSSI filters
+#define CHRE_BLE_FILTER_CAPABILITIES_RSSI UINT32_C(1 << 1)
+
+//! CHRE BLE supports Manufacturer Data filters (Corresponding HCI OCF: 0x0157,
+//! Sub-command: 0x06)
+//! @since v1.8
+#define CHRE_BLE_FILTER_CAPABILITIES_MANUFACTURER_DATA UINT32_C(1 << 6)
+
+//! CHRE BLE supports Service Data filters (Corresponding HCI OCF: 0x0157,
+//! Sub-command: 0x07)
+#define CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA UINT32_C(1 << 7)
+/** @} */
+
+/**
+ * Produce an event ID in the block of IDs reserved for BLE.
+ *
+ * Valid input range is [0, 15]. Do not add new events with ID > 15
+ * (see chre/event.h)
+ *
+ * @param offset Index into BLE event ID block; valid range is [0, 15].
+ *
+ * @defgroup CHRE_BLE_EVENT_ID
+ * @{
+ */
+#define CHRE_BLE_EVENT_ID(offset) (CHRE_EVENT_BLE_FIRST_EVENT + (offset))
+
+/**
+ * nanoappHandleEvent argument: struct chreAsyncResult
+ *
+ * Communicates the asynchronous result of a request to the BLE API. The
+ * requestType field in {@link #chreAsyncResult} is set to a value from enum
+ * chreBleRequestType.
+ *
+ * This is used for results of async config operations which need to
+ * interop with lower level code (potentially in a different thread) or send an
+ * HCI command to the FW and wait on the response.
+ */
+#define CHRE_EVENT_BLE_ASYNC_RESULT CHRE_BLE_EVENT_ID(0)
+
+/**
+ * nanoappHandleEvent argument: struct chreBleAdvertisementEvent
+ *
+ * Provides results of a BLE scan.
+ */
+#define CHRE_EVENT_BLE_ADVERTISEMENT CHRE_BLE_EVENT_ID(1)
+
+/**
+ * nanoappHandleEvent argument: struct chreAsyncResult
+ *
+ * Indicates that a flush request made via chreBleFlushAsync() is complete, and
+ * all batched advertisements resulting from the flush have been delivered via
+ * preceding CHRE_EVENT_BLE_ADVERTISEMENT events.
+ *
+ * @since v1.7
+ */
+#define CHRE_EVENT_BLE_FLUSH_COMPLETE CHRE_BLE_EVENT_ID(2)
+
+/**
+ * nanoappHandleEvent argument: struct chreBleReadRssiEvent
+ *
+ * Provides the RSSI of an LE ACL connection following a call to
+ * chreBleReadRssiAsync().
+ *
+ * @since v1.8
+ */
+#define CHRE_EVENT_BLE_RSSI_READ CHRE_BLE_EVENT_ID(3)
+
+/**
+ * nanoappHandleEvent argument: struct chreBatchCompleteEvent
+ *
+ * This event is generated if the platform enabled batching, and when all
+ * events in a single batch has been delivered (for example, batching
+ * CHRE_EVENT_BLE_ADVERTISEMENT events if the platform has
+ * CHRE_BLE_CAPABILITIES_SCAN_RESULT_BATCHING enabled, and a non-zero
+ * reportDelayMs in chreBleStartScanAsync() was accepted).
+ *
+ * If the nanoapp receives a CHRE_EVENT_BLE_SCAN_STATUS_CHANGE with a non-zero
+ * reportDelayMs and enabled set to true, then this event must be generated.
+ *
+ * @since v1.8
+ */
+#define CHRE_EVENT_BLE_BATCH_COMPLETE CHRE_BLE_EVENT_ID(4)
+
+/**
+ * nanoappHandleEvent argument: struct chreBleScanStatus
+ *
+ * This event is generated when the values in chreBleScanStatus changes.
+ *
+ * @since v1.8
+ */
+#define CHRE_EVENT_BLE_SCAN_STATUS_CHANGE CHRE_BLE_EVENT_ID(5)
+
+// NOTE: Do not add new events with ID > 15
+/** @} */
+
+/**
+ * Maximum BLE (legacy) advertisement payload data length, in bytes
+ * This is calculated by subtracting 2 (type + len) from 31 (max payload).
+ */
+#define CHRE_BLE_DATA_LEN_MAX (29)
+
+/**
+ * BLE device address length, in bytes.
+ */
+#define CHRE_BLE_ADDRESS_LEN (6)
+
+/**
+ * RSSI value (int8_t) indicating no RSSI threshold.
+ */
+#define CHRE_BLE_RSSI_THRESHOLD_NONE (-128)
+
+/**
+ * RSSI value (int8_t) indicating no RSSI value available.
+ */
+#define CHRE_BLE_RSSI_NONE (127)
+
+/**
+ * Tx power value (int8_t) indicating no Tx power value available.
+ */
+#define CHRE_BLE_TX_POWER_NONE (127)
+
+/**
+ * Indicates ADI field was not provided in advertisement.
+ */
+#define CHRE_BLE_ADI_NONE (0xFF)
+
+/**
+ * The CHRE BLE advertising event type is based on the BT Core Spec v5.2,
+ * Vol 4, Part E, Section 7.7.65.13, LE Extended Advertising Report event,
+ * Event_Type.
+ *
+ * Note: helper functions are provided to avoid bugs, e.g. a nanoapp doing
+ * (eventTypeAndDataStatus == ADV_IND) instead of properly masking off reserved
+ * and irrelevant bits.
+ *
+ * @defgroup CHRE_BLE_EVENT
+ * @{
+ */
+// Extended event types
+#define CHRE_BLE_EVENT_MASK_TYPE (0x1f)
+#define CHRE_BLE_EVENT_TYPE_FLAG_CONNECTABLE (1 << 0)
+#define CHRE_BLE_EVENT_TYPE_FLAG_SCANNABLE (1 << 1)
+#define CHRE_BLE_EVENT_TYPE_FLAG_DIRECTED (1 << 2)
+#define CHRE_BLE_EVENT_TYPE_FLAG_SCAN_RSP (1 << 3)
+#define CHRE_BLE_EVENT_TYPE_FLAG_LEGACY (1 << 4)
+
+// Data status
+#define CHRE_BLE_EVENT_MASK_DATA_STATUS (0x3 << 5)
+#define CHRE_BLE_EVENT_DATA_STATUS_COMPLETE (0x0 << 5)
+#define CHRE_BLE_EVENT_DATA_STATUS_MORE_DATA_PENDING (0x1 << 5)
+#define CHRE_BLE_EVENT_DATA_STATUS_DATA_TRUNCATED (0x2 << 5)
+
+// Legacy event types
+#define CHRE_BLE_EVENT_TYPE_LEGACY_ADV_IND                                  \
+  (CHRE_BLE_EVENT_TYPE_FLAG_LEGACY | CHRE_BLE_EVENT_TYPE_FLAG_CONNECTABLE | \
+   CHRE_BLE_EVENT_TYPE_FLAG_SCANNABLE)
+#define CHRE_BLE_EVENT_TYPE_LEGACY_DIRECT_IND \
+  (CHRE_BLE_EVENT_TYPE_FLAG_LEGACY | CHRE_BLE_EVENT_TYPE_FLAG_CONNECTABLE)
+#define CHRE_BLE_EVENT_TYPE_LEGACY_ADV_SCAN_IND \
+  (CHRE_BLE_EVENT_TYPE_FLAG_LEGACY | CHRE_BLE_EVENT_TYPE_FLAG_SCANNABLE)
+#define CHRE_BLE_EVENT_TYPE_LEGACY_ADV_NONCONN_IND \
+  (CHRE_BLE_EVENT_TYPE_FLAG_LEGACY)
+#define CHRE_BLE_EVENT_TYPE_LEGACY_SCAN_RESP_ADV_IND \
+  (CHRE_BLE_EVENT_TYPE_FLAG_SCAN_RSP | CHRE_BLE_EVENT_TYPE_LEGACY_ADV_IND)
+#define CHRE_BLE_EVENT_TYPE_LEGACY_SCAN_RESP_ADV_SCAN_IND \
+  (CHRE_BLE_EVENT_TYPE_FLAG_SCAN_RSP | CHRE_BLE_EVENT_TYPE_LEGACY_ADV_SCAN_IND)
+/** @} */
+
+/**
+ * The maximum amount of time allowed to elapse between the call to
+ * chreBleFlushAsync() and when CHRE_EVENT_BLE_FLUSH_COMPLETE is delivered to
+ * the nanoapp on a successful flush.
+ */
+#define CHRE_BLE_FLUSH_COMPLETE_TIMEOUT_NS (5 * CHRE_NSEC_PER_SEC)
+
+/**
+ * Indicates a type of request made in this API. Used to populate the resultType
+ * field of struct chreAsyncResult sent with CHRE_EVENT_BLE_ASYNC_RESULT.
+ */
+enum chreBleRequestType {
+  CHRE_BLE_REQUEST_TYPE_START_SCAN = 1,
+  CHRE_BLE_REQUEST_TYPE_STOP_SCAN = 2,
+  CHRE_BLE_REQUEST_TYPE_FLUSH = 3,      //!< @since v1.7
+  CHRE_BLE_REQUEST_TYPE_READ_RSSI = 4,  //!< @since v1.8
+};
+
+/**
+ * CHRE BLE scan modes identify functional scan levels without specifying or
+ * guaranteeing particular scan parameters (e.g. duty cycle, interval, radio
+ * chain).
+ *
+ * The actual scan parameters may be platform dependent and may change without
+ * notice in real time based on contextual cues, etc.
+ *
+ * Scan modes should be selected based on use cases as described.
+ */
+enum chreBleScanMode {
+  //! A background scan level for always-running ambient applications.
+  //! A representative duty cycle may be between 3 - 10 % (tentative, and
+  //! with no guarantees).
+  CHRE_BLE_SCAN_MODE_BACKGROUND = 1,
+
+  //! A foreground scan level to be used for short periods.
+  //! A representative duty cycle may be between 10 - 20 % (tentative, and
+  //! with no guarantees).
+  CHRE_BLE_SCAN_MODE_FOREGROUND = 2,
+
+  //! A very high duty cycle scan level to be used for very short durations.
+  //! A representative duty cycle may be between 50 - 100 % (tentative, and
+  //! with no guarantees).
+  CHRE_BLE_SCAN_MODE_AGGRESSIVE = 3,
+};
+
+/**
+ * Selected AD Types are available among those defined in the Bluetooth spec.
+ * Assigned Numbers, Generic Access Profile.
+ * ref: https://www.bluetooth.com/specifications/assigned-numbers/
+ */
+enum chreBleAdType {
+  //! Service Data with 16-bit UUID
+  //! @deprecated as of v1.8
+  //! TODO(b/285207430): Remove this enum once CHRE has been updated.
+  CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 = 0x16,
+
+  //! Service Data with 16-bit UUID
+  //! @since v1.8 CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 was renamed
+  //! CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE to reflect that nanoapps
+  //! compiled against v1.8+ should use OTA format for service data filters.
+  CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE = 0x16,
+
+  //! Manufacturer Specific Data
+  //! @since v1.8
+  CHRE_BLE_AD_TYPE_MANUFACTURER_DATA = 0xff,
+};
+
+/**
+ * Generic scan filters definition based on AD Type, mask, and values. The
+ * maximum data length is limited to the maximum possible legacy advertisement
+ * payload data length (29 bytes).
+ *
+ * The filter is matched when
+ *   data & dataMask == advData & dataMask
+ * where advData is the advertisement packet data for the specified AD type.
+ *
+ * The CHRE generic filter structure represents a generic filter on an AD Type
+ * as defined in the Bluetooth spec Assigned Numbers, Generic Access Profile
+ * (ref: https://www.bluetooth.com/specifications/assigned-numbers/). This
+ * generic structure is used by the Advertising Packet Content Filter
+ * (APCF) HCI generic AD type sub-command 0x09 (ref:
+ * https://source.android.com/devices/bluetooth/hci_requirements#le_apcf_command).
+ *
+ * Note that the CHRE implementation may not support every kind of filter that
+ * can be represented by this structure. Use chreBleGetFilterCapabilities() to
+ * discover supported filtering capabilities at runtime.
+ *
+ * @since v1.8 The data and dataMask must be in OTA format. The nanoapp support
+ * library will handle converting the data and dataMask values to the correct
+ * format if a pre v1.8 version of CHRE is running.
+ *
+ * NOTE: CHRE versions 1.6 and 1.7 expect the 2-byte UUID prefix in
+ * CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 to be given as big-endian. This
+ * was corrected in v1.8 to match the OTA format and Bluetooth specification,
+ * which uses little-endian. This enum has been removed and replaced with
+ * CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE to ensure that nanoapps
+ * compiled against v1.8 and later specify their UUID filter using
+ * little-endian. Nanoapps compiled against v1.7 or earlier should continue to
+ * use big-endian, as CHRE must provide cross-version compatibility for all
+ * possible version combinations.
+ *
+ * Example 1: To filter on a 16 bit service data UUID of 0xFE2C, the following
+ * settings would be used:
+ *   type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16
+ *   len = 2
+ *   data = {0x2C, 0xFE}
+ *   dataMask = {0xFF, 0xFF}
+ *
+ * Example 2: To filter for manufacturer data of 0x12, 0x34 from Google (0x00E0),
+ * the following settings would be used:
+ *   type = CHRE_BLE_AD_TYPE_MANUFACTURER_DATA
+ *   len = 4
+ *   data = {0xE0, 0x00, 0x12, 0x34}
+ *   dataMask = {0xFF, 0xFF, 0xFF, 0xFF}
+ *
+ * Refer to "Supplement to the Bluetooth Core Specification for details (v9,
+ * Part A, Section 1.4)" for details regarding the manufacturer data format.
+ */
+struct chreBleGenericFilter {
+  //! Acceptable values among enum chreBleAdType
+  uint8_t type;
+
+  /**
+   * Length of data and dataMask. AD payloads shorter than this length will not
+   * be matched by the filter. Length must be greater than 0.
+   */
+  uint8_t len;
+
+  //! Used in combination with dataMask to filter an advertisement
+  uint8_t data[CHRE_BLE_DATA_LEN_MAX];
+
+  //! Used in combination with data to filter an advertisement
+  uint8_t dataMask[CHRE_BLE_DATA_LEN_MAX];
+};
+
+/**
+ * CHRE Bluetooth LE scan filters are based on a combination of an RSSI
+ * threshold and generic scan filters as defined by AD Type, mask, and values.
+ *
+ * CHRE-provided filters are implemented in a best-effort manner, depending on
+ * HW capabilities of the system and available resources. Therefore, provided
+ * scan results may be a superset of the specified filters. Nanoapps should try
+ * to take advantage of CHRE scan filters as much as possible, but must design
+ * their logic as to not depend on CHRE filtering.
+ *
+ * The syntax of CHRE scan filter definitions are based on the Android
+ * Advertising Packet Content Filter (APCF) HCI requirement subtype 0x08
+ * ref:
+ * https://source.android.com/devices/bluetooth/hci_requirements#le_apcf_command-set_filtering_parameters_sub_cmd
+ * and AD Types as defined in the Bluetooth spec Assigned Numbers, Generic
+ * Access Profile
+ * ref: https://www.bluetooth.com/specifications/assigned-numbers/
+ *
+ * Even though the scan filters are defined in a generic manner, CHRE Bluetooth
+ * is expected to initially support only a limited set of AD Types.
+ */
+struct chreBleScanFilter {
+  //! RSSI threshold filter (Corresponding HCI OCF: 0x0157, Sub: 0x01), where
+  //! advertisements with RSSI values below this threshold may be disregarded.
+  //! An rssiThreshold value of CHRE_BLE_RSSI_THRESHOLD_NONE indicates no RSSI
+  //! filtering.
+  int8_t rssiThreshold;
+
+  //! Number of generic scan filters provided in the scanFilters array.
+  //! A scanFilterCount value of 0 indicates no generic scan filters.
+  uint8_t scanFilterCount;
+
+  //! Pointer to an array of scan filters. If the array contains more than one
+  //! entry, advertisements matching any of the entries will be returned
+  //! (functional OR).
+  const struct chreBleGenericFilter *scanFilters;
+};
+
+/**
+ * CHRE BLE advertising address type is based on the BT Core Spec v5.2, Vol 4,
+ * Part E, Section 7.7.65.13, LE Extended Advertising Report event,
+ * Address_Type.
+ */
+enum chreBleAddressType {
+  //! Public device address.
+  CHRE_BLE_ADDRESS_TYPE_PUBLIC = 0x00,
+
+  //! Random device address.
+  CHRE_BLE_ADDRESS_TYPE_RANDOM = 0x01,
+
+  //! Public identity address (corresponds to resolved private address).
+  CHRE_BLE_ADDRESS_TYPE_PUBLIC_IDENTITY = 0x02,
+
+  //! Random (static) Identity Address (corresponds to resolved private
+  //! address)
+  CHRE_BLE_ADDRESS_TYPE_RANDOM_IDENTITY = 0x03,
+
+  //! No address provided (anonymous advertisement).
+  CHRE_BLE_ADDRESS_TYPE_NONE = 0xff,
+};
+
+/**
+ * CHRE BLE physical (PHY) channel encoding type, if supported, is based on the
+ * BT Core Spec v5.2, Vol 4, Part E, Section 7.7.65.13, LE Extended Advertising
+ * Report event, entries Primary_PHY and Secondary_PHY.
+ */
+enum chreBlePhyType {
+  //! No packets on this PHY (only on the secondary channel), or feature not
+  //! supported.
+  CHRE_BLE_PHY_NONE = 0x00,
+
+  //! LE 1 MBPS PHY encoding.
+  CHRE_BLE_PHY_1M = 0x01,
+
+  //! LE 2 MBPS PHY encoding (only on the secondary channel).
+  CHRE_BLE_PHY_2M = 0x02,
+
+  //! LE long-range coded PHY encoding.
+  CHRE_BLE_PHY_CODED = 0x03,
+};
+
+/**
+ * The CHRE BLE Advertising Report event is based on the BT Core Spec v5.2,
+ * Vol 4, Part E, Section 7.7.65.13, LE Extended Advertising Report event, with
+ * the following differences:
+ *
+ * 1) A CHRE timestamp field, which can be useful if CHRE is batching results.
+ * 2) Reordering of the rssi and periodicAdvertisingInterval fields for memory
+ *    alignment (prevent padding).
+ * 3) Addition of four reserved bytes to reclaim padding.
+ */
+struct chreBleAdvertisingReport {
+  //! The base timestamp, in nanoseconds, in the same time base as chreGetTime()
+  uint64_t timestamp;
+
+  //! @see CHRE_BLE_EVENT
+  uint8_t eventTypeAndDataStatus;
+
+  //! Advertising address type as defined in enum chreBleAddressType
+  uint8_t addressType;
+
+  //! Advertising device address
+  uint8_t address[CHRE_BLE_ADDRESS_LEN];
+
+  //! Advertiser PHY on primary advertising physical channel, if supported, as
+  //! defined in enum chreBlePhyType.
+  uint8_t primaryPhy;
+
+  //! Advertiser PHY on secondary advertising physical channel, if supported, as
+  //! defined in enum chreBlePhyType.
+  uint8_t secondaryPhy;
+
+  //! Value of the Advertising SID subfield in the ADI field of the PDU among
+  //! the range of [0, 0x0f].
+  //! CHRE_BLE_ADI_NONE indicates no ADI field was provided.
+  //! Other values are reserved.
+  uint8_t advertisingSid;
+
+  //! Transmit (Tx) power in dBm. Typical values are [-127, 20].
+  //! CHRE_BLE_TX_POWER_NONE indicates Tx power not available.
+  int8_t txPower;
+
+  //! Interval of the periodic advertising in 1.25 ms intervals, i.e.
+  //! time = periodicAdvertisingInterval * 1.25 ms
+  //! 0 means no periodic advertising. Minimum value is otherwise 6 (7.5 ms).
+  uint16_t periodicAdvertisingInterval;
+
+  //! RSSI in dBm. Typical values are [-127, 20].
+  //! CHRE_BLE_RSSI_NONE indicates RSSI is not available.
+  int8_t rssi;
+
+  //! Direct address type (i.e. only accept connection requests from a known
+  //! peer device) as defined in enum chreBleAddressType.
+  uint8_t directAddressType;
+
+  //! Direct address (i.e. only accept connection requests from a known peer
+  //! device).
+  uint8_t directAddress[CHRE_BLE_ADDRESS_LEN];
+
+  //! Length of data field. Acceptable range is [0, 62] for legacy and
+  //! [0, 255] for extended advertisements.
+  uint16_t dataLength;
+
+  //! dataLength bytes of data, or null if dataLength is 0. This represents
+  //! the ADV_IND payload, optionally concatenated with SCAN_RSP, as indicated
+  //! by eventTypeAndDataStatus.
+  const uint8_t *data;
+
+  //! Reserved for future use; set to 0
+  uint32_t reserved;
+};
+
+/**
+ * A CHRE BLE Advertising Event can contain any number of CHRE BLE Advertising
+ * Reports (i.e. advertisements).
+ */
+struct chreBleAdvertisementEvent {
+  //! Reserved for future use; set to 0
+  uint16_t reserved;
+
+  //! Number of advertising reports in this event
+  uint16_t numReports;
+
+  //! Array of length numReports
+  const struct chreBleAdvertisingReport *reports;
+};
+
+/**
+ * The RSSI read on a particular LE connection handle, based on the parameters
+ * in BT Core Spec v5.3, Vol 4, Part E, Section 7.5.4, Read RSSI command
+ */
+struct chreBleReadRssiEvent {
+  //! Structure which contains the cookie associated with the original request,
+  //! along with an error code that indicates request success or failure.
+  struct chreAsyncResult result;
+
+  //! The handle upon which CHRE attempted to read RSSI.
+  uint16_t connectionHandle;
+
+  //! The RSSI of the last packet received on this connection, if valid
+  //! (-127 to 20)
+  int8_t rssi;
+};
+
+/**
+ * Describes the current status of the BLE request in the platform.
+ *
+ * @since v1.8
+ */
+struct chreBleScanStatus {
+  //! The currently configured report delay in the scan configuration.
+  //! If enabled is false, this value does not have meaning.
+  uint32_t reportDelayMs;
+
+  //! True if the BLE scan is currently enabled. This can be set to false
+  //! if BLE scan was temporarily disabled (e.g. BT subsystem is down,
+  //! or due to user settings).
+  bool enabled;
+
+  //! Reserved for future use - set to zero.
+  uint8_t reserved[3];
+};
+
+/**
+ * Retrieves a set of flags indicating the BLE features supported by the
+ * current CHRE implementation. The value returned by this function must be
+ * consistent for the entire duration of the nanoapp's execution.
+ *
+ * The client must allow for more flags to be set in this response than it knows
+ * about, for example if the implementation supports a newer version of the API
+ * than the client was compiled against.
+ *
+ * @return A bitmask with zero or more CHRE_BLE_CAPABILITIES_* flags set. @see
+ *         CHRE_BLE_CAPABILITIES
+ *
+ * @since v1.6
+ */
+uint32_t chreBleGetCapabilities(void);
+
+/**
+ * Retrieves a set of flags indicating the BLE filtering features supported by
+ * the current CHRE implementation. The value returned by this function must be
+ * consistent for the entire duration of the nanoapp's execution.
+ *
+ * The client must allow for more flags to be set in this response than it knows
+ * about, for example if the implementation supports a newer version of the API
+ * than the client was compiled against.
+ *
+ * @return A bitmask with zero or more CHRE_BLE_FILTER_CAPABILITIES_* flags set.
+ *         @see CHRE_BLE_FILTER_CAPABILITIES
+ *
+ * @since v1.6
+ */
+uint32_t chreBleGetFilterCapabilities(void);
+
+/**
+ * Helper function to extract event type from eventTypeAndDataStatus as defined
+ * in the BT Core Spec v5.2, Vol 4, Part E, Section 7.7.65.13, LE Extended
+ * Advertising Report event, entry Event_Type.
+ *
+ * @see CHRE_BLE_EVENT
+ *
+ * @param eventTypeAndDataStatus Combined event type and data status
+ *
+ * @return The event type portion of eventTypeAndDataStatus
+ */
+static inline uint8_t chreBleGetEventType(uint8_t eventTypeAndDataStatus) {
+  return (eventTypeAndDataStatus & CHRE_BLE_EVENT_MASK_TYPE);
+}
+
+/**
+ * Helper function to extract data status from eventTypeAndDataStatus as defined
+ * in the BT Core Spec v5.2, Vol 4, Part E, Section 7.7.65.13, LE Extended
+ * Advertising Report event, entry Event_Type.
+ *
+ * @see CHRE_BLE_EVENT
+ *
+ * @param eventTypeAndDataStatus Combined event type and data status
+ *
+ * @return The data status portion of eventTypeAndDataStatus
+ */
+static inline uint8_t chreBleGetDataStatus(uint8_t eventTypeAndDataStatus) {
+  return (eventTypeAndDataStatus & CHRE_BLE_EVENT_MASK_DATA_STATUS);
+}
+
+/**
+ * Helper function to to combine an event type with a data status to create
+ * eventTypeAndDataStatus as defined in the BT Core Spec v5.2, Vol 4, Part E,
+ * Section 7.7.65.13, LE Extended Advertising Report event, entry Event_Type.
+ *
+ * @see CHRE_BLE_EVENT
+ *
+ * @param eventType Event type
+ * @param dataStatus Data status
+ *
+ * @return A combined eventTypeAndDataStatus
+ */
+static inline uint8_t chreBleGetEventTypeAndDataStatus(uint8_t eventType,
+                                                       uint8_t dataStatus) {
+  return ((eventType & CHRE_BLE_EVENT_MASK_TYPE) |
+          (dataStatus & CHRE_BLE_EVENT_MASK_DATA_STATUS));
+}
+
+/**
+ * Nanoapps must define CHRE_NANOAPP_USES_BLE somewhere in their build
+ * system (e.g. Makefile) if the nanoapp needs to use the following BLE APIs.
+ * In addition to allowing access to these APIs, defining this macro will also
+ * ensure CHRE enforces that all host clients this nanoapp talks to have the
+ * required Android permissions needed to access BLE functionality by adding
+ * metadata to the nanoapp.
+ */
+#if defined(CHRE_NANOAPP_USES_BLE) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+/**
+ * Start Bluetooth LE (BLE) scanning on CHRE.
+ *
+ * The result of the operation will be delivered asynchronously via the CHRE
+ * event CHRE_EVENT_BLE_ASYNC_RESULT.
+ *
+ * The scan results will be delivered asynchronously via the CHRE event
+ * CHRE_EVENT_BLE_ADVERTISEMENT.
+ *
+ * If CHRE_USER_SETTING_BLE_AVAILABLE is disabled, CHRE is expected to return an
+ * async result with error CHRE_ERROR_FUNCTION_DISABLED. If this setting is
+ * enabled, the Bluetooth subsystem may still be powered down in the scenario
+ * where the main Bluetooth toggle is disabled, but the Bluetooth scanning
+ * setting is enabled, and there is no request for BLE to be enabled at the
+ * Android level. In this scenario, CHRE will return an async result with error
+ * CHRE_ERROR_FUNCTION_DISABLED.
+ *
+ * To ensure that Bluetooth remains powered on in this settings configuration so
+ * that a nanoapp can scan, the nanoapp's Android host entity should use the
+ * BluetoothAdapter.enableBLE() API to register this request with the Android
+ * Bluetooth stack.
+ *
+ * If chreBleStartScanAsync() is called while a previous scan has been started,
+ * the previous scan will be stopped first and replaced with the new scan.
+ *
+ * Note that some corresponding Android parameters are missing from the CHRE
+ * API, where the following default or typical parameters are used:
+ * Callback type: CALLBACK_TYPE_ALL_MATCHES
+ * Result type: SCAN_RESULT_TYPE_FULL
+ * Match mode: MATCH_MODE_AGGRESSIVE
+ * Number of matches per filter: MATCH_NUM_MAX_ADVERTISEMENT
+ * Legacy-only: false
+ * PHY type: PHY_LE_ALL_SUPPORTED
+ *
+ * For v1.8 and greater, a CHRE_EVENT_BLE_SCAN_STATUS_CHANGE will be generated
+ * if the values in chreBleScanStatus changes as a result of this call.
+ *
+ * @param mode Scanning mode selected among enum chreBleScanMode
+ * @param reportDelayMs Maximum requested batching delay in ms. 0 indicates no
+ *                      batching. Note that the system may deliver results
+ *                      before the maximum specified delay is reached.
+ * @param filter Pointer to the requested best-effort filter configuration as
+ *               defined by struct chreBleScanFilter. The ownership of filter
+ *               and its nested elements remains with the caller, and the caller
+ *               may release it as soon as chreBleStartScanAsync() returns.
+ *
+ * @return True to indicate that the request was accepted. False otherwise.
+ *
+ * @since v1.6
+ */
+bool chreBleStartScanAsync(enum chreBleScanMode mode, uint32_t reportDelayMs,
+                           const struct chreBleScanFilter *filter);
+/**
+ * Stops a CHRE BLE scan.
+ *
+ * The result of the operation will be delivered asynchronously via the CHRE
+ * event CHRE_EVENT_BLE_ASYNC_RESULT.
+ *
+ * @return True to indicate that the request was accepted. False otherwise.
+ *
+ * @since v1.6
+ */
+bool chreBleStopScanAsync(void);
+
+/**
+ * Requests to immediately deliver batched scan results. The nanoapp must
+ * have an active BLE scan request. If a request is accepted, it will be treated
+ * as though the reportDelayMs has expired for a batched scan. Upon accepting
+ * the request, CHRE works to immediately deliver scan results currently kept in
+ * batching memory, if any, via regular CHRE_EVENT_BLE_ADVERTISEMENT events,
+ * followed by a CHRE_EVENT_BLE_FLUSH_COMPLETE event.
+ *
+ * If the underlying system fails to complete the flush operation within
+ * CHRE_BLE_FLUSH_COMPLETE_TIMEOUT_NS, CHRE will send a
+ * CHRE_EVENT_BLE_FLUSH_COMPLETE event with CHRE_ERROR_TIMEOUT.
+ *
+ * If multiple flush requests are made prior to flush completion, then the
+ * requesting nanoapp will receive all batched samples existing at the time of
+ * the latest flush request. In this case, the number of
+ * CHRE_EVENT_BLE_FLUSH_COMPLETE events received must equal the number of flush
+ * requests made.
+ *
+ * If chreBleStopScanAsync() is called while a flush operation is in progress,
+ * it is unspecified whether the flush operation will complete successfully or
+ * return an error, such as CHRE_ERROR_FUNCTION_DISABLED, but in any case,
+ * CHRE_EVENT_BLE_FLUSH_COMPLETE must still be delivered. The same applies if
+ * the Bluetooth user setting is disabled during a flush operation.
+ *
+ * If called while running on a CHRE API version below v1.7, this function
+ * returns false and has no effect.
+ *
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *               sent as a response to this request.
+ *
+ * @return True to indicate the request was accepted. False otherwise.
+ *
+ * @since v1.7
+ */
+bool chreBleFlushAsync(const void *cookie);
+
+/**
+ * Requests to read the RSSI of a peer device on the given LE connection
+ * handle.
+ *
+ * If the request is accepted, the response will be delivered in a
+ * CHRE_EVENT_BLE_RSSI_READ event with the same cookie.
+ *
+ * The request may be rejected if resources are not available to service the
+ * request (such as if too many outstanding requests already exist). If so, the
+ * client may retry later.
+ *
+ * Note that the connectionHandle is valid only while the connection remains
+ * active. If a peer device disconnects then reconnects, the handle may change.
+ * BluetoothDevice#getConnectionHandle() can be used from the Android framework
+ * to get the latest handle upon reconnection.
+ *
+ * @param connectionHandle
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *               embedded in the response to this request.
+ * @return True if the request has been accepted and dispatched to the
+ *         controller. False otherwise.
+ *
+ * @since v1.8
+ *
+ */
+bool chreBleReadRssiAsync(uint16_t connectionHandle, const void *cookie);
+
+/**
+ * Retrieves the current state of the BLE scan on the platform.
+ *
+ * @param status A non-null pointer to where the scan status will be
+ *               populated.
+ *
+ * @return True if the status was obtained successfully.
+ *
+ * @since v1.8
+ */
+bool chreBleGetScanStatus(struct chreBleScanStatus *status);
+
+/**
+ * Definitions for handling unsupported CHRE BLE scenarios.
+ */
+#else  // defined(CHRE_NANOAPP_USES_BLE) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+#define CHRE_BLE_PERM_ERROR_STRING                                       \
+  "CHRE_NANOAPP_USES_BLE must be defined when building this nanoapp in " \
+  "order to refer to "
+
+#define chreBleStartScanAsync(...) \
+  CHRE_BUILD_ERROR(CHRE_BLE_PERM_ERROR_STRING "chreBleStartScanAsync")
+
+#define chreBleStopScanAsync(...) \
+  CHRE_BUILD_ERROR(CHRE_BLE_PERM_ERROR_STRING "chreBleStopScanAsync")
+
+#define chreBleFlushAsync(...) \
+  CHRE_BUILD_ERROR(CHRE_BLE_PERM_ERROR_STRING "chreBleFlushAsync")
+
+#define chreBleReadRssiAsync(...) \
+  CHRE_BUILD_ERROR(CHRE_BLE_PERM_ERROR_STRING "chreBleReadRssiAsync")
+
+#endif  // defined(CHRE_NANOAPP_USES_BLE) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CHRE_BLE_H_ */
diff --git a/chre_api/legacy/v1_8/chre/common.h b/chre_api/legacy/v1_8/chre/common.h
new file mode 100644
index 0000000..8e2df59
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/common.h
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_COMMON_H_
+#define _CHRE_COMMON_H_
+
+/**
+ * @file
+ * Definitions shared across multiple CHRE header files
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Mask of the 5 most significant bytes in a 64-bit nanoapp or CHRE platform
+ * identifier, which represents the vendor ID portion of the ID.
+ */
+#define CHRE_VENDOR_ID_MASK  UINT64_C(0xFFFFFFFFFF000000)
+
+/**
+ * Vendor ID "Googl".  Used in nanoapp IDs and CHRE platform IDs developed and
+ * released by Google.
+ */
+#define CHRE_VENDOR_ID_GOOGLE  UINT64_C(0x476F6F676C000000)
+
+/**
+ * Vendor ID "GoogT".  Used for nanoapp IDs associated with testing done by
+ * Google.
+ */
+#define CHRE_VENDOR_ID_GOOGLE_TEST  UINT64_C(0x476F6F6754000000)
+
+/**
+ * Helper macro to mask off all bytes other than the vendor ID (most significant
+ * 5 bytes) in 64-bit nanoapp and CHRE platform identifiers.
+ *
+ * @see chreGetNanoappInfo()
+ * @see chreGetPlatformId()
+ */
+#define CHRE_EXTRACT_VENDOR_ID(id)  ((id) & CHRE_VENDOR_ID_MASK)
+
+/**
+ * Number of nanoseconds in one second, represented as an unsigned 64-bit
+ * integer
+ */
+#define CHRE_NSEC_PER_SEC  UINT64_C(1000000000)
+
+/**
+ * General timeout for asynchronous API requests. Unless specified otherwise, a
+ * function call that returns data asynchronously via an event, such as
+ * CHRE_EVENT_ASYNC_GNSS_RESULT, must do so within this amount of time.
+ */
+#define CHRE_ASYNC_RESULT_TIMEOUT_NS  (5 * CHRE_NSEC_PER_SEC)
+
+
+/**
+ * A generic listing of error codes for use in {@link #chreAsyncResult} and
+ * elsewhere. In general, module-specific error codes may be added to this enum,
+ * but effort should be made to come up with a generic name that still captures
+ * the meaning of the error.
+ */
+// LINT.IfChange
+enum chreError {
+    //! No error occurred
+    CHRE_ERROR_NONE = 0,
+
+    //! An unspecified failure occurred
+    CHRE_ERROR = 1,
+
+    //! One or more supplied arguments are invalid
+    CHRE_ERROR_INVALID_ARGUMENT = 2,
+
+    //! Unable to satisfy request because the system is busy
+    CHRE_ERROR_BUSY = 3,
+
+    //! Unable to allocate memory
+    CHRE_ERROR_NO_MEMORY = 4,
+
+    //! The requested feature is not supported
+    CHRE_ERROR_NOT_SUPPORTED = 5,
+
+    //! A timeout occurred while processing the request
+    CHRE_ERROR_TIMEOUT = 6,
+
+    //! The relevant capability is disabled, for example due to a user
+    //! configuration that takes precedence over this request
+    CHRE_ERROR_FUNCTION_DISABLED = 7,
+
+    //! The request was rejected due to internal rate limiting of the requested
+    //! functionality - the client may try its request again after waiting an
+    //! unspecified amount of time
+    CHRE_ERROR_REJECTED_RATE_LIMIT = 8,
+
+    //! The requested functionality is not currently accessible from the CHRE,
+    //! because another client, such as the main applications processor, is
+    //! currently controlling it.
+    CHRE_ERROR_FUNCTION_RESTRICTED_TO_OTHER_MASTER = 9,
+    CHRE_ERROR_FUNCTION_RESTRICTED_TO_OTHER_CLIENT = 9,
+
+    //! This request is no longer valid. It may have been replaced by a newer
+    //! request before taking effect.
+    CHRE_ERROR_OBSOLETE_REQUEST = 10,
+
+    //!< Do not exceed this value when adding new error codes
+    CHRE_ERROR_LAST = UINT8_MAX,
+};
+// LINT.ThenChange(../../../../core/include/chre/core/api_manager_common.h)
+
+/**
+ * Generic data structure to indicate the result of an asynchronous operation.
+ *
+ * @note
+ * The general model followed by CHRE for asynchronous operations is that a
+ * request function returns a boolean value that indicates whether the request
+ * was accepted for further processing. The actual result of the operation is
+ * provided in a subsequent event sent with an event type that is defined in the
+ * specific API. Typically, a "cookie" parameter is supplied to allow the client
+ * to tie the response to a specific request, or pass data through, etc. The
+ * response is expected to be delivered within CHRE_ASYNC_RESULT_TIMEOUT_NS if
+ * not specified otherwise.
+ *
+ * The CHRE implementation must allow for multiple asynchronous requests to be
+ * outstanding at a given time, under reasonable resource constraints. Further,
+ * requests must be processed in the same order as supplied by the client of the
+ * API in order to maintain causality. Using GNSS as an example, if a client
+ * calls chreGnssLocationSessionStartAsync() and then immediately calls
+ * chreGnssLocationSessionStopAsync(), the final result must be that the
+ * location session is stopped. Whether requests always complete in the
+ * order that they are given is implementation-defined. For example, if a client
+ * calls chreGnssLocationSessionStart() and then immediately calls
+ * chreGnssMeasurementSessionStart(), it is possible for the
+ * CHRE_EVENT_GNSS_RESULT associated with the measurement session to be
+ * delivered before the one for the location session.
+ */
+struct chreAsyncResult {
+    //! Indicates the request associated with this result. The interpretation of
+    //! values in this field is dependent upon the event type provided when this
+    //! result was delivered.
+    uint8_t requestType;
+
+    //! Set to true if the request was successfully processed
+    bool success;
+
+    //! If the request failed (success is false), this is set to a value from
+    //! enum chreError (other than CHRE_ERROR_NONE), which may provide
+    //! additional information about the nature of the failure.
+    //! @see #chreError
+    uint8_t errorCode;
+
+    //! Reserved for future use, set to 0
+    uint8_t reserved;
+
+    //! Set to the cookie parameter given to the request function tied to this
+    //! result
+    const void *cookie;
+};
+
+/**
+ * A structure to store an event describing the end of batched events.
+ *
+ * @since v1.8
+ */
+struct chreBatchCompleteEvent {
+    //! Indicates the type of event (of type CHRE_EVENT_TYPE_*) that was
+    //! batched.
+    uint16_t eventType;
+
+    //! Reserved for future use, set to 0
+    uint8_t reserved[2];
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _CHRE_COMMON_H_ */
diff --git a/chre_api/legacy/v1_8/chre/event.h b/chre_api/legacy/v1_8/chre/event.h
new file mode 100644
index 0000000..e519c3d
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/event.h
@@ -0,0 +1,948 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_EVENT_H_
+#define _CHRE_EVENT_H_
+
+/**
+ * @file
+ * Context Hub Runtime Environment API dealing with events and messages.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <chre/toolchain.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The CHRE implementation is required to provide the following preprocessor
+ * defines via the build system.
+ *
+ * CHRE_MESSAGE_TO_HOST_MAX_SIZE: The maximum size, in bytes, allowed for
+ *     a message sent to chreSendMessageToHostEndpoint().  This must be at least
+ *     CHRE_MESSAGE_TO_HOST_MINIMUM_MAX_SIZE.
+ */
+
+#ifndef CHRE_MESSAGE_TO_HOST_MAX_SIZE
+#error CHRE_MESSAGE_TO_HOST_MAX_SIZE must be defined by the CHRE implementation
+#endif
+
+/**
+ * The minimum size, in bytes, any CHRE implementation will use for
+ * CHRE_MESSAGE_TO_HOST_MAX_SIZE is set to 1000 for v1.5+ CHRE implementations,
+ * and 128 for v1.0-v1.4 implementations (previously kept in
+ * CHRE_MESSAGE_TO_HOST_MINIMUM_MAX_SIZE, which has been removed).
+ *
+ * All CHRE implementations supporting v1.5+ must support the raised limit of
+ * 1000 bytes, however a nanoapp compiled against v1.5 cannot assume this
+ * limit if there is a possibility their binary will run on a v1.4 or earlier
+ * implementation that had a lower limit. To allow for nanoapp compilation in
+ * these situations, CHRE_MESSAGE_TO_HOST_MAX_SIZE must be set to the minimum
+ * value the nanoapp may encounter, and CHRE_NANOAPP_SUPPORTS_PRE_V1_5 can be
+ * defined to skip the compile-time check.
+ */
+#if (!defined(CHRE_NANOAPP_SUPPORTS_PRE_V1_5) && \
+     CHRE_MESSAGE_TO_HOST_MAX_SIZE < 1000) ||    \
+    (defined(CHRE_NANOAPP_SUPPORTS_PRE_V1_5) &&  \
+     CHRE_MESSAGE_TO_HOST_MAX_SIZE < 128)
+#error CHRE_MESSAGE_TO_HOST_MAX_SIZE is too small.
+#endif
+
+/**
+ * The lowest numerical value legal for a user-defined event.
+ *
+ * The system reserves all event values from 0 to 0x7FFF, inclusive.
+ * User events may use any value in the range 0x8000 to 0xFFFF, inclusive.
+ *
+ * Note that the same event values might be used by different nanoapps
+ * for different meanings.  This is not a concern, as these values only
+ * have meaning when paired with the originating nanoapp.
+ */
+#define CHRE_EVENT_FIRST_USER_VALUE  UINT16_C(0x8000)
+
+/**
+ * nanoappHandleEvent argument: struct chreMessageFromHostData
+ *
+ * The format of the 'message' part of this structure is left undefined,
+ * and it's up to the nanoapp and host to have an established protocol
+ * beforehand.
+ */
+#define CHRE_EVENT_MESSAGE_FROM_HOST  UINT16_C(0x0001)
+
+/**
+ * nanoappHandleEvent argument: 'cookie' given to chreTimerSet() method.
+ *
+ * Indicates that a timer has elapsed, in accordance with how chreTimerSet() was
+ * invoked.
+ */
+#define CHRE_EVENT_TIMER  UINT16_C(0x0002)
+
+/**
+ * nanoappHandleEvent argument: struct chreNanoappInfo
+ *
+ * Indicates that a nanoapp has successfully started (its nanoappStart()
+ * function has been called, and it returned true) and is able to receive events
+ * sent via chreSendEvent().  Note that this event is not sent for nanoapps that
+ * were started prior to the current nanoapp - use chreGetNanoappInfo() to
+ * determine if another nanoapp is already running.
+ *
+ * @see chreConfigureNanoappInfoEvents
+ * @since v1.1
+ */
+#define CHRE_EVENT_NANOAPP_STARTED  UINT16_C(0x0003)
+
+/**
+ * nanoappHandleEvent argument: struct chreNanoappInfo
+ *
+ * Indicates that a nanoapp has stopped executing and is no longer able to
+ * receive events sent via chreSendEvent().  Any events sent prior to receiving
+ * this event are not guaranteed to have been delivered.
+ *
+ * @see chreConfigureNanoappInfoEvents
+ * @since v1.1
+ */
+#define CHRE_EVENT_NANOAPP_STOPPED  UINT16_C(0x0004)
+
+/**
+ * nanoappHandleEvent argument: NULL
+ *
+ * Indicates that CHRE has observed the host wake from low-power sleep state.
+ *
+ * @see chreConfigureHostSleepStateEvents
+ * @since v1.2
+ */
+#define CHRE_EVENT_HOST_AWAKE  UINT16_C(0x0005)
+
+/**
+ * nanoappHandleEvent argument: NULL
+ *
+ * Indicates that CHRE has observed the host enter low-power sleep state.
+ *
+ * @see chreConfigureHostSleepStateEvents
+ * @since v1.2
+ */
+#define CHRE_EVENT_HOST_ASLEEP  UINT16_C(0x0006)
+
+/**
+ * nanoappHandleEvent argument: NULL
+ *
+ * Indicates that CHRE is collecting debug dumps. Nanoapps can call
+ * chreDebugDumpLog() to log their debug data while handling this event.
+ *
+ * @see chreConfigureDebugDumpEvent
+ * @see chreDebugDumpLog
+ * @since v1.4
+ */
+#define CHRE_EVENT_DEBUG_DUMP  UINT16_C(0x0007)
+
+/**
+ * nanoappHandleEvent argument: struct chreHostEndpointNotification
+ *
+ * Notifications event regarding a host endpoint.
+ *
+ * @see chreConfigureHostEndpointNotifications
+ * @since v1.6
+ */
+#define CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION UINT16_C(0x0008)
+
+/**
+ * First possible value for CHRE_EVENT_SENSOR events.
+ *
+ * This allows us to separately define our CHRE_EVENT_SENSOR_* events in
+ * chre/sensor.h, without fear of collision with other event values.
+ */
+#define CHRE_EVENT_SENSOR_FIRST_EVENT  UINT16_C(0x0100)
+
+/**
+ * Last possible value for CHRE_EVENT_SENSOR events.
+ *
+ * This allows us to separately define our CHRE_EVENT_SENSOR_* events in
+ * chre/sensor.h, without fear of collision with other event values.
+ */
+#define CHRE_EVENT_SENSOR_LAST_EVENT  UINT16_C(0x02FF)
+
+/**
+ * First event in the block reserved for GNSS. These events are defined in
+ * chre/gnss.h.
+ */
+#define CHRE_EVENT_GNSS_FIRST_EVENT  UINT16_C(0x0300)
+#define CHRE_EVENT_GNSS_LAST_EVENT   UINT16_C(0x030F)
+
+/**
+ * First event in the block reserved for WiFi. These events are defined in
+ * chre/wifi.h.
+ */
+#define CHRE_EVENT_WIFI_FIRST_EVENT  UINT16_C(0x0310)
+#define CHRE_EVENT_WIFI_LAST_EVENT   UINT16_C(0x031F)
+
+/**
+ * First event in the block reserved for WWAN. These events are defined in
+ * chre/wwan.h.
+ */
+#define CHRE_EVENT_WWAN_FIRST_EVENT  UINT16_C(0x0320)
+#define CHRE_EVENT_WWAN_LAST_EVENT   UINT16_C(0x032F)
+
+/**
+ * First event in the block reserved for audio. These events are defined in
+ * chre/audio.h.
+ */
+#define CHRE_EVENT_AUDIO_FIRST_EVENT UINT16_C(0x0330)
+#define CHRE_EVENT_AUDIO_LAST_EVENT  UINT16_C(0x033F)
+
+/**
+ * First event in the block reserved for settings changed notifications.
+ * These events are defined in chre/user_settings.h
+ *
+ * @since v1.5
+ */
+#define CHRE_EVENT_SETTING_CHANGED_FIRST_EVENT UINT16_C(0x340)
+#define CHRE_EVENT_SETTING_CHANGED_LAST_EVENT  UINT16_C(0x34F)
+
+/**
+ * First event in the block reserved for Bluetooth LE. These events are defined
+ * in chre/ble.h.
+ */
+#define CHRE_EVENT_BLE_FIRST_EVENT UINT16_C(0x0350)
+#define CHRE_EVENT_BLE_LAST_EVENT  UINT16_C(0x035F)
+
+/**
+ * First in the extended range of values dedicated for internal CHRE
+ * implementation usage.
+ *
+ * This range is semantically the same as the internal event range defined
+ * below, but has been extended to allow for more implementation-specific events
+ * to be used.
+ *
+ * @since v1.1
+ */
+#define CHRE_EVENT_INTERNAL_EXTENDED_FIRST_EVENT  UINT16_C(0x7000)
+
+/**
+ * First in a range of values dedicated for internal CHRE implementation usage.
+ *
+ * If a CHRE wishes to use events internally, any values within this range
+ * are assured not to be taken by future CHRE API additions.
+ */
+#define CHRE_EVENT_INTERNAL_FIRST_EVENT  UINT16_C(0x7E00)
+
+/**
+ * Last in a range of values dedicated for internal CHRE implementation usage.
+ *
+ * If a CHRE wishes to use events internally, any values within this range
+ * are assured not to be taken by future CHRE API additions.
+ */
+#define CHRE_EVENT_INTERNAL_LAST_EVENT  UINT16_C(0x7FFF)
+
+/**
+ * A special value for the hostEndpoint argument in
+ * chreSendMessageToHostEndpoint() that indicates that the message should be
+ * delivered to all host endpoints.  This value will not be used in the
+ * hostEndpoint field of struct chreMessageFromHostData supplied with
+ * CHRE_EVENT_MESSAGE_FROM_HOST.
+ *
+ * @since v1.1
+ */
+#define CHRE_HOST_ENDPOINT_BROADCAST  UINT16_C(0xFFFF)
+
+/**
+ * A special value for hostEndpoint in struct chreMessageFromHostData that
+ * indicates that a host endpoint is unknown or otherwise unspecified.  This
+ * value may be received in CHRE_EVENT_MESSAGE_FROM_HOST, but it is not valid to
+ * provide it to chreSendMessageToHostEndpoint().
+ *
+ * @since v1.1
+ */
+#define CHRE_HOST_ENDPOINT_UNSPECIFIED  UINT16_C(0xFFFE)
+
+/**
+ * Bitmask values that can be given as input to the messagePermissions parameter
+ * of chreSendMessageWithPermissions(). These values are typically used by
+ * nanoapps when they used data from the corresponding CHRE APIs to produce the
+ * message contents being sent and is used to attribute permissions usage on
+ * the Android side. See chreSendMessageWithPermissions() for more details on
+ * how these values are used when sending a message.
+ *
+ * Values in the range
+ * [CHRE_MESSAGE_PERMISSION_VENDOR_START, CHRE_MESSAGE_PERMISSION_VENDOR_END]
+ * are reserved for vendors to use when adding support for permission-gated APIs
+ * in their implementations.
+ *
+ * On the Android side, CHRE permissions are mapped as follows:
+ * - CHRE_MESSAGE_PERMISSION_AUDIO: android.permission.RECORD_AUDIO
+ * - CHRE_MESSAGE_PERMISSION_GNSS, CHRE_MESSAGE_PERMISSION_WIFI, and
+ *   CHRE_MESSAGE_PERMISSION_WWAN: android.permission.ACCESS_FINE_LOCATION, and
+ *   android.permissions.ACCESS_BACKGROUND_LOCATION
+ *
+ * @since v1.5
+ *
+ * @defgroup CHRE_MESSAGE_PERMISSION
+ * @{
+ */
+
+#define CHRE_MESSAGE_PERMISSION_NONE UINT32_C(0)
+#define CHRE_MESSAGE_PERMISSION_AUDIO UINT32_C(1)
+#define CHRE_MESSAGE_PERMISSION_GNSS (UINT32_C(1) << 1)
+#define CHRE_MESSAGE_PERMISSION_WIFI (UINT32_C(1) << 2)
+#define CHRE_MESSAGE_PERMISSION_WWAN (UINT32_C(1) << 3)
+#define CHRE_MESSAGE_PERMISSION_BLE (UINT32_C(1) << 4)
+#define CHRE_MESSAGE_PERMISSION_VENDOR_START (UINT32_C(1) << 24)
+#define CHRE_MESSAGE_PERMISSION_VENDOR_END (UINT32_C(1) << 31)
+
+/** @} */
+
+/**
+ * @see chrePublishRpcServices
+ *
+ * @since v1.8
+ */
+#define CHRE_MINIMUM_RPC_SERVICE_LIMIT UINT8_C(4)
+
+/**
+ * Data provided with CHRE_EVENT_MESSAGE_FROM_HOST.
+ */
+struct chreMessageFromHostData {
+    /**
+     * Message type supplied by the host.
+     *
+     * @note In CHRE API v1.0, support for forwarding this field from the host
+     * was not strictly required, and some implementations did not support it.
+     * However, its support is mandatory as of v1.1.
+     */
+    union {
+        /**
+         * The preferred name to use when referencing this field.
+         *
+         * @since v1.1
+         */
+        uint32_t messageType;
+
+        /**
+         * @deprecated This is the name for the messageType field used in v1.0.
+         * Left to allow code to compile against both v1.0 and v1.1 of the API
+         * definition without needing to use #ifdefs. This will be removed in a
+         * future API update - use messageType instead.
+         */
+        uint32_t reservedMessageType;
+    };
+
+    /**
+     * The size, in bytes of the following 'message'.
+     *
+     * This can be 0.
+     */
+    uint32_t messageSize;
+
+    /**
+     * The message from the host.
+     *
+     * These contents are of a format that the host and nanoapp must have
+     * established beforehand.
+     *
+     * This data is 'messageSize' bytes in length.  Note that if 'messageSize'
+     * is 0, this might be NULL.
+     */
+    const void *message;
+
+    /**
+     * An identifier for the host-side entity that sent this message.  Unless
+     * this is set to CHRE_HOST_ENDPOINT_UNSPECIFIED, it can be used in
+     * chreSendMessageToHostEndpoint() to send a directed reply that will only
+     * be received by the given entity on the host.  Endpoint identifiers are
+     * opaque values assigned at runtime, so they cannot be assumed to always
+     * describe a specific entity across restarts.
+     *
+     * If running on a CHRE API v1.0 implementation, this field will always be
+     * set to CHRE_HOST_ENDPOINT_UNSPECIFIED.
+     *
+     * @since v1.1
+     */
+    uint16_t hostEndpoint;
+};
+
+/**
+ * Provides metadata for a nanoapp in the system.
+ */
+struct chreNanoappInfo {
+    /**
+     * Nanoapp identifier. The convention for populating this value is to set
+     * the most significant 5 bytes to a value that uniquely identifies the
+     * vendor, and the lower 3 bytes identify the nanoapp.
+     */
+    uint64_t appId;
+
+    /**
+     * Nanoapp version.  The semantics of this field are defined by the nanoapp,
+     * however nanoapps are recommended to follow the same scheme used for the
+     * CHRE version exposed in chreGetVersion().  That is, the most significant
+     * byte represents the major version, the next byte the minor version, and
+     * the lower two bytes the patch version.
+     */
+    uint32_t version;
+
+    /**
+     * The instance ID of this nanoapp, which can be used in chreSendEvent() to
+     * address an event specifically to this nanoapp.  This identifier is
+     * guaranteed to be unique among all nanoapps in the system.
+     *
+     * As of CHRE API v1.6, instance ID is guaranteed to never be greater than
+     * UINT16_MAX. This allows for the instance ID be packed with other data
+     * inside a 32-bit integer (useful for RPC routing).
+     */
+    uint32_t instanceId;
+
+    /**
+     * Reserved for future use.
+     * Always set to 0.
+     */
+    uint8_t reserved[3];
+
+    /**
+     * The number of RPC services exposed by this nanoapp.
+     * The service details are available in the rpcServices array.
+     * Must always be set to 0 when running on a CHRE implementation prior to
+     * v1.8
+     *
+     * @since v1.8
+     */
+    uint8_t rpcServiceCount;
+
+    /*
+     * Array of RPC services published by this nanoapp.
+     * Services are published via chrePublishRpcServices.
+     * The array contains rpcServiceCount entries.
+     *
+     * The pointer is only valid when rpcServiceCount is greater than 0.
+     *
+     * @since v1.8
+     */
+    const struct chreNanoappRpcService *rpcServices;
+};
+
+/**
+ * The types of notification events that can be included in struct
+ * chreHostEndpointNotification.
+ *
+ * @defgroup HOST_ENDPOINT_NOTIFICATION_TYPE
+ * @{
+ */
+#define HOST_ENDPOINT_NOTIFICATION_TYPE_DISCONNECT UINT8_C(0)
+/** @} */
+
+/**
+ * Data provided in CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION.
+ */
+struct chreHostEndpointNotification {
+    /**
+     * The ID of the host endpoint that this notification is for.
+     */
+    uint16_t hostEndpointId;
+
+    /**
+     * The type of notification this event represents, which should be
+     * one of the HOST_ENDPOINT_NOTIFICATION_TYPE_* values.
+     */
+    uint8_t notificationType;
+
+    /**
+     * Reserved for future use, must be zero.
+     */
+    uint8_t reserved;
+};
+
+//! The maximum length of a host endpoint's name.
+#define CHRE_MAX_ENDPOINT_NAME_LEN (51)
+
+//! The maximum length of a host endpoint's tag.
+#define CHRE_MAX_ENDPOINT_TAG_LEN (51)
+
+/**
+ * The type of host endpoint that can be used in the hostEndpointType field
+ * of chreHostEndpointInfo.
+ *
+ * @since v1.6
+ *
+ * @defgroup CHRE_HOST_ENDPOINT_TYPE_
+ * @{
+ */
+
+//! The host endpoint is part of the Android system framework.
+#define CHRE_HOST_ENDPOINT_TYPE_FRAMEWORK UINT8_C(0)
+
+//! The host endpoint is an Android app.
+#define CHRE_HOST_ENDPOINT_TYPE_APP UINT8_C(1)
+
+//! The host endpoint is an Android native program.
+#define CHRE_HOST_ENDPOINT_TYPE_NATIVE UINT8_C(2)
+
+//! Values in the range [CHRE_HOST_ENDPOINT_TYPE_VENDOR_START,
+//! CHRE_HOST_ENDPOINT_TYPE_VENDOR_END] can be a custom defined host endpoint
+//! type for platform-specific vendor use.
+#define CHRE_HOST_ENDPOINT_TYPE_VENDOR_START UINT8_C(128)
+#define CHRE_HOST_ENDPOINT_TYPE_VENDOR_END UINT8_C(255)
+
+/** @} */
+
+/**
+ * Provides metadata for a host endpoint.
+ *
+ * @since v1.6
+ */
+struct chreHostEndpointInfo {
+    //! The endpoint ID of this host.
+    uint16_t hostEndpointId;
+
+    //! The type of host endpoint, which must be set to one of the
+    //! CHRE_HOST_ENDPOINT_TYPE_* values or a value in the vendor-reserved
+    //! range.
+    uint8_t hostEndpointType;
+
+    //! Flag indicating if the packageName/endpointName field is valid.
+    uint8_t isNameValid : 1;
+
+    //! Flag indicating if the attributionTag/endpointTag field is valid.
+    uint8_t isTagValid : 1;
+
+    //! A union of null-terminated host name strings.
+    union {
+        //! The Android package name associated with this host, valid if the
+        //! hostEndpointType is CHRE_HOST_ENDPOINT_TYPE_APP or
+        //! CHRE_HOST_ENDPOINT_TYPE_FRAMEWORK. Refer to the Android documentation
+        //! for the package attribute in the app manifest.
+        char packageName[CHRE_MAX_ENDPOINT_NAME_LEN];
+
+        //! A generic endpoint name that can be used for endpoints that
+        //! may not have a package name.
+        char endpointName[CHRE_MAX_ENDPOINT_NAME_LEN];
+    };
+
+    //! A union of null-terminated host tag strings for further identification.
+    union {
+        //! The attribution tag associated with this host that is used to audit
+        //! access to data, which can be valid if the hostEndpointType is
+        //! CHRE_HOST_ENDPOINT_TYPE_APP. Refer to the Android documentation
+        //! regarding data audit using attribution tags.
+        char attributionTag[CHRE_MAX_ENDPOINT_TAG_LEN];
+
+        //! A generic endpoint tag that can be used for endpoints that
+        //! may not have an attribution tag.
+        char endpointTag[CHRE_MAX_ENDPOINT_TAG_LEN];
+    };
+};
+
+/**
+ * An RPC service exposed by a nanoapp.
+ *
+ * The implementation of the RPC interface is not defined by the HAL, and is written
+ * at the messaging endpoint layers (Android app and/or CHRE nanoapp). NanoappRpcService
+ * contains the informational metadata to be consumed by the RPC interface layer.
+ */
+struct chreNanoappRpcService {
+    /**
+     * The unique 64-bit ID of an RPC service exposed by a nanoapp. Note that
+     * the uniqueness is only required within the nanoapp's domain (i.e. the
+     * combination of the nanoapp ID and service id must be unique).
+     */
+    uint64_t id;
+
+    /**
+     * The software version of this service, which follows the sematic
+     * versioning scheme (see semver.org). It follows the format
+     * major.minor.patch, where major and minor versions take up one byte
+     * each, and the patch version takes up the final 2 bytes.
+     */
+    uint32_t version;
+};
+
+/**
+ * Callback which frees data associated with an event.
+ *
+ * This callback is (optionally) provided to the chreSendEvent() method as
+ * a means for freeing the event data and performing any other cleanup
+ * necessary when the event is completed.  When this callback is invoked,
+ * 'eventData' is no longer needed and can be released.
+ *
+ * @param eventType  The 'eventType' argument from chreSendEvent().
+ * @param eventData  The 'eventData' argument from chreSendEvent().
+ *
+ * @see chreSendEvent
+ */
+typedef void (chreEventCompleteFunction)(uint16_t eventType, void *eventData);
+
+/**
+ * Callback which frees a message.
+ *
+ * This callback is (optionally) provided to the chreSendMessageToHostEndpoint()
+ * method as a means for freeing the message.  When this callback is invoked,
+ * 'message' is no longer needed and can be released.  Note that this in
+ * no way assures that said message did or did not make it to the host, simply
+ * that this memory is no longer needed.
+ *
+ * @param message  The 'message' argument from chreSendMessageToHostEndpoint().
+ * @param messageSize  The 'messageSize' argument from
+ *     chreSendMessageToHostEndpoint().
+ *
+ * @see chreSendMessageToHostEndpoint
+ */
+typedef void (chreMessageFreeFunction)(void *message, size_t messageSize);
+
+
+/**
+ * Enqueue an event to be sent to another nanoapp.
+ *
+ * @param eventType  This is a user-defined event type, of at least the
+ *     value CHRE_EVENT_FIRST_USER_VALUE.  It is illegal to attempt to use any
+ *     of the CHRE_EVENT_* values reserved for the CHRE.
+ * @param eventData  A pointer value that will be understood by the receiving
+ *     app.  Note that NULL is perfectly acceptable.  It also is not required
+ *     that this be a valid pointer, although if this nanoapp is intended to
+ *     work on arbitrary CHRE implementations, then the size of a
+ *     pointer cannot be assumed to be a certain size.  Note that the caller
+ *     no longer owns this memory after the call.
+ * @param freeCallback  A pointer to a callback function.  After the lifetime
+ *     of 'eventData' is over (either through successful delivery or the event
+ *     being dropped), this callback will be invoked.  This argument is allowed
+ *     to be NULL, in which case no callback will be invoked.
+ * @param targetInstanceId  The ID of the instance we're delivering this event
+ *     to.  Note that this is allowed to be our own instance.  The instance ID
+ *     of a nanoapp can be retrieved by using chreGetNanoappInfoByInstanceId().
+ * @return true if the event was enqueued, false otherwise.  Note that even
+ *     if this method returns 'false', the 'freeCallback' will be invoked,
+ *     if non-NULL.  Note in the 'false' case, the 'freeCallback' may be
+ *     invoked directly from within chreSendEvent(), so it's necessary
+ *     for nanoapp authors to avoid possible recursion with this.
+ *
+ * @see chreEventDataFreeFunction
+ */
+bool chreSendEvent(uint16_t eventType, void *eventData,
+                   chreEventCompleteFunction *freeCallback,
+                   uint32_t targetInstanceId);
+
+/**
+ * Send a message to the host, using the broadcast endpoint
+ * CHRE_HOST_ENDPOINT_BROADCAST.  Refer to chreSendMessageToHostEndpoint() for
+ * further details.
+ *
+ * @see chreSendMessageToHostEndpoint
+ *
+ * @deprecated New code should use chreSendMessageToHostEndpoint() instead of
+ * this function.  A future update to the API may cause references to this
+ * function to produce a compiler warning.
+ */
+bool chreSendMessageToHost(void *message, uint32_t messageSize,
+                           uint32_t messageType,
+                           chreMessageFreeFunction *freeCallback)
+    CHRE_DEPRECATED("Use chreSendMessageToHostEndpoint instead");
+
+/**
+ * Send a message to the host, using CHRE_MESSAGE_PERMISSION_NONE for the
+ * associated message permissions. This method must only be used if no data
+ * provided by CHRE's audio, GNSS, WiFi, and WWAN APIs was used to produce the
+ * contents of the message being sent. Refer to chreSendMessageWithPermissions()
+ * for further details.
+ *
+ * @see chreSendMessageWithPermissions
+ *
+ * @since v1.1
+ */
+bool chreSendMessageToHostEndpoint(void *message, size_t messageSize,
+                                   uint32_t messageType, uint16_t hostEndpoint,
+                                   chreMessageFreeFunction *freeCallback);
+
+/**
+ * Send a message to the host, waking it up if it is currently asleep.
+ *
+ * This message is by definition arbitrarily defined.  Since we're not
+ * just a passing a pointer to memory around the system, but need to copy
+ * this into various buffers to send it to the host, the CHRE
+ * implementation cannot be asked to support an arbitrarily large message
+ * size.  As a result, we have the CHRE implementation define
+ * CHRE_MESSAGE_TO_HOST_MAX_SIZE.
+ *
+ * CHRE_MESSAGE_TO_HOST_MAX_SIZE is not given a value by the Platform API.  The
+ * Platform API does define CHRE_MESSAGE_TO_HOST_MINIMUM_MAX_SIZE, and requires
+ * that CHRE_MESSAGE_TO_HOST_MAX_SIZE is at least that value.
+ *
+ * As a result, if your message sizes are all less than
+ * CHRE_MESSAGE_TO_HOST_MINIMUM_MAX_SIZE, then you have no concerns on any
+ * CHRE implementation.  If your message sizes are larger, you'll need to
+ * come up with a strategy for splitting your message across several calls
+ * to this method.  As long as that strategy works for
+ * CHRE_MESSAGE_TO_HOST_MINIMUM_MAX_SIZE, it will work across all CHRE
+ * implementations (although on some implementations less calls to this
+ * method may be necessary).
+ *
+ * When sending a message to the host, the ContextHub service will enforce
+ * the host client has been granted Android-level permissions corresponding to
+ * the ones the nanoapp declares it uses through CHRE_NANOAPP_USES_AUDIO, etc.
+ * In addition to this, the permissions bitmask provided as input to this method
+ * results in the Android framework using app-ops to verify and log access upon
+ * message delivery to an application. This is primarily useful for ensuring
+ * accurate attribution for messages generated using permission-controlled data.
+ * The bitmask declared by the nanoapp for this message must be a
+ * subset of the permissions it declared it would use at build time or the
+ * message will be rejected.
+ *
+ * Nanoapps must use this method if the data they are sending contains or was
+ * derived from any data sampled through CHRE's audio, GNSS, WiFi, or WWAN APIs.
+ * Additionally, if vendors add APIs to expose data that would be guarded by a
+ * permission in Android, vendors must support declaring a message permission
+ * through this method.
+ *
+ * @param message  Pointer to a block of memory to send to the host.
+ *     NULL is acceptable only if messageSize is 0.  If non-NULL, this
+ *     must be a legitimate pointer (that is, unlike chreSendEvent(), a small
+ *     integral value cannot be cast to a pointer for this).  Note that the
+ *     caller no longer owns this memory after the call.
+ * @param messageSize  The size, in bytes, of the given message. If this exceeds
+ *     CHRE_MESSAGE_TO_HOST_MAX_SIZE, the message will be rejected.
+ * @param messageType  Message type sent to the app on the host.
+ *     NOTE: In CHRE API v1.0, support for forwarding this field to the host was
+ *     not strictly required, and some implementations did not support it.
+ *     However, its support is mandatory as of v1.1.
+ * @param hostEndpoint  An identifier for the intended recipient of the message,
+ *     or CHRE_HOST_ENDPOINT_BROADCAST if all registered endpoints on the host
+ *     should receive the message.  Endpoint identifiers are assigned on the
+ *     host side, and nanoapps may learn of the host endpoint ID of an intended
+ *     recipient via an initial message sent by the host.  This parameter is
+ *     always treated as CHRE_HOST_ENDPOINT_BROADCAST if running on a CHRE API
+ *     v1.0 implementation. CHRE_HOST_ENDPOINT_BROADCAST isn't allowed to be
+ *     specified if anything other than CHRE_MESSAGE_PERMISSION_NONE is given
+ *     as messagePermissions since doing so would potentially attribute
+ *     permissions usage to host clients that don't intend to consume the data.
+ * @param messagePermissions Bitmasked CHRE_MESSAGE_PERMISSION_ values that will
+ *     be converted to corresponding Android-level permissions and attributed
+ *     the host endpoint upon consumption of the message.
+ * @param freeCallback  A pointer to a callback function.  After the lifetime
+ *     of 'message' is over (which does not assure that 'message' made it to
+ *     the host, just that the transport layer no longer needs this memory),
+ *     this callback will be invoked.  This argument is allowed
+ *     to be NULL, in which case no callback will be invoked.
+ * @return true if the message was accepted for transmission, false otherwise.
+ *     Note that even if this method returns 'false', the 'freeCallback' will
+ *     be invoked, if non-NULL.  In either case, the 'freeCallback' may be
+ *     invoked directly from within chreSendMessageToHostEndpoint(), so it's
+ *     necessary for nanoapp authors to avoid possible recursion with this.
+ *
+ * @see chreMessageFreeFunction
+ *
+ * @since v1.5
+ */
+bool chreSendMessageWithPermissions(void *message, size_t messageSize,
+                                    uint32_t messageType, uint16_t hostEndpoint,
+                                    uint32_t messagePermissions,
+                                    chreMessageFreeFunction *freeCallback);
+
+/**
+ * Queries for information about a nanoapp running in the system.
+ *
+ * In the current API, appId is required to be unique, i.e. there cannot be two
+ * nanoapps running concurrently with the same appId.  If this restriction is
+ * removed in a future API version and multiple instances of the same appId are
+ * present, this function must always return the first app to start.
+ *
+ * @param appId Identifier for the nanoapp that the caller is requesting
+ *     information about.
+ * @param info Output parameter.  If this function returns true, this structure
+ *     will be populated with details of the specified nanoapp.
+ * @return true if a nanoapp with the given ID is currently running, and the
+ *     supplied info parameter was populated with its information.
+ *
+ * @since v1.1
+ */
+bool chreGetNanoappInfoByAppId(uint64_t appId, struct chreNanoappInfo *info);
+
+/**
+ * Queries for information about a nanoapp running in the system, using the
+ * runtime unique identifier.  This method can be used to get information about
+ * the sender of an event.
+ *
+ * @param instanceId
+ * @param info Output parameter.  If this function returns true, this structure
+ *     will be populated with details of the specified nanoapp.
+ * @return true if a nanoapp with the given instance ID is currently running,
+ *     and the supplied info parameter was populated with its information.
+ *
+ * @since v1.1
+ */
+bool chreGetNanoappInfoByInstanceId(uint32_t instanceId,
+                                    struct chreNanoappInfo *info);
+
+/**
+ * Configures whether this nanoapp will be notified when other nanoapps in the
+ * system start and stop, via CHRE_EVENT_NANOAPP_STARTED and
+ * CHRE_EVENT_NANOAPP_STOPPED.  These events are disabled by default, and if a
+ * nanoapp is not interested in interacting with other nanoapps, then it does
+ * not need to register for them.  However, if inter-nanoapp communication is
+ * desired, nanoapps are recommended to call this function from nanoappStart().
+ *
+ * If running on a CHRE platform that only supports v1.0 of the CHRE API, this
+ * function has no effect.
+ *
+ * @param enable true to enable these events, false to disable
+ *
+ * @see CHRE_EVENT_NANOAPP_STARTED
+ * @see CHRE_EVENT_NANOAPP_STOPPED
+ *
+ * @since v1.1
+ */
+void chreConfigureNanoappInfoEvents(bool enable);
+
+/**
+ * Configures whether this nanoapp will be notified when the host (applications
+ * processor) transitions between wake and sleep, via CHRE_EVENT_HOST_AWAKE and
+ * CHRE_EVENT_HOST_ASLEEP.  As chreSendMessageToHostEndpoint() wakes the host if
+ * it is asleep, these events can be used to opportunistically send data to the
+ * host only when it wakes up for some other reason.  Note that this event is
+ * not instantaneous - there is an inherent delay in CHRE observing power state
+ * changes of the host processor, which may be significant depending on the
+ * implementation, especially in the wake to sleep direction.  Therefore,
+ * nanoapps are not guaranteed that messages sent to the host between AWAKE and
+ * ASLEEP events will not trigger a host wakeup.  However, implementations must
+ * ensure that the nominal wake-up notification latency is strictly less than
+ * the minimum wake-sleep time of the host processor.  Implementations are also
+ * encouraged to minimize this and related latencies where possible, to avoid
+ * unnecessary host wake-ups.
+ *
+ * These events are only sent on transitions, so the initial state will not be
+ * sent to the nanoapp as an event - use chreIsHostAwake().
+ *
+ * @param enable true to enable these events, false to disable
+ *
+ * @see CHRE_EVENT_HOST_AWAKE
+ * @see CHRE_EVENT_HOST_ASLEEP
+ *
+ * @since v1.2
+ */
+void chreConfigureHostSleepStateEvents(bool enable);
+
+/**
+ * Retrieves the current sleep/wake state of the host (applications processor).
+ * Note that, as with the CHRE_EVENT_HOST_AWAKE and CHRE_EVENT_HOST_ASLEEP
+ * events, there is no guarantee that CHRE's view of the host processor's sleep
+ * state is instantaneous, and it may also change between querying the state and
+ * performing a host-waking action like sending a message to the host.
+ *
+ * @return true if by CHRE's own estimation the host is currently awake,
+ *     false otherwise
+ *
+ * @since v1.2
+ */
+bool chreIsHostAwake(void);
+
+/**
+ * Configures whether this nanoapp will be notified when CHRE is collecting
+ * debug dumps, via CHRE_EVENT_DEBUG_DUMP. This event is disabled by default,
+ * and if a nanoapp is not interested in logging its debug data, then it does
+ * not need to register for it.
+ *
+ * @param enable true to enable receipt of this event, false to disable.
+ *
+ * @see CHRE_EVENT_DEBUG_DUMP
+ * @see chreDebugDumpLog
+ *
+ * @since v1.4
+ */
+void chreConfigureDebugDumpEvent(bool enable);
+
+/**
+ * Configures whether this nanoapp will receive updates regarding a host
+ * endpoint that is connected with the Context Hub.
+ *
+ * If this API succeeds, the nanoapp will receive disconnection notifications,
+ * via the CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION event with an eventData of type
+ * chreHostEndpointNotification with its notificationType set to
+ * HOST_ENDPOINT_NOTIFICATION_TYPE_DISCONNECT, which can be invoked if the host
+ * has disconnected from the Context Hub either explicitly or implicitly (e.g.
+ * crashes). Nanoapps can use this notifications to clean up any resources
+ * associated with this host endpoint.
+ *
+ * @param hostEndpointId The host endpoint ID to configure notifications for.
+ * @param enable true to enable notifications.
+ *
+ * @return true on success
+ *
+ * @see chreMessageFromHostData
+ * @see chreHostEndpointNotification
+ * @see CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION
+ *
+ * @since v1.6
+ */
+bool chreConfigureHostEndpointNotifications(uint16_t hostEndpointId,
+                                            bool enable);
+
+/**
+ * Publishes RPC services from this nanoapp.
+ *
+ * When this API is invoked, the list of RPC services will be provided to
+ * host applications interacting with the nanoapp.
+ *
+ * This function must be invoked from nanoappStart(), to guarantee stable output
+ * of the list of RPC services supported by the nanoapp.
+ *
+ * Although nanoapps are recommended to only call this API once with all
+ * services it intends to publish, if it is called multiple times, each
+ * call will append to the list of published services.
+ *
+ * Starting in CHRE API v1.8, the implementation must allow for a nanoapp to
+ * publish at least CHRE_MINIMUM_RPC_SERVICE_LIMIT services and at most
+ * UINT8_MAX services. If calling this function would result in exceeding
+ * the limit, the services must not be published and it must return false.
+ *
+ * @param services A non-null pointer to the list of RPC services to publish.
+ * @param numServices The number of services to publish, i.e. the length of the
+ *   services array.
+ *
+ * @return true if the publishing is successful.
+ *
+ * @since v1.6
+ */
+bool chrePublishRpcServices(struct chreNanoappRpcService *services,
+                            size_t numServices);
+
+/**
+ * Retrieves metadata for a given host endpoint ID.
+ *
+ * This API will provide metadata regarding an endpoint associated with a
+ * host endpoint ID. The nanoapp should use this API to determine more
+ * information about a host endpoint that has sent a message to the nanoapp,
+ * after receiving a chreMessageFromHostData (which includes the endpoint ID).
+ *
+ * If the given host endpoint ID is not associated with a valid host (or if the
+ * client has disconnected from the Android or CHRE framework, i.e. no longer
+ * able to send messages to CHRE), this method will return false and info will
+ * not be populated.
+ *
+ * @param hostEndpointId The endpoint ID of the host to get info for.
+ * @param info The non-null pointer to where the metadata will be stored.
+ *
+ * @return true if info has been successfully populated.
+ *
+ * @since v1.6
+ */
+bool chreGetHostEndpointInfo(uint16_t hostEndpointId,
+                             struct chreHostEndpointInfo *info);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_EVENT_H_ */
+
diff --git a/chre_api/legacy/v1_8/chre/gnss.h b/chre_api/legacy/v1_8/chre/gnss.h
new file mode 100644
index 0000000..79a8f46
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/gnss.h
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_GNSS_H_
+#define _CHRE_GNSS_H_
+
+/**
+ * @file
+ * Global Navigation Satellite System (GNSS) API.
+ *
+ * These structures and definitions are based on the Android N GPS HAL.
+ * Refer to that header file (located at this path as of the time of this
+ * comment: hardware/libhardware/include/hardware/gps.h) and associated
+ * documentation for further details and explanations for these fields.
+ * References in comments like "(ref: GnssAccumulatedDeltaRangeState)" map to
+ * the relevant element in the GPS HAL where additional information can be
+ * found.
+ *
+ * In general, the parts of this API that are taken from the GPS HAL follow the
+ * naming conventions established in that interface rather than the CHRE API
+ * conventions, in order to avoid confusion and enable code re-use where
+ * applicable.
+ */
+
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <chre/common.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The set of flags that may be returned by chreGnssGetCapabilities()
+ * @defgroup CHRE_GNSS_CAPABILITIES
+ * @{
+ */
+
+//! A lack of flags indicates that GNSS is not supported in this CHRE
+#define CHRE_GNSS_CAPABILITIES_NONE          UINT32_C(0)
+
+//! GNSS position fixes are supported via chreGnssLocationSessionStartAsync()
+#define CHRE_GNSS_CAPABILITIES_LOCATION      UINT32_C(1 << 0)
+
+//! GNSS raw measurements are supported via
+//! chreGnssMeasurementSessionStartAsync()
+#define CHRE_GNSS_CAPABILITIES_MEASUREMENTS  UINT32_C(1 << 1)
+
+//! Location fixes supplied from chreGnssConfigurePassiveLocationListener()
+//! are tapped in at the GNSS engine level, so they include additional fixes
+//! such as those requested by the AP, and not just those requested by other
+//! nanoapps within CHRE (which is the case when this flag is not set)
+#define CHRE_GNSS_CAPABILITIES_GNSS_ENGINE_BASED_PASSIVE_LISTENER \
+                                             UINT32_C(1 << 2)
+
+/** @} */
+
+/**
+ * The current version of struct chreGnssDataEvent associated with this API
+ */
+#define CHRE_GNSS_DATA_EVENT_VERSION  UINT8_C(0)
+
+/**
+ * The maximum time the CHRE implementation is allowed to elapse before sending
+ * an event with the result of an asynchronous request, unless specified
+ * otherwise
+ */
+#define CHRE_GNSS_ASYNC_RESULT_TIMEOUT_NS  (5 * CHRE_NSEC_PER_SEC)
+
+/**
+ * Produce an event ID in the block of IDs reserved for GNSS
+ * @param offset  Index into GNSS event ID block; valid range [0,15]
+ */
+#define CHRE_GNSS_EVENT_ID(offset)  (CHRE_EVENT_GNSS_FIRST_EVENT + (offset))
+
+/**
+ * nanoappHandleEvent argument: struct chreAsyncResult
+ *
+ * Communicates the asynchronous result of a request to the GNSS API, such as
+ * starting a location session via chreGnssLocationSessionStartAsync(). The
+ * requestType field in chreAsyncResult is set to a value from enum
+ * chreGnssRequestType.
+ */
+#define CHRE_EVENT_GNSS_ASYNC_RESULT  CHRE_GNSS_EVENT_ID(0)
+
+/**
+ * nanoappHandleEvent argument: struct chreGnssLocationEvent
+ *
+ * Represents a location fix provided by the GNSS subsystem.
+ */
+#define CHRE_EVENT_GNSS_LOCATION      CHRE_GNSS_EVENT_ID(1)
+
+/**
+ * nanoappHandleEvent argument: struct chreGnssDataEvent
+ *
+ * Represents a set of GNSS measurements with associated clock data.
+ */
+#define CHRE_EVENT_GNSS_DATA          CHRE_GNSS_EVENT_ID(2)
+
+// NOTE: Do not add new events with ID > 15; only values 0-15 are reserved
+// (see chre/event.h)
+
+// Flags indicating the Accumulated Delta Range's states
+// (ref: GnssAccumulatedDeltaRangeState)
+#define CHRE_GNSS_ADR_STATE_UNKNOWN     UINT16_C(0)
+#define CHRE_GNSS_ADR_STATE_VALID       UINT16_C(1 << 0)
+#define CHRE_GNSS_ADR_STATE_RESET       UINT16_C(1 << 1)
+#define CHRE_GNSS_ADR_STATE_CYCLE_SLIP  UINT16_C(1 << 2)
+
+// Flags to indicate what fields in chreGnssClock are valid (ref: GnssClockFlags)
+#define CHRE_GNSS_CLOCK_HAS_LEAP_SECOND        UINT16_C(1 << 0)
+#define CHRE_GNSS_CLOCK_HAS_TIME_UNCERTAINTY   UINT16_C(1 << 1)
+#define CHRE_GNSS_CLOCK_HAS_FULL_BIAS          UINT16_C(1 << 2)
+#define CHRE_GNSS_CLOCK_HAS_BIAS               UINT16_C(1 << 3)
+#define CHRE_GNSS_CLOCK_HAS_BIAS_UNCERTAINTY   UINT16_C(1 << 4)
+#define CHRE_GNSS_CLOCK_HAS_DRIFT              UINT16_C(1 << 5)
+#define CHRE_GNSS_CLOCK_HAS_DRIFT_UNCERTAINTY  UINT16_C(1 << 6)
+
+// Flags to indicate which values are valid in a GpsLocation
+// (ref: GpsLocationFlags)
+#define CHRE_GPS_LOCATION_HAS_LAT_LONG           UINT16_C(1 << 0)
+#define CHRE_GPS_LOCATION_HAS_ALTITUDE           UINT16_C(1 << 1)
+#define CHRE_GPS_LOCATION_HAS_SPEED              UINT16_C(1 << 2)
+#define CHRE_GPS_LOCATION_HAS_BEARING            UINT16_C(1 << 3)
+#define CHRE_GPS_LOCATION_HAS_ACCURACY           UINT16_C(1 << 4)
+
+//! @since v1.3
+#define CHRE_GPS_LOCATION_HAS_ALTITUDE_ACCURACY  UINT16_C(1 << 5)
+//! @since v1.3
+#define CHRE_GPS_LOCATION_HAS_SPEED_ACCURACY     UINT16_C(1 << 6)
+//! @since v1.3
+#define CHRE_GPS_LOCATION_HAS_BEARING_ACCURACY   UINT16_C(1 << 7)
+
+/**
+ * The maximum number of instances of struct chreGnssMeasurement that may be
+ * included in a single struct chreGnssDataEvent.
+ *
+ * The value of this struct was increased from 64 to 128 in CHRE v1.5. For
+ * nanoapps targeting CHRE v1.4 or lower, the measurement_count will be capped
+ * at 64.
+ */
+#define CHRE_GNSS_MAX_MEASUREMENT  UINT8_C(128)
+#define CHRE_GNSS_MAX_MEASUREMENT_PRE_1_5  UINT8_C(64)
+
+// Flags indicating the GNSS measurement state (ref: GnssMeasurementState)
+#define CHRE_GNSS_MEASUREMENT_STATE_UNKNOWN                UINT16_C(0)
+#define CHRE_GNSS_MEASUREMENT_STATE_CODE_LOCK              UINT16_C(1 << 0)
+#define CHRE_GNSS_MEASUREMENT_STATE_BIT_SYNC               UINT16_C(1 << 1)
+#define CHRE_GNSS_MEASUREMENT_STATE_SUBFRAME_SYNC          UINT16_C(1 << 2)
+#define CHRE_GNSS_MEASUREMENT_STATE_TOW_DECODED            UINT16_C(1 << 3)
+#define CHRE_GNSS_MEASUREMENT_STATE_MSEC_AMBIGUOUS         UINT16_C(1 << 4)
+#define CHRE_GNSS_MEASUREMENT_STATE_SYMBOL_SYNC            UINT16_C(1 << 5)
+#define CHRE_GNSS_MEASUREMENT_STATE_GLO_STRING_SYNC        UINT16_C(1 << 6)
+#define CHRE_GNSS_MEASUREMENT_STATE_GLO_TOD_DECODED        UINT16_C(1 << 7)
+#define CHRE_GNSS_MEASUREMENT_STATE_BDS_D2_BIT_SYNC        UINT16_C(1 << 8)
+#define CHRE_GNSS_MEASUREMENT_STATE_BDS_D2_SUBFRAME_SYNC   UINT16_C(1 << 9)
+#define CHRE_GNSS_MEASUREMENT_STATE_GAL_E1BC_CODE_LOCK     UINT16_C(1 << 10)
+#define CHRE_GNSS_MEASUREMENT_STATE_GAL_E1C_2ND_CODE_LOCK  UINT16_C(1 << 11)
+#define CHRE_GNSS_MEASUREMENT_STATE_GAL_E1B_PAGE_SYNC      UINT16_C(1 << 12)
+#define CHRE_GNSS_MEASUREMENT_STATE_SBAS_SYNC              UINT16_C(1 << 13)
+
+#define CHRE_GNSS_MEASUREMENT_CARRIER_FREQUENCY_UNKNOWN    0.f
+
+/**
+ * Indicates a type of request made in this API. Used to populate the resultType
+ * field of struct chreAsyncResult sent with CHRE_EVENT_GNSS_ASYNC_RESULT.
+ */
+enum chreGnssRequestType {
+    CHRE_GNSS_REQUEST_TYPE_LOCATION_SESSION_START    = 1,
+    CHRE_GNSS_REQUEST_TYPE_LOCATION_SESSION_STOP     = 2,
+    CHRE_GNSS_REQUEST_TYPE_MEASUREMENT_SESSION_START = 3,
+    CHRE_GNSS_REQUEST_TYPE_MEASUREMENT_SESSION_STOP  = 4,
+};
+
+/**
+ * Constellation type associated with an SV
+ */
+enum chreGnssConstellationType {
+    CHRE_GNSS_CONSTELLATION_UNKNOWN = 0,
+    CHRE_GNSS_CONSTELLATION_GPS     = 1,
+    CHRE_GNSS_CONSTELLATION_SBAS    = 2,
+    CHRE_GNSS_CONSTELLATION_GLONASS = 3,
+    CHRE_GNSS_CONSTELLATION_QZSS    = 4,
+    CHRE_GNSS_CONSTELLATION_BEIDOU  = 5,
+    CHRE_GNSS_CONSTELLATION_GALILEO = 6,
+};
+
+/**
+ * Enumeration of available values for the chreGnssMeasurement multipath indicator
+ */
+enum chreGnssMultipathIndicator {
+    //! The indicator is not available or unknown
+    CHRE_GNSS_MULTIPATH_INDICATOR_UNKNOWN     = 0,
+    //! The measurement is indicated to be affected by multipath
+    CHRE_GNSS_MULTIPATH_INDICATOR_PRESENT     = 1,
+    //! The measurement is indicated to be not affected by multipath
+    CHRE_GNSS_MULTIPATH_INDICATOR_NOT_PRESENT = 2,
+};
+
+/**
+ * Represents an estimate of the GNSS clock time (see the Android GPS HAL for
+ * more detailed information)
+ */
+struct chreGnssClock {
+    //! The GNSS receiver hardware clock value in nanoseconds, including
+    //! uncertainty
+    int64_t time_ns;
+
+    //! The difference between hardware clock inside GNSS receiver and the
+    //! estimated GNSS time in nanoseconds; contains bias uncertainty
+    int64_t full_bias_ns;
+
+    //! Sub-nanosecond bias, adds to full_bias_ns
+    float bias_ns;
+
+    //! The clock's drift in nanoseconds per second
+    float drift_nsps;
+
+    //! 1-sigma uncertainty associated with the clock's bias in nanoseconds
+    float bias_uncertainty_ns;
+
+    //! 1-sigma uncertainty associated with the clock's drift in nanoseconds
+    //! per second
+    float drift_uncertainty_nsps;
+
+    //! While this number stays the same, timeNs should flow continuously
+    uint32_t hw_clock_discontinuity_count;
+
+    //! A set of flags indicating the validity of the fields in this data
+    //! structure (see GNSS_CLOCK_HAS_*)
+    uint16_t flags;
+
+    //! Reserved for future use; set to 0
+    uint8_t reserved[2];
+};
+
+/**
+ * Represents a GNSS measurement; contains raw and computed information (see the
+ * Android GPS HAL for more detailed information)
+ */
+struct chreGnssMeasurement {
+    //! Hardware time offset from time_ns for this measurement, in nanoseconds
+    int64_t time_offset_ns;
+
+    //! Accumulated delta range since the last channel reset in micro-meters
+    int64_t accumulated_delta_range_um;
+
+    //! Received GNSS satellite time at the time of measurement, in nanoseconds
+    int64_t received_sv_time_in_ns;
+
+    //! 1-sigma uncertainty of received GNSS satellite time, in nanoseconds
+    int64_t received_sv_time_uncertainty_in_ns;
+
+    //! Pseudorange rate at the timestamp in meters per second (uncorrected)
+    float pseudorange_rate_mps;
+
+    //! 1-sigma uncertainty of pseudorange rate in meters per second
+    float pseudorange_rate_uncertainty_mps;
+
+    //! 1-sigma uncertainty of the accumulated delta range in meters
+    float accumulated_delta_range_uncertainty_m;
+
+    //! Carrier-to-noise density in dB-Hz, in the range of [0, 63]
+    float c_n0_dbhz;
+
+    //! Signal to noise ratio (dB), power above observed noise at correlators
+    float snr_db;
+
+    //! Satellite sync state flags (GNSS_MEASUREMENT_STATE_*) - sets modulus for
+    //! received_sv_time_in_ns
+    uint16_t state;
+
+    //! Set of ADR state flags (GNSS_ADR_STATE_*)
+    uint16_t accumulated_delta_range_state;
+
+    //! Satellite vehicle ID number
+    int16_t svid;
+
+    //! Constellation of the given satellite vehicle
+    //! @see #chreGnssConstellationType
+    uint8_t constellation;
+
+    //! @see #chreGnssMultipathIndicator
+    uint8_t multipath_indicator;
+
+    //! Carrier frequency of the signal tracked in Hz.
+    //! For example, it can be the GPS central frequency for L1 = 1575.45 MHz,
+    //! or L2 = 1227.60 MHz, L5 = 1176.45 MHz, various GLO channels, etc.
+    //!
+    //! Set to CHRE_GNSS_MEASUREMENT_CARRIER_FREQUENCY_UNKNOWN if not reported.
+    //!
+    //! For an L1, L5 receiver tracking a satellite on L1 and L5 at the same
+    //! time, two chreGnssMeasurement structs must be reported for this same
+    //! satellite, in one of the measurement structs, all the values related to
+    //! L1 must be filled, and in the other all of the values related to L5
+    //! must be filled.
+    //! @since v1.4
+    float carrier_frequency_hz;
+};
+
+/**
+ * Data structure sent with events associated with CHRE_EVENT_GNSS_DATA, enabled
+ * via chreGnssMeasurementSessionStartAsync()
+ */
+struct chreGnssDataEvent {
+    //! Indicates the version of the structure, for compatibility purposes.
+    //! Clients do not normally need to worry about this field; the CHRE
+    //! implementation guarantees that it only sends the client the structure
+    //! version it expects.
+    uint8_t version;
+
+    //! Number of chreGnssMeasurement entries included in this event. Must be in
+    //! the range [0, CHRE_GNSS_MAX_MEASUREMENT]
+    uint8_t measurement_count;
+
+    //! Reserved for future use; set to 0
+    uint8_t reserved[6];
+
+    struct chreGnssClock clock;
+
+    //! Pointer to an array containing measurement_count measurements
+    const struct chreGnssMeasurement *measurements;
+};
+
+/**
+ * Data structure sent with events of type CHRE_EVENT_GNSS_LOCATION, enabled via
+ * chreGnssLocationSessionStartAsync(). This is modeled after GpsLocation in the
+ * GPS HAL, but does not use the double data type.
+ */
+struct chreGnssLocationEvent {
+    //! UTC timestamp for location fix in milliseconds since January 1, 1970
+    uint64_t timestamp;
+
+    //! Fixed point latitude, degrees times 10^7 (roughly centimeter resolution)
+    int32_t latitude_deg_e7;
+
+    //! Fixed point longitude, degrees times 10^7 (roughly centimeter
+    //! resolution)
+    int32_t longitude_deg_e7;
+
+    //! Altitude in meters above the WGS 84 reference ellipsoid
+    float altitude;
+
+    //! Horizontal speed in meters per second
+    float speed;
+
+    //! Clockwise angle between north and current heading, in degrees; range
+    //! [0, 360)
+    float bearing;
+
+    //! Expected horizontal accuracy in meters such that a circle with a radius
+    //! of length 'accuracy' from the latitude and longitude has a 68%
+    //! probability of including the true location.
+    float accuracy;
+
+    //! A set of flags indicating which fields in this structure are valid.
+    //! If any fields are not available, the flag must not be set and the field
+    //! must be initialized to 0.
+    //! @see #GpsLocationFlags
+    uint16_t flags;
+
+    //! Reserved for future use; set to 0
+    //! @since v1.3
+    uint8_t reserved[2];
+
+    //! Expected vertical accuracy in meters such that a range of
+    //! 2 * altitude_accuracy centered around altitude has a 68% probability of
+    //! including the true altitude.
+    //! @since v1.3
+    float altitude_accuracy;
+
+    //! Expected speed accuracy in meters per second such that a range of
+    //! 2 * speed_accuracy centered around speed has a 68% probability of
+    //! including the true speed.
+    //! @since v1.3
+    float speed_accuracy;
+
+    //! Expected bearing accuracy in degrees such that a range of
+    //! 2 * bearing_accuracy centered around bearing has a 68% probability of
+    //! including the true bearing.
+    //! @since v1.3
+    float bearing_accuracy;
+};
+
+
+/**
+ * Retrieves a set of flags indicating the GNSS features supported by the
+ * current CHRE implementation. The value returned by this function must be
+ * consistent for the entire duration of the Nanoapp's execution.
+ *
+ * The client must allow for more flags to be set in this response than it knows
+ * about, for example if the implementation supports a newer version of the API
+ * than the client was compiled against.
+ *
+ * @return A bitmask with zero or more CHRE_GNSS_CAPABILITIES_* flags set
+ *
+ * @since v1.1
+ */
+uint32_t chreGnssGetCapabilities(void);
+
+/**
+ * Nanoapps must define CHRE_NANOAPP_USES_GNSS somewhere in their build
+ * system (e.g. Makefile) if the nanoapp needs to use the following GNSS APIs.
+ * In addition to allowing access to these APIs, defining this macro will also
+ * ensure CHRE enforces that all host clients this nanoapp talks to have the
+ * required Android permissions needed to listen to GNSS data by adding metadata
+ * to the nanoapp.
+ */
+#if defined(CHRE_NANOAPP_USES_GNSS) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+/**
+ * Initiates a GNSS positioning session, or changes the requested interval of an
+ * existing session. If starting or modifying the session was successful, then
+ * the GNSS engine will work on determining the device's position.
+ *
+ * This result of this request is delivered asynchronously via an event of type
+ * CHRE_EVENT_GNSS_ASYNC_RESULT. Refer to the note in {@link #chreAsyncResult}
+ * for more details. If the "Location" setting is disabled at the Android level,
+ * the CHRE implementation is expected to return a result with
+ * CHRE_ERROR_FUNCTION_DISABLED.
+ *
+ * If chreGnssGetCapabilities() returns a value that does not have the
+ * CHRE_GNSS_CAPABILITIES_LOCATION flag set, then this method will return false.
+ *
+ * @param minIntervalMs The desired minimum interval between location fixes
+ *        delivered to the client via CHRE_EVENT_GNSS_LOCATION, in milliseconds.
+ *        The requesting client must allow for fixes to be delivered at shorter
+ *        or longer interval than requested. For example, adverse RF conditions
+ *        may result in fixes arriving at a longer interval, etc.
+ * @param minTimeToNextFixMs The desired minimum time to the next location fix.
+ *        If this is 0, the GNSS engine should start working on the next fix
+ *        immediately. If greater than 0, the GNSS engine should not spend
+ *        measurable power to produce a location fix until this amount of time
+ *        has elapsed.
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ *
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires GNSS permission
+ */
+bool chreGnssLocationSessionStartAsync(uint32_t minIntervalMs,
+                                       uint32_t minTimeToNextFixMs,
+                                       const void *cookie);
+
+/**
+ * Terminates an existing GNSS positioning session. If no positioning session
+ * is active at the time of this request, it is treated as if an active session
+ * was successfully ended.
+ *
+ * This result of this request is delivered asynchronously via an event of type
+ * CHRE_EVENT_GNSS_ASYNC_RESULT. Refer to the note in {@link #chreAsyncResult}
+ * for more details.
+ *
+ * After CHRE_EVENT_GNSS_ASYNC_RESULT is delivered to the client, no more
+ * CHRE_EVENT_GNSS_LOCATION events will be delievered until a new location
+ * session is started.
+ *
+ * If chreGnssGetCapabilities() returns a value that does not have the
+ * CHRE_GNSS_CAPABILITIES_LOCATION flag set, then this method will return false.
+ *
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ *
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires GNSS permission
+ */
+bool chreGnssLocationSessionStopAsync(const void *cookie);
+
+/**
+ * Initiates a request to receive raw GNSS measurements. A GNSS measurement
+ * session can exist independently of location sessions. In other words, a
+ * Nanoapp is able to receive measurements at its requested interval both with
+ * and without an active location session.
+ *
+ * This result of this request is delivered asynchronously via an event of type
+ * CHRE_EVENT_GNSS_ASYNC_RESULT. Refer to the note in {@link #chreAsyncResult}
+ * for more details. If the "Location" setting is disabled at the Android level,
+ * the CHRE implementation is expected to return a result with
+ * CHRE_ERROR_FUNCTION_DISABLED.
+ *
+ * If chreGnssGetCapabilities() returns a value that does not have the
+ * CHRE_GNSS_CAPABILITIES_MEASUREMENTS flag set, then this method will return
+ * false.
+ *
+ * @param minIntervalMs The desired minimum interval between measurement reports
+ *        delivered via CHRE_EVENT_GNSS_DATA. When requested at 1000ms or
+ *        faster, and GNSS measurements are tracked, device should report
+ *        measurements as fast as requested, and shall report no slower than
+ *        once every 1000ms, on average.
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ *
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires GNSS permission
+ */
+bool chreGnssMeasurementSessionStartAsync(uint32_t minIntervalMs,
+                                          const void *cookie);
+
+/**
+ * Terminates an existing raw GNSS measurement session. If no measurement
+ * session is active at the time of this request, it is treated as if an active
+ * session was successfully ended.
+ *
+ * This result of this request is delivered asynchronously via an event of type
+ * CHRE_EVENT_GNSS_ASYNC_RESULT. Refer to the note in {@link #chreAsyncResult}
+ * for more details.
+ *
+ * If chreGnssGetCapabilities() returns a value that does not have the
+ * CHRE_GNSS_CAPABILITIES_MEASUREMENTS flag set, then this method will return
+ * false.
+ *
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ *
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires GNSS permission
+ */
+bool chreGnssMeasurementSessionStopAsync(const void *cookie);
+
+/**
+ * Controls whether this nanoapp will passively receive GNSS-based location
+ * fixes produced as a result of location sessions initiated by other entities.
+ * This function allows a nanoapp to opportunistically receive location fixes
+ * via CHRE_EVENT_GNSS_LOCATION events without imposing additional power cost,
+ * though with no guarantees as to when or how often those events will arrive.
+ * There will be no duplication of events if a passive location listener and
+ * location session are enabled in parallel.
+ *
+ * Enabling passive location listening is not required to receive events for an
+ * active location session started via chreGnssLocationSessionStartAsync(). This
+ * setting is independent of the active location session, so modifying one does
+ * not have an effect on the other.
+ *
+ * If chreGnssGetCapabilities() returns a value that does not have the
+ * CHRE_GNSS_CAPABILITIES_LOCATION flag set or the value returned by
+ * chreGetApiVersion() is less than CHRE_API_VERSION_1_2, then this method will
+ * return false.
+ *
+ * If chreGnssGetCapabilities() includes
+ * CHRE_GNSS_CAPABILITIES_GNSS_ENGINE_BASED_PASSIVE_LISTENER, the passive
+ * registration is recorded at the GNSS engine level, so events include fixes
+ * requested by the applications processor and potentially other non-CHRE
+ * clients. If this flag is not set, then only fixes requested by other nanoapps
+ * within CHRE are provided.
+ *
+ * @param enable true to receive opportunistic location fixes, false to disable
+ *
+ * @return true if the configuration was processed successfully, false on error
+ *     or if this feature is not supported
+ *
+ * @since v1.2
+ * @note Requires GNSS permission
+ */
+bool chreGnssConfigurePassiveLocationListener(bool enable);
+
+#else  /* defined(CHRE_NANOAPP_USES_GNSS) || !defined(CHRE_IS_NANOAPP_BUILD) */
+#define CHRE_GNSS_PERM_ERROR_STRING \
+    "CHRE_NANOAPP_USES_GNSS must be defined when building this nanoapp in " \
+    "order to refer to "
+#define chreGnssLocationSessionStartAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_GNSS_PERM_ERROR_STRING \
+                     "chreGnssLocationSessionStartAsync")
+#define chreGnssLocationSessionStopAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_GNSS_PERM_ERROR_STRING \
+                     "chreGnssLocationSessionStopAsync")
+#define chreGnssMeasurementSessionStartAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_GNSS_PERM_ERROR_STRING \
+                     "chreGnssMeasurementSessionStartAsync")
+#define chreGnssMeasurementSessionStopAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_GNSS_PERM_ERROR_STRING \
+                     "chreGnssMeasurementSessionStopAsync")
+#define chreGnssConfigurePassiveLocationListener(...) \
+    CHRE_BUILD_ERROR(CHRE_GNSS_PERM_ERROR_STRING \
+                     "chreGnssConfigurePassiveLocationListener")
+#endif  /* defined(CHRE_NANOAPP_USES_GNSS) || !defined(CHRE_IS_NANOAPP_BUILD) */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_GNSS_H_ */
diff --git a/chre_api/legacy/v1_8/chre/nanoapp.h b/chre_api/legacy/v1_8/chre/nanoapp.h
new file mode 100644
index 0000000..da199ee
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/nanoapp.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_NANOAPP_H_
+#define _CHRE_NANOAPP_H_
+
+/**
+ * @file
+ * Methods in the Context Hub Runtime Environment which must be implemented
+ * by the nanoapp.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Method invoked by the CHRE when loading the nanoapp.
+ *
+ * Every CHRE method is legal to call from this method.
+ *
+ * @return  'true' if the nanoapp successfully started.  'false' if the nanoapp
+ *     failed to properly initialize itself (for example, could not obtain
+ *     sufficient memory from the heap).  If this method returns 'false', the
+ *     nanoapp will be unloaded by the CHRE (and nanoappEnd will
+ *     _not_ be invoked in that case).
+ * @see nanoappEnd
+ */
+bool nanoappStart(void);
+
+/**
+ * Method invoked by the CHRE when there is an event for this nanoapp.
+ *
+ * Every CHRE method is legal to call from this method.
+ *
+ * @param senderInstanceId  The Instance ID for the source of this event.
+ *     Note that this may be CHRE_INSTANCE_ID, indicating that the event
+ *     was generated by the CHRE.
+ * @param eventType  The event type.  This might be one of the CHRE_EVENT_*
+ *     types defined in this API.  But it might also be a user-defined event.
+ * @param eventData  The associated data, if any, for this specific type of
+ *     event.  From the nanoapp's perspective, this eventData's lifetime ends
+ *     when this method returns, and thus any data the nanoapp wishes to
+ *     retain must be copied.  Note that interpretation of event data is
+ *     given by the event type, and for some events may not be a valid
+ *     pointer.  See documentation of the specific CHRE_EVENT_* types for how to
+ *     interpret this data for those.  Note that for user events, you will
+ *     need to establish what this data means.
+ */
+void nanoappHandleEvent(uint32_t senderInstanceId, uint16_t eventType,
+                        const void *eventData);
+
+/**
+ * Method invoked by the CHRE when unloading the nanoapp.
+ *
+ * It is not valid to attempt to send events or messages, or to invoke functions
+ * which will generate events to this app, within the nanoapp implementation of
+ * this function.  That means it is illegal for the nanoapp invoke any of the
+ * following:
+ *
+ * - chreSendEvent()
+ * - chreSendMessageToHost()
+ * - chreSensorConfigure()
+ * - chreSensorConfigureModeOnly()
+ * - chreTimerSet()
+ * - etc.
+ *
+ * @see nanoappStart
+ */
+void nanoappEnd(void);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_NANOAPP_H_ */
diff --git a/chre_api/legacy/v1_8/chre/re.h b/chre_api/legacy/v1_8/chre/re.h
new file mode 100644
index 0000000..20a69b6
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/re.h
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_RE_H_
+#define _CHRE_RE_H_
+
+/**
+ * @file
+ * Some of the core Runtime Environment utilities of the Context Hub
+ * Runtime Environment.
+ *
+ * This includes functions for memory allocation, logging, and timers.
+ */
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <chre/toolchain.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The instance ID for the CHRE.
+ *
+ * This ID is used to identify events generated by the CHRE (as
+ * opposed to events generated by another nanoapp).
+ */
+#define CHRE_INSTANCE_ID  UINT32_C(0)
+
+/**
+ * A timer ID representing an invalid timer.
+ *
+ * This valid is returned by chreTimerSet() if a timer cannot be
+ * started.
+ */
+#define CHRE_TIMER_INVALID  UINT32_C(-1)
+
+
+/**
+ * The maximum size, in characters including null terminator, guaranteed for
+ * logging debug data with one call of chreDebugDumpLog() without getting
+ * truncated.
+ *
+ * @see chreDebugDumpLog
+ * @since v1.4
+ */
+#define CHRE_DEBUG_DUMP_MINIMUM_MAX_SIZE 1000
+
+/**
+ * Logging levels used to indicate severity level of logging messages.
+ *
+ * CHRE_LOG_ERROR: Something fatal has happened, i.e. something that will have
+ *     user-visible consequences and won't be recoverable without explicitly
+ *     deleting some data, uninstalling applications, wiping the data
+ *     partitions or reflashing the entire phone (or worse).
+ * CHRE_LOG_WARN: Something that will have user-visible consequences but is
+ *     likely to be recoverable without data loss by performing some explicit
+ *     action, ranging from waiting or restarting an app all the way to
+ *     re-downloading a new version of an application or rebooting the device.
+ * CHRE_LOG_INFO: Something interesting to most people happened, i.e. when a
+ *     situation is detected that is likely to have widespread impact, though
+ *     isn't necessarily an error.
+ * CHRE_LOG_DEBUG: Used to further note what is happening on the device that
+ *     could be relevant to investigate and debug unexpected behaviors. You
+ *     should log only what is needed to gather enough information about what
+ *     is going on about your component.
+ *
+ * There is currently no API to turn on/off logging by level, but we anticipate
+ * adding such in future releases.
+ *
+ * @see chreLog
+ */
+enum chreLogLevel {
+    CHRE_LOG_ERROR,
+    CHRE_LOG_WARN,
+    CHRE_LOG_INFO,
+    CHRE_LOG_DEBUG
+};
+
+
+/**
+ * Get the application ID.
+ *
+ * The application ID is set by the loader of the nanoapp.  This is not
+ * assured to be unique among all nanoapps running in the system.
+ *
+ * @return The application ID.
+ */
+uint64_t chreGetAppId(void);
+
+/**
+ * Get the instance ID.
+ *
+ * The instance ID is the CHRE handle to this nanoapp.  This is assured
+ * to be unique among all nanoapps running in the system, and to be
+ * different from the CHRE_INSTANCE_ID.  This is the ID used to communicate
+ * between nanoapps.
+ *
+ * @return The instance ID
+ */
+uint32_t chreGetInstanceId(void);
+
+/**
+ * A method for logging information about the system.
+ *
+ * The chreLog logging activity alone must not cause host wake-ups. For
+ * example, logs could be buffered in internal memory when the host is asleep,
+ * and delivered when appropriate (e.g. the host wakes up). If done this way,
+ * the internal buffer is recommended to be large enough (at least a few KB), so
+ * that multiple messages can be buffered. When these logs are sent to the host,
+ * they are strongly recommended to be made visible under the tag 'CHRE' in
+ * logcat - a future version of the CHRE API may make this a hard requirement.
+ *
+ * A log entry can have a variety of levels (@see LogLevel).  This function
+ * allows a variable number of arguments, in a printf-style format.
+ *
+ * A nanoapp needs to be able to rely upon consistent printf format
+ * recognition across any platform, and thus we establish formats which
+ * are required to be handled by every CHRE implementation.  Some of the
+ * integral formats may seem obscure, but this API heavily uses types like
+ * uint32_t and uint16_t.  The platform independent macros for those printf
+ * formats, like PRId32 or PRIx16, end up using some of these "obscure"
+ * formats on some platforms, and thus are required.
+ *
+ * For the initial N release, our emphasis is on correctly getting information
+ * into the log, and minimizing the requirements for CHRE implementations
+ * beyond that.  We're not as concerned about how the information is visually
+ * displayed.  As a result, there are a number of format sub-specifiers which
+ * are "OPTIONAL" for the N implementation.  "OPTIONAL" in this context means
+ * that a CHRE implementation is allowed to essentially ignore the specifier,
+ * but it must understand the specifier enough in order to properly skip it.
+ *
+ * For a nanoapp author, an OPTIONAL format means you might not get exactly
+ * what you want on every CHRE implementation, but you will always get
+ * something valid.
+ *
+ * To be clearer, here's an example with the OPTIONAL 0-padding for integers
+ * for different hypothetical CHRE implementations.
+ * Compliant, chose to implement OPTIONAL format:
+ *   chreLog(level, "%04x", 20) ==> "0014"
+ * Compliant, chose not to implement OPTIONAL format:
+ *   chreLog(level, "%04x", 20) ==> "14"
+ * Non-compliant, discarded format because the '0' was assumed to be incorrect:
+ *   chreLog(level, "%04x", 20) ==> ""
+ *
+ * Note that some of the OPTIONAL specifiers will probably become
+ * required in future APIs.
+ *
+ * We also have NOT_SUPPORTED specifiers.  Nanoapp authors should not use any
+ * NOT_SUPPORTED specifiers, as unexpected things could happen on any given
+ * CHRE implementation.  A CHRE implementation is allowed to support this
+ * (for example, when using shared code which already supports this), but
+ * nanoapp authors need to avoid these.
+ *
+ * Unless specifically noted as OPTIONAL or NOT_SUPPORTED, format
+ * (sub-)specifiers listed below are required.
+ *
+ * OPTIONAL format sub-specifiers:
+ * - '-' (left-justify within the given field width)
+ * - '+' (precede the result with a '+' sign if it is positive)
+ * - ' ' (precede the result with a blank space if no sign is going to be
+ *        output)
+ * - '#' (For 'o', 'x' or 'X', precede output with "0", "0x" or "0X",
+ *        respectively.  For floating point, unconditionally output a decimal
+ *        point.)
+ * - '0' (left pad the number with zeroes instead of spaces when <width>
+ *        needs padding)
+ * - <width> (A number representing the minimum number of characters to be
+ *            output, left-padding with blank spaces if needed to meet the
+ *            minimum)
+ * - '.'<precision> (A number which has different meaning depending on context.)
+ *    - Integer context: Minimum number of digits to output, padding with
+ *          leading zeros if needed to meet the minimum.
+ *    - 'f' context: Number of digits to output after the decimal
+ *          point (to the right of it).
+ *    - 's' context: Maximum number of characters to output.
+ *
+ * Integral format specifiers:
+ * - 'd' (signed)
+ * - 'u' (unsigned)
+ * - 'o' (octal)
+ * - 'x' (hexadecimal, lower case)
+ * - 'X' (hexadecimal, upper case)
+ *
+ * Integral format sub-specifiers (as prefixes to an above integral format):
+ * - 'hh' (char)
+ * - 'h' (short)
+ * - 'l' (long)
+ * - 'll' (long long)
+ * - 'z' (size_t)
+ * - 't' (ptrdiff_t)
+ *
+ * Other format specifiers:
+ * - 'f' (floating point)
+ * - 'c' (character)
+ * - 's' (character string, terminated by '\0')
+ * - 'p' (pointer)
+ * - '%' (escaping the percent sign (i.e. "%%" becomes "%"))
+ *
+ * NOT_SUPPORTED specifiers:
+ * - 'n' (output nothing, but fill in a given pointer with the number
+ *        of characters written so far)
+ * - '*' (indicates that the width/precision value comes from one of the
+ *        arguments to the function)
+ * - 'e', 'E' (scientific notation output)
+ * - 'g', 'G' (Shortest floating point representation)
+ *
+ * @param level  The severity level for this message.
+ * @param formatStr  Either the entirety of the message, or a printf-style
+ *     format string of the format documented above.
+ * @param ...  A variable number of arguments necessary for the given
+ *     'formatStr' (there may be no additional arguments for some 'formatStr's).
+ */
+CHRE_PRINTF_ATTR(2, 3)
+void chreLog(enum chreLogLevel level, const char *formatStr, ...);
+
+/**
+ * Get the system time.
+ *
+ * This returns a time in nanoseconds in reference to some arbitrary
+ * time in the past.  This method is only useful for determining timing
+ * between events on the system, and is not useful for determining
+ * any sort of absolute time.
+ *
+ * This value must always increase (and must never roll over).  This
+ * value has no meaning across CHRE reboots.
+ *
+ * @return The system time, in nanoseconds.
+ */
+uint64_t chreGetTime(void);
+
+/**
+ * Retrieves CHRE's current estimated offset between the local CHRE clock
+ * exposed in chreGetTime(), and the host-side clock exposed in the Android API
+ * SystemClock.elapsedRealtimeNanos().  This offset is formed as host time minus
+ * CHRE time, so that it can be added to the value returned by chreGetTime() to
+ * determine the current estimate of the host time.
+ *
+ * A call to this function must not require waking up the host and should return
+ * quickly.
+ *
+ * This function must always return a valid value from the earliest point that
+ * it can be called by a nanoapp.  In other words, it is not valid to return
+ * some fixed/invalid value while waiting for the initial offset estimate to be
+ * determined - this initial offset must be ready before nanoapps are started.
+ *
+ * @return An estimate of the offset between CHRE's time returned in
+ *     chreGetTime() and the time on the host given in the Android API
+ *     SystemClock.elapsedRealtimeNanos(), accurate to within +/- 10
+ *     milliseconds, such that adding this offset to chreGetTime() produces the
+ *     estimated current time on the host.  This value may change over time to
+ *     account for drift, etc., so multiple calls to this API may produce
+ *     different results.
+ *
+ * @since v1.1
+ */
+int64_t chreGetEstimatedHostTimeOffset(void);
+
+/**
+ * Convenience function to retrieve CHRE's estimate of the current time on the
+ * host, corresponding to the Android API SystemClock.elapsedRealtimeNanos().
+ *
+ * @return An estimate of the current time on the host, accurate to within
+ *     +/- 10 milliseconds.  This estimate is *not* guaranteed to be
+ *     monotonically increasing, and may move backwards as a result of receiving
+ *     new information from the host.
+ *
+ * @since v1.1
+ */
+static inline uint64_t chreGetEstimatedHostTime(void) {
+    int64_t offset = chreGetEstimatedHostTimeOffset();
+    uint64_t time = chreGetTime();
+
+    // Just casting time to int64_t and adding the (potentially negative) offset
+    // should be OK under most conditions, but this way avoids issues if
+    // time >= 2^63, which is technically allowed since we don't specify a start
+    // value for chreGetTime(), though one would assume 0 is roughly boot time.
+    if (offset >= 0) {
+        time += (uint64_t) offset;
+    } else {
+        // Assuming chreGetEstimatedHostTimeOffset() is implemented properly,
+        // this will never underflow, because offset = hostTime - chreTime,
+        // and both times are monotonically increasing (e.g. when determining
+        // the offset, if hostTime is 0 and chreTime is 100 we'll have
+        // offset = -100, but chreGetTime() will always return >= 100 after that
+        // point).
+        time -= (uint64_t) (offset * -1);
+    }
+
+    return time;
+}
+
+/**
+ * Set a timer.
+ *
+ * When the timer fires, nanoappHandleEvent will be invoked with
+ * CHRE_EVENT_TIMER and with the given 'cookie'.
+ *
+ * A CHRE implementation is required to provide at least 32
+ * timers.  However, there's no assurance there will be any available
+ * for any given nanoapp (if it's loaded late, etc).
+ *
+ * @param duration  Time, in nanoseconds, before the timer fires.
+ * @param cookie  Argument that will be sent to nanoappHandleEvent upon the
+ *     timer firing.  This is allowed to be NULL and does not need to be
+ *     a valid pointer (assuming the nanoappHandleEvent code is expecting such).
+ * @param oneShot  If true, the timer will just fire once.  If false, the
+ *     timer will continue to refire every 'duration', until this timer is
+ *     canceled (@see chreTimerCancel).
+ *
+ * @return  The timer ID.  If the system is unable to set a timer
+ *     (no more available timers, etc.) then CHRE_TIMER_INVALID will
+ *     be returned.
+ *
+ * @see nanoappHandleEvent
+ */
+uint32_t chreTimerSet(uint64_t duration, const void *cookie, bool oneShot);
+
+/**
+ * Cancel a timer.
+ *
+ * After this method returns, the CHRE assures there will be no more
+ * events sent from this timer, and any enqueued events from this timer
+ * will need to be evicted from the queue by the CHRE.
+ *
+ * @param timerId  A timer ID obtained by this nanoapp via chreTimerSet().
+ * @return true if the timer was cancelled, false otherwise.  We may
+ *     fail to cancel the timer if it's a one shot which (just) fired,
+ *     or if the given timer ID is not owned by the calling app.
+ */
+bool chreTimerCancel(uint32_t timerId);
+
+/**
+ * Terminate this nanoapp.
+ *
+ * This takes effect immediately.
+ *
+ * The CHRE will no longer execute this nanoapp.  The CHRE will not invoke
+ * nanoappEnd(), nor will it call any memory free callbacks in the nanoapp.
+ *
+ * The CHRE will unload/evict this nanoapp's code.
+ *
+ * @param abortCode  A value indicating the reason for aborting.  (Note that
+ *    in this version of the API, there is no way for anyone to access this
+ *    code, but future APIs may expose it.)
+ * @return Never.  This method does not return, as the CHRE stops nanoapp
+ *    execution immediately.
+ */
+void chreAbort(uint32_t abortCode);
+
+/**
+ * Allocate a given number of bytes from the system heap.
+ *
+ * The nanoapp is required to free this memory via chreHeapFree() prior to
+ * the nanoapp ending.
+ *
+ * While the CHRE implementation is required to free up heap resources of
+ * a nanoapp when unloading it, future requirements and tests focused on
+ * nanoapps themselves may check for memory leaks, and will require nanoapps
+ * to properly manage their heap resources.
+ *
+ * @param bytes  The number of bytes requested.
+ * @return  A pointer to 'bytes' contiguous bytes of heap memory, or NULL
+ *     if the allocation could not be performed.  This pointer must be suitably
+ *     aligned for any kind of variable.
+ *
+ * @see chreHeapFree.
+ */
+void *chreHeapAlloc(uint32_t bytes);
+
+/**
+ * Free a heap allocation.
+ *
+ * This allocation must be from a value returned from a chreHeapAlloc() call
+ * made by this nanoapp.  In other words, it is illegal to free memory
+ * allocated by another nanoapp (or the CHRE).
+ *
+ * @param ptr  'ptr' is required to be a value returned from chreHeapAlloc().
+ *     Note that since chreHeapAlloc can return NULL, CHRE
+ *     implementations must safely handle 'ptr' being NULL.
+ *
+ * @see chreHeapAlloc.
+ */
+void chreHeapFree(void *ptr);
+
+/**
+ * Logs the nanoapp's debug data into debug dumps.
+ *
+ * A debug dump is a string representation of information that can be used to
+ * diagnose and debug issues. While chreLog() is useful for logging events as
+ * they happen, the debug dump is a complementary function typically used to
+ * output a snapshot of a nanoapp's state, history, vital statistics, etc. The
+ * CHRE framework is required to pass this information to the debug method in
+ * the Context Hub HAL, where it can be captured in Android bugreports, etc.
+ *
+ * This function must only be called while handling CHRE_DEBUG_DUMP_EVENT,
+ * otherwise it will have no effect. A nanoapp can call this function multiple
+ * times while handling the event. If the resulting formatted string from a
+ * single call to this function is longer than CHRE_DEBUG_DUMP_MINIMUM_MAX_SIZE
+ * characters, it may get truncated.
+ *
+ * @param formatStr A printf-style format string of the format documented in
+ *     chreLog().
+ * @param ... A variable number of arguments necessary for the given 'formatStr'
+ *     (there may be no additional arguments for some 'formatStr's).
+ *
+ * @see chreConfigureDebugDumpEvent
+ * @see chreLog
+ *
+ * @since v1.4
+ */
+CHRE_PRINTF_ATTR(1, 2)
+void chreDebugDumpLog(const char *formatStr, ...);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_RE_H_ */
+
diff --git a/chre_api/legacy/v1_8/chre/sensor.h b/chre_api/legacy/v1_8/chre/sensor.h
new file mode 100644
index 0000000..4166374
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/sensor.h
@@ -0,0 +1,1119 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_SENSOR_H_
+#define _CHRE_SENSOR_H_
+
+/**
+ * @file
+ * API dealing with sensor interaction in the Context Hub Runtime
+ * Environment.
+ *
+ * This includes the definition of our sensor types and the ability to
+ * configure them for receiving events.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <chre/common.h>
+#include <chre/event.h>
+#include <chre/sensor_types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/**
+ * Base value for all of the data events for sensors.
+ *
+ * The value for a data event FOO is
+ * CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_FOO
+ *
+ * This allows for easy mapping, and also explains why there are gaps
+ * in our values since we don't have all possible sensor types assigned.
+ */
+#define CHRE_EVENT_SENSOR_DATA_EVENT_BASE  CHRE_EVENT_SENSOR_FIRST_EVENT
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x', 'y', and 'z' fields within
+ * 'readings', or by the 3D array 'v' (v[0] == x; v[1] == y; v[2] == z).
+ *
+ * All values are in SI units (m/s^2) and measure the acceleration applied to
+ * the device.
+ */
+#define CHRE_EVENT_SENSOR_ACCELEROMETER_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_ACCELEROMETER)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorOccurrenceData
+ *
+ * Since this is a one-shot sensor, after this event is delivered to the
+ * nanoapp, the sensor automatically goes into DONE mode.  Sensors of this
+ * type must be configured with a ONE_SHOT mode.
+ */
+#define CHRE_EVENT_SENSOR_INSTANT_MOTION_DETECT_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_INSTANT_MOTION_DETECT)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorOccurrenceData
+ *
+ * Since this is a one-shot sensor, after this event is delivered to the
+ * nanoapp, the sensor automatically goes into DONE mode.  Sensors of this
+ * type must be configured with a ONE_SHOT mode.
+ */
+#define CHRE_EVENT_SENSOR_STATIONARY_DETECT_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_STATIONARY_DETECT)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x', 'y', and 'z' fields within
+ * 'readings', or by the 3D array 'v' (v[0] == x; v[1] == y; v[2] == z).
+ *
+ * All values are in radians/second and measure the rate of rotation
+ * around the X, Y and Z axis.
+ */
+#define CHRE_EVENT_SENSOR_GYROSCOPE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_GYROSCOPE)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x', 'y', and 'z' fields within
+ * 'readings', or by the 3D array 'v' (v[0] == x; v[1] == y; v[2] == z).
+ *
+ * All values are in micro-Tesla (uT) and measure the geomagnetic
+ * field in the X, Y and Z axis.
+ */
+#define CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFloatData
+ *
+ * The data can be interpreted using the 'pressure' field within 'readings'.
+ * This value is in hectopascals (hPa).
+ */
+#define CHRE_EVENT_SENSOR_PRESSURE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_PRESSURE)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFloatData
+ *
+ * The data can be interpreted using the 'light' field within 'readings'.
+ * This value is in SI lux units.
+ */
+#define CHRE_EVENT_SENSOR_LIGHT_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_LIGHT)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorByteData
+ *
+ * The data is interpreted from the following fields in 'readings':
+ * o 'isNear': If set to 1, we are nearby (on the order of centimeters);
+ *       if set to 0, we are far. The meaning of near/far in this field must be
+ *       consistent with the Android definition.
+ * o 'invalid': If set to 1, this is not a valid reading of this data.
+ *       As of CHRE API v1.2, this field is deprecated and must always be set to
+ *       0.  If an invalid reading is generated by the sensor hardware, it must
+ *       be dropped and not delivered to any nanoapp.
+ *
+ * In prior versions of the CHRE API, there can be an invalid event generated
+ * upon configuring this sensor.  Thus, the 'invalid' field must be checked on
+ * the first event before interpreting 'isNear'.
+ */
+#define CHRE_EVENT_SENSOR_PROXIMITY_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_PROXIMITY)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorOccurrenceData
+ *
+ * This data is generated every time a step is taken by the user.
+ *
+ * This is backed by the same algorithm that feeds Android's
+ * SENSOR_TYPE_STEP_DETECTOR, and therefore sacrifices some accuracy to target
+ * an update latency of under 2 seconds.
+ *
+ * @since v1.3
+ */
+#define CHRE_EVENT_SENSOR_STEP_DETECT_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_STEP_DETECT)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorUint64Data
+ *
+ * The value of the data is the cumulative number of steps taken by the user
+ * since the last reboot while the sensor is active. This data is generated
+ * every time a step is taken by the user.
+ *
+ * This is backed by the same algorithm that feeds Android's
+ * SENSOR_TYPE_STEP_COUNTER, and therefore targets high accuracy with under
+ * 10 seconds of update latency.
+ *
+ * @since v1.5
+ */
+#define CHRE_EVENT_SENSOR_STEP_COUNTER_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_STEP_COUNTER)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFloatData
+ *
+ * The value of the data is the measured hinge angle between 0 and 360 degrees
+ * inclusive.
+ *
+ * This is backed by the same algorithm that feeds Android's
+ * SENSOR_TYPE_HINGE_ANGLE.
+ *
+ * @since v1.5
+ */
+#define CHRE_EVENT_SENSOR_HINGE_ANGLE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_HINGE_ANGLE)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x', 'y', and 'z' fields within
+ * 'readings', or by the 3D array 'v' (v[0] == x; v[1] == y; v[2] == z).
+ *
+ * All values are in SI units (m/s^2) and measure the acceleration applied to
+ * the device.
+ */
+#define CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x', 'y', and 'z' fields within
+ * 'readings', or by the 3D array 'v' (v[0] == x; v[1] == y; v[2] == z).
+ *
+ * All values are in radians/second and measure the rate of rotation
+ * around the X, Y and Z axis.
+ */
+#define CHRE_EVENT_SENSOR_UNCALIBRATED_GYROSCOPE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_UNCALIBRATED_GYROSCOPE)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x', 'y', and 'z' fields within
+ * 'readings', or by the 3D array 'v' (v[0] == x; v[1] == y; v[2] == z).
+ *
+ * All values are in micro-Tesla (uT) and measure the geomagnetic
+ * field in the X, Y and Z axis.
+ */
+#define CHRE_EVENT_SENSOR_UNCALIBRATED_GEOMAGNETIC_FIELD_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_UNCALIBRATED_GEOMAGNETIC_FIELD)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFloatData
+ *
+ * The data can be interpreted using the 'temperature' field within 'readings'.
+ * This value is in degrees Celsius.
+ */
+#define CHRE_EVENT_SENSOR_ACCELEROMETER_TEMPERATURE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_ACCELEROMETER_TEMPERATURE)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFloatData
+ *
+ * The data can be interpreted using the 'temperature' field within 'readings'.
+ * This value is in degrees Celsius.
+ */
+#define CHRE_EVENT_SENSOR_GYROSCOPE_TEMPERATURE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_GYROSCOPE_TEMPERATURE)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFloatData
+ *
+ * The data can be interpreted using the 'temperature' field within 'readings'.
+ * This value is in degrees Celsius.
+ */
+#define CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_TEMPERATURE_DATA \
+    (CHRE_EVENT_SENSOR_DATA_EVENT_BASE + CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD_TEMPERATURE)
+
+/**
+ * First value for sensor events which are not data from the sensor.
+ *
+ * Unlike the data event values, these other event values don't have any
+ * mapping to sensor types.
+ */
+#define CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE \
+    (CHRE_EVENT_SENSOR_FIRST_EVENT + 0x0100)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorSamplingStatusEvent
+ *
+ * Indicates that the interval and/or the latency which this sensor is
+ * sampling at has changed.
+ */
+#define CHRE_EVENT_SENSOR_SAMPLING_CHANGE \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 0)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x_bias', 'y_bias', and 'z_bias'
+ * field within 'readings', or by the 3D array 'bias' (bias[0] == x_bias;
+ * bias[1] == y_bias; bias[2] == z_bias). Bias is subtracted from uncalibrated
+ * data to generate calibrated data.
+ *
+ * All values are in radians/second and measure the rate of rotation
+ * around the X, Y and Z axis.
+ *
+ * If bias delivery is supported, this event is generated by default when
+ * chreSensorConfigure is called to enable for the sensor of type
+ * CHRE_SENSOR_TYPE_GYROSCOPE, or if bias delivery is explicitly enabled
+ * through chreSensorConfigureBiasEvents() for the sensor.
+ */
+#define CHRE_EVENT_SENSOR_GYROSCOPE_BIAS_INFO \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 1)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x_bias', 'y_bias', and 'z_bias'
+ * field within 'readings', or by the 3D array 'bias' (bias[0] == x_bias;
+ * bias[1] == y_bias; bias[2] == z_bias). Bias is subtracted from uncalibrated
+ * data to generate calibrated data.
+ *
+ * All values are in micro-Tesla (uT) and measure the geomagnetic
+ * field in the X, Y and Z axis.
+ *
+ * If bias delivery is supported, this event is generated by default when
+ * chreSensorConfigure is called to enable for the sensor of type
+ * CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD, or if bias delivery is explicitly enabled
+ * through chreSensorConfigureBiasEvents() for the sensor.
+ */
+#define CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_BIAS_INFO \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 2)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data can be interpreted using the 'x_bias', 'y_bias', and 'z_bias'
+ * field within 'readings', or by the 3D array 'bias' (bias[0] == x_bias;
+ * bias[1] == y_bias; bias[2] == z_bias). Bias is subtracted from uncalibrated
+ * data to generate calibrated data.
+ *
+ * All values are in SI units (m/s^2) and measure the acceleration applied to
+ * the device.
+ *
+ * If bias delivery is supported, this event is generated by default when
+ * chreSensorConfigure is called to enable for the sensor of type
+ * CHRE_SENSOR_TYPE_ACCELEROMETER, or if bias delivery is explicitly enabled
+ * through chreSensorConfigureBiasEvents() for the sensor.
+ *
+ * @since v1.3
+ */
+#define CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 3)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorFlushCompleteEvent
+ *
+ * An event indicating that a flush request made by chreSensorFlushAsync has
+ * completed.
+ *
+ * @see chreSensorFlushAsync
+ *
+ * @since v1.3
+ */
+#define CHRE_EVENT_SENSOR_FLUSH_COMPLETE \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 4)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data of this event is the same as that of
+ * CHRE_EVENT_SENSOR_GYROSCOPE_BIAS_INFO, except the sensorHandle field of
+ * chreSensorDataHeader contains the handle of the sensor of type
+ * CHRE_SENSOR_TYPE_UNCALIBRATED_GYROSCOPE.
+ *
+ * This event is only generated if the bias reporting is explicitly enabled
+ * for a nanoapp through chreSensorConfigureBiasEvents() for the sensor of type
+ * CHRE_SENSOR_TYPE_UNCALIBRATED_GYROSCOPE.
+ *
+ * @see CHRE_EVENT_SENSOR_GYROSCOPE_BIAS_INFO
+ *
+ * @since v1.3
+ */
+#define CHRE_EVENT_SENSOR_UNCALIBRATED_GYROSCOPE_BIAS_INFO \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 5)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data of this event is the same as that of
+ * CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_BIAS_INFO, except the sensorHandle field
+ * of chreSensorDataHeader contains the handle of the sensor of type
+ * CHRE_SENSOR_TYPE_UNCALIBRATED_GEOMAGNETIC_FIELD.
+ *
+ * This event is only generated if the bias reporting is explicitly enabled
+ * for a nanoapp through chreSensorConfigureBiasEvents() for the sensor of type
+ * CHRE_SENSOR_TYPE_UNCALIBRATED_GEOMAGNETIC_FIELD.
+ *
+ * @see CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_BIAS_INFO
+ *
+ * @since v1.3
+ */
+#define CHRE_EVENT_SENSOR_UNCALIBRATED_GEOMAGNETIC_FIELD_BIAS_INFO \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 6)
+
+/**
+ * nanoappHandleEvent argument: struct chreSensorThreeAxisData
+ *
+ * The data of this event is the same as that of
+ * CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO, except the sensorHandle field
+ * of chreSensorDataHeader contains the handle of the sensor of type
+ * CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER.
+ *
+ * This event is only generated if the bias reporting is explicitly enabled
+ * for a nanoapp through chreSensorConfigureBiasEvents for the sensor of type
+ * CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER.
+ *
+ * @see CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO
+ *
+ * @since v1.3
+ */
+#define CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_BIAS_INFO \
+    (CHRE_EVENT_SENSOR_OTHER_EVENTS_BASE + 7)
+
+#if CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_BIAS_INFO > \
+    CHRE_EVENT_SENSOR_LAST_EVENT
+#error Too many sensor events.
+#endif
+
+/**
+ * Value indicating we want the smallest possible latency for a sensor.
+ *
+ * This literally translates to 0 nanoseconds for the chreSensorConfigure()
+ * argument.  While we won't get exactly 0 nanoseconds, the CHRE will
+ * queue up this event As Soon As Possible.
+ */
+#define CHRE_SENSOR_LATENCY_ASAP  UINT64_C(0)
+
+/**
+ * Special value indicating non-importance, or non-applicability of the sampling
+ * interval.
+ *
+ * @see chreSensorConfigure
+ * @see chreSensorSamplingStatus
+ */
+#define CHRE_SENSOR_INTERVAL_DEFAULT  UINT64_C(-1)
+
+/**
+ * Special value indicating non-importance of the latency.
+ *
+ * @see chreSensorConfigure
+ * @see chreSensorSamplingStatus
+ */
+#define CHRE_SENSOR_LATENCY_DEFAULT  UINT64_C(-1)
+
+/**
+ * A sensor index value indicating that it is the default sensor.
+ *
+ * @see chreSensorFind
+ */
+#define CHRE_SENSOR_INDEX_DEFAULT  UINT8_C(0)
+
+/**
+ * Special value indicating non-importance of the batch interval.
+ *
+ * @see chreSensorConfigureWithBatchInterval
+ */
+#define CHRE_SENSOR_BATCH_INTERVAL_DEFAULT  UINT64_C(-1)
+
+// This is used to define elements of enum chreSensorConfigureMode.
+#define CHRE_SENSOR_CONFIGURE_RAW_POWER_ON           (1 << 0)
+
+// This is used to define elements of enum chreSensorConfigureMode.
+#define CHRE_SENSOR_CONFIGURE_RAW_REPORT_CONTINUOUS  (1 << 1)
+
+// This is used to define elements of enum chreSensorConfigureMode.
+#define CHRE_SENSOR_CONFIGURE_RAW_REPORT_ONE_SHOT    (2 << 1)
+
+/**
+ * The maximum amount of time allowed to elapse between the call to
+ * chreSensorFlushAsync() and when CHRE_EVENT_SENSOR_FLUSH_COMPLETE is delivered
+ * to the nanoapp on a successful flush.
+ */
+#define CHRE_SENSOR_FLUSH_COMPLETE_TIMEOUT_NS  (5 * CHRE_NSEC_PER_SEC)
+
+/**
+ * Modes we can configure a sensor to use.
+ *
+ * Our mode will affect not only how/if we receive events, but
+ * also whether or not the sensor will be powered on our behalf.
+ *
+ * @see chreSensorConfigure
+ */
+enum chreSensorConfigureMode {
+    /**
+     * Get events from the sensor.
+     *
+     * Power: Turn on if not already on.
+     * Reporting: Continuous.  Send each new event as it comes (subject to
+     *     batching and latency).
+     */
+    CHRE_SENSOR_CONFIGURE_MODE_CONTINUOUS =
+        (CHRE_SENSOR_CONFIGURE_RAW_POWER_ON |
+         CHRE_SENSOR_CONFIGURE_RAW_REPORT_CONTINUOUS),
+
+    /**
+     * Get a single event from the sensor and then become DONE.
+     *
+     * Once the event is sent, the sensor automatically
+     * changes to CHRE_SENSOR_CONFIGURE_MODE_DONE mode.
+     *
+     * Power: Turn on if not already on.
+     * Reporting: One shot.  Send the next event and then be DONE.
+     */
+    CHRE_SENSOR_CONFIGURE_MODE_ONE_SHOT =
+        (CHRE_SENSOR_CONFIGURE_RAW_POWER_ON |
+         CHRE_SENSOR_CONFIGURE_RAW_REPORT_ONE_SHOT),
+
+    /**
+     * Get events from a sensor that are generated for any client in the system.
+     *
+     * This is considered passive because the sensor will not be powered on for
+     * the sake of our nanoapp.  If and only if another client in the system has
+     * requested this sensor power on will we get events.
+     *
+     * This can be useful for something which is interested in seeing data, but
+     * not interested enough to be responsible for powering on the sensor.
+     *
+     * Power: Do not power the sensor on our behalf.
+     * Reporting: Continuous.  Send each event as it comes.
+     */
+    CHRE_SENSOR_CONFIGURE_MODE_PASSIVE_CONTINUOUS =
+        CHRE_SENSOR_CONFIGURE_RAW_REPORT_CONTINUOUS,
+
+    /**
+     * Get a single event from a sensor that is generated for any client in the
+     * system.
+     *
+     * See CHRE_SENSOR_CONFIGURE_MODE_PASSIVE_CONTINUOUS for more details on
+     * what the "passive" means.
+     *
+     * Power: Do not power the sensor on our behalf.
+     * Reporting: One shot.  Send only the next event and then be DONE.
+     */
+    CHRE_SENSOR_CONFIGURE_MODE_PASSIVE_ONE_SHOT =
+        CHRE_SENSOR_CONFIGURE_RAW_REPORT_ONE_SHOT,
+
+    /**
+     * Indicate we are done using this sensor and no longer interested in it.
+     *
+     * See chreSensorConfigure for more details on expressing interest or
+     * lack of interest in a sensor.
+     *
+     * Power: Do not power the sensor on our behalf.
+     * Reporting: None.
+     */
+    CHRE_SENSOR_CONFIGURE_MODE_DONE = 0,
+};
+
+/**
+ * A structure containing information about a Sensor.
+ *
+ * See documentation of individual fields below.
+ */
+struct chreSensorInfo {
+    /**
+     * The name of the sensor.
+     *
+     * A text name, useful for logging/debugging, describing the Sensor.  This
+     * is not assured to be unique (i.e. there could be multiple sensors with
+     * the name "Temperature").
+     *
+     * CHRE implementations may not set this as NULL.  An empty
+     * string, while discouraged, is legal.
+     */
+    const char *sensorName;
+
+    /**
+     * One of the CHRE_SENSOR_TYPE_* defines above.
+     */
+    uint8_t sensorType;
+
+    /**
+     * Flag indicating if this sensor is on-change.
+     *
+     * An on-change sensor only generates events when underlying state
+     * changes.  This has the same meaning as on-change does in the Android
+     * Sensors HAL.  See sensors.h for much more details.
+     *
+     * A value of 1 indicates this is on-change.  0 indicates this is not
+     * on-change.
+     */
+    uint8_t isOnChange          : 1;
+
+    /**
+     * Flag indicating if this sensor is one-shot.
+     *
+     * A one-shot sensor only triggers a single event, and then automatically
+     * disables itself.
+     *
+     * A value of 1 indicates this is one-shot.  0 indicates this is not
+     * on-change.
+     */
+    uint8_t isOneShot           : 1;
+
+    /**
+     * Flag indicating if this sensor supports reporting bias info events.
+     *
+     * This field will be set to 0 when running on CHRE API versions prior to
+     * v1.3, but must be ignored (i.e. does not mean bias info event is not
+     * supported).
+     *
+     * @see chreSensorConfigureBiasEvents
+     *
+     * @since v1.3
+     */
+    uint8_t reportsBiasEvents   : 1;
+
+    /**
+     * Flag indicating if this sensor supports passive mode requests.
+     *
+     * This field will be set to 0 when running on CHRE API versions prior to
+     * v1.4, and must be ignored (i.e. does not mean passive mode requests are
+     * not supported).
+     *
+     * @see chreSensorConfigure
+     *
+     * @since v1.4
+     */
+    uint8_t supportsPassiveMode : 1;
+
+    uint8_t unusedFlags         : 4;
+
+    /**
+     * The minimum sampling interval supported by this sensor, in nanoseconds.
+     *
+     * Requests to chreSensorConfigure with a lower interval than this will
+     * fail.  If the sampling interval is not applicable to this sensor, this
+     * will be set to CHRE_SENSOR_INTERVAL_DEFAULT.
+     *
+     * This field will be set to 0 when running on CHRE API versions prior to
+     * v1.1, indicating that the minimum interval is not known.
+     *
+     * @since v1.1
+     */
+    uint64_t minInterval;
+
+    /**
+     * Uniquely identifies the sensor for a given type. A value of 0 indicates
+     * that this is the "default" sensor, which is returned by
+     * chreSensorFindDefault().
+     *
+     * The sensor index of a given type must be stable across boots (i.e. must
+     * not change), and a different sensor of the same type must have different
+     * sensor index values, and the set of sensorIndex values for a given sensor
+     * type must be continuguous.
+     *
+     * @since v1.5
+     */
+    uint8_t sensorIndex;
+};
+
+/**
+ * The status of a sensor's sampling configuration.
+ */
+struct chreSensorSamplingStatus {
+    /**
+     * The interval, in nanoseconds, at which sensor data is being sampled at.
+     * This should be used by nanoapps to determine the rate at which samples
+     * will be generated and not to indicate what the sensor is truly sampling
+     * at since resampling may occur to limit incoming data.
+     *
+     * If this is CHRE_SENSOR_INTERVAL_DEFAULT, then a sampling interval
+     * isn't meaningful for this sensor.
+     *
+     * Note that if 'enabled' is false, this value is not meaningful.
+     */
+    uint64_t interval;
+
+    /**
+     * The latency, in nanoseconds, at which the sensor is now reporting.
+     *
+     * If this is CHRE_SENSOR_LATENCY_DEFAULT, then a latency
+     * isn't meaningful for this sensor.
+     *
+     * The effective batch interval can be derived from this value by
+     * adding the current sampling interval.
+     *
+     * Note that if 'enabled' is false, this value is not meaningful.
+     */
+    uint64_t latency;
+
+    /**
+     * True if the sensor is actively powered and sampling; false otherwise.
+     */
+    bool enabled;
+};
+
+/**
+ * The nanoappHandleEvent argument for CHRE_EVENT_SENSOR_SAMPLING_CHANGE.
+ *
+ * Note that only at least one of 'interval' or 'latency' must be
+ * different than it was prior to this event.  Thus, one of these
+ * fields may be (but doesn't need to be) the same as before.
+ */
+struct chreSensorSamplingStatusEvent {
+    /**
+     * The handle of the sensor which has experienced a change in sampling.
+     */
+    uint32_t sensorHandle;
+
+    /**
+     * The new sampling status.
+     *
+     * At least one of the field in this struct will be different from
+     * the previous sampling status event.
+     */
+    struct chreSensorSamplingStatus status;
+};
+
+/**
+ * The nanoappHandleEvent argument for CHRE_EVENT_SENSOR_FLUSH_COMPLETE.
+ *
+ * @see chreSensorFlushAsync
+ *
+ * @since v1.3
+ */
+struct chreSensorFlushCompleteEvent {
+    /**
+     * The handle of the sensor which a flush was completed.
+     */
+    uint32_t sensorHandle;
+
+    /**
+     * Populated with a value from enum {@link #chreError}, indicating whether
+     * the flush failed, and if so, provides the cause of the failure.
+     */
+    uint8_t errorCode;
+
+    /**
+     * Reserved for future use. Set to 0.
+     */
+    uint8_t reserved[3];
+
+    /**
+     * Set to the cookie parameter given to chreSensorFlushAsync.
+     */
+    const void *cookie;
+};
+
+/**
+ * Find the default sensor for a given sensor type.
+ *
+ * @param sensorType One of the CHRE_SENSOR_TYPE_* constants.
+ * @param handle  If a sensor is found, then the memory will be filled with
+ *     the value for the sensor's handle.  This argument must be non-NULL.
+ * @return true if a sensor was found, false otherwise.
+ */
+bool chreSensorFindDefault(uint8_t sensorType, uint32_t *handle);
+
+/**
+ * Finds a sensor of a given index and sensor type.
+ *
+ * For CHRE implementations that support multiple sensors of the same sensor
+ * type, this method can be used to get the non-default sensor(s). The default
+ * sensor, as defined in the chreSensorFindDefault(), will be returned if
+ * a sensor index of zero is specified.
+ *
+ * A simple example of iterating all available sensors of a given type is
+ * provided here:
+ *
+ * uint32_t handle;
+ * for (uint8_t i = 0; chreSensorFind(sensorType, i, &handle); i++) {
+ *   chreLog(CHRE_LOG_INFO,
+ *           "Found sensor index %" PRIu8 ", which has handle %" PRIu32,
+ *           i, handle);
+ * }
+ *
+ * If this method is invoked for CHRE versions prior to v1.5, invocations with
+ * sensorIndex value of 0 will be equivalent to using chreSensorFindDefault, and
+ * if sensorIndex is non-zero will return false.
+ *
+ * In cases where multiple sensors are supported in both the Android sensors
+ * framework and CHRE, the sensorName of the chreSensorInfo struct for a given
+ * sensor instance must match exactly with that of the
+ * android.hardware.Sensor#getName() return value. This can be used to match a
+ * sensor instance between the Android and CHRE sensors APIs.
+ *
+ * @param sensorType One of the CHRE_SENSOR_TYPE_* constants.
+ * @param sensorIndex The index of the desired sensor.
+ * @param handle  If a sensor is found, then the memory will be filled with
+ *     the value for the sensor's handle.  This argument must be non-NULL.
+ * @return true if a sensor was found, false otherwise.
+ *
+ * @since v1.5
+ */
+bool chreSensorFind(uint8_t sensorType, uint8_t sensorIndex, uint32_t *handle);
+
+/**
+ * Get the chreSensorInfo struct for a given sensor.
+ *
+ * @param sensorHandle  The sensor handle, as obtained from
+ *     chreSensorFindDefault() or passed to nanoappHandleEvent().
+ * @param info  If the sensor is valid, then this memory will be filled with
+ *     the SensorInfo contents for this sensor.  This argument must be
+ *     non-NULL.
+ * @return true if the senor handle is valid and 'info' was filled in;
+ *     false otherwise.
+ */
+bool chreGetSensorInfo(uint32_t sensorHandle, struct chreSensorInfo *info);
+
+/**
+ * Get the chreSensorSamplingStatus struct for a given sensor.
+ *
+ * Note that this may be different from what was requested in
+ * chreSensorConfigure(), for multiple reasons.  It's possible that the sensor
+ * does not exactly support the interval requested in chreSensorConfigure(), so
+ * a faster one was chosen.
+ *
+ * It's also possible that there is another user of this sensor who has
+ * requested a faster interval and/or lower latency.  This latter scenario
+ * should be noted, because it means the sensor rate can change due to no
+ * interaction from this nanoapp.  Note that the
+ * CHRE_EVENT_SENSOR_SAMPLING_CHANGE event will trigger in this case, so it's
+ * not necessary to poll for such a change.
+ *
+ * This function must return a valid status if the provided sensor is being
+ * actively sampled by a nanoapp and a CHRE_EVENT_SENSOR_SAMPLING_CHANGE has
+ * been delivered indicating their request has taken effect. It is not required
+ * to return a valid status if no nanoapp is actively sampling the sensor.
+ *
+ * @param sensorHandle  The sensor handle, as obtained from
+ *     chreSensorFindDefault() or passed to nanoappHandleEvent().
+ * @param status  If the sensor is actively enabled by a nanoapp, then this
+ *     memory must be filled with the sampling status contents for this sensor.
+ *     This argument must be non-NULL.
+ * @return true if the sensor handle is valid and 'status' was filled in;
+ *     false otherwise.
+ */
+bool chreGetSensorSamplingStatus(uint32_t sensorHandle,
+                                 struct chreSensorSamplingStatus *status);
+
+/**
+ * Configures a given sensor at a specific interval and latency and mode.
+ *
+ * If this sensor's chreSensorInfo has isOneShot set to 1,
+ * then the mode must be one of the ONE_SHOT modes, or this method will fail.
+ *
+ * The CHRE wants to power as few sensors as possible, in keeping with its
+ * low power design.  As such, it only turns on sensors when there are clients
+ * actively interested in that sensor data, and turns off sensors as soon as
+ * there are no clients interested in them.  Calling this method generally
+ * indicates an interest, and using CHRE_SENSOR_CONFIGURE_MODE_DONE shows
+ * when we are no longer interested.
+ *
+ * Thus, each initial Configure of a sensor (per nanoapp) needs to eventually
+ * have a DONE call made, either directly or on its behalf.  Subsequent calls
+ * to a Configure method within the same nanoapp, when there has been no DONE
+ * in between, still only require a single DONE call.
+ *
+ * For example, the following is valid usage:
+ * <code>
+ *   chreSensorConfigure(myHandle, mode, interval0, latency0);
+ *   [...]
+ *   chreSensorConfigure(myHandle, mode, interval1, latency0);
+ *   [...]
+ *   chreSensorConfigure(myHandle, mode, interval1, latency1);
+ *   [...]
+ *   chreSensorConfigureModeOnly(myHandle, CHRE_SENSOR_CONFIGURE_MODE_DONE);
+ * </code>
+ *
+ * The first call to Configure is the one which creates the requirement
+ * to eventually call with DONE.  The subsequent calls are just changing the
+ * interval/latency.  They have not changed the fact that this nanoapp is
+ * still interested in output from the sensor 'myHandle'.  Thus, only one
+ * single call for DONE is needed.
+ *
+ * There is a special case.  One-shot sensors, sensors which
+ * just trigger a single event and never trigger again, implicitly go into
+ * DONE mode after that single event triggers.  Thus, the
+ * following are legitimate usages:
+ * <code>
+ *   chreSensorConfigure(myHandle, MODE_ONE_SHOT, interval, latency);
+ *   [...]
+ *   [myHandle triggers an event]
+ *   [no need to configure to DONE].
+ * </code>
+ *
+ * And:
+ * <code>
+ *   chreSensorConfigure(myHandle, MODE_ONE_SHOT, interval, latency);
+ *   [...]
+ *   chreSensorConfigureModeOnly(myHandle, MODE_DONE);
+ *   [we cancelled myHandle before it ever triggered an event]
+ * </code>
+ *
+ * Note that while PASSIVE modes, by definition, don't express an interest in
+ * powering the sensor, DONE is still necessary to silence the event reporting.
+ * Starting with CHRE API v1.4, for sensors that do not support passive mode, a
+ * request with mode set to CHRE_SENSOR_CONFIGURE_MODE_PASSIVE_CONTINUOUS or
+ * CHRE_SENSOR_CONFIGURE_MODE_PASSIVE_ONE_SHOT will be rejected. CHRE API
+ * versions 1.3 and older implicitly assume that passive mode is supported
+ * across all sensors, however this is not necessarily the case. Clients can
+ * call chreSensorInfo to identify whether a sensor supports passive mode.
+ *
+ * When a calibrated sensor (e.g. CHRE_SENSOR_TYPE_ACCELEROMETER) is
+ * successfully enabled through this method and if bias delivery is supported,
+ * by default CHRE will start delivering bias events for the sensor
+ * (e.g. CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO) to the nanoapp. If the
+ * nanoapp does not wish to receive these events, they can be disabled through
+ * chreSensorConfigureBiasEvents after enabling the sensor.
+ *
+ * @param sensorHandle  The handle to the sensor, as obtained from
+ *     chreSensorFindDefault().
+ * @param mode  The mode to use.  See descriptions within the
+ *     chreSensorConfigureMode enum.
+ * @param interval  The interval, in nanoseconds, at which we want events from
+ *     the sensor.  On success, the sensor will be set to 'interval', or a value
+ *     less than 'interval'.  There is a special value
+ *     CHRE_SENSOR_INTERVAL_DEFAULT, in which we don't express a preference for
+ *     the interval, and allow the sensor to choose what it wants.  Note that
+ *     due to batching, we may receive events less frequently than
+ *     'interval'.
+ * @param latency  The maximum latency, in nanoseconds, allowed before the
+ *     CHRE begins delivery of an event.  This will control how many events
+ *     can be queued by the sensor before requiring a delivery event.
+ *     Latency is defined as the "timestamp when event is queued by the CHRE"
+ *     minus "timestamp of oldest unsent data reading".
+ *     There is a special value CHRE_SENSOR_LATENCY_DEFAULT, in which we don't
+ *     express a preference for the latency, and allow the sensor to choose what
+ *     it wants.
+ *     Note that there is no assurance of how long it will take an event to
+ *     get through a CHRE's queueing system, and thus there is no ability to
+ *     request a minimum time from the occurrence of a phenomenon to when the
+ *     nanoapp receives the information.  The current CHRE API has no
+ *     real-time elements, although future versions may introduce some to
+ *     help with this issue.
+ * @return true if the configuration succeeded, false otherwise.
+ *
+ * @see chreSensorConfigureMode
+ * @see chreSensorFindDefault
+ * @see chreSensorInfo
+ * @see chreSensorConfigureBiasEvents
+ */
+bool chreSensorConfigure(uint32_t sensorHandle,
+                         enum chreSensorConfigureMode mode,
+                         uint64_t interval, uint64_t latency);
+
+/**
+ * Short cut for chreSensorConfigure where we only want to configure the mode
+ * and do not care about interval/latency.
+ *
+ * @see chreSensorConfigure
+ */
+static inline bool chreSensorConfigureModeOnly(
+        uint32_t sensorHandle, enum chreSensorConfigureMode mode) {
+    return chreSensorConfigure(sensorHandle,
+                               mode,
+                               CHRE_SENSOR_INTERVAL_DEFAULT,
+                               CHRE_SENSOR_LATENCY_DEFAULT);
+}
+
+/**
+ * Convenience function that wraps chreSensorConfigure but enables batching to
+ * be controlled by specifying the desired maximum batch interval rather
+ * than maximum sample latency.  Users may find the batch interval to be a more
+ * intuitive method of expressing the desired batching behavior.
+ *
+ * Batch interval is different from latency as the batch interval time is
+ * counted starting when the prior event containing a batch of sensor samples is
+ * delivered, while latency starts counting when the first sample is deferred to
+ * start collecting a batch.  In other words, latency ignores the time between
+ * the last sample in a batch to the first sample of the next batch, while it's
+ * included in the batch interval, as illustrated below.
+ *
+ *  Time      0   1   2   3   4   5   6   7   8
+ *  Batch             A           B           C
+ *  Sample   a1  a2  a3  b1  b2  b3  c1  c2  c3
+ *  Latency  [        ]  [        ]  [        ]
+ *  BatchInt          |           |           |
+ *
+ * In the diagram, the effective sample interval is 1 time unit, latency is 2
+ * time units, and batch interval is 3 time units.
+ *
+ * @param sensorHandle See chreSensorConfigure#sensorHandle
+ * @param mode See chreSensorConfigure#mode
+ * @param sampleInterval See chreSensorConfigure#interval, but note that
+ *     CHRE_SENSOR_INTERVAL_DEFAULT is not a supported input to this method.
+ * @param batchInterval The desired maximum interval, in nanoseconds, between
+ *     CHRE enqueuing each batch of sensor samples.
+ * @return Same as chreSensorConfigure
+ *
+ * @see chreSensorConfigure
+ *
+ * @since v1.1
+ */
+static inline bool chreSensorConfigureWithBatchInterval(
+        uint32_t sensorHandle, enum chreSensorConfigureMode mode,
+        uint64_t sampleInterval, uint64_t batchInterval) {
+    bool result = false;
+
+    if (sampleInterval != CHRE_SENSOR_INTERVAL_DEFAULT) {
+        uint64_t latency;
+        if (batchInterval == CHRE_SENSOR_BATCH_INTERVAL_DEFAULT) {
+            latency = CHRE_SENSOR_LATENCY_DEFAULT;
+        } else if (batchInterval > sampleInterval) {
+            latency = batchInterval - sampleInterval;
+        } else {
+            latency = CHRE_SENSOR_LATENCY_ASAP;
+        }
+        result = chreSensorConfigure(sensorHandle, mode, sampleInterval,
+                                     latency);
+    }
+
+    return result;
+}
+
+/**
+ * Configures the reception of bias events for a specific sensor.
+ *
+ * If bias event delivery is supported for a sensor, the sensor's chreSensorInfo
+ * has reportsBiasEvents set to 1. If supported, it must be supported for both
+ * calibrated and uncalibrated versions of the sensor. If supported, CHRE must
+ * provide bias events to the nanoapp by default when chreSensorConfigure is
+ * called to enable the calibrated version of the sensor (for backwards
+ * compatibility reasons, as this is the defined behavior for CHRE API v1.0).
+ * When configuring uncalibrated sensors, nanoapps must explicitly configure an
+ * enable request through this method to receive bias events. If bias event
+ * delivery is not supported for the sensor, this method will return false and
+ * no bias events will be generated.
+ *
+ * To enable bias event delivery (enable=true), the nanoapp must be registered
+ * to the sensor through chreSensorConfigure, and bias events will only be
+ * generated when the sensor is powered on. To disable the bias event delivery,
+ * this method can be invoked with enable=false.
+ *
+ * If an enable configuration is successful, the calling nanoapp will receive
+ * bias info events, e.g. CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO, when the
+ * bias status changes (or first becomes available). Calibrated data
+ * (e.g. CHRE_SENSOR_TYPE_ACCELEROMETER) is generated by subracting bias from
+ * uncalibrated data (e.g. CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER).
+ * Calibrated sensor events are generated by applying the most recent bias
+ * available (i.e. timestamp of calibrated data are greater than or equal to the
+ * timestamp of the bias data that has been applied to it). The configuration of
+ * bias event delivery persists until the sensor is unregistered by the nanoapp
+ * through chreSensorConfigure or modified through this method.
+ *
+ * To get an initial bias before new bias events, the nanoapp should get the
+ * bias synchronously after this method is invoked, e.g.:
+ *
+ * if (chreSensorConfigure(handle, ...)) {
+ *   chreSensorConfigureBiasEvents(handle, true);
+ *   chreSensorGetThreeAxisBias(handle, &bias);
+ * }
+ *
+ * Note that chreSensorGetThreeAxisBias() should be called after
+ * chreSensorConfigureBiasEvents() to ensure that no bias events are lost.
+ *
+ * If called while running on a CHRE API version below v1.3, this function
+ * returns false and has no effect. The default behavior regarding bias events
+ * is unchanged, meaning that the implementation may still send bias events
+ * when a calibrated sensor is registered (if supported), and will not send bias
+ * events when an uncalibrated sensor is registered.
+ *
+ * @param sensorHandle The handle to the sensor, as obtained from
+ *     chreSensorFindDefault().
+ * @param enable true to receive bias events, false otherwise
+ *
+ * @return true if the configuration succeeded, false otherwise
+ *
+ * @since v1.3
+ */
+bool chreSensorConfigureBiasEvents(uint32_t sensorHandle, bool enable);
+
+/**
+ * Synchronously provides the most recent bias info available for a sensor. The
+ * bias will only be provided for a sensor that supports bias event delivery
+ * using the chreSensorThreeAxisData type. If the bias is not yet available
+ * (but is supported), this method will store data with a bias of 0 and the
+ * accuracy field in chreSensorDataHeader set to CHRE_SENSOR_ACCURACY_UNKNOWN.
+ *
+ * If called while running on a CHRE API version below v1.3, this function
+ * returns false.
+ *
+ * @param sensorHandle The handle to the sensor, as obtained from
+ *     chreSensorFindDefault().
+ * @param bias A pointer to where the bias will be stored.
+ *
+ * @return true if the bias was successfully stored, false if sensorHandle was
+ *     invalid or the sensor does not support three axis bias delivery
+ *
+ * @since v1.3
+ *
+ * @see chreSensorConfigureBiasEvents
+ */
+bool chreSensorGetThreeAxisBias(uint32_t sensorHandle,
+                                struct chreSensorThreeAxisData *bias);
+
+/**
+ * Makes a request to flush all samples stored for batching. The nanoapp must be
+ * registered to the sensor through chreSensorConfigure, and the sensor must be
+ * powered on. If the request is accepted, all batched samples of the sensor
+ * are sent to nanoapps registered to the sensor. During a flush, it is treated
+ * as though the latency as given in chreSensorConfigure has expired. When all
+ * batched samples have been flushed (or the flush fails), the nanoapp will
+ * receive a unicast CHRE_EVENT_SENSOR_FLUSH_COMPLETE event. The time to deliver
+ * this event must not exceed CHRE_SENSOR_FLUSH_COMPLETE_TIMEOUT_NS after this
+ * method is invoked. If there are no samples in the batch buffer (either in
+ * hardware FIFO or software), then this method will return true and a
+ * CHRE_EVENT_SENSOR_FLUSH_COMPLETE event is delivered immediately.
+ *
+ * If a flush request is invalid (e.g. the sensor refers to a one-shot sensor,
+ * or the sensor was not enabled), and this API will return false and no
+ * CHRE_EVENT_SENSOR_FLUSH_COMPLETE event will be delivered.
+ *
+ * If multiple flush requests are made for a sensor prior to flush completion,
+ * then the requesting nanoapp will receive all batched samples existing at the
+ * time of the latest flush request. In this case, the number of
+ * CHRE_EVENT_SENSOR_FLUSH_COMPLETE events received must equal the number of
+ * flush requests made.
+ *
+ * If a sensor request is disabled after a flush request is made through this
+ * method but before the flush operation is completed, the nanoapp will receive
+ * a CHRE_EVENT_SENSOR_FLUSH_COMPLETE with the error code
+ * CHRE_ERROR_FUNCTION_DISABLED for any pending flush requests.
+ *
+ * Starting with CHRE API v1.3, implementations must support this capability
+ * across all exposed sensor types.
+ *
+ * @param sensorHandle  The handle to the sensor, as obtained from
+ *     chreSensorFindDefault().
+ * @param cookie  An opaque value that will be included in the
+ *     chreSensorFlushCompleteEvent sent in relation to this request.
+ *
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.3
+ */
+bool chreSensorFlushAsync(uint32_t sensorHandle, const void *cookie);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_SENSOR_H_ */
diff --git a/chre_api/legacy/v1_8/chre/sensor_types.h b/chre_api/legacy/v1_8/chre/sensor_types.h
new file mode 100644
index 0000000..63b495b
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/sensor_types.h
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2017 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 _CHRE_SENSOR_TYPES_H_
+#define _CHRE_SENSOR_TYPES_H_
+
+/**
+ * @file
+ * Standalone definition of sensor types, and the data structures of the sample
+ * events they emit.
+ */
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file
+ * The CHRE_SENSOR_TYPE_* defines are the sensor types supported.
+ *
+ * Unless otherwise noted, each of these sensor types is based off of a
+ * corresponding sensor type in the Android API's sensors.h interface.
+ * For a given CHRE_SENSOR_TYPE_FOO, it corresponds to the SENSOR_TYPE_FOO in
+ * hardware/libhardware/include/hardware/sensors.h of the Android code base.
+ *
+ * Unless otherwise noted below, a CHRE_SENSOR_TYPE_FOO should be assumed
+ * to work the same as the Android SENSOR_TYPE_FOO, as documented in the
+ * sensors.h documentation and as detailed within the Android Compatibility
+ * Definition Document.
+ *
+ * Note that every sensor will generate CHRE_EVENT_SENSOR_SAMPLING_CHANGE
+ * events, so it is not listed with each individual sensor.
+ */
+
+/**
+ * Start value for all of the vendor-defined private sensors.
+ *
+ * @since v1.2
+ */
+#define CHRE_SENSOR_TYPE_VENDOR_START  UINT8_C(192)
+
+/**
+ * Accelerometer.
+ *
+ * Generates: CHRE_EVENT_SENSOR_ACCELEROMETER_DATA and
+ *     optionally CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO
+ *
+ * Note that the ACCELEROMETER_DATA is always the fully calibrated data,
+ * including factory calibration and runtime calibration if available.
+ *
+ * @see chreConfigureSensorBiasEvents
+ */
+#define CHRE_SENSOR_TYPE_ACCELEROMETER  UINT8_C(1)
+
+/**
+ * Instantaneous motion detection.
+ *
+ * Generates: CHRE_EVENT_SENSOR_INSTANT_MOTION_DETECT_DATA
+ *
+ * This is a one-shot sensor.
+ *
+ * This does not have a direct analogy within sensors.h.  This is similar
+ * to SENSOR_TYPE_MOTION_DETECT, but this triggers instantly upon any
+ * motion, instead of waiting for a period of continuous motion.
+ */
+#define CHRE_SENSOR_TYPE_INSTANT_MOTION_DETECT  UINT8_C(2)
+
+/**
+ * Stationary detection.
+ *
+ * Generates: CHRE_EVENT_SENSOR_STATIONARY_DETECT_DATA
+ *
+ * This is a one-shot sensor.
+ */
+#define CHRE_SENSOR_TYPE_STATIONARY_DETECT  UINT8_C(3)
+
+/**
+ * Gyroscope.
+ *
+ * Generates: CHRE_EVENT_SENSOR_GYROSCOPE_DATA and
+ *     optionally CHRE_EVENT_SENSOR_GYROSCOPE_BIAS_INFO
+ *
+ * Note that the GYROSCOPE_DATA is always the fully calibrated data, including
+ * factory calibration and runtime calibration if available.
+ *
+ * @see chreConfigureSensorBiasEvents
+ */
+#define CHRE_SENSOR_TYPE_GYROSCOPE  UINT8_C(6)
+
+/**
+ * Uncalibrated gyroscope.
+ *
+ * Generates: CHRE_EVENT_SENSOR_UNCALIBRATED_GYROSCOPE_DATA
+ *
+ * Note that the UNCALIBRATED_GYROSCOPE_DATA must be factory calibrated data,
+ * but not runtime calibrated.
+ */
+#define CHRE_SENSOR_TYPE_UNCALIBRATED_GYROSCOPE  UINT8_C(7)
+
+/**
+ * Magnetometer.
+ *
+ * Generates: CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_DATA and
+ *     optionally CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_BIAS_INFO
+ *
+ * Note that the GEOMAGNETIC_FIELD_DATA is always the fully calibrated data,
+ * including factory calibration and runtime calibration if available.
+ *
+ * @see chreConfigureSensorBiasEvents
+ */
+#define CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD  UINT8_C(8)
+
+/**
+ * Uncalibrated magnetometer.
+ *
+ * Generates: CHRE_EVENT_SENSOR_UNCALIBRATED_GEOMAGNETIC_FIELD_DATA
+ *
+ * Note that the UNCALIBRATED_GEOMAGNETIC_FIELD_DATA must be factory calibrated
+ * data, but not runtime calibrated.
+ */
+#define CHRE_SENSOR_TYPE_UNCALIBRATED_GEOMAGNETIC_FIELD  UINT8_C(9)
+
+/**
+ * Barometric pressure sensor.
+ *
+ * Generates: CHRE_EVENT_SENSOR_PRESSURE_DATA
+ */
+#define CHRE_SENSOR_TYPE_PRESSURE  UINT8_C(10)
+
+/**
+ * Ambient light sensor.
+ *
+ * Generates: CHRE_EVENT_SENSOR_LIGHT_DATA
+ *
+ * This is an on-change sensor.
+ */
+#define CHRE_SENSOR_TYPE_LIGHT  UINT8_C(12)
+
+/**
+ * Proximity detection.
+ *
+ * Generates: CHRE_EVENT_SENSOR_PROXIMITY_DATA
+ *
+ * This is an on-change sensor.
+ */
+#define CHRE_SENSOR_TYPE_PROXIMITY  UINT8_C(13)
+
+/**
+ * Step detection.
+ *
+ * Generates: CHRE_EVENT_SENSOR_STEP_DETECT_DATA
+ *
+ * @since v1.3
+ */
+#define CHRE_SENSOR_TYPE_STEP_DETECT  UINT8_C(23)
+
+/**
+ * Step counter.
+ *
+ * Generates: CHRE_EVENT_SENSOR_STEP_COUNTER_DATA
+ *
+ * This is an on-change sensor. Note that the data returned by this sensor must
+ * match the value that can be obtained via the Android sensors framework at the
+ * same point in time. This means, if CHRE reboots from the rest of the system,
+ * the counter must not reset to 0.
+ *
+ * @since v1.5
+ */
+#define CHRE_SENSOR_TYPE_STEP_COUNTER UINT8_C(24)
+
+/**
+ * Hinge angle sensor.
+ *
+ * Generates: CHRE_EVENT_SENSOR_HINGE_ANGLE_DATA
+ *
+ * This is an on-change sensor.
+ *
+ * A sensor of this type measures the angle, in degrees, between two
+ * integral parts of the device. Movement of a hinge measured by this sensor
+ * type is expected to alter the ways in which the user may interact with
+ * the device, for example by unfolding or revealing a display.
+ *
+ * @since v1.5
+ */
+#define CHRE_SENSOR_TYPE_HINGE_ANGLE UINT8_C(36)
+
+/**
+ * Uncalibrated accelerometer.
+ *
+ * Generates: CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_DATA
+ *
+ * Note that the UNCALIBRATED_ACCELEROMETER_DATA must be factory calibrated
+ * data, but not runtime calibrated.
+ */
+#define CHRE_SENSOR_TYPE_UNCALIBRATED_ACCELEROMETER  UINT8_C(55)
+
+/**
+ * Accelerometer temperature.
+ *
+ * Generates: CHRE_EVENT_SENSOR_ACCELEROMETER_TEMPERATURE_DATA
+ */
+#define CHRE_SENSOR_TYPE_ACCELEROMETER_TEMPERATURE  UINT8_C(56)
+
+/**
+ * Gyroscope temperature.
+ *
+ * Generates: CHRE_EVENT_SENSOR_GYROSCOPE_TEMPERATURE_DATA
+ */
+#define CHRE_SENSOR_TYPE_GYROSCOPE_TEMPERATURE  UINT8_C(57)
+
+/**
+ * Magnetometer temperature.
+ *
+ * Generates: CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_TEMPERATURE_DATA
+ */
+#define CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD_TEMPERATURE  UINT8_C(58)
+
+#if CHRE_SENSOR_TYPE_GEOMAGNETIC_FIELD_TEMPERATURE >= CHRE_SENSOR_TYPE_VENDOR_START
+#error Too many sensor types
+#endif
+
+/**
+ * Values that can be stored in the accuracy field of chreSensorDataHeader.
+ * If CHRE_SENSOR_ACCURACY_UNKNOWN is returned, then the driver did not provide
+ * accuracy information with the data. Values in the range
+ * [CHRE_SENSOR_ACCURACY_VENDOR_START, CHRE_SENSOR_ACCURACY_VENDOR_END] are
+ * reserved for vendor-specific values for vendor sensor types, and are not used
+ * by CHRE for standard sensor types.
+ *
+ * Otherwise, the values have the same meaning as defined in the Android
+ * Sensors definition:
+ * https://developer.android.com/reference/android/hardware/SensorManager
+ *
+ * @since v1.3
+ *
+ * @defgroup CHRE_SENSOR_ACCURACY
+ * @{
+ */
+
+#define CHRE_SENSOR_ACCURACY_UNKNOWN       UINT8_C(0)
+#define CHRE_SENSOR_ACCURACY_UNRELIABLE    UINT8_C(1)
+#define CHRE_SENSOR_ACCURACY_LOW           UINT8_C(2)
+#define CHRE_SENSOR_ACCURACY_MEDIUM        UINT8_C(3)
+#define CHRE_SENSOR_ACCURACY_HIGH          UINT8_C(4)
+#define CHRE_SENSOR_ACCURACY_VENDOR_START  UINT8_C(192)
+#define CHRE_SENSOR_ACCURACY_VENDOR_END    UINT8_MAX
+
+/** @} */
+
+/**
+ * Header used in every structure containing batchable data from a sensor.
+ *
+ * The typical structure for sensor data looks like:
+ *
+ *   struct chreSensorTypeData {
+ *       struct chreSensorDataHeader header;
+ *       struct chreSensorTypeSampleData {
+ *           uint32_t timestampDelta;
+ *           union {
+ *               <type> value;
+ *               <type> interpretation0;
+ *               <type> interpretation1;
+ *           };
+ *       } readings[1];
+ *   };
+ *
+ * Despite 'readings' being declared as an array of 1 element,
+ * an instance of the struct will actually have 'readings' as
+ * an array of header.readingCount elements (which may be 1).
+ * The 'timestampDelta' is in relation to the previous 'readings' (or
+ * the baseTimestamp for readings[0].  So,
+ * Timestamp for readings[0] == header.baseTimestamp +
+ *     readings[0].timestampDelta.
+ * Timestamp for readings[1] == timestamp for readings[0] +
+ *     readings[1].timestampDelta.
+ * And thus, in order to determine the timestamp for readings[N], it's
+ * necessary to process through all of the N-1 readings.  The advantage,
+ * though, is that our entire readings can span an arbitrary length of time,
+ * just as long as any two consecutive readings differ by no more than
+ * 4.295 seconds (timestampDelta, like all time in the CHRE, is in
+ * nanoseconds).
+ *
+ * If a sensor has batched readings where two consecutive readings differ by
+ * more than 4.295 seconds, the CHRE will split them across multiple
+ * instances of the struct, and send multiple events.
+ *
+ * The value from the sensor is typically expressed in a union,
+ * allowing a generic access to the data ('value'), along with
+ * differently named access giving a more natural interpretation
+ * of the data for the specific sensor types which use this
+ * structure.  This allows, for example, barometer code to
+ * reference readings[N].pressure, and an ambient light sensor
+ * to reference readings[N].light, while both use the same
+ * structure.
+ */
+struct chreSensorDataHeader {
+    /**
+     * The base timestamp, in nanoseconds; must be in the same time base as
+     * chreGetTime().
+     */
+    uint64_t baseTimestamp;
+
+    /**
+     * The handle of the sensor producing this event.
+     */
+    uint32_t sensorHandle;
+
+    /**
+     * The number elements in the 'readings' array.
+     *
+     * This must be at least 1.
+     */
+    uint16_t readingCount;
+
+    /**
+     * The accuracy of the sensor data.
+     *
+     * @ref CHRE_SENSOR_ACCURACY
+     *
+     * @since v1.3
+     */
+    uint8_t accuracy;
+
+    /**
+     * Reserved bytes.
+     *
+     * This must be 0.
+     */
+    uint8_t reserved;
+};
+
+/**
+ * Data for a sensor which reports on three axes.
+ *
+ * This is used by CHRE_EVENT_SENSOR_ACCELEROMETER_DATA,
+ * CHRE_EVENT_SENSOR_ACCELEROMETER_BIAS_INFO,
+ * CHRE_EVENT_SENSOR_UNCALIBRATED_ACCELEROMETER_DATA,
+ * CHRE_EVENT_SENSOR_GYROSCOPE_DATA,
+ * CHRE_EVENT_SENSOR_GYROSCOPE_BIAS_INFO,
+ * CHRE_EVENT_SENSOR_UNCALIBRATED_GYROSCOPE_DATA,
+ * CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_DATA,
+ * CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_BIAS_INFO, and
+ * CHRE_EVENT_SENSOR_UNCALIBRATED_GEOMAGNETIC_FIELD_DATA.
+ */
+struct chreSensorThreeAxisData {
+    /**
+     * @see chreSensorDataHeader
+     */
+    struct chreSensorDataHeader header;
+    struct chreSensorThreeAxisSampleData {
+        /**
+         * @see chreSensorDataHeader
+         */
+        uint32_t timestampDelta;
+        union {
+            float values[3];
+            float v[3];
+            struct {
+                float x;
+                float y;
+                float z;
+            };
+            float bias[3];
+            struct {
+                float x_bias;
+                float y_bias;
+                float z_bias;
+            };
+        };
+    } readings[1];
+};
+
+/**
+ * Data from a sensor where we only care about a event occurring.
+ *
+ * This is a bit unusual in that our readings have no data in addition
+ * to the timestamp.  But since we only care about the occurrence, we
+ * don't need to know anything else.
+ *
+ * Used by: CHRE_EVENT_SENSOR_INSTANT_MOTION_DETECT_DATA,
+ *     CHRE_EVENT_SENSOR_STATIONARY_DETECT_DATA, and
+ *     CHRE_EVENT_SENSOR_STEP_DETECT_DATA.
+ */
+struct chreSensorOccurrenceData {
+    struct chreSensorDataHeader header;
+    struct chreSensorOccurrenceSampleData {
+        uint32_t timestampDelta;
+        // This space intentionally left blank.
+        // Only the timestamp is meaningful here, there
+        // is no additional data.
+    } readings[1];
+};
+
+/**
+ * This is used by CHRE_EVENT_SENSOR_LIGHT_DATA,
+ * CHRE_EVENT_SENSOR_PRESSURE_DATA,
+ * CHRE_EVENT_SENSOR_ACCELEROMETER_TEMPERATURE_DATA,
+ * CHRE_EVENT_SENSOR_GYROSCOPE_TEMPERATURE_DATA,
+ * CHRE_EVENT_SENSOR_GEOMAGNETIC_FIELD_TEMPERATURE_DATA, and
+ * CHRE_EVENT_SENSOR_HINGE_ANGLE_DATA.
+ */
+struct chreSensorFloatData {
+    struct chreSensorDataHeader header;
+    struct chreSensorFloatSampleData {
+        uint32_t timestampDelta;
+        union {
+            float value;
+            float light;        //!< Unit: lux
+            float pressure;     //!< Unit: hectopascals (hPa)
+            float temperature;  //!< Unit: degrees Celsius
+            float angle;        //!< Unit: angular degrees
+        };
+    } readings[1];
+};
+
+/**
+ * CHRE_EVENT_SENSOR_PROXIMITY_DATA.
+ */
+struct chreSensorByteData {
+    struct chreSensorDataHeader header;
+    struct chreSensorByteSampleData {
+        uint32_t timestampDelta;
+        union {
+            uint8_t value;
+            struct {
+                uint8_t isNear : 1;
+                //! @deprecated As of v1.2, this field is deprecated and must
+                //! always be set to 0
+                uint8_t invalid : 1;
+                uint8_t padding0 : 6;
+            };
+        };
+    } readings[1];
+};
+
+/**
+ * Data for a sensor which reports a single uint64 value.
+ *
+ * This is used by CHRE_EVENT_SENSOR_STEP_COUNTER_DATA.
+ */
+struct chreSensorUint64Data {
+    struct chreSensorDataHeader header;
+    struct chreSensorUint64SampleData {
+        uint32_t timestampDelta;
+        uint64_t value;
+    } readings[1];
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_SENSOR_TYPES_H_ */
diff --git a/chre_api/legacy/v1_8/chre/toolchain.h b/chre_api/legacy/v1_8/chre/toolchain.h
new file mode 100644
index 0000000..c2b8722
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/toolchain.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef CHRE_TOOLCHAIN_H_
+#define CHRE_TOOLCHAIN_H_
+
+/**
+ * @file
+ * Compiler/build toolchain-specific macros used by the CHRE API
+ */
+
+#if defined(__GNUC__) || defined(__clang__)
+// For GCC and clang
+
+#define CHRE_DEPRECATED(message) \
+  __attribute__((deprecated(message)))
+
+// Enable printf-style compiler warnings for mismatched format string and args
+#define CHRE_PRINTF_ATTR(formatPos, argStart) \
+  __attribute__((format(printf, formatPos, argStart)))
+
+#define CHRE_BUILD_ERROR(message) CHRE_DO_PRAGMA(GCC error message)
+#define CHRE_DO_PRAGMA(message) _Pragma(#message)
+
+#elif defined(__ICCARM__) || defined(__CC_ARM)
+// For IAR ARM and Keil MDK-ARM compilers
+
+#define CHRE_PRINTF_ATTR(formatPos, argStart)
+
+#elif defined(_MSC_VER)
+// For Microsoft Visual Studio
+
+#define CHRE_PRINTF_ATTR(formatPos, argStart)
+
+#else  // if !defined(__GNUC__) && !defined(__clang__)
+
+#error Need to add support for new compiler
+
+#endif
+
+// For platforms that don't support error pragmas, utilize the best method of
+// showing an error depending on the platform support.
+#ifndef CHRE_BUILD_ERROR
+#ifdef __cplusplus  // C++17 or greater assumed
+#define CHRE_BUILD_ERROR(message) static_assert(0, message)
+#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
+#define CHRE_BUILD_ERROR(message) _Static_assert(0, message)
+#else
+#define CHRE_BUILD_ERROR(message) char buildError[-1] = message
+#endif
+#endif
+
+#endif  // CHRE_TOOLCHAIN_H_
diff --git a/chre_api/legacy/v1_8/chre/user_settings.h b/chre_api/legacy/v1_8/chre/user_settings.h
new file mode 100644
index 0000000..c40be90
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/user_settings.h
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 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 _CHRE_USER_SETTINGS_H_
+#define _CHRE_USER_SETTINGS_H_
+
+/**
+ * @file
+ * The API for requesting notifications on changes in the settings of the
+ * active user. If the device is set up with one or more secondary users
+ * (see https://source.android.com/devices/tech/admin/multi-user), the user
+ * settings in CHRE reflect that of the currently active user.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <chre/event.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The user settings that nanoapps can request notifications for on a status
+ * change.
+ *
+ * NOTE: The WIFI available setting indicates the overall availability
+ * of WIFI related functionality. For example, if wifi is disabled for
+ * connectivity but enabled for location, the WIFI available setting is
+ * enabled.
+ *
+ * NOTE: The BLE available setting is the logical OR of the main Bluetooth
+ * setting and the Bluetooth scanning setting found under Location settings.
+ * Note that this indicates whether the user is allowing Bluetooth to be used,
+ * however the system may still fully power down the BLE chip in some scenarios
+ * if no request for it exists on the Android host side. See the
+ * chreBleStartScanAsync() API documentation for more information.
+ *
+ * @defgroup CHRE_USER_SETTINGS
+ * @{
+ */
+#define CHRE_USER_SETTING_LOCATION             UINT8_C(0)
+#define CHRE_USER_SETTING_WIFI_AVAILABLE       UINT8_C(1)
+#define CHRE_USER_SETTING_AIRPLANE_MODE        UINT8_C(2)
+#define CHRE_USER_SETTING_MICROPHONE           UINT8_C(3)
+#define CHRE_USER_SETTING_BLE_AVAILABLE        UINT8_C(4)
+
+/** @} */
+
+/**
+ * Produce an event ID in the block of IDs reserved for settings notifications.
+ *
+ * @param offset Index into the event ID block, valid in the range [0,15]
+ */
+#define CHRE_SETTING_EVENT_ID(offset) (CHRE_EVENT_SETTING_CHANGED_FIRST_EVENT + (offset))
+
+/**
+ * nanoappHandleEvent argument: struct chreUserSettingChangedEvent
+ *
+ * Notify nanoapps of a change in the associated setting. Nanoapps must first
+ * register (via chreUserSettingConfigureEvents) for events before they are
+ * sent out.
+ */
+#define CHRE_EVENT_SETTING_CHANGED_LOCATION         CHRE_SETTING_EVENT_ID(0)
+#define CHRE_EVENT_SETTING_CHANGED_WIFI_AVAILABLE   CHRE_SETTING_EVENT_ID(1)
+#define CHRE_EVENT_SETTING_CHANGED_AIRPLANE_MODE    CHRE_SETTING_EVENT_ID(2)
+#define CHRE_EVENT_SETTING_CHANGED_MICROPHONE       CHRE_SETTING_EVENT_ID(3)
+#define CHRE_EVENT_SETTING_CHANGED_BLE_AVAILABLE    CHRE_SETTING_EVENT_ID(4)
+
+#if CHRE_EVENT_SETTING_CHANGED_BLE_AVAILABLE > CHRE_EVENT_SETTING_CHANGED_LAST_EVENT
+#error Too many setting changed events.
+#endif
+
+/**
+ * Indicates the current state of a setting.
+ * The setting state is 'unknown' only in the following scenarios:
+ *  - CHRE hasn't received the initial state yet on a restart.
+ *  - The nanoapp is running on CHRE v1.4 or older
+ *  - Nanoapp provided in invalid setting ID to chreUserSettingGetStatus.
+ */
+enum chreUserSettingState {
+  CHRE_USER_SETTING_STATE_UNKNOWN = -1,
+  CHRE_USER_SETTING_STATE_DISABLED = 0,
+  CHRE_USER_SETTING_STATE_ENABLED = 1
+};
+
+/**
+ * The nanoappHandleEvent argument for CHRE settings changed notifications.
+ */
+struct chreUserSettingChangedEvent {
+  //! Indicates the setting whose state has changed.
+  uint8_t setting;
+
+  //! A value that corresponds to a member in enum chreUserSettingState,
+  // indicating the latest value of the setting.
+  int8_t settingState;
+};
+
+/**
+ * Get the current state of a given setting.
+ *
+ * @param setting The setting to get the current status of.
+ *
+ * @return The current state of the requested setting. The state is returned
+ * as an int8_t to be consistent with the associated event data, but is
+ * guaranteed to be a valid enum chreUserSettingState member.
+ *
+ * @since v1.5
+ */
+int8_t chreUserSettingGetState(uint8_t setting);
+
+/**
+ * Register or deregister for a notification on a status change for a given
+ * setting. Note that registration does not produce an event with the initial
+ * (or current) state, though nanoapps can use chreUserSettingGetState() for
+ * this purpose.
+ *
+ * @param setting The setting on whose change a notification is desired.
+ * @param enable The nanoapp is registered to receive notifications on a
+ * change in the user settings if this parameter is true, otherwise the
+ * nanoapp receives no further notifications for this setting.
+ *
+ * @since v1.5
+ */
+void chreUserSettingConfigureEvents(uint8_t setting, bool enable);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_USER_SETTINGS_H_ */
diff --git a/chre_api/legacy/v1_8/chre/version.h b/chre_api/legacy/v1_8/chre/version.h
new file mode 100644
index 0000000..6872fdd
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/version.h
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_VERSION_H_
+#define _CHRE_VERSION_H_
+
+/**
+ * @file
+ * Definitions and methods for the versioning of the Context Hub Runtime
+ * Environment.
+ *
+ * The CHRE API versioning pertains to all header files in the CHRE API.
+ */
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Value for version 0.1 of the Context Hub Runtime Environment API interface.
+ *
+ * This is a legacy version of the CHRE API. Version 1.0 is considered the first
+ * official CHRE API version.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_0_1 UINT32_C(0x00010000)
+
+/**
+ * Value for version 1.0 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API shipped with the Android Nougat release.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_0 UINT32_C(0x01000000)
+
+/**
+ * Value for version 1.1 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API shipped with the Android O release. It adds
+ * initial support for new GNSS, WiFi, and WWAN modules.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_1 UINT32_C(0x01010000)
+
+/**
+ * Value for version 1.2 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API shipped with the Android P release. It adds
+ * initial support for the new audio module.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_2 UINT32_C(0x01020000)
+
+/**
+ * Value for version 1.3 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API shipped with the Android Q release. It adds
+ * support for GNSS location altitude/speed/bearing accuracy. It also adds step
+ * detect as a standard CHRE sensor and supports bias event delivery and sensor
+ * data flushing.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_3 UINT32_C(0x01030000)
+
+/**
+ * Value for version 1.4 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API shipped with the Android R release. It adds
+ * support for collecting debug dump information from nanoapps, receiving L5
+ * GNSS measurements, determining if a sensor supports passive requests,
+ * receiving 5G cell info, and deprecates chreSendMessageToHost.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_4 UINT32_C(0x01040000)
+
+/**
+ * Value for version 1.5 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API shipped with the Android S release. It adds
+ * support for multiple sensors of the same type, permissions for sensitive CHRE
+ * APIs / data usage, ability to receive user settings updates, step counter and
+ * hinge angle sensors, improved WiFi scan preferences to support power
+ * optimization, new WiFi security types, increased the lower bound for the
+ * maximum CHRE to host message size, and increased GNSS measurements in
+ * chreGnssDataEvent.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_5 UINT32_C(0x01050000)
+
+/**
+ * Value for version 1.6 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API is shipped with the Android T release. It adds
+ * support for BLE scanning, subscribing to the WiFi NAN discovery engine,
+ * subscribing to host endpoint notifications, requesting metadata for a host
+ * endpoint ID, nanoapps publishing RPC services they support, and limits the
+ * nanoapp instance ID size to INT16_MAX.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_6 UINT32_C(0x01060000)
+
+/**
+ * Value for version 1.7 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API is shipped with a post-launch update to the
+ * Android T release. It adds the BLE flush API.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_7 UINT32_C(0x01070000)
+
+/**
+ * Value for version 1.8 of the Context Hub Runtime Environment API interface.
+ *
+ * This version of the CHRE API is shipped with the Android U release.
+ *
+ * @note This version of the CHRE API has not been finalized yet, and is
+ * currently considered a preview that is subject to change.
+ *
+ * @see CHRE_API_VERSION
+ */
+#define CHRE_API_VERSION_1_8 UINT32_C(0x01080000)
+
+/**
+ * Major and Minor Version of this Context Hub Runtime Environment API.
+ *
+ * The major version changes when there is an incompatible API change.
+ *
+ * The minor version changes when there is an addition in functionality
+ * in a backwards-compatible manner.
+ *
+ * We define the version number as an unsigned 32-bit value.  The most
+ * significant byte is the Major Version.  The second-most significant byte
+ * is the Minor Version.  The two least significant bytes are the Patch
+ * Version.  The Patch Version is not defined by this header API, but
+ * is provided by a specific CHRE implementation (see chreGetVersion()).
+ *
+ * Note that version numbers can always be numerically compared with
+ * expected results, so 1.0.0 < 1.0.4 < 1.1.0 < 2.0.300 < 3.5.0.
+ */
+#define CHRE_API_VERSION CHRE_API_VERSION_1_8
+
+/**
+ * Utility macro to extract only the API major version of a composite CHRE
+ * version.
+ *
+ * @param version A uint32_t version, e.g. the value returned by
+ *     chreGetApiVersion()
+ *
+ * @return The API major version in the least significant byte, e.g. 0x01
+ */
+#define CHRE_EXTRACT_MAJOR_VERSION(version) \
+  (uint32_t)(((version) & UINT32_C(0xFF000000)) >> 24)
+
+/**
+ * Utility macro to extract only the API minor version of a composite CHRE
+ * version.
+ *
+ * @param version A uint32_t version, e.g. the CHRE_API_VERSION constant
+ *
+ * @return The API minor version in the least significant byte, e.g. 0x01
+ */
+#define CHRE_EXTRACT_MINOR_VERSION(version) \
+  (uint32_t)(((version) & UINT32_C(0x00FF0000)) >> 16)
+
+/**
+ * Utility macro to extract only the API minor version of a composite CHRE
+ * version.
+ *
+ * @param version A complete uint32_t version, e.g. the value returned by
+ *     chreGetVersion()
+ *
+ * @return The implementation patch version in the least significant two bytes,
+ *     e.g. 0x0123, with all other bytes set to 0
+ */
+#define CHRE_EXTRACT_PATCH_VERSION(version) (uint32_t)((version) & UINT32_C(0xFFFF))
+
+/**
+ * Get the API version the CHRE implementation was compiled against.
+ *
+ * This is not necessarily the CHRE_API_VERSION in the header the nanoapp was
+ * built against, and indeed may not have even appeared in the context_hub_os.h
+ * header which this nanoapp was built against.
+ *
+ * By definition, this will have the two least significant bytes set to 0,
+ * and only contain the major and minor version number.
+ *
+ * @return The API version.
+ */
+uint32_t chreGetApiVersion(void);
+
+/**
+ * Get the version of this CHRE implementation.
+ *
+ * By definition, ((chreGetApiVersion() & UINT32_C(0xFFFF0000)) ==
+ *                 (chreGetVersion()    & UINT32_C(0xFFFF0000))).
+ *
+ * The Patch Version, in the lower two bytes, only have meaning in context
+ * of this specific platform ID.  It is increased by the platform every time
+ * a backwards-compatible bug fix is released.
+ *
+ * @return The version.
+ *
+ * @see chreGetPlatformId()
+ */
+uint32_t chreGetVersion(void);
+
+/**
+ * Get the Platform ID of this CHRE.
+ *
+ * The most significant five bytes are the vendor ID as set out by the
+ * NANOAPP_VENDOR convention in the original context hub HAL header file
+ * (context_hub.h), also used by nanoapp IDs.
+ *
+ * The least significant three bytes are set by the vendor, but must be
+ * unique for each different CHRE implementation/hardware that the vendor
+ * supplies.
+ *
+ * The idea is that in the case of known bugs in the field, a new nanoapp could
+ * be shipped with a workaround that would use this value, and chreGetVersion(),
+ * to have code that can conditionally work around the bug on a buggy version.
+ * Thus, we require this uniqueness to allow such a setup to work.
+ *
+ * @return The platform ID.
+ *
+ * @see CHRE_EXTRACT_VENDOR_ID
+ */
+uint64_t chreGetPlatformId(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _CHRE_VERSION_H_ */
diff --git a/chre_api/legacy/v1_8/chre/wifi.h b/chre_api/legacy/v1_8/chre/wifi.h
new file mode 100644
index 0000000..b4bda9c
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/wifi.h
@@ -0,0 +1,1313 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_WIFI_H_
+#define _CHRE_WIFI_H_
+
+/**
+ * @file
+ * WiFi (IEEE 802.11) API, currently covering scanning features useful for
+ * determining location and offloading certain connectivity scans.
+ *
+ * In this file, specification references use the following shorthand:
+ *
+ *    Shorthand | Full specification name
+ *   ---------- | ------------------------
+ *     "802.11" | IEEE Std 802.11-2007
+ *     "HT"     | IEEE Std 802.11n-2009
+ *     "VHT"    | IEEE Std 802.11ac-2013
+ *     "WiFi 6" | IEEE Std 802.11ax draft
+ *     "NAN"    | Wi-Fi Neighbor Awareness Networking (NAN) Technical
+ *                Specification (v3.2)
+ *
+ * In the current version of CHRE API, the 6GHz band introduced in WiFi 6 is
+ * not supported. A scan request from CHRE should not result in scanning 6GHz
+ * channels. In particular, if a 6GHz channel is specified in scanning or
+ * ranging request parameter, CHRE should return an error code of
+ * CHRE_ERROR_NOT_SUPPORTED. Additionally, CHRE implementations must not include
+ * observations of access points on 6GHz channels in scan results, especially
+ * those produced due to scan monitoring.
+ */
+
+#include "common.h"
+#include <chre/common.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The set of flags returned by chreWifiGetCapabilities().
+ * @defgroup CHRE_WIFI_CAPABILITIES
+ * @{
+ */
+
+//! No WiFi APIs are supported
+#define CHRE_WIFI_CAPABILITIES_NONE              UINT32_C(0)
+
+//! Listening to scan results is supported, as enabled via
+//! chreWifiConfigureScanMonitorAsync()
+#define CHRE_WIFI_CAPABILITIES_SCAN_MONITORING   UINT32_C(1 << 0)
+
+//! Requesting WiFi scans on-demand is supported via chreWifiRequestScanAsync()
+#define CHRE_WIFI_CAPABILITIES_ON_DEMAND_SCAN    UINT32_C(1 << 1)
+
+//! Specifying the radio chain preference in on-demand scan requests, and
+//! reporting it in scan events is supported
+//! @since v1.2
+#define CHRE_WIFI_CAPABILITIES_RADIO_CHAIN_PREF  UINT32_C(1 << 2)
+
+//! Requesting RTT ranging is supported via chreWifiRequestRangingAsync()
+//! @since v1.2
+#define CHRE_WIFI_CAPABILITIES_RTT_RANGING       UINT32_C(1 << 3)
+
+//! Specifies if WiFi NAN service subscription is supported. If a platform
+//! supports subscriptions, then it must also support RTT ranging for NAN
+//! services via chreWifiNanRequestRangingAsync()
+//! @since v1.6
+#define CHRE_WIFI_CAPABILITIES_NAN_SUB           UINT32_C(1 << 4)
+
+/** @} */
+
+/**
+ * Produce an event ID in the block of IDs reserved for WiFi
+ * @param offset  Index into WiFi event ID block; valid range [0,15]
+ */
+#define CHRE_WIFI_EVENT_ID(offset)  (CHRE_EVENT_WIFI_FIRST_EVENT + (offset))
+
+/**
+ * nanoappHandleEvent argument: struct chreAsyncResult
+ *
+ * Communicates the asynchronous result of a request to the WiFi API. The
+ * requestType field in {@link #chreAsyncResult} is set to a value from enum
+ * chreWifiRequestType.
+ */
+#define CHRE_EVENT_WIFI_ASYNC_RESULT  CHRE_WIFI_EVENT_ID(0)
+
+/**
+ * nanoappHandleEvent argument: struct chreWifiScanEvent
+ *
+ * Provides results of a WiFi scan.
+ */
+#define CHRE_EVENT_WIFI_SCAN_RESULT  CHRE_WIFI_EVENT_ID(1)
+
+/**
+ * nanoappHandleEvent argument: struct chreWifiRangingEvent
+ *
+ * Provides results of an RTT ranging request.
+ */
+#define CHRE_EVENT_WIFI_RANGING_RESULT  CHRE_WIFI_EVENT_ID(2)
+
+/**
+ * nanoappHandleEvent argument: struct chreWifiNanIdentifierEvent
+ *
+ * Lets the client know if the NAN engine was able to successfully assign
+ * an identifier to the subscribe call. The 'cookie' field in the event
+ * argument struct can be used to track which subscribe request this identifier
+ * maps to.
+ */
+#define CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT   CHRE_WIFI_EVENT_ID(3)
+
+/**
+ * nanoappHandleEvent argument: struct chreWifiNanDiscoveryEvent
+ *
+ * Event that is sent whenever a NAN service matches the criteria specified
+ * in a subscription request.
+ */
+#define CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT  CHRE_WIFI_EVENT_ID(4)
+
+/**
+ * nanoappHandleEvent argument: struct chreWifiNanSessionLostEvent
+ *
+ * Informs the client that a discovered service is no longer available or
+ * visible.
+ * The ID of the service on the client that was communicating with the extinct
+ * service is indicated by the event argument.
+ */
+#define CHRE_EVENT_WIFI_NAN_SESSION_LOST  CHRE_WIFI_EVENT_ID(5)
+
+/**
+ * nanoappHandleEvent argument: struct chreWifiNanSessionTerminatedEvent
+ *
+ * Signals the end of a NAN subscription session. The termination can be due to
+ * the user turning the WiFi off, or other platform reasons like not being able
+ * to support NAN concurrency with the host. The terminated event will have a
+ * reason code appropriately populated to denote why the event was sent.
+ */
+#define CHRE_EVENT_WIFI_NAN_SESSION_TERMINATED  CHRE_WIFI_EVENT_ID(6)
+
+// NOTE: Do not add new events with ID > 15; only values 0-15 are reserved
+// (see chre/event.h)
+
+/**
+ * The maximum amount of time that is allowed to elapse between a call to
+ * chreWifiRequestScanAsync() that returns true, and the associated
+ * CHRE_EVENT_WIFI_ASYNC_RESULT used to indicate whether the scan completed
+ * successfully or not.
+ */
+#define CHRE_WIFI_SCAN_RESULT_TIMEOUT_NS  (30 * CHRE_NSEC_PER_SEC)
+
+/**
+ * The maximum amount of time that is allowed to elapse between a call to
+ * chreWifiRequestRangingAsync() that returns true, and the associated
+ * CHRE_EVENT_WIFI_RANGING_RESULT used to indicate whether the ranging operation
+ * completed successfully or not.
+ */
+#define CHRE_WIFI_RANGING_RESULT_TIMEOUT_NS  (30 * CHRE_NSEC_PER_SEC)
+
+/**
+ * The current compatibility version of the chreWifiScanEvent structure,
+ * including nested structures.
+ */
+#define CHRE_WIFI_SCAN_EVENT_VERSION  UINT8_C(1)
+
+/**
+ * The current compatibility version of the chreWifiRangingEvent structure,
+ * including nested structures.
+ */
+#define CHRE_WIFI_RANGING_EVENT_VERSION  UINT8_C(0)
+
+/**
+ * Maximum number of frequencies that can be explicitly specified when
+ * requesting a scan
+ * @see #chreWifiScanParams
+ */
+#define CHRE_WIFI_FREQUENCY_LIST_MAX_LEN  (20)
+
+/**
+ * Maximum number of SSIDs that can be explicitly specified when requesting a
+ * scan
+ * @see #chreWifiScanParams
+ */
+#define CHRE_WIFI_SSID_LIST_MAX_LEN  (20)
+
+/**
+ * The maximum number of devices that can be specified in a single RTT ranging
+ * request.
+ * @see #chreWifiRangingParams
+ */
+#define CHRE_WIFI_RANGING_LIST_MAX_LEN  (10)
+
+/**
+ * The maximum number of octets in an SSID (see 802.11 7.3.2.1)
+ */
+#define CHRE_WIFI_SSID_MAX_LEN  (32)
+
+/**
+ * The number of octets in a BSSID (see 802.11 7.1.3.3.3)
+ */
+#define CHRE_WIFI_BSSID_LEN  (6)
+
+/**
+ * Set of flags which can either indicate a frequency band. Specified as a bit
+ * mask to allow for combinations in future API versions.
+ * @defgroup CHRE_WIFI_BAND_MASK
+ * @{
+ */
+
+#define CHRE_WIFI_BAND_MASK_2_4_GHZ  UINT8_C(1 << 0)  //!< 2.4 GHz
+#define CHRE_WIFI_BAND_MASK_5_GHZ    UINT8_C(1 << 1)  //!< 5 GHz
+
+/** @} */
+
+/**
+ * Characteristics of a scanned device given in struct chreWifiScanResult.flags
+ * @defgroup CHRE_WIFI_SCAN_RESULT_FLAGS
+ * @{
+ */
+
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_NONE                         UINT8_C(0)
+
+//! Element ID 61 (HT Operation) is present (see HT 7.3.2)
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_HT_OPS_PRESENT               UINT8_C(1 << 0)
+
+//! Element ID 192 (VHT Operation) is present (see VHT 8.4.2)
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_VHT_OPS_PRESENT              UINT8_C(1 << 1)
+
+//! Element ID 127 (Extended Capabilities) is present, and bit 70 (Fine Timing
+//! Measurement Responder) is set to 1 (see IEEE Std 802.11-2016 9.4.2.27)
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_IS_FTM_RESPONDER             UINT8_C(1 << 2)
+
+//! Retained for backwards compatibility
+//! @see CHRE_WIFI_SCAN_RESULT_FLAGS_IS_FTM_RESPONDER
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_IS_80211MC_RTT_RESPONDER \
+    CHRE_WIFI_SCAN_RESULT_FLAGS_IS_FTM_RESPONDER
+
+//! HT Operation element indicates that a secondary channel is present
+//! (see HT 7.3.2.57)
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_HAS_SECONDARY_CHANNEL_OFFSET UINT8_C(1 << 3)
+
+//! HT Operation element indicates that the secondary channel is below the
+//! primary channel (see HT 7.3.2.57)
+#define CHRE_WIFI_SCAN_RESULT_FLAGS_SECONDARY_CHANNEL_OFFSET_IS_BELOW  \
+                                                                 UINT8_C(1 << 4)
+
+/** @} */
+
+/**
+ * Identifies the authentication methods supported by an AP. Note that not every
+ * combination of flags may be possible. Based on WIFI_PNO_AUTH_CODE_* from
+ * hardware/libhardware_legacy/include/hardware_legacy/gscan.h in Android.
+ * @defgroup CHRE_WIFI_SECURITY_MODE_FLAGS
+ * @{
+ */
+
+#define CHRE_WIFI_SECURITY_MODE_UNKONWN  UINT8_C(0)
+
+#define CHRE_WIFI_SECURITY_MODE_OPEN  UINT8_C(1 << 0)  //!< No auth/security
+#define CHRE_WIFI_SECURITY_MODE_WEP   UINT8_C(1 << 1)
+#define CHRE_WIFI_SECURITY_MODE_PSK   UINT8_C(1 << 2)  //!< WPA-PSK or WPA2-PSK
+#define CHRE_WIFI_SECURITY_MODE_EAP   UINT8_C(1 << 3)  //!< WPA-EAP or WPA2-EAP
+
+//! @since v1.5
+#define CHRE_WIFI_SECURITY_MODE_SAE   UINT8_C(1 << 4)
+
+//! @since v1.5
+#define CHRE_WIFI_SECURITY_MODE_EAP_SUITE_B  UINT8_C(1 << 5)
+
+//! @since v1.5
+#define CHRE_WIFI_SECURITY_MODE_OWE   UINT8_C(1 << 6)
+
+/** @} */
+
+/**
+ * Identifies which radio chain was used to discover an AP. The underlying
+ * hardware does not necessarily support more than one radio chain.
+ * @defgroup CHRE_WIFI_RADIO_CHAIN_FLAGS
+ * @{
+ */
+
+#define CHRE_WIFI_RADIO_CHAIN_UNKNOWN  UINT8_C(0)
+#define CHRE_WIFI_RADIO_CHAIN_0        UINT8_C(1 << 0)
+#define CHRE_WIFI_RADIO_CHAIN_1        UINT8_C(1 << 1)
+
+/** @} */
+
+//! Special value indicating that an LCI uncertainty fields is not provided
+//! Ref: RFC 6225
+#define CHRE_WIFI_LCI_UNCERTAINTY_UNKNOWN  UINT8_C(0)
+
+/**
+ * Defines the flags that may be returned in
+ * {@link #chreWifiRangingResult.flags}. Undefined bits are reserved for future
+ * use and must be ignored by nanoapps.
+ * @defgroup CHRE_WIFI_RTT_RESULT_FLAGS
+ * @{
+ */
+
+//! If set, the nested chreWifiLci structure is populated; otherwise it is
+//! invalid and must be ignored
+#define CHRE_WIFI_RTT_RESULT_HAS_LCI  UINT8_C(1 << 0)
+
+/** @} */
+
+/**
+ * Identifies a WiFi frequency band
+ */
+enum chreWifiBand {
+    CHRE_WIFI_BAND_2_4_GHZ = CHRE_WIFI_BAND_MASK_2_4_GHZ,
+    CHRE_WIFI_BAND_5_GHZ   = CHRE_WIFI_BAND_MASK_5_GHZ,
+};
+
+/**
+ * Indicates the BSS operating channel width determined from the VHT and/or HT
+ * Operation elements. Refer to VHT 8.4.2.161 and HT 7.3.2.57.
+ */
+enum chreWifiChannelWidth {
+    CHRE_WIFI_CHANNEL_WIDTH_20_MHZ         = 0,
+    CHRE_WIFI_CHANNEL_WIDTH_40_MHZ         = 1,
+    CHRE_WIFI_CHANNEL_WIDTH_80_MHZ         = 2,
+    CHRE_WIFI_CHANNEL_WIDTH_160_MHZ        = 3,
+    CHRE_WIFI_CHANNEL_WIDTH_80_PLUS_80_MHZ = 4,
+};
+
+/**
+ * Indicates the type of scan requested or performed
+ */
+enum chreWifiScanType {
+    //! Perform a purely active scan using probe requests. Do not scan channels
+    //! restricted to use via Dynamic Frequency Selection (DFS) only.
+    CHRE_WIFI_SCAN_TYPE_ACTIVE = 0,
+
+    //! Perform an active scan on unrestricted channels, and also perform a
+    //! passive scan on channels that are restricted to use via Dynamic
+    //! Frequency Selection (DFS), e.g. the U-NII bands 5250-5350MHz and
+    //! 5470-5725MHz in the USA as mandated by FCC regulation.
+    CHRE_WIFI_SCAN_TYPE_ACTIVE_PLUS_PASSIVE_DFS = 1,
+
+    //! Perform a passive scan, only listening for beacons.
+    CHRE_WIFI_SCAN_TYPE_PASSIVE = 2,
+
+    //! Client has no preference for a particular scan type.
+    //! Only valid in a {@link #chreWifiScanParams}.
+    //!
+    //! On a v1.4 or earlier platform, this will fall back to
+    //! CHRE_WIFI_SCAN_TYPE_ACTIVE if {@link #chreWifiScanParams.channelSet} is
+    //! set to CHRE_WIFI_CHANNEL_SET_NON_DFS, and to
+    //! CHRE_WIFI_SCAN_TYPE_ACTIVE_PLUS_PASSIVE_DFS otherwise.
+    //!
+    //! If CHRE_WIFI_CAPABILITIES_RADIO_CHAIN_PREF is supported, a v1.5 or
+    //! later platform shall perform a type of scan optimized for {@link
+    //! #chreWifiScanParams.radioChainPref}.
+    //!
+    //! Clients are strongly encouraged to set this value in {@link
+    //! #chreWifiScanParams.scanType} and instead express their preferences
+    //! through {@link #chreWifiRadioChainPref} and {@link #chreWifiChannelSet}
+    //! so the platform can best optimize power and performance.
+    //!
+    //! @since v1.5
+    CHRE_WIFI_SCAN_TYPE_NO_PREFERENCE = 3,
+};
+
+/**
+ * Indicates whether RTT ranging with a specific device succeeded
+ */
+enum chreWifiRangingStatus {
+    //! Ranging completed successfully
+    CHRE_WIFI_RANGING_STATUS_SUCCESS = 0,
+
+    //! Ranging failed due to an unspecified error
+    CHRE_WIFI_RANGING_STATUS_ERROR   = 1,
+};
+
+/**
+ * Possible values for {@link #chreWifiLci.altitudeType}. Ref: RFC 6225 2.4
+ */
+enum chreWifiLciAltitudeType {
+    CHRE_WIFI_LCI_ALTITUDE_TYPE_UNKNOWN = 0,
+    CHRE_WIFI_LCI_ALTITUDE_TYPE_METERS  = 1,
+    CHRE_WIFI_LCI_ALTITUDE_TYPE_FLOORS  = 2,
+};
+
+/**
+ * Indicates a type of request made in this API. Used to populate the resultType
+ * field of struct chreAsyncResult sent with CHRE_EVENT_WIFI_ASYNC_RESULT.
+ */
+enum chreWifiRequestType {
+    CHRE_WIFI_REQUEST_TYPE_CONFIGURE_SCAN_MONITOR = 1,
+    CHRE_WIFI_REQUEST_TYPE_REQUEST_SCAN           = 2,
+    CHRE_WIFI_REQUEST_TYPE_RANGING                = 3,
+    CHRE_WIFI_REQUEST_TYPE_NAN_SUBSCRIBE          = 4,
+};
+
+/**
+ * Allows a nanoapp to express its preference for how multiple available
+ * radio chains should be used when performing an on-demand scan. This is only a
+ * preference from the nanoapp and is not guaranteed to be honored by the WiFi
+ * firmware.
+ */
+enum chreWifiRadioChainPref {
+    //! No preference for radio chain usage
+    CHRE_WIFI_RADIO_CHAIN_PREF_DEFAULT = 0,
+
+    //! In a scan result, indicates that the radio chain preference used for the
+    //! scan is not known
+    CHRE_WIFI_RADIO_CHAIN_PREF_UNKNOWN = CHRE_WIFI_RADIO_CHAIN_PREF_DEFAULT,
+
+    //! Prefer to use available radio chains in a way that minimizes time to
+    //! complete the scan
+    CHRE_WIFI_RADIO_CHAIN_PREF_LOW_LATENCY = 1,
+
+    //! Prefer to use available radio chains in a way that minimizes total power
+    //! consumed for the scan
+    CHRE_WIFI_RADIO_CHAIN_PREF_LOW_POWER = 2,
+
+    //! Prefer to use available radio chains in a way that maximizes accuracy of
+    //! the scan result, e.g. RSSI measurements
+    CHRE_WIFI_RADIO_CHAIN_PREF_HIGH_ACCURACY = 3,
+};
+
+/**
+ * WiFi NAN subscription type.
+ */
+enum chreWifiNanSubscribeType {
+    //! In the active mode, explicit transmission of a subscribe message is
+    //! requested, and publish messages are processed.
+    CHRE_WIFI_NAN_SUBSCRIBE_TYPE_ACTIVE = 0,
+
+    //! In the passive mode, no transmission of a subscribe message is
+    //! requested, but received publish messages are checked for matches.
+    CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE = 1,
+};
+
+/**
+ * Indicates the reason for a subscribe session termination.
+ */
+enum chreWifiNanTerminatedReason {
+    CHRE_WIFI_NAN_TERMINATED_BY_USER_REQUEST = 0,
+    CHRE_WIFI_NAN_TERMINATED_BY_TIMEOUT = 1,
+    CHRE_WIFI_NAN_TERMINATED_BY_FAILURE = 2,
+};
+
+/**
+ * SSID with an explicit length field, used when an array of SSIDs is supplied.
+ */
+struct chreWifiSsidListItem {
+    //! Number of valid bytes in ssid. Valid range [0, CHRE_WIFI_SSID_MAX_LEN]
+    uint8_t ssidLen;
+
+    //! Service Set Identifier (SSID)
+    uint8_t ssid[CHRE_WIFI_SSID_MAX_LEN];
+};
+
+/**
+ * Indicates the set of channels to be scanned.
+ *
+ * @since v1.5
+ */
+enum chreWifiChannelSet {
+    //! The set of channels that allows active scan using probe request.
+    CHRE_WIFI_CHANNEL_SET_NON_DFS = 0,
+
+    //! The set of all channels supported.
+    CHRE_WIFI_CHANNEL_SET_ALL = 1,
+};
+
+/**
+ * Data structure passed to chreWifiRequestScanAsync
+ */
+struct chreWifiScanParams {
+    //! Set to a value from @ref enum chreWifiScanType
+    uint8_t scanType;
+
+    //! Indicates whether the client is willing to tolerate receiving cached
+    //! results of a previous scan, and if so, the maximum age of the scan that
+    //! the client will accept. "Age" in this case is defined as the elapsed
+    //! time between when the most recent scan was completed and the request is
+    //! received, in milliseconds. If set to 0, no cached results may be
+    //! provided, and all scan results must come from a "fresh" WiFi scan, i.e.
+    //! one that completes strictly after this request is received. If more than
+    //! one scan is cached and meets this age threshold, only the newest scan is
+    //! provided.
+    uint32_t maxScanAgeMs;
+
+    //! If set to 0, scan all frequencies. Otherwise, this indicates the number
+    //! of frequencies to scan, as specified in the frequencyList array. Valid
+    //! range [0, CHRE_WIFI_FREQUENCY_LIST_MAX_LEN].
+    uint16_t frequencyListLen;
+
+    //! Pointer to an array of frequencies to scan, given as channel center
+    //! frequencies in MHz. This field may be NULL if frequencyListLen is 0.
+    const uint32_t *frequencyList;
+
+    //! If set to 0, do not restrict scan to any SSIDs. Otherwise, this
+    //! indicates the number of SSIDs in the ssidList array to be used for
+    //! directed probe requests. Not applicable and ignore when scanType is
+    //! CHRE_WIFI_SCAN_TYPE_PASSIVE.
+    uint8_t ssidListLen;
+
+    //! Pointer to an array of SSIDs to use for directed probe requests. May be
+    //! NULL if ssidListLen is 0.
+    const struct chreWifiSsidListItem *ssidList;
+
+    //! Set to a value from enum chreWifiRadioChainPref to specify the desired
+    //! trade-off between power consumption, accuracy, etc. If
+    //! chreWifiGetCapabilities() does not have the applicable bit set, this
+    //! parameter is ignored.
+    //! @since v1.2
+    uint8_t radioChainPref;
+
+    //! Set to a value from enum chreWifiChannelSet to specify the set of
+    //! channels to be scanned. This field is considered by the platform only
+    //! if scanType is CHRE_WIFI_SCAN_TYPE_NO_PREFERENCE and frequencyListLen
+    //! is equal to zero.
+    //!
+    //! @since v1.5
+    uint8_t channelSet;
+};
+
+/**
+ * Provides information about a single access point (AP) detected in a scan.
+ */
+struct chreWifiScanResult {
+    //! Number of milliseconds prior to referenceTime in the enclosing
+    //! chreWifiScanEvent struct when the probe response or beacon frame that
+    //! was used to populate this structure was received.
+    uint32_t ageMs;
+
+    //! Capability Information field sent by the AP (see 802.11 7.3.1.4). This
+    //! field must reflect native byte order and bit ordering, such that
+    //! (capabilityInfo & 1) gives the bit for the ESS subfield.
+    uint16_t capabilityInfo;
+
+    //! Number of valid bytes in ssid. Valid range [0, CHRE_WIFI_SSID_MAX_LEN]
+    uint8_t ssidLen;
+
+    //! Service Set Identifier (SSID), a series of 0 to 32 octets identifying
+    //! the access point. Note that this is commonly a human-readable ASCII
+    //! string, but this is not the required encoding per the standard.
+    uint8_t ssid[CHRE_WIFI_SSID_MAX_LEN];
+
+    //! Basic Service Set Identifier (BSSID), represented in big-endian byte
+    //! order, such that the first octet of the OUI is accessed in byte index 0.
+    uint8_t bssid[CHRE_WIFI_BSSID_LEN];
+
+    //! A set of flags from CHRE_WIFI_SCAN_RESULT_FLAGS_*
+    uint8_t flags;
+
+    //! RSSI (Received Signal Strength Indicator), in dBm. Typically negative.
+    //! If multiple radio chains were used to scan this AP, this is a "best
+    //! available" measure that may be a composite of measurements taken across
+    //! the radio chains.
+    int8_t  rssi;
+
+    //! Operating band, set to a value from enum chreWifiBand
+    uint8_t band;
+
+    /**
+     * Indicates the center frequency of the primary 20MHz channel, given in
+     * MHz. This value is derived from the channel number via the formula:
+     *
+     *     primaryChannel (MHz) = CSF + 5 * primaryChannelNumber
+     *
+     * Where CSF is the channel starting frequency (in MHz) given by the
+     * operating class/band (i.e. 2407 or 5000), and primaryChannelNumber is the
+     * channel number in the range [1, 200].
+     *
+     * Refer to VHT 22.3.14.
+     */
+    uint32_t primaryChannel;
+
+    /**
+     * If the channel width is 20 MHz, this field is not relevant and set to 0.
+     * If the channel width is 40, 80, or 160 MHz, then this denotes the channel
+     * center frequency (in MHz). If the channel is 80+80 MHz, then this denotes
+     * the center frequency of segment 0, which contains the primary channel.
+     * This value is derived from the frequency index using the same formula as
+     * for primaryChannel.
+     *
+     * Refer to VHT 8.4.2.161, and VHT 22.3.14.
+     *
+     * @see #primaryChannel
+     */
+    uint32_t centerFreqPrimary;
+
+    /**
+     * If the channel width is 80+80MHz, then this denotes the center frequency
+     * of segment 1, which does not contain the primary channel. Otherwise, this
+     * field is not relevant and set to 0.
+     *
+     * @see #centerFreqPrimary
+     */
+    uint32_t centerFreqSecondary;
+
+    //! @see #chreWifiChannelWidth
+    uint8_t channelWidth;
+
+    //! Flags from CHRE_WIFI_SECURITY_MODE_* indicating supported authentication
+    //! and associated security modes
+    //! @see CHRE_WIFI_SECURITY_MODE_FLAGS
+    uint8_t securityMode;
+
+    //! Identifies the radio chain(s) used to discover this AP
+    //! @see CHRE_WIFI_RADIO_CHAIN_FLAGS
+    //! @since v1.2
+    uint8_t radioChain;
+
+    //! If the CHRE_WIFI_RADIO_CHAIN_0 bit is set in radioChain, gives the RSSI
+    //! measured on radio chain 0 in dBm; otherwise invalid and set to 0. This
+    //! field, along with its relative rssiChain1, can be used to determine RSSI
+    //! measurements from each radio chain when multiple chains were used to
+    //! discover this AP.
+    //! @see #radioChain
+    //! @since v1.2
+    int8_t rssiChain0;
+    int8_t rssiChain1;  //!< @see #rssiChain0
+
+    //! Reserved; set to 0
+    uint8_t reserved[7];
+};
+
+/**
+ * Data structure sent with events of type CHRE_EVENT_WIFI_SCAN_RESULT.
+ */
+struct chreWifiScanEvent {
+    //! Indicates the version of the structure, for compatibility purposes.
+    //! Clients do not normally need to worry about this field; the CHRE
+    //! implementation guarantees that the client only receives the structure
+    //! version it expects.
+    uint8_t version;
+
+    //! The number of entries in the results array in this event. The CHRE
+    //! implementation may split scan results across multiple events for memory
+    //! concerns, etc.
+    uint8_t resultCount;
+
+    //! The total number of results returned by the scan. Allows an event
+    //! consumer to identify when it has received all events associated with a
+    //! scan.
+    uint8_t resultTotal;
+
+    //! Sequence number for this event within the series of events comprising a
+    //! complete scan result. Scan events are delivered strictly in order, i.e.
+    //! this is monotonically increasing for the results of a single scan. Valid
+    //! range [0, <number of events for scan> - 1]. The number of events for a
+    //! scan is typically given by
+    //! ceil(resultTotal / <max results per event supported by platform>).
+    uint8_t eventIndex;
+
+    //! A value from enum chreWifiScanType indicating the type of scan performed
+    uint8_t scanType;
+
+    //! If a directed scan was performed to a limited set of SSIDs, then this
+    //! identifies the number of unique SSIDs included in the probe requests.
+    //! Otherwise, this is set to 0, indicating that the scan was not limited by
+    //! SSID. Note that if this is non-zero, the list of SSIDs used is not
+    //! included in the scan event.
+    uint8_t ssidSetSize;
+
+    //! If 0, indicates that all frequencies applicable for the scanType were
+    //! scanned. Otherwise, indicates the number of frequencies scanned, as
+    //! specified in scannedFreqList.
+    uint16_t scannedFreqListLen;
+
+    //! Timestamp when the scan was completed, from the same time base as
+    //! chreGetTime() (in nanoseconds)
+    uint64_t referenceTime;
+
+    //! Pointer to an array containing scannedFreqListLen values comprising the
+    //! set of frequencies that were scanned. Frequencies are specified as
+    //! channel center frequencies in MHz. May be NULL if scannedFreqListLen is
+    //! 0.
+    const uint32_t *scannedFreqList;
+
+    //! Pointer to an array containing resultCount entries. May be NULL if
+    //! resultCount is 0.
+    const struct chreWifiScanResult *results;
+
+    //! Set to a value from enum chreWifiRadioChainPref indicating the radio
+    //! chain preference used for the scan. If the applicable bit is not set in
+    //! chreWifiGetCapabilities(), this will always be set to
+    //! CHRE_WIFI_RADIO_CHAIN_PREF_UNKNOWN.
+    //! @since v1.2
+    uint8_t radioChainPref;
+};
+
+/**
+ * Identifies a device to perform RTT ranging against. These values are normally
+ * populated based on the contents of a scan result.
+ * @see #chreWifiScanResult
+ * @see chreWifiRangingTargetFromScanResult()
+ */
+struct chreWifiRangingTarget {
+    //! Device MAC address, specified in the same byte order as
+    //! {@link #chreWifiScanResult.bssid}
+    uint8_t macAddress[CHRE_WIFI_BSSID_LEN];
+
+    //! Center frequency of the primary 20MHz channel, in MHz
+    //! @see #chreWifiScanResult.primaryChannel
+    uint32_t primaryChannel;
+
+    //! Channel center frequency, in MHz, or 0 if not relevant
+    //! @see #chreWifiScanResult.centerFreqPrimary
+    uint32_t centerFreqPrimary;
+
+    //! Channel center frequency of segment 1 if channel width is 80+80MHz,
+    //! otherwise 0
+    //! @see #chreWifiScanResult.centerFreqSecondary
+    uint32_t centerFreqSecondary;
+
+    //! @see #chreWifiChannelWidth
+    uint8_t channelWidth;
+
+    //! Reserved for future use and ignored by CHRE
+    uint8_t reserved[3];
+};
+
+/**
+ * Parameters for an RTT ("Fine Timing Measurement" in terms of 802.11-2016)
+ * ranging request, supplied to chreWifiRequestRangingAsync().
+ */
+struct chreWifiRangingParams {
+    //! Number of devices to perform ranging against and the length of
+    //! targetList, in range [1, CHRE_WIFI_RANGING_LIST_MAX_LEN].
+    uint8_t targetListLen;
+
+    //! Array of macAddressListLen MAC addresses (e.g. BSSIDs) with which to
+    //! attempt RTT ranging.
+    const struct chreWifiRangingTarget *targetList;
+};
+
+/**
+ * Provides the result of RTT ranging with a single device.
+ */
+struct chreWifiRangingResult {
+    //! Time when the ranging operation on this device was performed, in the
+    //! same time base as chreGetTime() (in nanoseconds)
+    uint64_t timestamp;
+
+    //! MAC address of the device for which ranging was requested
+    uint8_t macAddress[CHRE_WIFI_BSSID_LEN];
+
+    //! Gives the result of ranging to this device. If not set to
+    //! CHRE_WIFI_RANGING_STATUS_SUCCESS, the ranging attempt to this device
+    //! failed, and other fields in this structure may be invalid.
+    //! @see #chreWifiRangingStatus
+    uint8_t status;
+
+    //! The mean RSSI measured during the RTT burst, in dBm. Typically negative.
+    //! If status is not CHRE_WIFI_RANGING_STATUS_SUCCESS, will be set to 0.
+    int8_t rssi;
+
+    //! Estimated distance to the device with the given BSSID, in millimeters.
+    //! Generally the mean of multiple measurements performed in a single burst.
+    //! If status is not CHRE_WIFI_RANGING_STATUS_SUCCESS, will be set to 0.
+    uint32_t distance;
+
+    //! Standard deviation of estimated distance across multiple measurements
+    //! performed in a single RTT burst, in millimeters. If status is not
+    //! CHRE_WIFI_RANGING_STATUS_SUCCESS, will be set to 0.
+    uint32_t distanceStdDev;
+
+    //! Location Configuration Information (LCI) information optionally returned
+    //! during the ranging procedure. Only valid if {@link #flags} has the
+    //! CHRE_WIFI_RTT_RESULT_HAS_LCI bit set. Refer to IEEE 802.11-2016
+    //! 9.4.2.22.10, 11.24.6.7, and RFC 6225 (July 2011) for more information.
+    //! Coordinates are to be interpreted according to the WGS84 datum.
+    struct chreWifiLci {
+        //! Latitude in degrees as 2's complement fixed-point with 25 fractional
+        //! bits, i.e. degrees * 2^25. Ref: RFC 6225 2.3
+        int64_t latitude;
+
+        //! Longitude, same format as {@link #latitude}
+        int64_t longitude;
+
+        //! Altitude represented as a 2's complement fixed-point value with 8
+        //! fractional bits. Interpretation depends on {@link #altitudeType}. If
+        //! UNKNOWN, this field must be ignored. If *METERS, distance relative
+        //! to the zero point in the vertical datum. If *FLOORS, a floor value
+        //! relative to the ground floor, potentially fractional, e.g. to
+        //! indicate mezzanine levels. Ref: RFC 6225 2.4
+        int32_t altitude;
+
+        //! Maximum extent of latitude uncertainty in degrees, decoded via this
+        //! formula: 2 ^ (8 - x) where "x" is the encoded value passed in this
+        //! field. Unknown if set to CHRE_WIFI_LCI_UNCERTAINTY_UNKNOWN.
+        //! Ref: RFC 6225 2.3.2
+        uint8_t latitudeUncertainty;
+
+        //! @see #latitudeUncertainty
+        uint8_t longitudeUncertainty;
+
+        //! Defines how to interpret altitude, set to a value from enum
+        //! chreWifiLciAltitudeType
+        uint8_t altitudeType;
+
+        //! Uncertainty in altitude, decoded via this formula: 2 ^ (21 - x)
+        //! where "x" is the encoded value passed in this field. Unknown if set
+        //! to CHRE_WIFI_LCI_UNCERTAINTY_UNKNOWN. Only applies when altitudeType
+        //! is CHRE_WIFI_LCI_ALTITUDE_TYPE_METERS. Ref: RFC 6225 2.4.5
+        uint8_t altitudeUncertainty;
+    } lci;
+
+    //! Refer to CHRE_WIFI_RTT_RESULT_FLAGS
+    uint8_t flags;
+
+    //! Reserved; set to 0
+    uint8_t reserved[7];
+};
+
+/**
+ * Data structure sent with events of type CHRE_EVENT_WIFI_RANGING_RESULT.
+ */
+struct chreWifiRangingEvent {
+    //! Indicates the version of the structure, for compatibility purposes.
+    //! Clients do not normally need to worry about this field; the CHRE
+    //! implementation guarantees that the client only receives the structure
+    //! version it expects.
+    uint8_t version;
+
+    //! The number of ranging results included in the results array; matches the
+    //! number of MAC addresses specified in the request
+    uint8_t resultCount;
+
+    //! Reserved; set to 0
+    uint8_t reserved[2];
+
+    //! Pointer to an array containing resultCount entries
+    const struct chreWifiRangingResult *results;
+};
+
+/**
+ * Indicates the WiFi NAN capabilities of the device. Must contain non-zero
+ * values if WiFi NAN is supported.
+ */
+struct chreWifiNanCapabilities {
+    //! Maximum length of the match filter arrays (applies to both tx and rx
+    //! match filters).
+    uint32_t maxMatchFilterLength;
+
+    //! Maximum length of the service specific information byte array.
+    uint32_t maxServiceSpecificInfoLength;
+
+    //! Maximum length of the service name. Includes the NULL terminator.
+    uint8_t maxServiceNameLength;
+
+    //! Reserved for future use.
+    uint8_t reserved[3];
+};
+
+/**
+ * Data structure sent with events of type
+ * CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT
+ */
+struct chreWifiNanIdentifierEvent {
+    //! A unique ID assigned by the NAN engine for the subscribe request
+    //! associated with the cookie encapsulated in the async result below. The
+    //! ID is set to 0 if there was a request failure in which case the async
+    //! result below contains the appropriate error code indicating the failure
+    //! reason.
+    uint32_t id;
+
+    //! Structure which contains the cookie associated with the publish/
+    //! subscribe request, along with an error code that indicates request
+    //! success or failure.
+    struct chreAsyncResult result;
+};
+
+/**
+ * Indicates the desired configuration for a WiFi NAN ranging request.
+ */
+struct chreWifiNanRangingParams {
+    //! MAC address of the NAN device for which range is to be determined.
+    uint8_t macAddress[CHRE_WIFI_BSSID_LEN];
+};
+
+/**
+ * Configuration parameters specific to the Subscribe Function (Spec 4.1.1.1)
+ */
+struct chreWifiNanSubscribeConfig {
+    //! Indicates the subscribe type, set to a value from @ref
+    //! chreWifiNanSubscribeType.
+    uint8_t subscribeType;
+
+    //! UTF-8 name string that identifies the service/application. Must be NULL
+    //! terminated. Note that the string length cannot be greater than the
+    //! maximum length specified by @ref chreWifiNanCapabilities. No
+    //! restriction is placed on the string case, since the service name
+    //! matching is expected to be case insensitive.
+    const char *service;
+
+    //! An array of bytes (and the associated array length) of service-specific
+    //! information. Note that the array length must be less than the
+    //! maxServiceSpecificInfoLength parameter obtained from the NAN
+    //! capabilities (@see struct chreWifiNanCapabilities).
+    const uint8_t *serviceSpecificInfo;
+    uint32_t serviceSpecificInfoSize;
+
+    //! Ordered sequence of {length | value} pairs that specify match criteria
+    //! beyond the service name. 'length' uses 1 byte, and its value indicates
+    //! the number of bytes of the match criteria that follow. The length of
+    //! the match filter array should not exceed the maximum match filter
+    //! length obtained from @ref chreWifiNanGetCapabilities. When a service
+    //! publish message discovery frame containing the Service ID being
+    //! subscribed to is received, the matching is done as follows:
+    //! Each {length | value} pair in the kth position (1 <= k <= #length-value
+    //! pairs) is compared against the kth {length | value} pair in the
+    //! matching filter field of the publish message.
+    //! - For a kth position {length | value} pair in the rx match filter with
+    //!   a length of 0, a match is declared regardless of the tx match filter
+    //!   contents.
+    //! - For a kth position {length | value} pair in the rx match with a non-
+    //!   zero length, there must be an exact match with the kth position pair
+    //!    in the match filter field of the received service descriptor for a
+    //!    match to be found.
+    //! Please refer to Appendix H of the NAN spec for examples on matching.
+    //! The match filter length should not exceed the maxMatchFilterLength
+    //! obtained from @ref chreWifiNanCapabilities.
+    const uint8_t *matchFilter;
+    uint32_t matchFilterLength;
+};
+
+/**
+ * Data structure sent with events of type
+ * CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT.
+ */
+struct chreWifiNanDiscoveryEvent {
+    //! Identifier of the subscribe function instance that requested a
+    //! discovery.
+    uint32_t subscribeId;
+
+    //! Identifier of the publisher on the remote NAN device.
+    uint32_t publishId;
+
+    //! NAN interface address of the publisher
+    uint8_t publisherAddress[CHRE_WIFI_BSSID_LEN];
+
+    //! An array of bytes (and the associated array length) of service-specific
+    //! information. Note that the array length must be less than the
+    //! maxServiceSpecificInfoLength parameter obtained from the NAN
+    //! capabilities (@see struct chreWifiNanCapabilities).
+    const uint8_t *serviceSpecificInfo;
+    uint32_t serviceSpecificInfoSize;
+};
+
+/**
+ * Data structure sent with events of type CHRE_EVENT_WIFI_NAN_SESSION_LOST.
+ */
+struct chreWifiNanSessionLostEvent {
+    //! The original ID (returned by the NAN discovery engine) of the subscriber
+    //! instance.
+    uint32_t id;
+
+    //! The ID of the previously discovered publisher on a peer NAN device that
+    //! is no longer connected.
+    uint32_t peerId;
+};
+
+/**
+ * Data structure sent with events of type
+ * CHRE_EVENT_WIFI_NAN_SESSION_TERMINATED.
+ */
+struct chreWifiNanSessionTerminatedEvent {
+    //! The original ID (returned by the NAN discovery engine) of the subscriber
+    //! instance that was terminated.
+    uint32_t id;
+
+    //! A value that maps to one of the termination reasons in @ref enum
+    //! chreWifiNanTerminatedReason.
+    uint8_t reason;
+
+    //! Reserved for future use.
+    uint8_t reserved[3];
+};
+
+/**
+ * Retrieves a set of flags indicating the WiFi features supported by the
+ * current CHRE implementation. The value returned by this function must be
+ * consistent for the entire duration of the Nanoapp's execution.
+ *
+ * The client must allow for more flags to be set in this response than it knows
+ * about, for example if the implementation supports a newer version of the API
+ * than the client was compiled against.
+ *
+ * @return A bitmask with zero or more CHRE_WIFI_CAPABILITIES_* flags set
+ *
+ * @since v1.1
+ */
+uint32_t chreWifiGetCapabilities(void);
+
+/**
+ * Retrieves device-specific WiFi NAN capabilities, and populates them in
+ * the @ref chreWifiNanCapabilities structure.
+ *
+ * @param capabilities Structure into which the WiFi NAN capabilities of
+ *        the device are populated into. Must not be NULL.
+ * @return true if WiFi NAN is supported, false otherwise.
+ *
+ * @since v1.6
+ */
+bool chreWifiNanGetCapabilities(struct chreWifiNanCapabilities *capabilities);
+
+/**
+ * Nanoapps must define CHRE_NANOAPP_USES_WIFI somewhere in their build
+ * system (e.g. Makefile) if the nanoapp needs to use the following WiFi APIs.
+ * In addition to allowing access to these APIs, defining this macro will also
+ * ensure CHRE enforces that all host clients this nanoapp talks to have the
+ * required Android permissions needed to listen to WiFi data by adding metadata
+ * to the nanoapp.
+ */
+#if defined(CHRE_NANOAPP_USES_WIFI) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+/**
+ * Manages a client's request to receive the results of WiFi scans performed for
+ * other purposes, for example scans done to maintain connectivity and scans
+ * requested by other clients. The presence of this request has no effect on the
+ * frequency or configuration of the WiFi scans performed - it is purely a
+ * registration by the client to receive the results of scans that would
+ * otherwise occur normally. This should include all available scan results,
+ * including those that are not normally sent to the applications processor,
+ * such as Preferred Network Offload (PNO) scans. Scan results provided because
+ * of this registration must not contain cached results - they are always
+ * expected to contain the fresh results from a recent scan.
+ *
+ * An active scan monitor subscription must persist across temporary conditions
+ * under which no WiFi scans will be performed, for example if WiFi is
+ * completely disabled via user-controlled settings, or if the WiFi system
+ * restarts independently of CHRE. Likewise, a request to enable a scan monitor
+ * subscription must succeed under normal conditions, even in circumstances
+ * where no WiFi scans will be performed. In these cases, the scan monitor
+ * implementation must produce scan results once the temporary condition is
+ * cleared, for example after WiFi is enabled by the user.
+ *
+ * These scan results are delivered to the Nanoapp's handle event callback using
+ * CHRE_EVENT_WIFI_SCAN_RESULT.
+ *
+ * An active scan monitor subscription is not necessary to receive the results
+ * of an on-demand scan request sent via chreWifiRequestScanAsync(), and it does
+ * not result in duplicate delivery of scan results generated from
+ * chreWifiRequestScanAsync().
+ *
+ * If no monitor subscription is active at the time of a request with
+ * enable=false, it is treated as if an active subscription was successfully
+ * ended.
+ *
+ * The result of this request is delivered asynchronously via an event of type
+ * CHRE_EVENT_WIFI_ASYNC_RESULT. Refer to the note in {@link #chreAsyncResult}
+ * for more details.
+ *
+ * @param enable Set to true to enable monitoring scan results, false to
+ *        disable
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires WiFi permission
+ */
+bool chreWifiConfigureScanMonitorAsync(bool enable, const void *cookie);
+
+/**
+ * Sends an on-demand request for WiFi scan results. This may trigger a new
+ * scan, or be entirely serviced from cache, depending on the maxScanAgeMs
+ * parameter.
+ *
+ * This resulting status of this request is delivered asynchronously via an
+ * event of type CHRE_EVENT_WIFI_ASYNC_RESULT. The result must be delivered
+ * within CHRE_WIFI_SCAN_RESULT_TIMEOUT_NS of the this request. Refer to the
+ * note in {@link #chreAsyncResult} for more details.
+ *
+ * A successful result provided in CHRE_EVENT_WIFI_ASYNC_RESULT indicates that
+ * the scan results are ready to be delivered in a subsequent event (or events,
+ * which arrive consecutively without any other scan results in between)
+ * of type CHRE_EVENT_WIFI_SCAN_RESULT.
+ *
+ * WiFi scanning must be disabled if both "WiFi scanning" and "WiFi" settings
+ * are disabled at the Android level. In this case, the CHRE implementation is
+ * expected to return a result with CHRE_ERROR_FUNCTION_DISABLED.
+ *
+ * It is not valid for a client to request a new scan while a result is pending
+ * based on a previous scan request from the same client. In this situation, the
+ * CHRE implementation is expected to return a result with CHRE_ERROR_BUSY.
+ * However, if a scan is currently pending or in progress due to a request from
+ * another client, whether within the CHRE or otherwise, the implementation must
+ * not fail the request for this reason. If the pending scan satisfies the
+ * client's request parameters, then the implementation should use its results
+ * to satisfy the request rather than scheduling a new scan.
+ *
+ * @param params A set of parameters for the scan request. Must not be NULL.
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires WiFi permission
+ */
+bool chreWifiRequestScanAsync(const struct chreWifiScanParams *params,
+                              const void *cookie);
+
+/**
+ * Convenience function which calls chreWifiRequestScanAsync() with a default
+ * set of scan parameters.
+ *
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires WiFi permission
+ */
+static inline bool chreWifiRequestScanAsyncDefault(const void *cookie) {
+    static const struct chreWifiScanParams params = {
+        /*.scanType=*/         CHRE_WIFI_SCAN_TYPE_NO_PREFERENCE,
+        /*.maxScanAgeMs=*/     5000,  // 5 seconds
+        /*.frequencyListLen=*/ 0,
+        /*.frequencyList=*/    NULL,
+        /*.ssidListLen=*/      0,
+        /*.ssidList=*/         NULL,
+        /*.radioChainPref=*/   CHRE_WIFI_RADIO_CHAIN_PREF_DEFAULT,
+        /*.channelSet=*/       CHRE_WIFI_CHANNEL_SET_NON_DFS
+    };
+    return chreWifiRequestScanAsync(&params, cookie);
+}
+
+/**
+ * Issues a request to initiate distance measurements using round-trip time
+ * (RTT), aka Fine Timing Measurement (FTM), to one or more devices identified
+ * by MAC address. Within CHRE, MACs are typically the BSSIDs of scanned APs
+ * that have the CHRE_WIFI_SCAN_RESULT_FLAGS_IS_FTM_RESPONDER flag set.
+ *
+ * This resulting status of this request is delivered asynchronously via an
+ * event of type CHRE_EVENT_WIFI_ASYNC_RESULT. The result must be delivered
+ * within CHRE_WIFI_RANGING_RESULT_TIMEOUT_NS of the this request. Refer to the
+ * note in {@link #chreAsyncResult} for more details.
+ *
+ * WiFi RTT ranging must be disabled if any of the following is true:
+ * - Both "WiFi" and "WiFi Scanning" settings are disabled at the Android level.
+ * - The "Location" setting is disabled at the Android level.
+ * In this case, the CHRE implementation is expected to return a result with
+ * CHRE_ERROR_FUNCTION_DISABLED.
+ *
+ * A successful result provided in CHRE_EVENT_WIFI_ASYNC_RESULT indicates that
+ * the results of ranging will be delivered in a subsequent event of type
+ * CHRE_EVENT_WIFI_RANGING_RESULT. Note that the CHRE_EVENT_WIFI_ASYNC_RESULT
+ * gives an overall status - for example, it is used to indicate failure if the
+ * entire ranging request was rejected because WiFi is disabled. However, it is
+ * valid for this event to indicate success, but RTT ranging to fail for all
+ * requested devices - for example, they may be out of range. Therefore, it is
+ * also necessary to check the status field in {@link #chreWifiRangingResult}.
+ *
+ * @param params Structure containing the parameters of the scan request,
+ *        including the list of devices to attempt ranging.
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.2
+ * @note Requires WiFi permission
+ */
+bool chreWifiRequestRangingAsync(const struct chreWifiRangingParams *params,
+                                 const void *cookie);
+
+/**
+ * Helper function to populate an instance of struct chreWifiRangingTarget with
+ * the contents of a scan result provided in struct chreWifiScanResult.
+ * Populates other parameters that are not directly derived from the scan result
+ * with default values.
+ *
+ * @param scanResult The scan result to parse as input
+ * @param rangingTarget The RTT ranging target to populate as output
+ *
+ * @note Requires WiFi permission
+ */
+static inline void chreWifiRangingTargetFromScanResult(
+        const struct chreWifiScanResult *scanResult,
+        struct chreWifiRangingTarget *rangingTarget) {
+    memcpy(rangingTarget->macAddress, scanResult->bssid,
+           sizeof(rangingTarget->macAddress));
+    rangingTarget->primaryChannel      = scanResult->primaryChannel;
+    rangingTarget->centerFreqPrimary   = scanResult->centerFreqPrimary;
+    rangingTarget->centerFreqSecondary = scanResult->centerFreqSecondary;
+    rangingTarget->channelWidth        = scanResult->channelWidth;
+
+    // Note that this is not strictly necessary (CHRE can see which API version
+    // the nanoapp was built against, so it knows to ignore these fields), but
+    // we do it here to keep things nice and tidy
+    memset(rangingTarget->reserved, 0, sizeof(rangingTarget->reserved));
+}
+
+/**
+ * Subscribe to a NAN service.
+ *
+ * Sends a subscription request to the NAN discovery engine with the
+ * specified configration parameters. If successful, a unique non-zero
+ * subscription ID associated with this instance of the subscription
+ * request is assigned by the NAN discovery engine. The subscription request
+ * is active until explicitly canceled, or if the connection was interrupted.
+ *
+ * Note that CHRE forwards any discovery events that it receives to the
+ * subscribe function instance, and does no duplicate filtering. If
+ * multiple events of the same discovery are undesirable, it is up to the
+ * platform NAN discovery engine implementation to implement redundancy
+ * detection mechanisms.
+ *
+ * If WiFi is turned off by the user at the Android level, an existing
+ * subscribe session is canceled, and a CHRE_EVENT_WIFI_ASYNC_RESULT event is
+ * event is sent to the subscriber. Nanoapps are expected to register for user
+ * settings notifications (@see chreUserSettingConfigureEvents), and
+ * re-establish a subscribe session on a WiFi re-enabled settings changed
+ * notification.
+ *
+ * @param config Service subscription configuration
+ * @param cookie A value that the nanoapp uses to track this particular
+ *        subscription request.
+ * @return true if NAN is enabled and a subscription request was successfully
+ *         made to the NAN engine. The actual result of the service discovery
+ *         is sent via a CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT event.
+ *
+ * @since v1.6
+ * @note Requires WiFi permission
+ */
+bool chreWifiNanSubscribe(struct chreWifiNanSubscribeConfig *config,
+                          const void *cookie);
+
+/**
+ * Cancel a subscribe function instance.
+ *
+ * @param subscriptionId The ID that was originally assigned to this instance
+ *        of the subscribe function.
+ * @return true if NAN is enabled, the subscribe ID  was found and the instance
+ *         successfully canceled.
+ *
+ * @since v1.6
+ * @note Requires WiFi permission
+ */
+bool chreWifiNanSubscribeCancel(uint32_t subscriptionID);
+
+/**
+ * Request RTT ranging from a peer NAN device.
+ *
+ * Nanoapps can use this API to explicitly request measurement reports from
+ * the peer device. Note that both end points have to support ranging for a
+ * successful request. The MAC address of the peer NAN device for which ranging
+ * is desired may be obtained either from a NAN service discovery or from an
+ * out-of-band source (HAL service, BLE, etc.).
+ *
+ * If WiFi is turned off by the user at the Android level, an existing
+ * ranging session is canceled, and a CHRE_EVENT_WIFI_ASYNC_RESULT event is
+ * sent to the subscriber. Nanoapps are expected to register for user settings
+ * notifications (@see chreUserSettingConfigureEvents), and perform another
+ * ranging request on a WiFi re-enabled settings changed notification.
+ *
+ * A successful result provided in CHRE_EVENT_WIFI_ASYNC_RESULT indicates that
+ * the results of ranging will be delivered in a subsequent event of type
+ * CHRE_EVENT_WIFI_RANGING_RESULT.
+ *
+ * @param params Structure containing the parameters of the ranging request,
+ *        including the MAC address of the peer NAN device to attempt ranging.
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ * @return true if the request was accepted for processing, false otherwise.
+ */
+bool chreWifiNanRequestRangingAsync(const struct chreWifiNanRangingParams *params,
+                                    const void *cookie);
+
+#else  /* defined(CHRE_NANOAPP_USES_WIFI) || !defined(CHRE_IS_NANOAPP_BUILD) */
+#define CHRE_WIFI_PERM_ERROR_STRING \
+    "CHRE_NANOAPP_USES_WIFI must be defined when building this nanoapp in " \
+    "order to refer to "
+#define chreWifiConfigureScanMonitorAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING \
+                     "chreWifiConfigureScanMonitorAsync")
+#define chreWifiRequestScanAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING \
+                     "chreWifiRequestScanAsync")
+#define chreWifiRequestScanAsyncDefault(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING \
+                     "chreWifiRequestScanAsyncDefault")
+#define chreWifiRequestRangingAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING "chreWifiRequestRangingAsync")
+#define chreWifiRangingTargetFromScanResult(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING \
+                     "chreWifiRangingTargetFromScanResult")
+#define chreWifiNanSubscribe(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING "chreWifiNanSubscribe")
+#define chreWifiNanSubscribeCancel(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING "chreWifiNanSubscribeCancel")
+#define chreWifiNanRequestRangingAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_WIFI_PERM_ERROR_STRING "chreWifiNanRequestRangingAsync")
+#endif  /* defined(CHRE_NANOAPP_USES_WIFI) || !defined(CHRE_IS_NANOAPP_BUILD) */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_WIFI_H_ */
diff --git a/chre_api/legacy/v1_8/chre/wwan.h b/chre_api/legacy/v1_8/chre/wwan.h
new file mode 100644
index 0000000..51cf5f9
--- /dev/null
+++ b/chre_api/legacy/v1_8/chre/wwan.h
@@ -0,0 +1,591 @@
+/*
+ * Copyright (C) 2016 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 _CHRE_WWAN_H_
+#define _CHRE_WWAN_H_
+
+/**
+ * @file
+ * Wireless Wide Area Network (WWAN, i.e. mobile/cellular network) API relevant
+ * for querying cell tower identity and associated information that can be
+ * useful in determining location.
+ *
+ * Based on Android N RIL definitions (located at this path as of the time of
+ * this comment: hardware/ril/include/telephony/ril.h), version 12. Updated
+ * based on Android radio HAL definition (hardware/interfaces/radio) for more
+ * recent Android builds. Refer to those files and associated documentation for
+ * further details.
+ *
+ * In general, the parts of this API that are taken from the RIL follow the
+ * field naming conventions established in that interface rather than the CHRE
+ * API conventions, in order to avoid confusion and enable code re-use where
+ * applicable. Note that structure names include the chreWwan* prefix rather
+ * than RIL_*, but field names are the same. If necessary to enable code
+ * sharing, it is recommended to create typedefs that map from the CHRE
+ * structures to the associated RIL type names, for example "typedef struct
+ * chreWwanCellIdentityGsm RIL_CellIdentityGsm_v12", etc.
+ */
+
+#include <chre/common.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The set of flags returned by chreWwanGetCapabilities().
+ * @defgroup CHRE_WWAN_CAPABILITIES
+ * @{
+ */
+
+//! No WWAN APIs are supported
+#define CHRE_WWAN_CAPABILITIES_NONE  UINT32_C(0)
+
+//! Current cell information can be queried via chreWwanGetCellInfoAsync()
+#define CHRE_WWAN_GET_CELL_INFO      UINT32_C(1 << 0)
+
+/** @} */
+
+/**
+ * Produce an event ID in the block of IDs reserved for WWAN
+ * @param offset  Index into WWAN event ID block; valid range [0,15]
+ */
+#define CHRE_WWAN_EVENT_ID(offset)  (CHRE_EVENT_WWAN_FIRST_EVENT + (offset))
+
+/**
+ * nanoappHandleEvent argument: struct chreWwanCellInfoResult
+ *
+ * Provides the result of an asynchronous request for cell info sent via
+ * chreWwanGetCellInfoAsync().
+ */
+#define CHRE_EVENT_WWAN_CELL_INFO_RESULT  CHRE_WWAN_EVENT_ID(0)
+
+// NOTE: Do not add new events with ID > 15; only values 0-15 are reserved
+// (see chre/event.h)
+
+/**
+ * The current version of struct chreWwanCellInfoResult associated with this
+ * API definition.
+ */
+#define CHRE_WWAN_CELL_INFO_RESULT_VERSION  UINT8_C(1)
+
+//! Reference: RIL_CellIdentityGsm_v12
+struct chreWwanCellIdentityGsm {
+    //! 3-digit Mobile Country Code, 0..999, INT32_MAX if unknown
+    int32_t mcc;
+
+    //! 2 or 3-digit Mobile Network Code, 0..999, INT32_MAX if unknown
+    int32_t mnc;
+
+    //! 16-bit Location Area Code, 0..65535, INT32_MAX if unknown
+    int32_t lac;
+
+    //! 16-bit GSM Cell Identity described in TS 27.007, 0..65535,
+    //! INT32_MAX if unknown
+    int32_t cid;
+
+    //! 16-bit GSM Absolute RF channel number, INT32_MAX if unknown
+    int32_t arfcn;
+
+    //! 6-bit Base Station Identity Code, UINT8_MAX if unknown
+    uint8_t bsic;
+
+    //! Reserved for future use; must be set to 0
+    uint8_t reserved[3];
+};
+
+//! Reference: RIL_CellIdentityWcdma_v12
+struct chreWwanCellIdentityWcdma {
+    //! 3-digit Mobile Country Code, 0..999, INT32_MAX if unknown
+    int32_t mcc;
+
+    //! 2 or 3-digit Mobile Network Code, 0..999, INT32_MAX if unknown
+    int32_t mnc;
+
+    //! 16-bit Location Area Code, 0..65535, INT32_MAX if unknown
+    int32_t lac;
+
+    //! 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455,
+    //! INT32_MAX if unknown
+    int32_t cid;
+
+    //! 9-bit UMTS Primary Scrambling Code described in TS 25.331, 0..511,
+    //! INT32_MAX if unknown
+    int32_t psc;
+
+    //! 16-bit UMTS Absolute RF Channel Number, INT32_MAX if unknown
+    int32_t uarfcn;
+};
+
+//! Reference: RIL_CellIdentityCdma
+struct chreWwanCellIdentityCdma {
+    //! Network Id 0..65535, INT32_MAX if unknown
+    int32_t networkId;
+
+    //! CDMA System Id 0..32767, INT32_MAX if unknown
+    int32_t systemId;
+
+    //! Base Station Id 0..65535, INT32_MAX if unknown
+    int32_t basestationId;
+
+    //! Longitude is a decimal number as specified in 3GPP2 C.S0005-A v6.0.
+    //! It is represented in units of 0.25 seconds and ranges from -2592000
+    //! to 2592000, both values inclusive (corresponding to a range of -180
+    //! to +180 degrees). INT32_MAX if unknown
+    int32_t longitude;
+
+    //! Latitude is a decimal number as specified in 3GPP2 C.S0005-A v6.0.
+    //! It is represented in units of 0.25 seconds and ranges from -1296000
+    //! to 1296000, both values inclusive (corresponding to a range of -90
+    //! to +90 degrees). INT32_MAX if unknown
+    int32_t latitude;
+};
+
+//! Reference: RIL_CellIdentityLte_v12
+struct chreWwanCellIdentityLte {
+    //! 3-digit Mobile Country Code, 0..999, INT32_MAX if unknown
+    int32_t mcc;
+
+    //! 2 or 3-digit Mobile Network Code, 0..999, INT32_MAX if unknown
+    int32_t mnc;
+
+    //! 28-bit Cell Identity described in TS ???, INT32_MAX if unknown
+    int32_t ci;
+
+    //! physical cell id 0..503, INT32_MAX if unknown
+    int32_t pci;
+
+    //! 16-bit tracking area code, INT32_MAX if unknown
+    int32_t tac;
+
+    //! 18-bit LTE Absolute RF Channel Number, INT32_MAX if unknown
+    int32_t earfcn;
+};
+
+//! Reference: RIL_CellIdentityTdscdma
+struct chreWwanCellIdentityTdscdma {
+    //! 3-digit Mobile Country Code, 0..999, INT32_MAX if unknown
+    int32_t mcc;
+
+    //! 2 or 3-digit Mobile Network Code, 0..999, INT32_MAX if unknown
+    int32_t mnc;
+
+    //! 16-bit Location Area Code, 0..65535, INT32_MAX if unknown
+    int32_t lac;
+
+    //! 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455,
+    //! INT32_MAX if unknown
+    int32_t cid;
+
+    //! 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT32_MAX if
+    //! unknown
+    int32_t cpid;
+};
+
+//! Reference: android.hardware.radio@1.4 CellIdentityNr
+//! @since v1.4
+struct chreWwanCellIdentityNr {
+    //! 3-digit Mobile Country Code, in range [0, 999]. This value must be valid
+    //! for registered or camped cells. INT32_MAX means invalid/unreported.
+    int32_t mcc;
+
+    //! 2 or 3-digit Mobile Network Code, in range [0, 999]. This value must be
+    //! valid for registered or camped cells. INT32_MAX means
+    //! invalid/unreported.
+    int32_t mnc;
+
+    //! NR Cell Identity in range [0, 68719476735] (36 bits), which
+    //! unambiguously identifies a cell within a public land mobile network
+    //! (PLMN). This value must be valid for registered or camped cells.
+    //! Reference: TS 38.413 section 9.3.1.7.
+    //!
+    //! Note: for backward compatibility reasons, the nominally int64_t nci is
+    //! split into two uint32_t values, with nci0 being the least significant 4
+    //! bytes. If chreWwanUnpackNrNci returns INT64_MAX, it means nci is
+    //! invalid/unreported.
+    //!
+    //! Users are recommended to use the helper accessor chreWwanUnpackNrNci to
+    //! access the nci field.
+    //!
+    //! @see chreWwanUnpackNrNci
+    uint32_t nci0;
+    uint32_t nci1;
+
+    //! Physical cell id in range [0, 1007]. This value must be valid.
+    //! Reference: TS 38.331 section 6.3.2.
+    int32_t pci;
+
+    //! 24-bit tracking area code in range [0, 16777215]. INT32_MAX means
+    //! invalid/unreported.
+    //! Reference: TS 38.413 section 9.3.3.10 and TS 29.571 section 5.4.2.
+    int32_t tac;
+
+    //! NR Absolute Radio Frequency Channel Number, in range [0, 3279165]. This
+    //! value must be valid.
+    //! Reference: TS 38.101-1 section 5.4.2.1 and TS 38.101-2 section 5.4.2.1.
+    int32_t nrarfcn;
+};
+
+//! Reference: RIL_GSM_SignalStrength_v12
+struct chreWwanSignalStrengthGsm {
+    //! Valid values are (0-31, 99) as defined in TS 27.007 8.5
+    //! INT32_MAX means invalid/unreported.
+    int32_t signalStrength;
+
+    //! bit error rate (0-7, 99) as defined in TS 27.007 8.5
+    //! INT32_MAX means invalid/unreported.
+    int32_t bitErrorRate;
+
+    //! Timing Advance in bit periods. 1 bit period = 48.13 us.
+    //! INT32_MAX means invalid/unreported.
+    int32_t timingAdvance;
+};
+
+//! Reference: RIL_SignalStrengthWcdma
+struct chreWwanSignalStrengthWcdma {
+    //! Valid values are (0-31, 99) as defined in TS 27.007 8.5
+    //! INT32_MAX means invalid/unreported.
+    int32_t signalStrength;
+
+    //! bit error rate (0-7, 99) as defined in TS 27.007 8.5
+    //! INT32_MAX means invalid/unreported.
+    int32_t bitErrorRate;
+};
+
+//! Reference: RIL_CDMA_SignalStrength
+struct chreWwanSignalStrengthCdma {
+    //! Valid values are positive integers.  This value is the actual RSSI value
+    //! multiplied by -1.  Example: If the actual RSSI is -75, then this
+    //! response value will be 75.
+    //! INT32_MAX means invalid/unreported.
+    int32_t dbm;
+
+    //! Valid values are positive integers.  This value is the actual Ec/Io
+    //! multiplied by -10.  Example: If the actual Ec/Io is -12.5 dB, then this
+    //! response value will be 125.
+    //! INT32_MAX means invalid/unreported.
+    int32_t ecio;
+};
+
+//! Reference: RIL_EVDO_SignalStrength
+struct chreWwanSignalStrengthEvdo {
+    //! Valid values are positive integers.  This value is the actual RSSI value
+    //! multiplied by -1.  Example: If the actual RSSI is -75, then this
+    //! response value will be 75.
+    //! INT32_MAX means invalid/unreported.
+    int32_t dbm;
+
+    //! Valid values are positive integers.  This value is the actual Ec/Io
+    //! multiplied by -10.  Example: If the actual Ec/Io is -12.5 dB, then this
+    //! response value will be 125.
+    //! INT32_MAX means invalid/unreported.
+    int32_t ecio;
+
+    //! Valid values are 0-8.  8 is the highest signal to noise ratio.
+    //! INT32_MAX means invalid/unreported.
+    int32_t signalNoiseRatio;
+};
+
+//! Reference: RIL_LTE_SignalStrength_v8
+struct chreWwanSignalStrengthLte {
+    //! Valid values are (0-31, 99) as defined in TS 27.007 8.5
+    int32_t signalStrength;
+
+    //! The current Reference Signal Receive Power in dBm multiplied by -1.
+    //! Range: 44 to 140 dBm
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: 3GPP TS 36.133 9.1.4
+    int32_t rsrp;
+
+    //! The current Reference Signal Receive Quality in dB multiplied by -1.
+    //! Range: 3 to 20 dB.
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: 3GPP TS 36.133 9.1.7
+    int32_t rsrq;
+
+    //! The current reference signal signal-to-noise ratio in 0.1 dB units.
+    //! Range: -200 to +300 (-200 = -20.0 dB, +300 = 30dB).
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: 3GPP TS 36.101 8.1.1
+    int32_t rssnr;
+
+    //! The current Channel Quality Indicator.
+    //! Range: 0 to 15.
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: 3GPP TS 36.101 9.2, 9.3, A.4
+    int32_t cqi;
+
+    //! timing advance in micro seconds for a one way trip from cell to device.
+    //! Approximate distance can be calculated using 300m/us * timingAdvance.
+    //! Range: 0 to 0x7FFFFFFE
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: 3GPP 36.321 section 6.1.3.5
+    //! also: http://www.cellular-planningoptimization.com/2010/02/timing-advance-with-calculation.html
+    int32_t timingAdvance;
+};
+
+//! Reference: RIL_TD_SCDMA_SignalStrength
+struct chreWwanSignalStrengthTdscdma {
+    //! The Received Signal Code Power in dBm multiplied by -1.
+    //! Range : 25 to 120
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: 3GPP TS 25.123, section 9.1.1.1
+    int32_t rscp;
+};
+
+//! Reference: android.hardware.radio@1.4 NrSignalStrength
+//! @since v1.4
+struct chreWwanSignalStrengthNr {
+    //! SS (second synchronization) reference signal received power in dBm
+    //! multiplied by -1.
+    //! Range [44, 140], INT32_MAX means invalid/unreported.
+    //! Reference: TS 38.215 section 5.1.1 and TS 38.133 section 10.1.6.
+    int32_t ssRsrp;
+
+    //! SS reference signal received quality in 0.5 dB units.
+    //! Range [-86, 41] with -86 = -43.0 dB and 41 = 20.5 dB.
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: TS 38.215 section 5.1.3 and TS 38.133 section 10.1.11.1.
+    int32_t ssRsrq;
+
+    //! SS signal-to-noise and interference ratio in 0.5 dB units.
+    //! Range [-46, 81] with -46 = -23.0 dB and 81 = 40.5 dB.
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: TS 38.215 section 5.1.5 and TS 38.133 section 10.1.16.1.
+    int32_t ssSinr;
+
+    //! CSI reference signal received power in dBm multiplied by -1.
+    //! Range [44, 140], INT32_MAX means invalid/unreported.
+    //! Reference: TS 38.215 section 5.1.2 and TS 38.133 section 10.1.6.
+    int32_t csiRsrp;
+
+    //! CSI reference signal received quality in 0.5 dB units.
+    //! Range [-86, 41] with -86 = -43.0 dB and 41 = 20.5 dB.
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: TS 38.215 section 5.1.4 and TS 38.133 section 10.1.11.1.
+    int32_t csiRsrq;
+
+    //! CSI signal-to-noise and interference ratio in 0.5 dB units.
+    //! Range [-46, 81] with -46 = -23.0 dB and 81 = 40.5 dB.
+    //! INT32_MAX means invalid/unreported.
+    //! Reference: TS 38.215 section 5.1.6 and TS 38.133 section 10.1.16.1.
+    int32_t csiSinr;
+};
+
+//! Reference: RIL_CellInfoGsm_v12
+struct chreWwanCellInfoGsm {
+    struct chreWwanCellIdentityGsm    cellIdentityGsm;
+    struct chreWwanSignalStrengthGsm  signalStrengthGsm;
+};
+
+//! Reference: RIL_CellInfoWcdma_v12
+struct chreWwanCellInfoWcdma {
+    struct chreWwanCellIdentityWcdma    cellIdentityWcdma;
+    struct chreWwanSignalStrengthWcdma  signalStrengthWcdma;
+};
+
+//! Reference: RIL_CellInfoCdma
+struct chreWwanCellInfoCdma {
+    struct chreWwanCellIdentityCdma    cellIdentityCdma;
+    struct chreWwanSignalStrengthCdma  signalStrengthCdma;
+    struct chreWwanSignalStrengthEvdo  signalStrengthEvdo;
+};
+
+//! Reference: RIL_CellInfoLte_v12
+struct chreWwanCellInfoLte {
+    struct chreWwanCellIdentityLte    cellIdentityLte;
+    struct chreWwanSignalStrengthLte  signalStrengthLte;
+};
+
+//! Reference: RIL_CellInfoTdscdma
+struct chreWwanCellInfoTdscdma {
+    struct chreWwanCellIdentityTdscdma    cellIdentityTdscdma;
+    struct chreWwanSignalStrengthTdscdma  signalStrengthTdscdma;
+};
+
+//! Reference: android.hardware.radio@1.4 CellInfoNr
+//! @since v1.4
+struct chreWwanCellInfoNr {
+    struct chreWwanCellIdentityNr    cellIdentityNr;
+    struct chreWwanSignalStrengthNr  signalStrengthNr;
+};
+
+//! Reference: RIL_CellInfoType
+//! All other values are reserved and should be ignored by nanoapps.
+enum chreWwanCellInfoType {
+    CHRE_WWAN_CELL_INFO_TYPE_GSM      = 1,
+    CHRE_WWAN_CELL_INFO_TYPE_CDMA     = 2,
+    CHRE_WWAN_CELL_INFO_TYPE_LTE      = 3,
+    CHRE_WWAN_CELL_INFO_TYPE_WCDMA    = 4,
+    CHRE_WWAN_CELL_INFO_TYPE_TD_SCDMA = 5,
+    CHRE_WWAN_CELL_INFO_TYPE_NR       = 6,  //! @since v1.4
+};
+
+//! Reference: RIL_TimeStampType
+enum chreWwanCellTimeStampType {
+    CHRE_WWAN_CELL_TIMESTAMP_TYPE_UNKNOWN  = 0,
+    CHRE_WWAN_CELL_TIMESTAMP_TYPE_ANTENNA  = 1,
+    CHRE_WWAN_CELL_TIMESTAMP_TYPE_MODEM    = 2,
+    CHRE_WWAN_CELL_TIMESTAMP_TYPE_OEM_RIL  = 3,
+    CHRE_WWAN_CELL_TIMESTAMP_TYPE_JAVA_RIL = 4,
+};
+
+//! Reference: RIL_CellInfo_v12
+struct chreWwanCellInfo {
+    //! Timestamp in nanoseconds; must be in the same time base as chreGetTime()
+    uint64_t timeStamp;
+
+    //! A value from enum {@link #CellInfoType} indicating the radio access
+    //! technology of the cell, and which field in union CellInfo can be used
+    //! to retrieve additional information
+    uint8_t cellInfoType;
+
+    //! A value from enum {@link #CellTimeStampType} that identifies the source
+    //! of the value in timeStamp. This is typically set to
+    //! CHRE_WWAN_CELL_TIMESTAMP_TYPE_OEM_RIL, and indicates the time given by
+    //! chreGetTime() that an intermediate module received the data from the
+    //! modem and forwarded it to the requesting CHRE client.
+    uint8_t timeStampType;
+
+    //! !0 if this cell is registered, 0 if not registered
+    uint8_t registered;
+
+    //! Reserved for future use; must be set to 0
+    uint8_t reserved;
+
+    //! The value in cellInfoType indicates which field in this union is valid
+    union chreWwanCellInfoPerRat {
+        struct chreWwanCellInfoGsm     gsm;
+        struct chreWwanCellInfoCdma    cdma;
+        struct chreWwanCellInfoLte     lte;
+        struct chreWwanCellInfoWcdma   wcdma;
+        struct chreWwanCellInfoTdscdma tdscdma;
+        struct chreWwanCellInfoNr      nr;  //! @since v1.4
+    } CellInfo;
+};
+
+/**
+ * Data structure provided with events of type CHRE_EVENT_WWAN_CELL_INFO_RESULT.
+ */
+struct chreWwanCellInfoResult {
+    //! Indicates the version of the structure, for compatibility purposes.
+    //! Clients do not normally need to worry about this field; the CHRE
+    //! implementation guarantees that the client only receives the structure
+    //! version it expects.
+    uint8_t version;
+
+    //! Populated with a value from enum {@link #chreError}, indicating whether
+    //! the request failed, and if so, provides the cause of the failure
+    uint8_t errorCode;
+
+    //! The number of valid entries in cells[]
+    uint8_t cellInfoCount;
+
+    //! Reserved for future use; must be set to 0
+    uint8_t reserved;
+
+    //! Set to the cookie parameter given to chreWwanGetCellInfoAsync()
+    const void *cookie;
+
+    //! Pointer to an array of cellInfoCount elements containing information
+    //! about serving and neighbor cells
+    const struct chreWwanCellInfo *cells;
+};
+
+
+/**
+ * Retrieves a set of flags indicating the WWAN features supported by the
+ * current CHRE implementation. The value returned by this function must be
+ * consistent for the entire duration of the Nanoapp's execution.
+ *
+ * The client must allow for more flags to be set in this response than it knows
+ * about, for example if the implementation supports a newer version of the API
+ * than the client was compiled against.
+ *
+ * @return A bitmask with zero or more CHRE_WWAN_CAPABILITIES_* flags set
+ *
+ * @since v1.1
+ */
+uint32_t chreWwanGetCapabilities(void);
+
+/**
+ * Nanoapps must define CHRE_NANOAPP_USES_WWAN somewhere in their build
+ * system (e.g. Makefile) if the nanoapp needs to use the following WWAN APIs.
+ * In addition to allowing access to these APIs, defining this macro will also
+ * ensure CHRE enforces that all host clients this nanoapp talks to have the
+ * required Android permissions needed to listen to WWAN data by adding metadata
+ * to the nanoapp.
+ */
+#if defined(CHRE_NANOAPP_USES_WWAN) || !defined(CHRE_IS_NANOAPP_BUILD)
+
+/**
+ * Query information about the current serving cell and its neighbors. This does
+ * not perform a network scan, but should return state from the current network
+ * registration data stored in the cellular modem. This is effectively the same
+ * as a request for RIL_REQUEST_GET_CELL_INFO_LIST in the RIL.
+ *
+ * The requested cellular information is returned asynchronously via
+ * CHRE_EVENT_WWAN_CELL_INFO_RESULT. The implementation must send this event,
+ * either with successful data or an error status, within
+ * CHRE_ASYNC_RESULT_TIMEOUT_NS.
+ *
+ * If the airplane mode setting is enabled at the Android level, the CHRE
+ * implementation is expected to return a successful asynchronous result with an
+ * empty cell info list.
+ *
+ * @param cookie An opaque value that will be included in the chreAsyncResult
+ *        sent in relation to this request.
+ *
+ * @return true if the request was accepted for processing, false otherwise
+ *
+ * @since v1.1
+ * @note Requires WWAN permission
+ */
+bool chreWwanGetCellInfoAsync(const void *cookie);
+
+/**
+ * Helper accessor for nci in the chreWwanCellIdentityNr struct.
+ *
+ * @return nci or INT64_MAX if invalid/unreported.
+ *
+ * @see chreWwanCellIdentityNr
+ *
+ * @since v1.4
+ * @note Requires WWAN permission
+ */
+static inline int64_t chreWwanUnpackNrNci(
+    const struct chreWwanCellIdentityNr *nrCellId) {
+  return (int64_t) (((uint64_t) nrCellId->nci1 << 32) | nrCellId->nci0);
+}
+
+#else  /* defined(CHRE_NANOAPP_USES_WWAN) || !defined(CHRE_IS_NANOAPP_BUILD) */
+#define CHRE_WWAN_PERM_ERROR_STRING \
+    "CHRE_NANOAPP_USES_WWAN must be defined when building this nanoapp in " \
+    "order to refer to "
+#define chreWwanGetCellInfoAsync(...) \
+    CHRE_BUILD_ERROR(CHRE_WWAN_PERM_ERROR_STRING "chreWwanGetCellInfoAsync")
+#define chreWwanUnpackNrNci(...) \
+    CHRE_BUILD_ERROR(CHRE_WWAN_PERM_ERROR_STRING "chreWwanUnpackNrNci")
+#endif  /* defined(CHRE_NANOAPP_USES_WWAN) || !defined(CHRE_IS_NANOAPP_BUILD) */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* _CHRE_WWAN_H_ */
diff --git a/chre_daemon.rc b/chre_daemon.rc
index 120da10..58bf3d9 100644
--- a/chre_daemon.rc
+++ b/chre_daemon.rc
@@ -14,6 +14,9 @@
 # limitations under the License.
 #
 
+on post-fs-data
+    mkdir /data/vendor/chre 0770 context_hub context_hub
+
 service vendor.chre /vendor/bin/chre
     class late_start
     user context_hub
diff --git a/chre_flags.aconfig b/chre_flags.aconfig
new file mode 100644
index 0000000..70c6586
--- /dev/null
+++ b/chre_flags.aconfig
@@ -0,0 +1,36 @@
+package: "android.chre.flags"
+
+flag {
+  name: "flag_log_nanoapp_load_metrics"
+  namespace: "context_hub"
+  description: "This flag controls nanoapp load failure logging in the HAL and the addition of MetricsReporter"
+  bug: "298459533"
+}
+
+flag {
+  name: "metrics_reporter_in_the_daemon"
+  namespace: "context_hub"
+  description: "This flag controls the addition of MetricsReporter into the CHRE daemon"
+  bug: "298459533"
+}
+
+flag {
+  name: "wait_for_preloaded_nanoapp_start"
+  namespace: "context_hub"
+  description: "This flag controls the waiting-for-nanoapp-start behavior in the CHRE daemon"
+  bug: "298459533"
+}
+
+flag {
+  name: "remove_ap_wakeup_metric_report_limit"
+  namespace: "context_hub"
+  description: "This flag controls removing a count limit on reporting the AP wakeup metric"
+  bug: "298459533"
+}
+
+flag {
+  name: "context_hub_callback_uuid_enabled"
+  namespace: "context_hub"
+  description: "Call IContextHubCallback.getUuid() to retrieve the UUID when this flag is on"
+  bug: "247124878"
+}
diff --git a/core/ble_request.cc b/core/ble_request.cc
index 7924349..f097c0c 100644
--- a/core/ble_request.cc
+++ b/core/ble_request.cc
@@ -32,31 +32,49 @@
          (memcmp(filter.dataMask, otherFilter.dataMask, filter.len) == 0);
 }
 
+bool broadcasterFiltersMatch(
+    const chreBleBroadcasterAddressFilter &filter,
+    const chreBleBroadcasterAddressFilter &otherFilter) {
+  return (memcmp(filter.broadcasterAddress, otherFilter.broadcasterAddress,
+                 sizeof(filter.broadcasterAddress)) == 0);
+}
+
 }  // namespace
 
-BleRequest::BleRequest() : BleRequest(0, false) {}
+BleRequest::BleRequest()
+    : BleRequest(0 /* instanceId */, false /* enable */, nullptr /* cookie */) {
+}
 
-BleRequest::BleRequest(uint16_t instanceId, bool enable)
+BleRequest::BleRequest(uint16_t instanceId, bool enable, const void *cookie)
     : BleRequest(instanceId, enable, CHRE_BLE_SCAN_MODE_BACKGROUND,
-                 0 /* reportDelayMs */, nullptr /* filter */) {}
+                 0 /* reportDelayMs */, nullptr /* filter */, cookie) {}
 
 BleRequest::BleRequest(uint16_t instanceId, bool enable, chreBleScanMode mode,
-                       uint32_t reportDelayMs, const chreBleScanFilter *filter)
+                       uint32_t reportDelayMs,
+                       const chreBleScanFilterV1_9 *filter, const void *cookie)
     : mReportDelayMs(reportDelayMs),
       mInstanceId(instanceId),
       mMode(mode),
       mEnabled(enable),
       mRssiThreshold(CHRE_BLE_RSSI_THRESHOLD_NONE),
-      mStatus(RequestStatus::PENDING_REQ) {
+      mStatus(RequestStatus::PENDING_REQ),
+      mCookie(cookie) {
   if (filter != nullptr) {
     mRssiThreshold = filter->rssiThreshold;
-    if (filter->scanFilterCount > 0) {
-      if (!mFilters.resize(filter->scanFilterCount)) {
+    if (filter->genericFilterCount > 0) {
+      if (!mGenericFilters.resize(filter->genericFilterCount)) {
         FATAL_ERROR("Unable to reserve filter count");
       }
-      for (size_t i = 0; i < filter->scanFilterCount; i++) {
-        mFilters[i] = filter->scanFilters[i];
+      memcpy(mGenericFilters.data(), filter->genericFilters,
+             sizeof(chreBleGenericFilter) * filter->genericFilterCount);
+    }
+    if (filter->broadcasterAddressFilterCount > 0) {
+      if (!mBroadcasterFilters.resize(filter->broadcasterAddressFilterCount)) {
+        FATAL_ERROR("Unable to reserve broadcaster address filter count");
       }
+      memcpy(mBroadcasterFilters.data(), filter->broadcasterAddressFilters,
+             sizeof(chreBleBroadcasterAddressFilter) *
+                 filter->broadcasterAddressFilterCount);
     }
   }
 }
@@ -70,9 +88,11 @@
   mMode = other.mMode;
   mReportDelayMs = other.mReportDelayMs;
   mRssiThreshold = other.mRssiThreshold;
-  mFilters = std::move(other.mFilters);
+  mGenericFilters = std::move(other.mGenericFilters);
+  mBroadcasterFilters = std::move(other.mBroadcasterFilters);
   mEnabled = other.mEnabled;
   mStatus = other.mStatus;
+  mCookie = other.mCookie;
   return *this;
 }
 
@@ -103,10 +123,11 @@
       attributesChanged = true;
     }
   }
-  const DynamicVector<chreBleGenericFilter> &otherFilters = request.mFilters;
+  const DynamicVector<chreBleGenericFilter> &otherFilters =
+      request.mGenericFilters;
   for (const chreBleGenericFilter &otherFilter : otherFilters) {
     bool addFilter = true;
-    for (const chreBleGenericFilter &filter : mFilters) {
+    for (const chreBleGenericFilter &filter : mGenericFilters) {
       if (filtersMatch(filter, otherFilter)) {
         addFilter = false;
         break;
@@ -114,7 +135,25 @@
     }
     if (addFilter) {
       attributesChanged = true;
-      if (!mFilters.push_back(otherFilter)) {
+      if (!mGenericFilters.push_back(otherFilter)) {
+        FATAL_ERROR("Unable to merge filters");
+      }
+    }
+  }
+  const DynamicVector<chreBleBroadcasterAddressFilter>
+      &otherBroadcasterFilters = request.mBroadcasterFilters;
+  for (const chreBleBroadcasterAddressFilter &otherFilter :
+       otherBroadcasterFilters) {
+    bool addFilter = true;
+    for (const chreBleBroadcasterAddressFilter &filter : mBroadcasterFilters) {
+      if (broadcasterFiltersMatch(filter, otherFilter)) {
+        addFilter = false;
+        break;
+      }
+    }
+    if (addFilter) {
+      attributesChanged = true;
+      if (!mBroadcasterFilters.push_back(otherFilter)) {
         FATAL_ERROR("Unable to merge filters");
       }
     }
@@ -123,14 +162,26 @@
 }
 
 bool BleRequest::isEquivalentTo(const BleRequest &request) {
-  const DynamicVector<chreBleGenericFilter> &otherFilters = request.mFilters;
-  bool isEquivalent = (mEnabled && request.mEnabled && mMode == request.mMode &&
-                       mReportDelayMs == request.mReportDelayMs &&
-                       mRssiThreshold == request.mRssiThreshold &&
-                       mFilters.size() == otherFilters.size());
+  const DynamicVector<chreBleGenericFilter> &otherFilters =
+      request.mGenericFilters;
+  const DynamicVector<chreBleBroadcasterAddressFilter>
+      &otherBroadcasterFilters = request.mBroadcasterFilters;
+  bool isEquivalent =
+      (mEnabled && request.mEnabled && mMode == request.mMode &&
+       mReportDelayMs == request.mReportDelayMs &&
+       mRssiThreshold == request.mRssiThreshold &&
+       mGenericFilters.size() == otherFilters.size() &&
+       mBroadcasterFilters.size() == otherBroadcasterFilters.size());
   if (isEquivalent) {
     for (size_t i = 0; i < otherFilters.size(); i++) {
-      if (!filtersMatch(mFilters[i], otherFilters[i])) {
+      if (!filtersMatch(mGenericFilters[i], otherFilters[i])) {
+        isEquivalent = false;
+        break;
+      }
+    }
+    for (size_t i = 0; i < otherBroadcasterFilters.size(); i++) {
+      if (!broadcasterFiltersMatch(mBroadcasterFilters[i],
+                                   otherBroadcasterFilters[i])) {
         isEquivalent = false;
         break;
       }
@@ -165,18 +216,29 @@
 
 const DynamicVector<chreBleGenericFilter> &BleRequest::getGenericFilters()
     const {
-  return mFilters;
+  return mGenericFilters;
 }
 
-chreBleScanFilter BleRequest::getScanFilter() const {
-  return chreBleScanFilter{
-      mRssiThreshold, static_cast<uint8_t>(mFilters.size()), mFilters.data()};
+const DynamicVector<chreBleBroadcasterAddressFilter> &
+BleRequest::getBroadcasterFilters() const {
+  return mBroadcasterFilters;
+}
+
+chreBleScanFilterV1_9 BleRequest::getScanFilter() const {
+  return chreBleScanFilterV1_9{
+      mRssiThreshold, static_cast<uint8_t>(mGenericFilters.size()),
+      mGenericFilters.data(), static_cast<uint8_t>(mBroadcasterFilters.size()),
+      mBroadcasterFilters.data()};
 }
 
 bool BleRequest::isEnabled() const {
   return mEnabled;
 }
 
+const void *BleRequest::getCookie() const {
+  return mCookie;
+}
+
 void BleRequest::logStateToBuffer(DebugDumpWrapper &debugDump,
                                   bool isPlatformRequest) const {
   if (!isPlatformRequest) {
@@ -189,8 +251,8 @@
         " mode=%" PRIu8 " reportDelayMs=%" PRIu32 " rssiThreshold=%" PRId8,
         static_cast<uint8_t>(mMode), mReportDelayMs, mRssiThreshold);
     if (isPlatformRequest) {
-      debugDump.print(" filters=[");
-      for (const chreBleGenericFilter &filter : mFilters) {
+      debugDump.print(" genericFilters=[");
+      for (const chreBleGenericFilter &filter : mGenericFilters) {
         debugDump.print("(type=%" PRIx8, filter.type);
         if (filter.len > 0) {
           debugDump.print(" data=%s dataMask=%s len=%" PRIu8 "), ",
@@ -200,9 +262,21 @@
         }
       }
       debugDump.print("]\n");
+      debugDump.print(" broadcasterAddressFilters=[");
+      for (const chreBleBroadcasterAddressFilter &filter :
+           mBroadcasterFilters) {
+        debugDump.print(
+            "(address=%02X:%02X:%02X:%02X:%02X:%02X), ",
+            filter.broadcasterAddress[5], filter.broadcasterAddress[4],
+            filter.broadcasterAddress[3], filter.broadcasterAddress[2],
+            filter.broadcasterAddress[1], filter.broadcasterAddress[0]);
+      }
+      debugDump.print("]\n");
     } else {
-      debugDump.print(" filterCount=%" PRIu8 "\n",
-                      static_cast<uint8_t>(mFilters.size()));
+      debugDump.print(" genericFilterCount=%" PRIu8
+                      " broadcasterFilterCount=%" PRIu8 "\n",
+                      static_cast<uint8_t>(mGenericFilters.size()),
+                      static_cast<uint8_t>(mBroadcasterFilters.size()));
     }
   }
 }
diff --git a/core/ble_request_manager.cc b/core/ble_request_manager.cc
index 469b357..0b1237f 100644
--- a/core/ble_request_manager.cc
+++ b/core/ble_request_manager.cc
@@ -19,7 +19,9 @@
 #include "chre/core/event_loop_manager.h"
 #include "chre/platform/fatal_error.h"
 #include "chre/platform/log.h"
+#include "chre/util/fixed_size_vector.h"
 #include "chre/util/nested_data_ptr.h"
+#include "chre/util/system/ble_util.h"
 #include "chre/util/system/event_callbacks.h"
 
 namespace chre {
@@ -46,19 +48,20 @@
       foundRequest->getRequestStatus() != RequestStatus::APPLIED) {
     handleAsyncResult(instanceId, foundRequest->isEnabled(),
                       false /* success */, CHRE_ERROR_OBSOLETE_REQUEST,
-                      true /* forceUnregister */);
+                      foundRequest->getCookie(), true /* forceUnregister */);
   }
 }
 
 bool BleRequestManager::compliesWithBleSetting(uint16_t instanceId,
                                                bool enabled,
                                                bool hasExistingRequest,
-                                               size_t requestIndex) {
+                                               size_t requestIndex,
+                                               const void *cookie) {
   bool success = true;
   if (enabled && !bleSettingEnabled()) {
     success = false;
     handleAsyncResult(instanceId, enabled, false /* success */,
-                      CHRE_ERROR_FUNCTION_DISABLED);
+                      CHRE_ERROR_FUNCTION_DISABLED, cookie);
     if (hasExistingRequest) {
       bool requestChanged = false;
       mRequests.removeRequest(requestIndex, &requestChanged);
@@ -85,18 +88,18 @@
   return success;
 }
 
-bool BleRequestManager::startScanAsync(Nanoapp *nanoapp, chreBleScanMode mode,
-                                       uint32_t reportDelayMs,
-                                       const struct chreBleScanFilter *filter) {
+bool BleRequestManager::startScanAsync(
+    Nanoapp *nanoapp, chreBleScanMode mode, uint32_t reportDelayMs,
+    const struct chreBleScanFilterV1_9 *filter, const void *cookie) {
   CHRE_ASSERT(nanoapp);
-  BleRequest request(nanoapp->getInstanceId(), true, mode, reportDelayMs,
-                     filter);
+  BleRequest request(nanoapp->getInstanceId(), true /* enable */, mode,
+                     reportDelayMs, filter, cookie);
   return configure(std::move(request));
 }
 
-bool BleRequestManager::stopScanAsync(Nanoapp *nanoapp) {
+bool BleRequestManager::stopScanAsync(Nanoapp *nanoapp, const void *cookie) {
   CHRE_ASSERT(nanoapp);
-  BleRequest request(nanoapp->getInstanceId(), false /* enable */);
+  BleRequest request(nanoapp->getInstanceId(), false /* enable */, cookie);
   return configure(std::move(request));
 }
 
@@ -112,7 +115,8 @@
     return 0;
   }
 
-  BleRequest request(nanoapp->getInstanceId(), false /* enable */);
+  BleRequest request(nanoapp->getInstanceId(), false /* enable */,
+                     nullptr /* cookie */);
   configure(std::move(request));
   return 1;
 }
@@ -141,6 +145,32 @@
 }
 #endif
 
+bool BleRequestManager::flushAsync(Nanoapp *nanoapp, const void *cookie) {
+  CHRE_ASSERT(nanoapp);
+
+  bool supportsFlush =
+      getCapabilities() & CHRE_BLE_CAPABILITIES_SCAN_RESULT_BATCHING;
+  if (!supportsFlush) {
+    return false;
+  }
+
+  bool success = false;
+  const BleRequest *foundRequest =
+      mRequests.findRequest(nanoapp->getInstanceId(), nullptr);
+  if (foundRequest == nullptr) {
+    LOGE("Nanoapp with instance ID: %" PRIu16
+         " does not have an existing BLE request and cannot flush",
+         nanoapp->getInstanceId());
+  } else if (mFlushRequestQueue.full()) {
+    LOG_OOM();
+  } else {
+    mFlushRequestQueue.emplace(nanoapp->getInstanceId(), cookie);
+    success = processFlushRequests();
+  }
+
+  return success;
+}
+
 void BleRequestManager::addBleRequestLog(uint32_t instanceId, bool enabled,
                                          size_t requestIndex,
                                          bool compliesWithBleSetting) {
@@ -165,8 +195,9 @@
     uint16_t instanceId = request.getInstanceId();
     uint8_t enabled = request.isEnabled();
     handleExistingRequest(instanceId, &hasExistingRequest, &requestIndex);
-    bool compliant = compliesWithBleSetting(instanceId, enabled,
-                                            hasExistingRequest, requestIndex);
+    bool compliant =
+        compliesWithBleSetting(instanceId, enabled, hasExistingRequest,
+                               requestIndex, request.getCookie());
     if (compliant) {
       success = updateRequests(std::move(request), hasExistingRequest,
                                &requestChanged, &requestIndex);
@@ -174,7 +205,7 @@
         if (!mPlatformRequestInProgress) {
           if (!requestChanged) {
             handleAsyncResult(instanceId, enabled, true /* success */,
-                              CHRE_ERROR_NONE);
+                              CHRE_ERROR_NONE, request.getCookie());
             if (requestIndex < mRequests.getRequests().size()) {
               mRequests.getMutableRequests()[requestIndex].setRequestStatus(
                   RequestStatus::APPLIED);
@@ -202,17 +233,20 @@
   bool success = false;
   const BleRequest &maxRequest = mRequests.getCurrentMaximalRequest();
   bool enable = bleSettingEnabled() && maxRequest.isEnabled();
+
   if (enable) {
-    chreBleScanFilter filter = maxRequest.getScanFilter();
+    chreBleScanFilterV1_9 filter = maxRequest.getScanFilter();
     success = mPlatformBle.startScanAsync(
         maxRequest.getMode(), maxRequest.getReportDelayMs(), &filter);
-    mPendingPlatformRequest =
-        BleRequest(0, enable, maxRequest.getMode(),
-                   maxRequest.getReportDelayMs(), &filter);
+    mPendingPlatformRequest = BleRequest(
+        0 /* instanceId */, enable, maxRequest.getMode(),
+        maxRequest.getReportDelayMs(), &filter, nullptr /* cookie */);
   } else {
     success = mPlatformBle.stopScanAsync();
-    mPendingPlatformRequest = BleRequest(0, enable);
+    mPendingPlatformRequest =
+        BleRequest(0 /* instanceId */, enable, nullptr /* cookie */);
   }
+
   if (success) {
     for (BleRequest &req : mRequests.getMutableRequests()) {
       if (req.getRequestStatus() == RequestStatus::PENDING_REQ) {
@@ -240,17 +274,21 @@
 
 void BleRequestManager::handleAdvertisementEvent(
     struct chreBleAdvertisementEvent *event) {
+  for (uint16_t i = 0; i < event->numReports; i++) {
+    populateLegacyAdvertisingReportFields(
+        const_cast<chreBleAdvertisingReport &>(event->reports[i]));
+  }
   EventLoopManagerSingleton::get()->getEventLoop().postEventOrDie(
       CHRE_EVENT_BLE_ADVERTISEMENT, event, freeAdvertisingEventCallback);
 }
 
 void BleRequestManager::handlePlatformChange(bool enable, uint8_t errorCode) {
   auto callback = [](uint16_t /*type*/, void *data, void *extraData) {
-    bool enable = NestedDataPtr<bool>(data);
-    uint8_t errorCode = NestedDataPtr<uint8_t>(extraData);
+    bool enableCb = NestedDataPtr<bool>(data);
+    uint8_t errorCodeCb = NestedDataPtr<uint8_t>(extraData);
     EventLoopManagerSingleton::get()
         ->getBleRequestManager()
-        .handlePlatformChangeSync(enable, errorCode);
+        .handlePlatformChangeSync(enableCb, errorCodeCb);
   };
 
   EventLoopManagerSingleton::get()->deferCallback(
@@ -272,7 +310,7 @@
   for (BleRequest &req : mRequests.getMutableRequests()) {
     if (req.getRequestStatus() == RequestStatus::PENDING_RESP) {
       handleAsyncResult(req.getInstanceId(), req.isEnabled(), success,
-                        errorCode);
+                        errorCode, req.getCookie());
       if (success) {
         req.setRequestStatus(RequestStatus::APPLIED);
       }
@@ -288,56 +326,46 @@
     mActivePlatformRequest = std::move(mPendingPlatformRequest);
   }
 
-  dispatchPendingRequests();
+  if (mRequests.hasRequests(RequestStatus::PENDING_REQ)) {
+    dispatchPendingRequests();
+  } else if (!success && mResyncPending) {
+    updatePlatformRequest(true /* forceUpdate */);
+  }
 
-  // Only clear mResyncPending if the request succeeded or after all pending
-  // requests are dispatched and a resync request can be issued with only the
-  // requests that were previously applied.
-  if (mResyncPending) {
-    if (success) {
-      mResyncPending = false;
-    } else if (!success && !mPlatformRequestInProgress) {
-      mResyncPending = false;
-      updatePlatformRequest(true /* forceUpdate */);
-    }
-  }
-  // Finish dispatching pending requests before processing the setting change
-  // request to ensure nanoapps receive CHRE_ERROR_FUNCTION_DISABLED responses.
-  // If both a resync and a setting change are pending, prioritize the resync.
-  // If the resync successfully completes, the PAL will be in the correct state
-  // and updatePlatformRequest will not begin a new request.
-  if (mSettingChangePending && !mPlatformRequestInProgress) {
+  if (!mPlatformRequestInProgress && mSettingChangePending) {
     updatePlatformRequest();
-    mSettingChangePending = false;
   }
+
+  mResyncPending = false;
+  mSettingChangePending = false;
 }
 
 void BleRequestManager::dispatchPendingRequests() {
-  if (mRequests.hasRequests(RequestStatus::PENDING_REQ)) {
-    uint8_t errorCode = CHRE_ERROR_NONE;
-    if (!bleSettingEnabled() && mRequests.isMaximalRequestEnabled()) {
-      errorCode = CHRE_ERROR_FUNCTION_DISABLED;
-    } else if (!controlPlatform()) {
-      errorCode = CHRE_ERROR;
-    }
-    if (errorCode != CHRE_ERROR_NONE) {
-      for (const BleRequest &req : mRequests.getRequests()) {
-        if (req.getRequestStatus() == RequestStatus::PENDING_REQ) {
-          handleAsyncResult(req.getInstanceId(), req.isEnabled(),
-                            false /* success */, errorCode);
-        }
+  uint8_t errorCode = CHRE_ERROR_NONE;
+  if (!bleSettingEnabled() && mRequests.isMaximalRequestEnabled()) {
+    errorCode = CHRE_ERROR_FUNCTION_DISABLED;
+  } else if (!controlPlatform()) {
+    errorCode = CHRE_ERROR;
+  }
+  if (errorCode != CHRE_ERROR_NONE) {
+    for (const BleRequest &req : mRequests.getRequests()) {
+      if (req.getRequestStatus() == RequestStatus::PENDING_REQ) {
+        handleAsyncResult(req.getInstanceId(), req.isEnabled(),
+                          false /* success */, errorCode, req.getCookie());
       }
-      mRequests.removeRequests(RequestStatus::PENDING_REQ);
     }
+    mRequests.removeRequests(RequestStatus::PENDING_REQ);
   }
 }
 
 void BleRequestManager::handleAsyncResult(uint16_t instanceId, bool enabled,
                                           bool success, uint8_t errorCode,
+                                          const void *cookie,
                                           bool forceUnregister) {
   uint8_t requestType = enabled ? CHRE_BLE_REQUEST_TYPE_START_SCAN
                                 : CHRE_BLE_REQUEST_TYPE_STOP_SCAN;
-  postAsyncResultEventFatal(instanceId, requestType, success, errorCode);
+  postAsyncResultEventFatal(instanceId, requestType, success, errorCode,
+                            cookie);
   handleNanoappEventRegistration(instanceId, enabled, success, forceUnregister);
 }
 
@@ -464,6 +492,21 @@
 }
 #endif
 
+void BleRequestManager::handleFlushComplete(uint8_t errorCode) {
+  if (mFlushRequestTimerHandle != CHRE_TIMER_INVALID) {
+    EventLoopManagerSingleton::get()->cancelDelayedCallback(
+        mFlushRequestTimerHandle);
+    mFlushRequestTimerHandle = CHRE_TIMER_INVALID;
+  }
+
+  handleFlushCompleteInternal(errorCode);
+}
+
+void BleRequestManager::handleFlushCompleteTimeout() {
+  mFlushRequestTimerHandle = CHRE_TIMER_INVALID;
+  handleFlushCompleteInternal(CHRE_ERROR_TIMEOUT);
+}
+
 bool BleRequestManager::getScanStatus(struct chreBleScanStatus * /* status */) {
   // TODO(b/266820139): Implement this
   return false;
@@ -496,6 +539,106 @@
   }
 }
 
+void BleRequestManager::handleFlushCompleteInternal(uint8_t errorCode) {
+  auto callback = [](uint16_t /* type */, void *data, void * /* extraData */) {
+    uint8_t cbErrorCode = NestedDataPtr<uint8_t>(data);
+    EventLoopManagerSingleton::get()
+        ->getBleRequestManager()
+        .handleFlushCompleteSync(cbErrorCode);
+  };
+
+  if (!EventLoopManagerSingleton::get()->deferCallback(
+          SystemCallbackType::BleFlushComplete,
+          NestedDataPtr<uint8_t>(errorCode), callback)) {
+    FATAL_ERROR("Unable to defer flush complete callback");
+  }
+}
+
+void BleRequestManager::handleFlushCompleteSync(uint8_t errorCode) {
+  if (mFlushRequestQueue.empty() || !mFlushRequestQueue.front().isActive) {
+    LOGE(
+        "handleFlushCompleteSync was called, but there is no active flush "
+        "request");
+    return;
+  }
+
+  FlushRequest &flushRequest = mFlushRequestQueue.front();
+  sendFlushCompleteEventOrDie(flushRequest, errorCode);
+  mFlushRequestQueue.pop();
+
+  processFlushRequests();
+}
+
+uint8_t BleRequestManager::doFlushRequest() {
+  CHRE_ASSERT(!mFlushRequestQueue.empty());
+
+  FlushRequest &flushRequest = mFlushRequestQueue.front();
+  if (flushRequest.isActive) {
+    return CHRE_ERROR_NONE;
+  }
+
+  Nanoseconds now = SystemTime::getMonotonicTime();
+  uint8_t errorCode = CHRE_ERROR_NONE;
+  if (now >= flushRequest.deadlineTimestamp) {
+    LOGE("BLE flush request for nanoapp with instance ID: %" PRIu16
+         " failed: deadline exceeded",
+         flushRequest.nanoappInstanceId);
+    errorCode = CHRE_ERROR_TIMEOUT;
+  } else {
+    auto timeoutCallback = [](uint16_t /* type */, void * /* data */,
+                              void * /* extraData */) {
+      EventLoopManagerSingleton::get()
+          ->getBleRequestManager()
+          .handleFlushCompleteTimeout();
+    };
+    mFlushRequestTimerHandle =
+        EventLoopManagerSingleton::get()->setDelayedCallback(
+            SystemCallbackType::BleFlushTimeout, nullptr, timeoutCallback,
+            flushRequest.deadlineTimestamp - now);
+
+    if (!mPlatformBle.flushAsync()) {
+      LOGE("Could not request flush from BLE platform");
+      errorCode = CHRE_ERROR;
+      EventLoopManagerSingleton::get()->cancelDelayedCallback(
+          mFlushRequestTimerHandle);
+      mFlushRequestTimerHandle = CHRE_TIMER_INVALID;
+    } else {
+      flushRequest.isActive = true;
+    }
+  }
+  return errorCode;
+}
+
+void BleRequestManager::sendFlushCompleteEventOrDie(
+    const FlushRequest &flushRequest, uint8_t errorCode) {
+  chreAsyncResult *event = memoryAlloc<chreAsyncResult>();
+  if (event == nullptr) {
+    FATAL_ERROR("Unable to allocate chreAsyncResult");
+  }
+
+  event->requestType = CHRE_BLE_REQUEST_TYPE_FLUSH;
+  event->success = errorCode == CHRE_ERROR_NONE;
+  event->errorCode = errorCode;
+  event->reserved = 0;
+  event->cookie = flushRequest.cookie;
+  EventLoopManagerSingleton::get()->getEventLoop().postEventOrDie(
+      CHRE_EVENT_BLE_FLUSH_COMPLETE, event, freeEventDataCallback,
+      flushRequest.nanoappInstanceId);
+}
+
+bool BleRequestManager::processFlushRequests() {
+  while (!mFlushRequestQueue.empty()) {
+    uint8_t errorCode = doFlushRequest();
+    if (errorCode == CHRE_ERROR_NONE) {
+      return true;
+    }
+
+    sendFlushCompleteEventOrDie(mFlushRequestQueue.front(), errorCode);
+    mFlushRequestQueue.pop();
+  }
+  return false;
+}
+
 // TODO(b/290860901): require data & ~mask == 0
 bool BleRequestManager::validateParams(const BleRequest &request) {
   bool valid = true;
@@ -517,7 +660,8 @@
 void BleRequestManager::postAsyncResultEventFatal(uint16_t instanceId,
                                                   uint8_t requestType,
                                                   bool success,
-                                                  uint8_t errorCode) {
+                                                  uint8_t errorCode,
+                                                  const void *cookie) {
   chreAsyncResult *event = memoryAlloc<chreAsyncResult>();
   if (event == nullptr) {
     FATAL_ERROR("Failed to alloc BLE async result");
@@ -525,6 +669,7 @@
     event->requestType = requestType;
     event->success = success;
     event->errorCode = errorCode;
+    event->cookie = cookie;
     event->reserved = 0;
 
     EventLoopManagerSingleton::get()->getEventLoop().postEventOrDie(
@@ -566,10 +711,11 @@
                     log.timestamp.toRawNanoseconds(), log.instanceId,
                     log.enable ? "enable" : "disable\n");
     if (log.enable && log.compliesWithBleSetting) {
-      debugDump.print(" mode=%" PRIu8 " reportDelayMs=%" PRIu32
-                      " rssiThreshold=%" PRId8 " scanCount=%" PRIu8 "\n",
-                      static_cast<uint8_t>(log.mode), log.reportDelayMs,
-                      log.rssiThreshold, log.scanFilterCount);
+      debugDump.print(
+          " mode=%" PRIu8 " reportDelayMs=%" PRIu32 " rssiThreshold=%" PRId8
+          " scanCount=%" PRIu8 " broadcasterAddressCount=%" PRIu8 "\n",
+          static_cast<uint8_t>(log.mode), log.reportDelayMs, log.rssiThreshold,
+          log.scanFilterCount, log.broadcasterFilterCount);
     } else if (log.enable) {
       debugDump.print(" request did not comply with BLE setting\n");
     }
diff --git a/core/ble_request_multiplexer.cc b/core/ble_request_multiplexer.cc
index eb32b33..13a9ec1 100644
--- a/core/ble_request_multiplexer.cc
+++ b/core/ble_request_multiplexer.cc
@@ -26,9 +26,11 @@
 
 const BleRequest *BleRequestMultiplexer::findRequest(uint16_t instanceId,
                                                      size_t *index) {
-  for (size_t i = 0; i < mRequests.size(); i++) {
+  for (size_t i = 0; i < mRequests.size(); ++i) {
     if (mRequests[i].getInstanceId() == instanceId) {
-      *index = i;
+      if (index != nullptr) {
+        *index = i;
+      }
       return &mRequests[i];
     }
   }
diff --git a/core/event_loop.cc b/core/event_loop.cc
index 56354c9..493d457 100644
--- a/core/event_loop.cc
+++ b/core/event_loop.cc
@@ -192,10 +192,6 @@
   } else if (!mNanoapps.prepareForPush()) {
     LOG_OOM();
   } else {
-    nanoapp->setInstanceId(eventLoopManager->getNextInstanceId());
-    LOGD("Instance ID %" PRIu16 " assigned to app ID 0x%016" PRIx64,
-         nanoapp->getInstanceId(), nanoapp->getAppId());
-
     Nanoapp *newNanoapp = nanoapp.get();
     {
       LockGuard<Mutex> lock(mNanoappsLock);
@@ -208,16 +204,10 @@
     success = newNanoapp->start();
     mCurrentApp = nullptr;
     if (!success) {
-      // TODO: to be fully safe, need to purge/flush any events and messages
-      // sent by the nanoapp here (but don't call nanoappEnd). For now, we just
-      // destroy the Nanoapp instance.
       LOGE("Nanoapp %" PRIu16 " failed to start", newNanoapp->getInstanceId());
-
-      // Note that this lock protects against concurrent read and modification
-      // of mNanoapps, but we are assured that no new nanoapps were added since
-      // we pushed the new nanoapp
-      LockGuard<Mutex> lock(mNanoappsLock);
-      mNanoapps.pop_back();
+      unloadNanoapp(newNanoapp->getInstanceId(),
+                    /*allowSystemNanoappUnload=*/true,
+                    /*nanoappStarted=*/false);
     } else {
       notifyAppStatusChange(CHRE_EVENT_NANOAPP_STARTED, *newNanoapp);
     }
@@ -227,7 +217,8 @@
 }
 
 bool EventLoop::unloadNanoapp(uint16_t instanceId,
-                              bool allowSystemNanoappUnload) {
+                              bool allowSystemNanoappUnload,
+                              bool nanoappStarted) {
   bool unloaded = false;
 
   for (size_t i = 0; i < mNanoapps.size(); i++) {
@@ -252,13 +243,17 @@
         flushInboundEventQueue();
 
         // Post the unload event now (so we can reference the Nanoapp instance
-        // directly), but nanoapps won't get it until after the unload completes
-        notifyAppStatusChange(CHRE_EVENT_NANOAPP_STOPPED, *mStoppingNanoapp);
+        // directly), but nanoapps won't get it until after the unload
+        // completes. No need to notify status change if nanoapps failed to
+        // start.
+        if (nanoappStarted) {
+          notifyAppStatusChange(CHRE_EVENT_NANOAPP_STOPPED, *mStoppingNanoapp);
+        }
 
         // Finally, we are at a point where there should not be any pending
         // events or messages sent by the app that could potentially reference
         // the nanoapp's memory, so we are safe to unload it
-        unloadNanoappAtIndex(i);
+        unloadNanoappAtIndex(i, nanoappStarted);
         mStoppingNanoapp = nullptr;
 
         LOGD("Unloaded nanoapp with instanceId %" PRIu16, instanceId);
@@ -303,7 +298,7 @@
   if (mRunning) {
     if (hasNoSpaceForHighPriorityEvent() ||
         !allocateAndPostEvent(eventType, eventData, freeCallback,
-                              false /*isLowPriority*/, kSystemInstanceId,
+                              /* isLowPriority= */ false, kSystemInstanceId,
                               targetInstanceId, targetGroupMask)) {
       FATAL_ERROR("Failed to post critical system event 0x%" PRIx16, eventType);
     }
@@ -343,20 +338,14 @@
   bool eventPosted = false;
 
   if (mRunning) {
-#ifdef CHRE_STATIC_EVENT_LOOP
-    if (mEventPool.getFreeBlockCount() > kMinReservedHighPriorityEventCount)
-#else
-    if (mEventPool.getFreeSpaceCount() > kMinReservedHighPriorityEventCount)
-#endif
-    {
-      eventPosted = allocateAndPostEvent(
-          eventType, eventData, freeCallback, true /*isLowPriority*/,
-          senderInstanceId, targetInstanceId, targetGroupMask);
-      if (!eventPosted) {
-        LOGE("Failed to allocate event 0x%" PRIx16 " to instanceId %" PRIu16,
-             eventType, targetInstanceId);
-        ++mNumDroppedLowPriEvents;
-      }
+    eventPosted =
+        allocateAndPostEvent(eventType, eventData, freeCallback,
+                             /* isLowPriority= */ true, senderInstanceId,
+                             targetInstanceId, targetGroupMask);
+    if (!eventPosted) {
+      LOGE("Failed to allocate event 0x%" PRIx16 " to instanceId %" PRIu16,
+           eventType, targetInstanceId);
+      ++mNumDroppedLowPriEvents;
     }
   }
 
@@ -421,12 +410,25 @@
   uint64_t durationMins =
       kIntervalWakeupBucket.toRawNanoseconds() / kOneMinuteInNanoseconds;
   debugDump.print("  Nanoapp host wakeup tracking: cycled %" PRIu64
-                  "mins ago, bucketDuration=%" PRIu64 "mins\n",
+                  " mins ago, bucketDuration=%" PRIu64 "mins\n",
                   timeSinceMins, durationMins);
 
   debugDump.print("\nNanoapps:\n");
-  for (const UniquePtr<Nanoapp> &app : mNanoapps) {
-    app->logStateToBuffer(debugDump);
+
+  if (mNanoapps.size()) {
+    for (const UniquePtr<Nanoapp> &app : mNanoapps) {
+      app->logStateToBuffer(debugDump);
+    }
+
+    mNanoapps[0]->logMemAndComputeHeader(debugDump);
+    for (const UniquePtr<Nanoapp> &app : mNanoapps) {
+      app->logMemAndComputeEntry(debugDump);
+    }
+
+    mNanoapps[0]->logMessageHistoryHeader(debugDump);
+    for (const UniquePtr<Nanoapp> &app : mNanoapps) {
+      app->logMessageHistoryEntry(debugDump);
+    }
   }
 }
 
@@ -534,7 +536,7 @@
   }
 }
 
-void EventLoop::unloadNanoappAtIndex(size_t index) {
+void EventLoop::unloadNanoappAtIndex(size_t index, bool nanoappStarted) {
   const UniquePtr<Nanoapp> &nanoapp = mNanoapps[index];
 
   // Lock here to prevent the nanoapp instance from being accessed between the
@@ -543,7 +545,12 @@
 
   // Let the app know it's going away
   mCurrentApp = nanoapp.get();
-  nanoapp->end();
+
+  // nanoappEnd() is not invoked for nanoapps that return false in
+  // nanoappStart(), per CHRE API
+  if (nanoappStarted) {
+    nanoapp->end();
+  }
 
   // Cleanup resources.
 #ifdef CHRE_WIFI_SUPPORT_ENABLED
@@ -594,21 +601,19 @@
           nanoapp.get());
   logDanglingResources("heap blocks", numFreedBlocks);
 
-  mCurrentApp = nullptr;
-
   // Destroy the Nanoapp instance
   mNanoapps.erase(index);
+
+  mCurrentApp = nullptr;
 }
 
 void EventLoop::handleNanoappWakeupBuckets() {
   Nanoseconds now = SystemTime::getMonotonicTime();
   Nanoseconds duration = now - mTimeLastWakeupBucketCycled;
   if (duration > kIntervalWakeupBucket) {
-    size_t numBuckets = static_cast<size_t>(
-        duration.toRawNanoseconds() / kIntervalWakeupBucket.toRawNanoseconds());
     mTimeLastWakeupBucketCycled = now;
     for (auto &nanoapp : mNanoapps) {
-      nanoapp->cycleWakeupBuckets(numBuckets);
+      nanoapp->cycleWakeupBuckets(now);
     }
   }
 }
diff --git a/core/event_loop_manager.cc b/core/event_loop_manager.cc
index 5116771..4baf5d6 100644
--- a/core/event_loop_manager.cc
+++ b/core/event_loop_manager.cc
@@ -16,6 +16,7 @@
 
 #include "chre/core/event_loop_manager.h"
 
+#include "chre/platform/atomic.h"
 #include "chre/platform/fatal_error.h"
 #include "chre/platform/memory.h"
 #include "chre/util/lock_guard.h"
@@ -31,18 +32,19 @@
 }
 
 uint16_t EventLoopManager::getNextInstanceId() {
-  ++mLastInstanceId;
+  // Get the next available instance ID and mask off the upper 16 bit.
+  uint16_t instanceId =
+      static_cast<uint16_t>(mNextInstanceId.fetch_increment() & 0x0000FFFF);
 
-  // ~4 billion instance IDs should be enough for anyone... if we need to
+  // 65536 instance IDs should be enough for normal use cases. If we need to
   // support wraparound for stress testing load/unload, then we can set a flag
   // when wraparound occurs and use EventLoop::findNanoappByInstanceId to ensure
   // we avoid conflicts
-  if (mLastInstanceId == kBroadcastInstanceId ||
-      mLastInstanceId == kSystemInstanceId) {
+  if (instanceId == kBroadcastInstanceId || instanceId == kSystemInstanceId) {
     FATAL_ERROR("Exhausted instance IDs!");
   }
 
-  return mLastInstanceId;
+  return instanceId;
 }
 
 void EventLoopManager::lateInit() {
diff --git a/core/host_comms_manager.cc b/core/host_comms_manager.cc
index 81afa58..102813f 100644
--- a/core/host_comms_manager.cc
+++ b/core/host_comms_manager.cc
@@ -20,6 +20,7 @@
 #include "chre/core/event_loop_manager.h"
 #include "chre/core/host_comms_manager.h"
 #include "chre/platform/assert.h"
+#include "chre/platform/context.h"
 #include "chre/platform/host_link.h"
 #include "chre/util/macros.h"
 
@@ -67,13 +68,17 @@
       success = HostLink::sendMessage(msgToHost);
       if (!success) {
         mMessagePool.deallocate(msgToHost);
-      } else if (wokeHost) {
-        // If message successfully sent and host was suspended before sending
-        EventLoopManagerSingleton::get()
-            ->getEventLoop()
-            .handleNanoappWakeupBuckets();
-        mIsNanoappBlamedForWakeup = true;
-        nanoapp->blameHostWakeup();
+      } else {
+        if (wokeHost) {
+          // If message successfully sent and host was suspended before sending
+          EventLoopManagerSingleton::get()
+              ->getEventLoop()
+              .handleNanoappWakeupBuckets();
+          mIsNanoappBlamedForWakeup = true;
+          nanoapp->blameHostWakeup();
+        }
+        // Record the nanoapp having sent a message to the host
+        nanoapp->blameHostMessageSent();
       }
     }
   }
@@ -196,6 +201,11 @@
   // EventLoop context.
   if (msgToHost->toHostData.nanoappFreeFunction == nullptr) {
     mMessagePool.deallocate(msgToHost);
+  } else if (inEventLoopThread()) {
+    // If we're already within the event pool context, it is safe to call the
+    // free callback synchronously.
+    EventLoopManagerSingleton::get()->getHostCommsManager().freeMessageToHost(
+        msgToHost);
   } else {
     auto freeMsgCallback = [](uint16_t /*type*/, void *data,
                               void * /*extraData*/) {
diff --git a/core/include/chre/core/ble_request.h b/core/include/chre/core/ble_request.h
index 66667fb..e442e79 100644
--- a/core/include/chre/core/ble_request.h
+++ b/core/include/chre/core/ble_request.h
@@ -40,10 +40,11 @@
  public:
   BleRequest();
 
-  BleRequest(uint16_t instanceId, bool enable);
+  BleRequest(uint16_t instanceId, bool enable, const void *cookie);
 
   BleRequest(uint16_t instanceId, bool enable, chreBleScanMode mode,
-             uint32_t reportDelayMs, const chreBleScanFilter *filter);
+             uint32_t reportDelayMs, const chreBleScanFilterV1_9 *filter,
+             const void *cookie);
 
   BleRequest(BleRequest &&other);
 
@@ -103,10 +104,21 @@
   const DynamicVector<chreBleGenericFilter> &getGenericFilters() const;
 
   /**
-   * @return chreBleScanFilter that is valid only as long as the internal
+   * @return Broadcaster address filters of this request.
+   */
+  const DynamicVector<chreBleBroadcasterAddressFilter> &getBroadcasterFilters()
+      const;
+
+  /**
+   * @return The cookie this request.
+   */
+  const void *getCookie() const;
+
+  /**
+   * @return chreBleScanFilterV1_9 that is valid only as long as the internal
    *    contents of this class are not modified
    */
-  chreBleScanFilter getScanFilter() const;
+  chreBleScanFilterV1_9 getScanFilter() const;
 
   /**
    * @return true if nanoapp intends to enable a request.
@@ -136,7 +148,7 @@
   chreBleScanMode mMode;
 
   // Whether a nanoapp intends to enable this request. If set to false, the
-  // following members are invalid: mMode, mReportDelayMs, mFilter.
+  // following members are invalid: mMode, mReportDelayMs, mGenericFilters.
   bool mEnabled;
 
   // RSSI threshold filter.
@@ -148,7 +160,13 @@
   RequestStatus mStatus;
 
   // Generic scan filters.
-  DynamicVector<chreBleGenericFilter> mFilters;
+  DynamicVector<chreBleGenericFilter> mGenericFilters;
+
+  // Broadcaster address filters.
+  DynamicVector<chreBleBroadcasterAddressFilter> mBroadcasterFilters;
+
+  // Cookie value included in this request, supplied by the nanoapp.
+  const void *mCookie;
 };
 
 }  // namespace chre
diff --git a/core/include/chre/core/ble_request_manager.h b/core/include/chre/core/ble_request_manager.h
index 747b5a7..16d280f 100644
--- a/core/include/chre/core/ble_request_manager.h
+++ b/core/include/chre/core/ble_request_manager.h
@@ -21,7 +21,9 @@
 #include "chre/core/ble_request_multiplexer.h"
 #include "chre/core/nanoapp.h"
 #include "chre/core/settings.h"
+#include "chre/core/timer_pool.h"
 #include "chre/platform/platform_ble.h"
+#include "chre/platform/system_time.h"
 #include "chre/util/array_queue.h"
 #include "chre/util/non_copyable.h"
 #include "chre/util/system/debug_dump.h"
@@ -61,24 +63,29 @@
    *                      batching. Note that the system may deliver results
    *                      before the maximum specified delay is reached.
    * @param filter Pointer to the requested best-effort filter configuration as
-   *               defined by struct chreBleScanFilter. The ownership of filter
-   *               and its nested elements remains with the caller, and the
-   *               caller may release it as soon as chreBleStartScanAsync()
-   *               returns.
+   *               defined by struct chreBleScanFilterV1_9. The ownership of
+   *               filter and its nested elements remains with the caller, and
+   *               the caller may release it as soon as
+   *               chreBleStartScanAsyncV1_9() returns.
+   * @param cookie The cookie to be provided to the nanoapp. This is
+   *               round-tripped from the nanoapp to provide context.
    * @return true if scan was successfully enabled.
    */
   bool startScanAsync(Nanoapp *nanoapp, chreBleScanMode mode,
                       uint32_t reportDelayMs,
-                      const struct chreBleScanFilter *filter);
+                      const struct chreBleScanFilterV1_9 *filter,
+                      const void *cookie);
 
   /**
    * End a BLE scan asynchronously. The result is delivered through a
    * CHRE_EVENT_BLE_ASYNC_RESULT event.
    *
    * @param nanoapp The nanoapp stopping the request.
+   * @param cookie A cookie that is round-tripped back to the nanoapp to
+   *               provide a context when making the request.
    * @return whether the scan was successfully ended.
    */
-  bool stopScanAsync(Nanoapp *nanoapp);
+  bool stopScanAsync(Nanoapp *nanoapp, const void *cookie);
 
 #ifdef CHRE_BLE_READ_RSSI_SUPPORT_ENABLED
   /**
@@ -111,6 +118,18 @@
 #endif
 
   /**
+   * Initiates a flush operation where all batched advertisement events will be
+   * immediately processed and delivered. The nanoapp must have an existing
+   * active BLE scan.
+   *
+   * @param nanoapp the nanoapp requesting the flush operation.
+   * @param cookie the cookie value stored with the request.
+   * @return true if the request has been accepted and dispatched to the
+   *         controller. false otherwise.
+   */
+  bool flushAsync(Nanoapp *nanoapp, const void *cookie);
+
+  /**
    * Disables active scan for a nanoapp (no-op if no active scan).
    *
    * @param nanoapp A non-null pointer to the nanoapp.
@@ -173,6 +192,17 @@
 #endif
 
   /**
+   * Handler for the flush complete operation. Called when a flush operation is
+   * complete. Processes in an asynchronous manner.
+   *
+   * @param errorCode the error code from the flush operation.
+   */
+  void handleFlushComplete(uint8_t errorCode);
+
+  //! Timeout handler for the flush operation. Called on a timeout.
+  void handleFlushCompleteTimeout();
+
+  /**
    * Retrieves the current scan status.
    *
    * @param status A non-null pointer to where the scan status will be
@@ -201,6 +231,23 @@
   void logStateToBuffer(DebugDumpWrapper &debugDump) const;
 
  private:
+  //! An internal structure to store incoming sensor flush requests
+  struct FlushRequest {
+    FlushRequest(uint16_t id, const void *cookiePtr)
+        : nanoappInstanceId(id), cookie(cookiePtr) {}
+
+    //! The timestamp at which this request should complete.
+    Nanoseconds deadlineTimestamp =
+        SystemTime::getMonotonicTime() +
+        Nanoseconds(CHRE_BLE_FLUSH_COMPLETE_TIMEOUT_NS);
+    //! The ID of the nanoapp that requested the flush.
+    uint16_t nanoappInstanceId;
+    //! The opaque pointer provided in flushAsync().
+    const void *cookie;
+    //! True if this flush request is active and is pending completion.
+    bool isActive = false;
+  };
+
   // Multiplexer used to keep track of BLE requests from nanoapps.
   BleRequestMultiplexer mRequests;
 
@@ -222,6 +269,13 @@
   // True if a setting change request is pending to be processed.
   bool mSettingChangePending;
 
+  //! A queue of flush requests made by nanoapps.
+  static constexpr size_t kMaxFlushRequests = 16;
+  ArrayQueue<FlushRequest, kMaxFlushRequests> mFlushRequestQueue;
+
+  //! The timer handle for the flush operation. Used to track a flush timeout.
+  TimerHandle mFlushRequestTimerHandle = CHRE_TIMER_INVALID;
+
 #ifdef CHRE_BLE_READ_RSSI_SUPPORT_ENABLED
   // A pending request from a nanoapp
   struct BleReadRssiRequest {
@@ -238,17 +292,19 @@
 
   // Struct to hold ble request data for logging
   struct BleRequestLog {
-    BleRequestLog(Nanoseconds timestamp, uint32_t instanceId, bool enable,
-                  bool compliesWithBleSetting)
-        : timestamp(timestamp),
-          instanceId(instanceId),
-          enable(enable),
-          compliesWithBleSetting(compliesWithBleSetting) {}
+    BleRequestLog(Nanoseconds timestamp_, uint32_t instanceId_, bool enable_,
+                  bool compliesWithBleSetting_)
+        : timestamp(timestamp_),
+          instanceId(instanceId_),
+          enable(enable_),
+          compliesWithBleSetting(compliesWithBleSetting_) {}
     void populateRequestData(const BleRequest &req) {
       mode = req.getMode();
       reportDelayMs = req.getReportDelayMs();
       rssiThreshold = req.getRssiThreshold();
       scanFilterCount = static_cast<uint8_t>(req.getGenericFilters().size());
+      broadcasterFilterCount =
+          static_cast<uint8_t>(req.getBroadcasterFilters().size());
     }
     Nanoseconds timestamp;
     uint32_t instanceId;
@@ -258,6 +314,7 @@
     uint32_t reportDelayMs;
     int8_t rssiThreshold;
     uint8_t scanFilterCount;
+    uint8_t broadcasterFilterCount;
   };
 
   // List of most recent ble request logs
@@ -301,11 +358,13 @@
    * to the nanoapp instance id of the new request.
    * @param requestIndex If hasExistingRequest is true, requestIndex
    * corresponds to the index of that request.
+   * @param cookie The cookie to be provided to the nanoapp.
    * @return true if the request does not attempt to enable the platform while
    * the BLE setting is disabled.
    */
   bool compliesWithBleSetting(uint16_t instanceId, bool enabled,
-                              bool hasExistingRequest, size_t requestIndex);
+                              bool hasExistingRequest, size_t requestIndex,
+                              const void *cookie);
 
   /**
    * Add a log to list of BLE request logs possibly pushing out the oldest log.
@@ -375,10 +434,13 @@
    * @param success Whether the request was processed by the PAL successfully
    * @param errorCode Error code resulting from the request
    * @param forceUnregister Whether the nanoapp should be force unregistered
+   * @param cookie The cookie to be provided to the nanoapp. This is
+   *               round-tripped from the nanoapp to provide context.
    *                        from BLE broadcast events.
    */
   void handleAsyncResult(uint16_t instanceId, bool enabled, bool success,
-                         uint8_t errorCode, bool forceUnregister = false);
+                         uint8_t errorCode, const void *cookie,
+                         bool forceUnregister = false);
 
   /**
    * Invoked as a result of a requestStateResync() callback from the BLE PAL.
@@ -396,6 +458,54 @@
   void updatePlatformRequest(bool forceUpdate = false);
 
   /**
+   * Helper function for flush complete handling in all cases - normal and
+   * timeout. This function defers a call to handleFlushCompleteSync.
+   *
+   * @param errorCode the error code for the flush operation.
+   */
+  void handleFlushCompleteInternal(uint8_t errorCode);
+
+  /**
+   * Synchronously processed a flush complete operation. Starts a new flush
+   * operation if there is one in the queue. Properly sends the flush complete
+   * event.
+   *
+   * @param errorCode the error code for the flush operation.
+   */
+  void handleFlushCompleteSync(uint8_t errorCode);
+
+  /**
+   * Sends the flush request to the controller if there is a non-active flush
+   * request in the flush request queue. Sets the timer callback to handle
+   * timeouts.
+   *
+   * @return the error code, chreError enum (CHRE_ERROR_NONE for success).
+   */
+  uint8_t doFlushRequest();
+
+  /**
+   * Sends the flush complete event or aborts CHRE.
+   *
+   * @param flushRequest the current active flush request.
+   * @param errorCode the error code, chreError enum.
+   */
+  void sendFlushCompleteEventOrDie(const FlushRequest &flushRequest,
+                                   uint8_t errorCode);
+
+  /**
+   * Processes flush requests in the flush request queue in order. Calls
+   * doFlushRequest on the request. If an error is detected, it sends the flush
+   * complete event with the error. This function continues to process requests
+   * until one flush request is successfully made. Once this happens, the
+   * request manager waits for a timeout or for a callback from the BLE
+   * platform.
+   *
+   * @return true if there was one flush request that was successfully
+   * initiated, false otherwise.
+   */
+  bool processFlushRequests();
+
+  /**
    * Validates the parameters given to ensure that they can be issued to the
    * PAL.
    *
@@ -410,10 +520,11 @@
    * @param requestType The type of BLE request the nanoapp issued.
    * @param success true if the operation was successful.
    * @param errorCode the error code as a result of this operation.
+   * @param cookie The cookie to be provided to the nanoapp.
    */
   static void postAsyncResultEventFatal(uint16_t instanceId,
                                         uint8_t requestType, bool success,
-                                        uint8_t errorCode);
+                                        uint8_t errorCode, const void *cookie);
 
   /**
    * @return True if the given advertisement type is valid
diff --git a/core/include/chre/core/ble_request_multiplexer.h b/core/include/chre/core/ble_request_multiplexer.h
index 4635785..e25747c 100644
--- a/core/include/chre/core/ble_request_multiplexer.h
+++ b/core/include/chre/core/ble_request_multiplexer.h
@@ -43,12 +43,12 @@
 
   /**
    * Searches through the list of BLE requests for a request owned by the
-   * given nanoapp. The provided non-null index pointer is populated with the
-   * index of the request if it is found.
+   * given nanoapp. The provided index pointer is populated with the
+   * index of the request if it is found and the pointer is not null.
    *
    * @param instanceId The instance ID of the nanoapp whose request is being
    *        searched for.
-   * @param index A non-null pointer to an index that is populated if a
+   * @param index A pointer to an index that is populated if a
    *        request for this nanoapp is found.
    * @return A pointer to a BleRequest that is owned by the provided
    *         nanoapp if one is found otherwise nullptr.
diff --git a/core/include/chre/core/event_loop.h b/core/include/chre/core/event_loop.h
index 3403410..6a64956 100644
--- a/core/include/chre/core/event_loop.h
+++ b/core/include/chre/core/event_loop.h
@@ -57,13 +57,6 @@
 #define CHRE_MAX_EVENT_BLOCKS 4
 #endif
 
-#ifndef CHRE_UNSCHEDULED_EVENT_PER_BLOCK
-#define CHRE_UNSCHEDULED_EVENT_PER_BLOCK 24
-#endif
-
-#ifndef CHRE_MAX_UNSCHEDULED_EVENT_BLOCKS
-#define CHRE_MAX_UNSCHEDULED_EVENT_BLOCKS 4
-#endif
 #endif
 
 namespace chre {
@@ -78,7 +71,7 @@
   EventLoop()
       :
 #ifndef CHRE_STATIC_EVENT_LOOP
-        mEvents(kMaxUnscheduleEventBlocks),
+        mEvents(kMaxEventBlock),
 #endif
         mTimeLastWakeupBucketCycled(SystemTime::getMonotonicTime()),
         mRunning(true) {
@@ -159,10 +152,12 @@
    * @param instanceId The nanoapp's unique instance identifier
    * @param allowSystemNanoappUnload If false, this function will reject
    *        attempts to unload a system nanoapp
+   * @param nanoappStarted Indicates whether the nanoapp successfully started
    *
    * @return true if the nanoapp with the given instance ID was found & unloaded
    */
-  bool unloadNanoapp(uint16_t instanceId, bool allowSystemNanoappUnload);
+  bool unloadNanoapp(uint16_t instanceId, bool allowSystemNanoappUnload,
+                     bool nanoappStarted = true);
 
   /**
    * Executes the loop that blocks on the event queue and delivers received
@@ -356,10 +351,6 @@
   //! The maximum number of events that can be active in the system.
   static constexpr size_t kMaxEventCount = CHRE_MAX_EVENT_COUNT;
 
-  //! The minimum number of events to reserve in the event pool for high
-  //! priority events.
-  static constexpr size_t kMinReservedHighPriorityEventCount = 16;
-
   //! The maximum number of events that are awaiting to be scheduled. These
   //! events are in a queue to be distributed to apps.
   static constexpr size_t kMaxUnscheduledEventCount =
@@ -382,28 +373,15 @@
   static constexpr size_t kMaxEventCount =
       CHRE_EVENT_PER_BLOCK * CHRE_MAX_EVENT_BLOCKS;
 
-  //! The minimum number of events to reserve in the event pool for high
-  //! priority events.
-  static constexpr size_t kMinReservedHighPriorityEventCount = 16;
-
-  //! The maximum number of events per block that are awaiting to be scheduled.
-  //! These events are in a queue to be distributed to apps.
-  static constexpr size_t kMaxUnscheduledEventPerBlock =
-      CHRE_UNSCHEDULED_EVENT_PER_BLOCK;
-
-  //! The maximum number of event blocks that mEvents can hold.
-  static constexpr size_t kMaxUnscheduleEventBlocks =
-      CHRE_MAX_UNSCHEDULED_EVENT_BLOCKS;
-
   //! The memory pool to allocate incoming events from.
   SynchronizedExpandableMemoryPool<Event, kEventPerBlock, kMaxEventBlock>
       mEventPool;
 
   //! The blocking queue of incoming events from the system that have not been
   //! distributed out to apps yet.
-  BlockingSegmentedQueue<Event *, kMaxUnscheduledEventPerBlock> mEvents;
+  BlockingSegmentedQueue<Event *, kEventPerBlock> mEvents;
 #endif
-  //! The time interval of nanoapp wakeup buckets, adjust in conjuction with
+  //! The time interval of nanoapp wakeup buckets, adjust in conjunction with
   //! Nanoapp::kMaxSizeWakeupBuckets.
   static constexpr Nanoseconds kIntervalWakeupBucket =
       Nanoseconds(180 * kOneMinuteInNanoseconds);
@@ -551,8 +529,12 @@
    * be any pending events in this nanoapp's queue, and there must not be any
    * outstanding events sent by this nanoapp, as they may reference the
    * nanoapp's own memory (even if there is no free callback).
+   *
+   * @param index Index of the nanoapp in the list of nanoapps managed by event
+   * loop.
+   * @param nanoappStarted Indicates whether the nanoapp successfully started
    */
-  void unloadNanoappAtIndex(size_t index);
+  void unloadNanoappAtIndex(size_t index, bool nanoappStarted = true);
 
   /**
    * Logs dangling resources when a nanoapp is unloaded.
diff --git a/core/include/chre/core/event_loop_common.h b/core/include/chre/core/event_loop_common.h
index e487cbc..c6f4946 100644
--- a/core/include/chre/core/event_loop_common.h
+++ b/core/include/chre/core/event_loop_common.h
@@ -70,6 +70,9 @@
   BleRequestResyncEvent,
   RequestTimeoutEvent,
   BleReadRssiEvent,
+  BleFlushComplete,
+  BleFlushTimeout,
+  PulseResponse,
 };
 
 //! Deferred/delayed callbacks use the event subsystem but are invariably sent
diff --git a/core/include/chre/core/event_loop_manager.h b/core/include/chre/core/event_loop_manager.h
index f6e7d74..b5f52b3 100644
--- a/core/include/chre/core/event_loop_manager.h
+++ b/core/include/chre/core/event_loop_manager.h
@@ -24,6 +24,7 @@
 #include "chre/core/host_endpoint_manager.h"
 #include "chre/core/settings.h"
 #include "chre/core/system_health_monitor.h"
+#include "chre/platform/atomic.h"
 #include "chre/platform/memory_manager.h"
 #include "chre/platform/mutex.h"
 #include "chre/util/always_false.h"
@@ -349,8 +350,8 @@
   void lateInit();
 
  private:
-  //! The instance ID that was previously generated by getNextInstanceId()
-  uint16_t mLastInstanceId = kSystemInstanceId;
+  //! The instance ID generated by getNextInstanceId().
+  AtomicUint32 mNextInstanceId{kSystemInstanceId + 1};
 
 #ifdef CHRE_AUDIO_SUPPORT_ENABLED
   //! The audio request manager handles requests for all nanoapps and manages
diff --git a/core/include/chre/core/gnss_manager.h b/core/include/chre/core/gnss_manager.h
index 7b88d31..d4885ff 100644
--- a/core/include/chre/core/gnss_manager.h
+++ b/core/include/chre/core/gnss_manager.h
@@ -413,11 +413,11 @@
 
   GnssSession &getLocationSession() {
     return mLocationSession;
-  };
+  }
 
   GnssSession &getMeasurementSession() {
     return mMeasurementSession;
-  };
+  }
 
   /**
    * Invoked when the host notifies CHRE of a settings change.
diff --git a/core/include/chre/core/host_endpoint_manager.h b/core/include/chre/core/host_endpoint_manager.h
index 40781bb..8fd7e09 100644
--- a/core/include/chre/core/host_endpoint_manager.h
+++ b/core/include/chre/core/host_endpoint_manager.h
@@ -90,6 +90,6 @@
   auto getHostNotificationCallback();
 };
 
-};  // namespace chre
+}  // namespace chre
 
 #endif  // CHRE_CORE_HOST_ENDPOINT_MANAGER_H_
diff --git a/core/include/chre/core/nanoapp.h b/core/include/chre/core/nanoapp.h
index ae72327..9fb9a98 100644
--- a/core/include/chre/core/nanoapp.h
+++ b/core/include/chre/core/nanoapp.h
@@ -25,6 +25,7 @@
 #include "chre/core/event_ref_queue.h"
 #include "chre/platform/heap_block_header.h"
 #include "chre/platform/platform_nanoapp.h"
+#include "chre/platform/system_time.h"
 #include "chre/util/dynamic_vector.h"
 #include "chre/util/fixed_size_vector.h"
 #include "chre/util/system/debug_dump.h"
@@ -56,6 +57,10 @@
 
   Nanoapp();
 
+  // The nanoapp instance ID should only come from the event loop manager. This
+  // constructor should never be called except for use in unit tests.
+  Nanoapp(uint16_t instanceId);
+
   /**
    * Calls the start function of the nanoapp. For dynamically loaded nanoapps,
    * this must also result in calling through to any of the nanoapp's static
@@ -76,14 +81,6 @@
   }
 
   /**
-   * Assigns an instance ID to this Nanoapp. This must be called prior to
-   * starting this Nanoapp.
-   */
-  void setInstanceId(uint16_t instanceId) {
-    mInstanceId = instanceId;
-  }
-
-  /**
    * @return The current total number of bytes the nanoapp has allocated.
    */
   size_t getTotalAllocatedBytes() const {
@@ -188,14 +185,23 @@
    */
   void blameHostWakeup();
 
+  /**
+   * Log info about a single message sent to the host that this nanoapp
+   * triggered by storing the count of messages in mNumMessagesSentSinceBoot.
+   */
+  void blameHostMessageSent();
+
   /*
    * If buckets not full, then just pushes a 0 to back of buckets. If full, then
    * shifts down all buckets from back to front and sets back to 0, losing the
    * latest bucket value that was in front.
    *
-   * @param numBuckets the number of buckets to cycle into to mWakeupBuckets
+   * With nanoapps tracking their cycling time, there is no reason to ever
+   * cycle more than one bucket at a time. Doing more wastes valuable data
+   *
+   * @param timestamp the current time when this bucket was created
    */
-  void cycleWakeupBuckets(size_t numBuckets);
+  void cycleWakeupBuckets(Nanoseconds timestamp);
 
   /**
    * Prints state in a string buffer. Must only be called from the context of
@@ -206,6 +212,39 @@
   void logStateToBuffer(DebugDumpWrapper &debugDump) const;
 
   /**
+   * Prints header for memory allocation and event processing time stats table
+   * in a string buffer. Must only be called from the context of the main CHRE
+   * thread.
+   *
+   * @param debugDump The object that is printed into for debug dump logs.
+   */
+  void logMemAndComputeHeader(DebugDumpWrapper &debugDump) const;
+
+  /**
+   * Prints memory allocation and event processing time stats in a string
+   * buffer. Must only be called from the context of the main CHRE thread.
+   *
+   * @param debugDump The object that is printed into for debug dump logs.
+   */
+  void logMemAndComputeEntry(DebugDumpWrapper &debugDump) const;
+
+  /**
+   * Prints header for wakeup and host message stats table in a string buffer.
+   * Must only be called from the context of the main CHRE thread.
+   *
+   * @param debugDump The object that is printed into for debug dump logs.
+   */
+  void logMessageHistoryHeader(DebugDumpWrapper &debugDump) const;
+
+  /**
+   * Prints wakeup and host message stats in a string buffer. Must only be
+   * called from the context of the main CHRE thread.
+   *
+   * @param debugDump The object that is printed into for debug dump logs.
+   */
+  void logMessageHistoryEntry(DebugDumpWrapper &debugDump) const;
+
+  /**
    * @return true if the nanoapp is permitted to use the provided permission.
    */
   bool permitPermissionUse(uint32_t permission) const;
@@ -269,6 +308,12 @@
   //! The total number of wakeup counts for a nanoapp.
   uint32_t mNumWakeupsSinceBoot = 0;
 
+  //! The total number of messages sent to host by this nanoapp.
+  uint32_t mNumMessagesSentSinceBoot = 0;
+
+  //! The total time in ms spend processing events by this nanoapp.
+  uint64_t mEventProcessTimeSinceBoot = 0;
+
   /**
    * Head of the singly linked list of heap block headers.
    *
@@ -284,13 +329,28 @@
   //! The peak total number of bytes allocated by the nanoapp.
   size_t mPeakAllocatedBytes = 0;
 
+  //! Container for "bucketed" stats associated with wakeup logging
+  struct BucketedStats {
+    BucketedStats(uint16_t wakeupCount_, uint16_t hostMessageCount_,
+                  uint64_t eventProcessTime_, uint64_t creationTimestamp_)
+        : wakeupCount(wakeupCount_),
+          hostMessageCount(hostMessageCount_),
+          eventProcessTime(eventProcessTime_),
+          creationTimestamp(creationTimestamp_) {}
+
+    uint16_t wakeupCount = 0;
+    uint16_t hostMessageCount = 0;
+    uint64_t eventProcessTime = 0;
+    uint64_t creationTimestamp = 0;
+  };
+
   //! The number of buckets for wakeup logging, adjust along with
-  //! EventLoop::kIntervalWakupBucketInMins.
-  static constexpr size_t kMaxSizeWakeupBuckets = 4;
+  //! EventLoop::kIntervalWakeupBucket.
+  static constexpr size_t kMaxSizeWakeupBuckets = 5;
 
   //! A fixed size buffer of buckets that keeps track of the number of host
   //! wakeups over time intervals.
-  FixedSizeVector<uint16_t, kMaxSizeWakeupBuckets> mWakeupBuckets;
+  FixedSizeVector<BucketedStats, kMaxSizeWakeupBuckets> mWakeupBuckets;
 
   //! Collects process time in nanoseconds of each event
   StatsContainer<uint64_t> mEventProcessTime;
diff --git a/core/include/chre/core/request_multiplexer_impl.h b/core/include/chre/core/request_multiplexer_impl.h
index 63b742f..a5dfb34 100644
--- a/core/include/chre/core/request_multiplexer_impl.h
+++ b/core/include/chre/core/request_multiplexer_impl.h
@@ -27,7 +27,7 @@
                                                  size_t *index,
                                                  bool *maximalRequestChanged) {
   CHRE_ASSERT_NOT_NULL(index);
-  CHRE_ASSERT(maximalRequestChanged);
+  CHRE_ASSERT_NOT_NULL(maximalRequestChanged);
 
   bool requestStored = mRequests.push_back(request);
   if (requestStored) {
@@ -43,7 +43,7 @@
                                                  size_t *index,
                                                  bool *maximalRequestChanged) {
   CHRE_ASSERT_NOT_NULL(index);
-  CHRE_ASSERT(maximalRequestChanged);
+  CHRE_ASSERT_NOT_NULL(maximalRequestChanged);
 
   bool requestStored = mRequests.push_back(std::move(request));
   if (requestStored) {
@@ -57,7 +57,7 @@
 template <typename RequestType>
 void RequestMultiplexer<RequestType>::updateRequest(
     size_t index, const RequestType &request, bool *maximalRequestChanged) {
-  CHRE_ASSERT(maximalRequestChanged);
+  CHRE_ASSERT_NOT_NULL(maximalRequestChanged);
   CHRE_ASSERT(index < mRequests.size());
 
   if (index < mRequests.size()) {
@@ -114,6 +114,8 @@
 template <typename RequestType>
 void RequestMultiplexer<RequestType>::updateMaximalRequest(
     bool *maximalRequestChanged) {
+  CHRE_ASSERT_NOT_NULL(maximalRequestChanged);
+
   RequestType maximalRequest;
   for (size_t i = 0; i < mRequests.size(); i++) {
     maximalRequest.mergeWith(mRequests[i]);
diff --git a/core/include/chre/core/system_health_monitor.h b/core/include/chre/core/system_health_monitor.h
index 2b203a4..ea4af24 100644
--- a/core/include/chre/core/system_health_monitor.h
+++ b/core/include/chre/core/system_health_monitor.h
@@ -17,6 +17,8 @@
 #ifndef CHRE_CORE_SYSTEM_HEALTH_MONITOR_H_
 #define CHRE_CORE_SYSTEM_HEALTH_MONITOR_H_
 
+#include <cstdint>
+
 #include "chre/platform/assert.h"
 #include "chre/platform/log.h"
 #include "chre/util/enum.h"
diff --git a/core/include/chre/core/timer_pool.h b/core/include/chre/core/timer_pool.h
index fc44785..d6dd204 100644
--- a/core/include/chre/core/timer_pool.h
+++ b/core/include/chre/core/timer_pool.h
@@ -149,10 +149,10 @@
     uint16_t instanceId;
 
     /**
-     * Provides a greater than comparison of TimerRequests.
+     * Returns whether the current request expires after the passed one.
      *
-     * @param request The other request to compare against.
-     * @return Returns true if this request is greater than the provided
+     * @param request The other request.
+     * @return Returns whether this request expires after the provided
      *         request.
      */
     bool operator>(const TimerRequest &request) const;
diff --git a/core/log.cc b/core/log.cc
index c8ac766..f95b888 100644
--- a/core/log.cc
+++ b/core/log.cc
@@ -16,6 +16,7 @@
 
 #ifdef CHRE_TOKENIZED_LOGGING_ENABLED
 #include "chre/platform/log.h"
+#include "pw_log_tokenized/config.h"
 #include "pw_tokenizer/encode_args.h"
 #include "pw_tokenizer/tokenize.h"
 
@@ -26,7 +27,8 @@
                             pw_tokenizer_ArgTypes types, ...) {
   va_list args;
   va_start(args, types);
-  pw::tokenizer::EncodedMessage encodedMessage(token, types, args);
+  pw::tokenizer::EncodedMessage<pw::log_tokenized::kEncodingBufferSizeBytes>
+      encodedMessage(token, types, args);
   va_end(args);
 
   chrePlatformEncodedLogToBuffer(static_cast<chreLogLevel>(level),
diff --git a/core/nanoapp.cc b/core/nanoapp.cc
index 789f2da..faf00da 100644
--- a/core/nanoapp.cc
+++ b/core/nanoapp.cc
@@ -36,13 +36,18 @@
 
 constexpr size_t Nanoapp::kMaxSizeWakeupBuckets;
 
-Nanoapp::Nanoapp() {
+Nanoapp::Nanoapp()
+    : Nanoapp(EventLoopManagerSingleton::get()->getNextInstanceId()) {}
+
+Nanoapp::Nanoapp(uint16_t instanceId) {
   // Push first bucket onto wakeup bucket queue
-  cycleWakeupBuckets(1);
+  cycleWakeupBuckets(SystemTime::getMonotonicTime());
+  mInstanceId = instanceId;
 }
 
 bool Nanoapp::start() {
-  traceRegisterNanoapp(getInstanceId(), getAppName());
+  // TODO(b/294116163): update trace with nanoapp instance id and nanoapp name
+  CHRE_TRACE_INSTANT("Nanoapp start");
   mIsInNanoappStart = true;
   bool success = PlatformNanoapp::start();
   mIsInNanoappStart = false;
@@ -134,37 +139,49 @@
 
 void Nanoapp::processEvent(Event *event) {
   Nanoseconds eventStartTime = SystemTime::getMonotonicTime();
-  traceNanoappHandleEventStart(getInstanceId(), event->eventType);
+  // TODO(b/294116163): update trace with event type and nanoapp name so it can
+  //                    be differentiated from other events
+  CHRE_TRACE_START("Handle event", "nanoapp", getInstanceId());
   if (event->eventType == CHRE_EVENT_GNSS_DATA) {
     handleGnssMeasurementDataEvent(event);
   } else {
     handleEvent(event->senderInstanceId, event->eventType, event->eventData);
   }
-  traceNanoappHandleEventEnd(getInstanceId());
+  // TODO(b/294116163): update trace with nanoapp name
+  CHRE_TRACE_END("Handle event", "nanoapp", getInstanceId());
   Nanoseconds eventProcessTime =
       SystemTime::getMonotonicTime() - eventStartTime;
+  uint64_t eventTimeMs = Milliseconds(eventProcessTime).getMilliseconds();
   if (Milliseconds(eventProcessTime) >= Milliseconds(100)) {
     LOGE("Nanoapp 0x%" PRIx64 " took %" PRIu64
-         " ms to process event type %" PRIu16,
-         getAppId(), Milliseconds(eventProcessTime).getMilliseconds(),
-         event->eventType);
+         " ms to process event type 0x%" PRIx16,
+         getAppId(), eventTimeMs, event->eventType);
   }
-  mEventProcessTime.addValue(Milliseconds(eventProcessTime).getMilliseconds());
+  mEventProcessTime.addValue(eventTimeMs);
+  mEventProcessTimeSinceBoot += eventTimeMs;
+  mWakeupBuckets.back().eventProcessTime += eventTimeMs;
 }
 
 void Nanoapp::blameHostWakeup() {
-  if (mWakeupBuckets.back() < UINT16_MAX) ++mWakeupBuckets.back();
+  if (mWakeupBuckets.back().wakeupCount < UINT16_MAX) {
+    ++mWakeupBuckets.back().wakeupCount;
+  }
   if (mNumWakeupsSinceBoot < UINT32_MAX) ++mNumWakeupsSinceBoot;
 }
 
-void Nanoapp::cycleWakeupBuckets(size_t numBuckets) {
-  numBuckets = std::min(numBuckets, kMaxSizeWakeupBuckets);
-  for (size_t i = 0; i < numBuckets; ++i) {
-    if (mWakeupBuckets.full()) {
-      mWakeupBuckets.erase(0);
-    }
-    mWakeupBuckets.push_back(0);
+void Nanoapp::blameHostMessageSent() {
+  if (mWakeupBuckets.back().hostMessageCount < UINT16_MAX) {
+    ++mWakeupBuckets.back().hostMessageCount;
   }
+  if (mNumMessagesSentSinceBoot < UINT32_MAX) ++mNumMessagesSentSinceBoot;
+}
+
+void Nanoapp::cycleWakeupBuckets(Nanoseconds timestamp) {
+  if (mWakeupBuckets.full()) {
+    mWakeupBuckets.erase(0);
+  }
+  mWakeupBuckets.push_back(
+      BucketedStats(0, 0, 0, timestamp.toRawNanoseconds()));
 }
 
 void Nanoapp::logStateToBuffer(DebugDumpWrapper &debugDump) const {
@@ -172,27 +189,134 @@
                   getAppId());
   PlatformNanoapp::logStateToBuffer(debugDump);
   debugDump.print(" v%" PRIu32 ".%" PRIu32 ".%" PRIu32 " tgtAPI=%" PRIu32
-                  ".%" PRIu32 " curAlloc=%zu peakAlloc=%zu",
+                  ".%" PRIu32 "\n",
                   CHRE_EXTRACT_MAJOR_VERSION(getAppVersion()),
                   CHRE_EXTRACT_MINOR_VERSION(getAppVersion()),
                   CHRE_EXTRACT_PATCH_VERSION(getAppVersion()),
                   CHRE_EXTRACT_MAJOR_VERSION(getTargetApiVersion()),
-                  CHRE_EXTRACT_MINOR_VERSION(getTargetApiVersion()),
-                  getTotalAllocatedBytes(), getPeakAllocatedBytes());
-  debugDump.print(" hostWakeups=[ cur->");
-  // Get buckets latest -> earliest except last one
-  for (size_t i = mWakeupBuckets.size() - 1; i > 0; --i) {
-    debugDump.print("%" PRIu16 ", ", mWakeupBuckets[i]);
+                  CHRE_EXTRACT_MINOR_VERSION(getTargetApiVersion()));
+}
+
+void Nanoapp::logMemAndComputeHeader(DebugDumpWrapper &debugDump) const {
+  // Print table header
+  // Nanoapp column sized to accommodate largest known name
+  debugDump.print("\n%10sNanoapp%9s| Mem Alloc (Bytes) |%7sEvent Time (Ms)\n",
+                  "", "", "");
+  debugDump.print("%26s| Current |     Max |    Mean |     Max |   Total\n",
+                  "");
+}
+
+void Nanoapp::logMemAndComputeEntry(DebugDumpWrapper &debugDump) const {
+  debugDump.print("%*s |", 25, getAppName());
+  debugDump.print(" %*zu |", 7, getTotalAllocatedBytes());
+  debugDump.print(" %*zu |", 7, getPeakAllocatedBytes());
+  debugDump.print(" %*" PRIu64 " |", 7, mEventProcessTime.getMean());
+  debugDump.print(" %*" PRIu64 " |", 7, mEventProcessTime.getMax());
+  debugDump.print(" %*" PRIu64 "\n", 7, mEventProcessTimeSinceBoot);
+}
+
+void Nanoapp::logMessageHistoryHeader(DebugDumpWrapper &debugDump) const {
+  // Print time ranges for buckets
+  Nanoseconds now = SystemTime::getMonotonicTime();
+  uint64_t currentTimeMins = 0;
+  uint64_t nextTimeMins = 0;
+  uint64_t nanosecondsSince = 0;
+  char bucketLabel = 'A';
+
+  char bucketTags[kMaxSizeWakeupBuckets][4];
+  for (int32_t i = kMaxSizeWakeupBuckets - 1; i >= 0; --i) {
+    bucketTags[i][0] = '[';
+    bucketTags[i][1] = bucketLabel++;
+    bucketTags[i][2] = ']';
+    bucketTags[i][3] = '\0';
   }
-  // Earliest bucket gets no comma
-  debugDump.print("%" PRIu16 " ]", mWakeupBuckets.front());
 
-  // Print total wakeups since boot
-  debugDump.print(" totWakeups=%" PRIu32 " ", mNumWakeupsSinceBoot);
+  debugDump.print(
+      "\nHistogram stat buckets cover the following time ranges:\n");
 
-  // Print mean and max event process time
-  debugDump.print("eventProcessTimeMs: mean=%" PRIu64 ", max=%" PRIu64 "\n",
-                  mEventProcessTime.getMean(), mEventProcessTime.getMax());
+  for (int32_t i = kMaxSizeWakeupBuckets - 1;
+       i > static_cast<int32_t>(mWakeupBuckets.size() - 1); --i) {
+    debugDump.print(" Bucket%s: N/A (unused)\n", bucketTags[i]);
+  }
+
+  for (int32_t i = static_cast<int32_t>(mWakeupBuckets.size() - 1); i >= 0;
+       --i) {
+    size_t idx = static_cast<size_t>(i);
+    nanosecondsSince =
+        now.toRawNanoseconds() - mWakeupBuckets[idx].creationTimestamp;
+    currentTimeMins = (nanosecondsSince / kOneMinuteInNanoseconds);
+
+    debugDump.print(" Bucket%s:", bucketTags[idx]);
+    debugDump.print(" %*" PRIu64 "", 3, nextTimeMins);
+    debugDump.print(" - %*" PRIu64 " mins ago\n", 3, currentTimeMins);
+    nextTimeMins = currentTimeMins;
+  }
+
+  int wuHistColWidth = 2 + (4 * kMaxSizeWakeupBuckets);
+  int messageHistColWidth = 2 + (4 * kMaxSizeWakeupBuckets);
+  int eventHistColWidth = 2 + (7 * kMaxSizeWakeupBuckets);
+
+  // Print table header
+  debugDump.print("\n%*s|", 26, " Nanoapp ");
+  debugDump.print("%*s|", 11, " Total w/u ");
+  debugDump.print("%*s|", wuHistColWidth, " Wakeup Histogram ");
+  debugDump.print("%*s|", 12, " Total Msgs ");
+  debugDump.print("%*s|", messageHistColWidth, " Message Histogram ");
+  debugDump.print("%*s|", 12, " Event Time ");
+  debugDump.print("%*s", eventHistColWidth, " Event Time Histogram (ms) ");
+
+  debugDump.print("\n%26s|%11s|", "", "");
+  for (int32_t i = kMaxSizeWakeupBuckets - 1; i >= 0; --i) {
+    debugDump.print(" %*s", 3, bucketTags[i]);
+  }
+  debugDump.print("  |%*s|", 12, "");
+  for (int32_t i = kMaxSizeWakeupBuckets - 1; i >= 0; --i) {
+    debugDump.print(" %*s", 3, bucketTags[i]);
+  }
+  debugDump.print("  |%*s|", 12, "");
+  for (int32_t i = kMaxSizeWakeupBuckets - 1; i >= 0; --i) {
+    debugDump.print(" %*s", 7, bucketTags[i]);
+  }
+  debugDump.print("\n");
+}
+
+void Nanoapp::logMessageHistoryEntry(DebugDumpWrapper &debugDump) const {
+  debugDump.print("%*s |", 25, getAppName());
+
+  // Print wakeupCount and histogram
+  debugDump.print(" %*" PRIu32 " | ", 9, mNumWakeupsSinceBoot);
+  for (size_t i = kMaxSizeWakeupBuckets - 1; i > 0; --i) {
+    if (i >= mWakeupBuckets.size()) {
+      debugDump.print(" --,");
+    } else {
+      debugDump.print(" %*" PRIu16 ",", 2, mWakeupBuckets[i].wakeupCount);
+    }
+  }
+  debugDump.print(" %*" PRIu16 "  |", 2, mWakeupBuckets.front().wakeupCount);
+
+  // Print hostMessage count and histogram
+  debugDump.print(" %*" PRIu32 " | ", 10, mNumMessagesSentSinceBoot);
+  for (size_t i = kMaxSizeWakeupBuckets - 1; i > 0; --i) {
+    if (i >= mWakeupBuckets.size()) {
+      debugDump.print(" --,");
+    } else {
+      debugDump.print(" %*" PRIu16 ",", 2, mWakeupBuckets[i].hostMessageCount);
+    }
+  }
+  debugDump.print(" %*" PRIu16 "  |", 2,
+                  mWakeupBuckets.front().hostMessageCount);
+
+  // Print eventProcessingTime count and histogram
+  debugDump.print(" %*" PRIu64 " | ", 10, mEventProcessTimeSinceBoot);
+  for (size_t i = kMaxSizeWakeupBuckets - 1; i > 0; --i) {
+    if (i >= mWakeupBuckets.size()) {
+      debugDump.print("     --,");
+    } else {
+      debugDump.print(" %*" PRIu64 ",", 6, mWakeupBuckets[i].eventProcessTime);
+    }
+  }
+  debugDump.print(" %*" PRIu64 "\n", 6,
+                  mWakeupBuckets.front().eventProcessTime);
 }
 
 bool Nanoapp::permitPermissionUse(uint32_t permission) const {
diff --git a/core/telemetry_manager.cc b/core/telemetry_manager.cc
index 1cb47f0..6772cb5 100644
--- a/core/telemetry_manager.cc
+++ b/core/telemetry_manager.cc
@@ -24,7 +24,7 @@
 #include "chre/util/macros.h"
 #include "chre/util/nested_data_ptr.h"
 #include "chre/util/time.h"
-#include "chre_metrics.nanopb.h"
+#include "core/chre_metrics.nanopb.h"
 
 namespace chre {
 
@@ -42,11 +42,15 @@
   _android_chre_metrics_ChrePalType:: \
       android_chre_metrics_ChrePalType_CHRE_PAL_TYPE_##x
 
+// These IDs must be kept in sync with
+// hardware/google/pixel/pixelstats/pixelatoms.proto.
+constexpr uint32_t kEventQueueSnapshotReportedId = 105035;
+constexpr uint32_t kPalOpenedFailedId = 105032;
+
 void sendMetricToHost(uint32_t atomId, const pb_field_t fields[],
                       const void *data) {
   size_t size;
-  if (!pb_get_encoded_size(&size, CHREATOMS_GET(ChrePalOpenFailed_fields),
-                           data)) {
+  if (!pb_get_encoded_size(&size, fields, data)) {
     LOGE("Failed to get message size");
   } else {
     pb_byte_t *bytes = static_cast<pb_byte_t *>(memoryAlloc(size));
@@ -54,13 +58,12 @@
       LOG_OOM();
     } else {
       pb_ostream_t stream = pb_ostream_from_buffer(bytes, size);
-      if (!pb_encode(&stream, CHREATOMS_GET(ChrePalOpenFailed_fields), data)) {
+      if (!pb_encode(&stream, fields, data)) {
         LOGE("Failed to metric error %s", PB_GET_ERROR(&stream));
       } else {
         HostCommsManager &manager =
             EventLoopManagerSingleton::get()->getHostCommsManager();
-        if (!manager.sendMetricLog(CHREATOMS_GET(Atom_chre_pal_open_failed_tag),
-                                   bytes, size)) {
+        if (!manager.sendMetricLog(atomId, bytes, size)) {
           LOGE("Failed to send metric message");
         }
       }
@@ -78,8 +81,8 @@
   result.type = _android_chre_metrics_ChrePalOpenFailed_Type::
       android_chre_metrics_ChrePalOpenFailed_Type_INITIAL_OPEN;
 
-  sendMetricToHost(CHREATOMS_GET(Atom_chre_pal_open_failed_tag),
-                   CHREATOMS_GET(ChrePalOpenFailed_fields), &result);
+  sendMetricToHost(kPalOpenedFailedId, CHREATOMS_GET(ChrePalOpenFailed_fields),
+                   &result);
 }
 
 void sendEventLoopStats(uint32_t maxQueueSize, uint32_t meanQueueSize,
@@ -97,7 +100,7 @@
   result.has_num_dropped_events = true;
   result.num_dropped_events = numDroppedEvents;
 
-  sendMetricToHost(CHREATOMS_GET(Atom_chre_event_queue_snapshot_reported_tag),
+  sendMetricToHost(kEventQueueSnapshotReportedId,
                    CHREATOMS_GET(ChreEventQueueSnapshotReported_fields),
                    &result);
 }
@@ -158,7 +161,7 @@
 }
 
 void TelemetryManager::scheduleMetricTimer() {
-  constexpr Seconds kDelay = Seconds(60 * 60 * 24);  // 24 hours
+  constexpr Seconds kDelay = Seconds(kOneDayInSeconds);
   auto callback = [](uint16_t /* eventType */, void * /* data */,
                      void * /* extraData */) {
     EventLoopManagerSingleton::get()
diff --git a/core/tests/ble_request_test.cc b/core/tests/ble_request_test.cc
index bc0dbc5..e8ed98e 100644
--- a/core/tests/ble_request_test.cc
+++ b/core/tests/ble_request_test.cc
@@ -30,12 +30,12 @@
 }
 
 TEST(BleRequest, AggressiveModeIsHigherThanBackground) {
-  BleRequest backgroundMode(0 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_BACKGROUND,
-                            0 /* reportDelayMs */, nullptr /* filter */);
-  BleRequest aggressiveMode(0 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_AGGRESSIVE,
-                            0 /* reportDelayMs */, nullptr /* filter */);
+  BleRequest backgroundMode(
+      0 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_BACKGROUND,
+      0 /* reportDelayMs */, nullptr /* filter */, nullptr /* cookie */);
+  BleRequest aggressiveMode(
+      0 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_AGGRESSIVE,
+      0 /* reportDelayMs */, nullptr /* filter */, nullptr /* cookie */);
 
   BleRequest mergedRequest;
   EXPECT_TRUE(mergedRequest.mergeWith(aggressiveMode));
@@ -48,14 +48,15 @@
 }
 
 TEST(BleRequest, MergeWithReplacesParametersOfDisabledRequest) {
-  chreBleScanFilter filter;
+  chreBleScanFilterV1_9 filter;
   filter.rssiThreshold = -5;
-  filter.scanFilterCount = 1;
+  filter.genericFilterCount = 1;
   auto scanFilters = std::make_unique<chreBleGenericFilter>();
   scanFilters->type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE;
   scanFilters->len = 2;
-  filter.scanFilters = scanFilters.get();
-  BleRequest enabled(0, true, CHRE_BLE_SCAN_MODE_AGGRESSIVE, 20, &filter);
+  filter.genericFilters = scanFilters.get();
+  BleRequest enabled(0, true, CHRE_BLE_SCAN_MODE_AGGRESSIVE, 20, &filter,
+                     nullptr /* cookie */);
 
   BleRequest mergedRequest;
   EXPECT_FALSE(mergedRequest.isEnabled());
@@ -71,72 +72,84 @@
 }
 
 TEST(BleRequest, IsEquivalentToBasic) {
-  BleRequest backgroundMode(0 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_BACKGROUND,
-                            0 /* reportDelayMs */, nullptr /* filter */);
+  BleRequest backgroundMode(
+      0 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_BACKGROUND,
+      0 /* reportDelayMs */, nullptr /* filter */, nullptr /* cookie */);
   EXPECT_TRUE(backgroundMode.isEquivalentTo(backgroundMode));
 }
 
 TEST(BleRequest, IsNotEquivalentToBasic) {
-  BleRequest backgroundMode(0 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_BACKGROUND,
-                            0 /* reportDelayMs */, nullptr /* filter */);
-  BleRequest aggressiveMode(0 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_AGGRESSIVE,
-                            0 /* reportDelayMs */, nullptr /* filter */);
+  BleRequest backgroundMode(
+      0 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_BACKGROUND,
+      0 /* reportDelayMs */, nullptr /* filter */, nullptr /* cookie */);
+  BleRequest aggressiveMode(
+      0 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_AGGRESSIVE,
+      0 /* reportDelayMs */, nullptr /* filter */, nullptr /* cookie */);
   EXPECT_FALSE(backgroundMode.isEquivalentTo(aggressiveMode));
 }
 
+TEST(BleRequest, IsNotEquivalentWithCookie) {
+  constexpr uint32_t kCookieOne = 123;
+  constexpr uint32_t kCookieTwo = 234;
+  BleRequest requestOne(0 /* instanceId */, true /* enable */,
+                        CHRE_BLE_SCAN_MODE_BACKGROUND, 0 /* reportDelayMs */,
+                        nullptr /* filter */, &kCookieOne);
+  BleRequest requestTwo(0 /* instanceId */, true /* enable */,
+                        CHRE_BLE_SCAN_MODE_BACKGROUND, 0 /* reportDelayMs */,
+                        nullptr /* filter */, &kCookieTwo);
+  EXPECT_TRUE(requestOne.isEquivalentTo(requestTwo));
+}
+
 TEST(BleRequest, IsEquivalentToAdvanced) {
-  chreBleScanFilter filter;
+  chreBleScanFilterV1_9 filter;
   filter.rssiThreshold = -5;
-  filter.scanFilterCount = 1;
+  filter.genericFilterCount = 1;
   auto scanFilters = std::make_unique<chreBleGenericFilter>();
   scanFilters->type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE;
   scanFilters->len = 4;
-  filter.scanFilters = scanFilters.get();
+  filter.genericFilters = scanFilters.get();
 
-  BleRequest backgroundMode(100 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_BACKGROUND,
-                            100 /* reportDelayMs */, &filter);
+  BleRequest backgroundMode(
+      100 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_BACKGROUND,
+      100 /* reportDelayMs */, &filter, nullptr /* cookie */);
   EXPECT_TRUE(backgroundMode.isEquivalentTo(backgroundMode));
 }
 
 TEST(BleRequest, IsNotEquivalentToAdvanced) {
-  chreBleScanFilter filter;
+  chreBleScanFilterV1_9 filter;
   filter.rssiThreshold = -5;
-  filter.scanFilterCount = 1;
+  filter.genericFilterCount = 1;
   auto scanFilters = std::make_unique<chreBleGenericFilter>();
   scanFilters->type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE;
   scanFilters->len = 4;
-  filter.scanFilters = scanFilters.get();
+  filter.genericFilters = scanFilters.get();
 
-  BleRequest backgroundMode(100 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_BACKGROUND,
-                            100 /* reportDelayMs */, &filter /* filter */);
-  BleRequest aggressiveMode(0 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_AGGRESSIVE,
-                            0 /* reportDelayMs */, nullptr /* filter */);
+  BleRequest backgroundMode(
+      100 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_BACKGROUND,
+      100 /* reportDelayMs */, &filter /* filter */, nullptr /* cookie */);
+  BleRequest aggressiveMode(
+      0 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_AGGRESSIVE,
+      0 /* reportDelayMs */, nullptr /* filter */, nullptr /* cookie */);
 
   EXPECT_FALSE(backgroundMode.isEquivalentTo(aggressiveMode));
 }
 
 TEST(BleRequest, GetScanFilter) {
-  chreBleScanFilter filter;
+  chreBleScanFilterV1_9 filter;
   filter.rssiThreshold = -5;
-  filter.scanFilterCount = 1;
+  filter.genericFilterCount = 1;
   auto scanFilters = std::make_unique<chreBleGenericFilter>();
   scanFilters->type = CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE;
   scanFilters->len = 4;
-  filter.scanFilters = scanFilters.get();
+  filter.genericFilters = scanFilters.get();
 
-  BleRequest backgroundMode(100 /* instanceId */, true /* enable */,
-                            CHRE_BLE_SCAN_MODE_BACKGROUND,
-                            100 /* reportDelayMs */, &filter /* filter */);
+  BleRequest backgroundMode(
+      100 /* instanceId */, true /* enable */, CHRE_BLE_SCAN_MODE_BACKGROUND,
+      100 /* reportDelayMs */, &filter /* filter */, nullptr /* cookie */);
 
-  chreBleScanFilter retFilter = backgroundMode.getScanFilter();
+  chreBleScanFilterV1_9 retFilter = backgroundMode.getScanFilter();
   EXPECT_EQ(filter.rssiThreshold, retFilter.rssiThreshold);
-  EXPECT_EQ(filter.scanFilterCount, retFilter.scanFilterCount);
-  EXPECT_EQ(0, memcmp(scanFilters.get(), retFilter.scanFilters,
+  EXPECT_EQ(filter.genericFilterCount, retFilter.genericFilterCount);
+  EXPECT_EQ(0, memcmp(scanFilters.get(), retFilter.genericFilters,
                       sizeof(chreBleGenericFilter)));
 }
diff --git a/core/tests/memory_manager_test.cc b/core/tests/memory_manager_test.cc
index 69e07a0..b7b151c 100644
--- a/core/tests/memory_manager_test.cc
+++ b/core/tests/memory_manager_test.cc
@@ -16,10 +16,12 @@
 
 #include "gtest/gtest.h"
 
+#include "chre/core/event.h"
 #include "chre/platform/log.h"
 #include "chre/platform/memory.h"
 #include "chre/platform/memory_manager.h"
 
+using chre::kInvalidInstanceId;
 using chre::MemoryManager;
 using chre::Nanoapp;
 
@@ -36,7 +38,7 @@
 
 TEST(MemoryManager, BasicAllocationFree) {
   MemoryManager manager;
-  Nanoapp app;
+  Nanoapp app(kInvalidInstanceId);
   void *ptr = manager.nanoappAlloc(&app, 1u);
   EXPECT_NE(ptr, nullptr);
   EXPECT_EQ(manager.getTotalAllocatedBytes(), 1u);
@@ -48,7 +50,7 @@
 
 TEST(MemoryManager, NullPointerFree) {
   MemoryManager manager;
-  Nanoapp app;
+  Nanoapp app(kInvalidInstanceId);
   manager.nanoappFree(&app, nullptr);
   EXPECT_EQ(manager.getTotalAllocatedBytes(), 0u);
   EXPECT_EQ(manager.getAllocationCount(), 0u);
@@ -56,7 +58,7 @@
 
 TEST(MemoryManager, ZeroAllocationFails) {
   MemoryManager manager;
-  Nanoapp app;
+  Nanoapp app(kInvalidInstanceId);
   void *ptr = manager.nanoappAlloc(&app, 0u);
   EXPECT_EQ(ptr, nullptr);
   EXPECT_EQ(manager.getTotalAllocatedBytes(), 0u);
@@ -65,7 +67,7 @@
 
 TEST(MemoryManager, HugeAllocationFails) {
   MemoryManager manager;
-  Nanoapp app;
+  Nanoapp app(kInvalidInstanceId);
   void *ptr = manager.nanoappAlloc(&app, manager.getMaxAllocationBytes() + 1);
   EXPECT_EQ(ptr, nullptr);
   EXPECT_EQ(manager.getTotalAllocatedBytes(), 0u);
@@ -73,7 +75,7 @@
 
 TEST(MemoryManager, ManyAllocationsTest) {
   MemoryManager manager;
-  Nanoapp app;
+  Nanoapp app(kInvalidInstanceId);
   size_t maxCount = manager.getMaxAllocationCount();
   node *head = static_cast<node *>(manager.nanoappAlloc(&app, sizeof(node)));
   node *curr = nullptr, *prev = head;
@@ -99,7 +101,7 @@
 
 TEST(MemoryManager, NegativeAllocationFails) {
   MemoryManager manager;
-  Nanoapp app;
+  Nanoapp app(kInvalidInstanceId);
   void *ptr = manager.nanoappAlloc(&app, -1u);
   EXPECT_EQ(ptr, nullptr);
   EXPECT_EQ(manager.getTotalAllocatedBytes(), 0u);
diff --git a/core/timer_pool.cc b/core/timer_pool.cc
index 2db3a29..521300c 100644
--- a/core/timer_pool.cc
+++ b/core/timer_pool.cc
@@ -81,16 +81,22 @@
   timerRequest.callbackType = callbackType;
   timerRequest.isOneShot = isOneShot;
 
-  bool newTimerExpiresEarliest =
-      (!mTimerRequests.empty() && mTimerRequests.top() > timerRequest);
   bool success = insertTimerRequestLocked(timerRequest);
 
   if (success) {
-    if (newTimerExpiresEarliest) {
-      mSystemTimer.set(handleSystemTimerCallback, this, duration);
-    } else if (mTimerRequests.size() == 1) {
+    if (mTimerRequests.size() == 1) {
       // If this timer request was the first, schedule it.
       handleExpiredTimersAndScheduleNextLocked();
+    } else {
+      // If there was already a timer pending before this, and we just inserted
+      // to the top of the queue, just update the system timer. This is slightly
+      // more efficient than calling into
+      // handleExpiredTimersAndScheduleNextLocked().
+      bool newRequestExpiresFirst =
+          timerRequest.timerHandle == mTimerRequests.top().timerHandle;
+      if (newRequestExpiresFirst) {
+        mSystemTimer.set(handleSystemTimerCallback, this, duration);
+      }
     }
   }
 
@@ -132,7 +138,7 @@
 }
 
 bool TimerPool::TimerRequest::operator>(const TimerRequest &request) const {
-  return (expirationTime > request.expirationTime);
+  return expirationTime > request.expirationTime;
 }
 
 TimerHandle TimerPool::generateTimerHandleLocked() {
@@ -264,7 +270,7 @@
         // insert operation (thereby invalidating it).
         TimerRequest cyclicTimerRequest = currentTimerRequest;
         cyclicTimerRequest.expirationTime =
-            currentTime + currentTimerRequest.duration;
+            currentTimerRequest.expirationTime + currentTimerRequest.duration;
         popTimerRequestLocked();
         CHRE_ASSERT(insertTimerRequestLocked(cyclicTimerRequest));
       } else {
diff --git a/doc/porting_guide.md b/doc/porting_guide.md
index 1c960b7..7fa4345 100644
--- a/doc/porting_guide.md
+++ b/doc/porting_guide.md
@@ -51,7 +51,7 @@
   `platform_sensor_base.h` respectively, or required macros
 
 * **Fully platform-specific headers**: these typically appear at
-  `platform/<platform_name>/include/chre/platform/<platform_name/<file_name>.h`
+  `platform/<platform_name>/include/chre/platform/<platform_name>/<file_name>.h`
   and may only be included by other platform-specific code
 
 ## Open Sourcing
diff --git a/external/pigweed/pw_rpc.mk b/external/pigweed/pw_rpc.mk
deleted file mode 100644
index 35fb25f..0000000
--- a/external/pigweed/pw_rpc.mk
+++ /dev/null
@@ -1,241 +0,0 @@
-#
-# Makefile for Pigweed's RPC module
-#
-# NOTE: In order to use this, you *must* have the following:
-# - Installed mypy-protobuf and protoc
-# - nanopb-c git repo checked out
-#
-
-ifneq ($(PW_RPC_SRCS),)
-
-# Environment Checks ###########################################################
-
-# Location of various Pigweed modules
-PIGWEED_DIR = $(ANDROID_BUILD_TOP)/external/pigweed
-CHRE_PREFIX = $(ANDROID_BUILD_TOP)/system/chre
-CHRE_UTIL_DIR = $(CHRE_PREFIX)/util
-CHRE_API_DIR = $(CHRE_PREFIX)/chre_api
-PIGWEED_CHRE_DIR=$(CHRE_PREFIX)/external/pigweed
-PIGWEED_CHRE_UTIL_DIR = $(CHRE_UTIL_DIR)/pigweed
-
-ifeq ($(NANOPB_PREFIX),)
-$(error "PW_RPC_SRCS is non-empty. You must supply a NANOPB_PREFIX environment \
-         variable containing a path to the nanopb project. Example: \
-         export NANOPB_PREFIX=$$HOME/path/to/nanopb/nanopb-c")
-endif
-
-ifeq ($(PROTOC),)
-PROTOC=protoc
-endif
-
-PW_RPC_GEN_PATH = $(OUT)/pw_rpc_gen
-
-# Create proto used for header generation ######################################
-
-PW_RPC_PROTO_GENERATOR = $(PIGWEED_DIR)/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
-PW_RPC_GENERATOR_PROTO = $(PIGWEED_DIR)/pw_rpc/internal/packet.proto
-PW_RPC_GENERATOR_COMPILED_PROTO = $(PW_RPC_GEN_PATH)/py/pw_rpc/internal/packet_pb2.py
-PW_PROTOBUF_PROTOS = $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos/common.proto \
-	  $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos/field_options.proto \
-	  $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos/status.proto
-
-# Modifies PYTHONPATH so that python can see all of pigweed's modules used by
-# their protoc plugins
-PW_RPC_GENERATOR_CMD = PYTHONPATH=$$PYTHONPATH:$(PW_RPC_GEN_PATH)/py:$\
-  $(PIGWEED_DIR)/pw_status/py:$(PIGWEED_DIR)/pw_protobuf/py:$\
-  $(PIGWEED_DIR)/pw_protobuf_compiler/py $(PYTHON)
-
-$(PW_RPC_GENERATOR_COMPILED_PROTO): $(PW_RPC_GENERATOR_PROTO)
-	@echo " [PW_RPC] $<"
-	$(V)mkdir -p $(PW_RPC_GEN_PATH)/py/pw_rpc/internal
-	$(V)mkdir -p $(PW_RPC_GEN_PATH)/py/pw_protobuf_codegen_protos
-	$(V)mkdir -p $(PW_RPC_GEN_PATH)/py/pw_protobuf_protos
-	$(V)cp -R $(PIGWEED_DIR)/pw_rpc/py/pw_rpc $(PW_RPC_GEN_PATH)/py/
-
-	$(PROTOC) -I$(PIGWEED_DIR)/pw_protobuf/pw_protobuf_protos \
-	  --experimental_allow_proto3_optional \
-	  --python_out=$(PW_RPC_GEN_PATH)/py/pw_protobuf_protos \
-	  $(PW_PROTOBUF_PROTOS)
-
-	$(PROTOC) -I$(PIGWEED_DIR)/pw_protobuf/pw_protobuf_codegen_protos \
-	  --experimental_allow_proto3_optional \
-	  --python_out=$(PW_RPC_GEN_PATH)/py/pw_protobuf_codegen_protos \
-	  $(PIGWEED_DIR)/pw_protobuf/pw_protobuf_codegen_protos/codegen_options.proto
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) --out-dir=$(PW_RPC_GEN_PATH)/py/pw_rpc/internal \
-	  --compile-dir=$(dir $<) --sources $(PW_RPC_GENERATOR_PROTO) \
-	  --language python
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_protobuf/py/pw_protobuf/plugin.py \
-	  --compile-dir=$(dir $<) --sources $(PW_RPC_GENERATOR_PROTO) \
-	  --language pwpb
-
-# Generated PW RPC Files #######################################################
-
-PW_RPC_GEN_SRCS = $(patsubst %.proto, \
-                             $(PW_RPC_GEN_PATH)/%.pb.c, \
-                             $(PW_RPC_SRCS))
-
-# Include to-be-generated files
-COMMON_CFLAGS += -I$(PW_RPC_GEN_PATH)
-COMMON_CFLAGS += -I$(PW_RPC_GEN_PATH)/$(PIGWEED_DIR)
-COMMON_CFLAGS += $(addprefix -I$(PW_RPC_GEN_PATH)/, $(abspath $(dir $(PW_RPC_SRCS))))
-
-COMMON_SRCS += $(PW_RPC_GEN_SRCS)
-
-# PW RPC library ###############################################################
-
-# Pigweed RPC include paths
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_assert/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_bytes/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_containers/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_function/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_log/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public_overrides
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/standard_library_public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_preprocessor/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_protobuf/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_result/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/nanopb/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/pwpb/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_rpc/raw/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public_overrides
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_status/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_stream/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_string/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_sync/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_toolchain/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_varint/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/third_party/fuchsia/repo/sdk/lib/fit/include
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/third_party/fuchsia/repo/sdk/lib/stdcompat/include
-
-# Pigweed RPC sources
-COMMON_SRCS += $(PIGWEED_DIR)/pw_assert_log/assert_log.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_containers/intrusive_list.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_protobuf/decoder.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_protobuf/encoder.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_protobuf/stream_decoder.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/call.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/channel.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/channel_list.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/client.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/client_call.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/endpoint.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/packet.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/server.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/server_call.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/service.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/nanopb/common.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/nanopb/method.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/nanopb/server_reader_writer.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_rpc/pwpb/server_reader_writer.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_stream/memory_stream.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/stream.cc
-COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/varint.cc
-
-# NanoPB header includes
-COMMON_CFLAGS += -I$(NANOPB_PREFIX)
-
-COMMON_CFLAGS += -DPW_RPC_USE_GLOBAL_MUTEX=0
-COMMON_CFLAGS += -DPW_RPC_YIELD_MODE=PW_RPC_YIELD_MODE_BUSY_LOOP
-
-# Enable closing a client stream.
-COMMON_CFLAGS += -DPW_RPC_CLIENT_STREAM_END_CALLBACK
-
-
-# Use dynamic channel allocation
-COMMON_CFLAGS += -DPW_RPC_DYNAMIC_ALLOCATION
-COMMON_CFLAGS += -DPW_RPC_DYNAMIC_CONTAINER\(type\)="chre::DynamicVector<type>"
-COMMON_CFLAGS += -DPW_RPC_DYNAMIC_CONTAINER_INCLUDE='"chre/util/dynamic_vector.h"'
-
-# NanoPB sources
-COMMON_SRCS += $(NANOPB_PREFIX)/pb_common.c
-COMMON_SRCS += $(NANOPB_PREFIX)/pb_decode.c
-COMMON_SRCS += $(NANOPB_PREFIX)/pb_encode.c
-
-COMMON_CFLAGS += -DPB_NO_PACKED_STRUCTS=1
-
-# Add CHRE Pigweed util sources since nanoapps should always use these
-COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/chre_channel_output.cc
-COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/rpc_client.cc
-COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/rpc_helper.cc
-COMMON_SRCS += $(PIGWEED_CHRE_UTIL_DIR)/rpc_server.cc
-COMMON_SRCS += $(CHRE_UTIL_DIR)/nanoapp/callbacks.cc
-COMMON_SRCS += $(CHRE_UTIL_DIR)/dynamic_vector_base.cc
-
-# CHRE Pigweed overrides
-COMMON_CFLAGS += -I$(PIGWEED_CHRE_DIR)/pw_log_nanoapp/public_overrides
-COMMON_CFLAGS += -I$(PIGWEED_CHRE_DIR)/pw_assert_nanoapp/public_overrides
-
-# Generate PW RPC headers ######################################################
-
-$(PW_RPC_GEN_PATH)/%.pb.c \
-        $(PW_RPC_GEN_PATH)/%.pb.h \
-        $(PW_RPC_GEN_PATH)/%.rpc.pb.h \
-        $(PW_RPC_GEN_PATH)/%.raw_rpc.pb.h: %.proto \
-                                           %.options \
-                                           $(NANOPB_GENERATOR_SRCS) \
-                                           $(PW_RPC_GENERATOR_COMPILED_PROTO)
-	@echo " [PW_RPC] $<"
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(NANOPB_PROTOC) \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_protobuf/py/pw_protobuf/plugin.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb \
-		--sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_nanopb.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb_rpc \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_raw.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language raw_rpc \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_pwpb.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb_rpc \
-	  --sources $<
-
-$(PW_RPC_GEN_PATH)/%.pb.c \
-        $(PW_RPC_GEN_PATH)/%.pb.h \
-        $(PW_RPC_GEN_PATH)/%.rpc.pb.h \
-        $(PW_RPC_GEN_PATH)/%.raw_rpc.pb.h: %.proto \
-                                           $(NANOPB_OPTIONS) \
-                                           $(NANOPB_GENERATOR_SRCS) \
-                                           $(PW_RPC_GENERATOR_COMPILED_PROTO)
-	@echo " [PW_RPC] $<"
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(NANOPB_PROTOC) \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_protobuf/py/pw_protobuf/plugin.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_nanopb.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language nanopb_rpc \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_raw.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language raw_rpc \
-	  --sources $<
-
-	$(V)$(PW_RPC_GENERATOR_CMD) $(PW_RPC_PROTO_GENERATOR) \
-	  --plugin-path=$(PIGWEED_DIR)/pw_rpc/py/pw_rpc/plugin_pwpb.py \
-	  --out-dir=$(PW_RPC_GEN_PATH)/$(dir $<) --compile-dir=$(dir $<) --language pwpb_rpc \
-	  --sources $<
-endif
\ No newline at end of file
diff --git a/external/pigweed/pw_tokenizer.mk b/external/pigweed/pw_tokenizer.mk
index 15b5c5a..e214e6b 100644
--- a/external/pigweed/pw_tokenizer.mk
+++ b/external/pigweed/pw_tokenizer.mk
@@ -24,13 +24,17 @@
 COMMON_SRCS += $(PIGWEED_DIR)/pw_tokenizer/encode_args.cc
 COMMON_SRCS += $(PIGWEED_DIR)/pw_tokenizer/tokenize.cc
 COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/varint.cc
+COMMON_SRCS += $(PIGWEED_DIR)/pw_varint/varint_c.c
 
 # Pigweed include paths
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_containers/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_log_tokenized/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_log/public
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/public
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_polyfill/standard_library_public
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_preprocessor/public
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_span/public_overrides
 COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_tokenizer/public
-COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_varint/public/
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_varint/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/third_party/fuchsia/repo/sdk/lib/stdcompat/include
\ No newline at end of file
diff --git a/external/pigweed/pw_trace.mk b/external/pigweed/pw_trace.mk
new file mode 100644
index 0000000..1a8328b
--- /dev/null
+++ b/external/pigweed/pw_trace.mk
@@ -0,0 +1,17 @@
+#
+# Makefile for Pigweed's trace module
+#
+
+# Environment Checks
+ifeq ($(ANDROID_BUILD_TOP),)
+$(error "You should supply an ANDROID_BUILD_TOP environment variable \
+         containing a path to the Android source tree. This is typically \
+         provided by initializing the Android build environment.")
+endif
+
+# Location of Pigweed
+PIGWEED_DIR = $(ANDROID_BUILD_TOP)/external/pigweed
+
+# Pigweed include paths
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_trace/public
+COMMON_CFLAGS += -I$(PIGWEED_DIR)/pw_preprocessor/public
\ No newline at end of file
diff --git a/host/common/chre_aidl_hal_client.cc b/host/common/chre_aidl_hal_client.cc
index eb2f425..d4a60ea 100644
--- a/host/common/chre_aidl_hal_client.cc
+++ b/host/common/chre_aidl_hal_client.cc
@@ -30,26 +30,30 @@
 #include <regex>
 #include <stdexcept>
 #include <string>
+#include <unordered_set>
 #include <vector>
 
 #include "chre_api/chre/version.h"
 #include "chre_host/file_stream.h"
+#include "chre_host/hal_client.h"
 #include "chre_host/napp_header.h"
 
-using aidl::android::hardware::contexthub::AsyncEventType;
-using aidl::android::hardware::contexthub::BnContextHubCallback;
-using aidl::android::hardware::contexthub::ContextHubInfo;
-using aidl::android::hardware::contexthub::ContextHubMessage;
-using aidl::android::hardware::contexthub::HostEndpointInfo;
-using aidl::android::hardware::contexthub::IContextHub;
-using aidl::android::hardware::contexthub::NanoappBinary;
-using aidl::android::hardware::contexthub::NanoappInfo;
-using aidl::android::hardware::contexthub::NanSessionRequest;
-using aidl::android::hardware::contexthub::Setting;
-using android::chre::NanoAppBinaryHeader;
-using android::chre::readFileContents;
-using android::internal::ToString;
-using ndk::ScopedAStatus;
+using ::aidl::android::hardware::contexthub::AsyncEventType;
+using ::aidl::android::hardware::contexthub::BnContextHubCallback;
+using ::aidl::android::hardware::contexthub::ContextHubInfo;
+using ::aidl::android::hardware::contexthub::ContextHubMessage;
+using ::aidl::android::hardware::contexthub::HostEndpointInfo;
+using ::aidl::android::hardware::contexthub::IContextHub;
+using ::aidl::android::hardware::contexthub::MessageDeliveryStatus;
+using ::aidl::android::hardware::contexthub::NanoappBinary;
+using ::aidl::android::hardware::contexthub::NanoappInfo;
+using ::aidl::android::hardware::contexthub::NanSessionRequest;
+using ::aidl::android::hardware::contexthub::Setting;
+using ::android::chre::HalClient;
+using ::android::chre::NanoAppBinaryHeader;
+using ::android::chre::readFileContents;
+using ::android::internal::ToString;
+using ::ndk::ScopedAStatus;
 
 namespace {
 // A default id 0 is used for every command requiring a context hub id. When
@@ -58,7 +62,17 @@
 constexpr uint32_t kContextHubId = 0;
 constexpr int32_t kLoadTransactionId = 1;
 constexpr int32_t kUnloadTransactionId = 2;
+
+// Though IContextHub.aidl says loading operation is capped at 30s to finish,
+// multiclient HAL can terminate a load/unload transaction after 5s to avoid
+// blocking other load/unload transactions.
 constexpr auto kTimeOutThresholdInSec = std::chrono::seconds(5);
+
+// 34a3a27e-9b83-4098-b564-e83b0c28d4bb
+constexpr std::array<uint8_t, 16> kUuid = {0x34, 0xa3, 0xa2, 0x7e, 0x9b, 0x83,
+                                           0x40, 0x98, 0xb5, 0x64, 0xe8, 0x3b,
+                                           0x0c, 0x28, 0xd4, 0xbb};
+
 // Locations should be searched in the sequence defined below:
 const char *kPredefinedNanoappPaths[] = {
     "/vendor/etc/chre/",
@@ -66,41 +80,8 @@
     "/vendor/dsp/sdsp/",
     "/vendor/lib/rfsa/adsp/",
 };
-// Please keep kUsage in alphabetical order
-constexpr char kUsage[] = R"(
-Usage: chre_aidl_hal_client COMMAND [ARGS]
-COMMAND ARGS...:
-  connect                     - connect to HAL, register the callback and keep
-                                the session alive while user can execute other
-                                commands. Use `exit` to quit the session.
-  connectEndpoint <HEX_HOST_ENDPOINT_ID>
-                              - associate an endpoint with the current client
-                                and notify HAL.
-  disableSetting <SETTING>    - disable a setting identified by a number defined
-                                in android/hardware/contexthub/Setting.aidl.
-  disableTestMode             - disable test mode.
-  disconnectEndpoint <HEX_HOST_ENDPOINT_ID>
-                              - remove an endpoint with the current client and
-                                notify HAL.
-  enableSetting <SETTING>     - enable a setting identified by a number defined
-                                in android/hardware/contexthub/Setting.aidl.
-  enableTestMode              - enable test mode.
-  getContextHubs              - get all the context hubs.
-  list <PATH_OF_NANOAPPS>     - list all the nanoapps' header info in the path.
-  load <APP_NAME>             - load the nanoapp specified by the name.
-                                If an absolute path like /path/to/awesome.so,
-                                which is optional, is not provided then default
-                                locations are searched.
-  query                       - show all loaded nanoapps (system apps excluded)
-  sendMessage <HEX_HOST_ENDPOINT_ID> <HEX_NANOAPP_ID | APP_NAME> <HEX_PAYLOAD>
-                              - send a payload to a nanoapp.
-  unload <HEX_NANOAPP_ID | APP_NAME>
-                              - unload the nanoapp specified by either the
-                                nanoapp id in hex format or the app name.
-                                If an absolute path like /path/to/awesome.so,
-                                which is optional, is not provided then default
-                                locations are searched.
-)";
+
+const std::string kClientName{"ChreAidlHalClient"};
 
 inline void throwError(const std::string &message) {
   throw std::system_error{std::error_code(), message};
@@ -176,29 +157,30 @@
                 << "\n\trpcServices: " << ToString(app.rpcServices) << "\n}"
                 << std::endl;
     }
-    setPromiseAndRefresh();
+    resetPromise();
     return ScopedAStatus::ok();
   }
 
   ScopedAStatus handleContextHubMessage(
       const ContextHubMessage &message,
       const std::vector<std::string> & /*msgContentPerms*/) override {
-    std::cout << "Received a message with type " << message.messageType
-              << " size " << message.messageBody.size() << " from nanoapp 0x"
-              << std::hex << message.nanoappId
-              << " sent to the host endpoint 0x"
-              << static_cast<int>(message.hostEndPoint) << std::endl;
-    std::cout << "message: 0x";
+    std::cout << "Received a message!" << std::endl
+              << "   From: 0x" << std::hex << message.nanoappId << std::endl
+              << "     To: 0x" << static_cast<int>(message.hostEndPoint)
+              << std::endl
+              << "   Body: (type " << message.messageType << " size "
+              << message.messageBody.size() << ") 0x";
     for (const uint8_t &data : message.messageBody) {
-      std::cout << std::hex << static_cast<uint32_t>(data);
+      std::cout << std::hex << static_cast<uint16_t>(data);
     }
-    std::cout << std::endl;
-    setPromiseAndRefresh();
+    std::cout << std::endl << std::endl;
+    resetPromise();
     return ScopedAStatus::ok();
   }
 
-  ScopedAStatus handleContextHubAsyncEvent(AsyncEventType /*event*/) override {
-    setPromiseAndRefresh();
+  ScopedAStatus handleContextHubAsyncEvent(AsyncEventType event) override {
+    std::cout << "Received async event " << toString(event) << std::endl;
+    resetPromise();
     return ScopedAStatus::ok();
   }
 
@@ -207,27 +189,66 @@
                                         bool success) override {
     std::cout << parseTransactionId(transactionId) << " transaction is "
               << (success ? "successful" : "failed") << std::endl;
-    setPromiseAndRefresh();
+    resetPromise();
     return ScopedAStatus::ok();
   }
 
   ScopedAStatus handleNanSessionRequest(
       const NanSessionRequest & /* request */) override {
+    resetPromise();
     return ScopedAStatus::ok();
   }
 
-  std::promise<void> promise;
+  ScopedAStatus handleMessageDeliveryStatus(
+      char16_t /* hostEndPointId */,
+      const MessageDeliveryStatus & /* messageDeliveryStatus */) override {
+    resetPromise();
+    return ScopedAStatus::ok();
+  }
 
- private:
-  void setPromiseAndRefresh() {
+  ScopedAStatus getUuid(std::array<uint8_t, 16> *out_uuid) override {
+    *out_uuid = kUuid;
+    return ScopedAStatus::ok();
+  }
+
+  ScopedAStatus getName(std::string *out_name) override {
+    *out_name = kClientName;
+    return ScopedAStatus::ok();
+  }
+
+  void resetPromise() {
     promise.set_value();
     promise = std::promise<void>{};
   }
+
+  // TODO(b/247124878):
+  // This promise is shared among all the HAL callbacks to simplify the
+  // implementation. This is based on the assumption that every command should
+  // get a response before timeout and the first callback triggered is for the
+  // response.
+  //
+  // In very rare cases, however, the assumption doesn't hold:
+  //  - multiple callbacks are triggered by a command and come back out of order
+  //  - one command is timed out and the user typed in another command then the
+  //  first callback for the first command is triggered
+  // Once we have a chance we should consider refactor this design to let each
+  // callback use their specific promises.
+  std::promise<void> promise;
 };
 
 std::shared_ptr<IContextHub> gContextHub = nullptr;
 std::shared_ptr<ContextHubCallback> gCallback = nullptr;
 
+void registerHostCallback() {
+  if (gCallback != nullptr) {
+    gCallback.reset();
+  }
+  gCallback = ContextHubCallback::make<ContextHubCallback>();
+  if (!gContextHub->registerCallback(kContextHubId, gCallback).isOk()) {
+    throwError("Failed to register the callback");
+  }
+}
+
 /** Initializes gContextHub and register gCallback. */
 std::shared_ptr<IContextHub> getContextHub() {
   if (gContextHub == nullptr) {
@@ -238,11 +259,9 @@
       throwError("Could not find Context Hub HAL");
     }
     gContextHub = IContextHub::fromBinder(binder);
-    gCallback = ContextHubCallback::make<ContextHubCallback>();
-
-    if (!gContextHub->registerCallback(kContextHubId, gCallback).isOk()) {
-      throwError("Failed to register the callback");
-    }
+  }
+  if (gCallback == nullptr) {
+    registerHostCallback();
   }
   return gContextHub;
 }
@@ -304,6 +323,7 @@
 
 void verifyStatus(const std::string &operation, const ScopedAStatus &status) {
   if (!status.isOk()) {
+    gCallback->resetPromise();
     throwError(operation + " fails with abnormal status " +
                ToString(status.getMessage()) + " error code " +
                ToString(status.getServiceSpecificError()));
@@ -317,6 +337,7 @@
   std::future_status future_status =
       future_signal.wait_for(kTimeOutThresholdInSec);
   if (future_status != std::future_status::ready) {
+    gCallback->resetPromise();
     throwError(operation + " doesn't finish within " +
                ToString(kTimeOutThresholdInSec.count()) + " seconds");
   }
@@ -359,7 +380,6 @@
       continue;
     }
     pathAndName = predefinedPath + appName + ".so";
-    std::cout << "Found the nanoapp header for " << pathAndName << std::endl;
     return result;
   }
   throwError("Unable to find the nanoapp header for " + pathAndName);
@@ -435,19 +455,23 @@
                         gCallback->promise.get_future());
 }
 
-void onEndpointConnected(const std::string &hexEndpointId) {
-  auto contextHub = getContextHub();
+HostEndpointInfo createHostEndpointInfo(const std::string &hexEndpointId) {
   uint16_t hostEndpointId = verifyAndConvertEndpointHexId(hexEndpointId);
-  HostEndpointInfo info = {
+  return {
       .hostEndpointId = hostEndpointId,
       .type = HostEndpointInfo::Type::NATIVE,
       .packageName = "chre_aidl_hal_client",
       .attributionTag{},
   };
+}
+
+void onEndpointConnected(const std::string &hexEndpointId) {
+  auto contextHub = getContextHub();
+  HostEndpointInfo info = createHostEndpointInfo(hexEndpointId);
   // connect the endpoint to HAL
   verifyStatus(/* operation= */ "connect endpoint",
                contextHub->onHostEndpointConnected(info));
-  std::cout << "onHostEndpointConnected() is called. " << std::endl;
+  std::cout << "Connected." << std::endl;
 }
 
 void onEndpointDisconnected(const std::string &hexEndpointId) {
@@ -456,13 +480,12 @@
   // disconnect the endpoint from HAL
   verifyStatus(/* operation= */ "disconnect endpoint",
                contextHub->onHostEndpointDisconnected(hostEndpointId));
-  std::cout << "onHostEndpointDisconnected() is called. " << std::endl;
+  std::cout << "Disconnected." << std::endl;
 }
 
-/** Sends a hexPayload from hexHostEndpointId to appIdOrName. */
-void sendMessageToNanoapp(const std::string &hexHostEndpointId,
-                          std::string &appIdOrName,
-                          const std::string &hexPayload) {
+ContextHubMessage createContextHubMessage(const std::string &hexHostEndpointId,
+                                          std::string &appIdOrName,
+                                          const std::string &hexPayload) {
   if (!isValidHexNumber(hexPayload)) {
     throwError("Invalid hex payload.");
   }
@@ -479,13 +502,20 @@
     contextHubMessage.messageBody.push_back(
         std::stoi(hexPayload.substr(i, 2), /* idx= */ nullptr, /* base= */ 16));
   }
+  return contextHubMessage;
+}
+
+/** Sends a hexPayload from hexHostEndpointId to appIdOrName. */
+void sendMessageToNanoapp(const std::string &hexHostEndpointId,
+                          std::string &appIdOrName,
+                          const std::string &hexPayload) {
+  ContextHubMessage contextHubMessage =
+      createContextHubMessage(hexHostEndpointId, appIdOrName, hexPayload);
   // send the message
   auto contextHub = getContextHub();
-  onEndpointConnected(hexHostEndpointId);
   auto status = contextHub->sendMessageToHub(kContextHubId, contextHubMessage);
   verifyStatusAndSignal(/* operation= */ "sending a message to " + appIdOrName,
                         status, gCallback->promise.get_future());
-  onEndpointDisconnected(hexHostEndpointId);
 }
 
 void changeSetting(const std::string &setting, bool enabled) {
@@ -504,14 +534,23 @@
 
 void enableTestModeOnContextHub() {
   auto status = getContextHub()->setTestMode(true);
-  verifyStatusAndSignal(/* operation= */ "enabling test mode", status,
-                        gCallback->promise.get_future());
+  verifyStatus(/* operation= */ "enabling test mode", status);
+  std::cout << "Test mode is enabled" << std::endl;
 }
 
 void disableTestModeOnContextHub() {
   auto status = getContextHub()->setTestMode(false);
-  verifyStatusAndSignal(/* operation= */ "disabling test mode", status,
-                        gCallback->promise.get_future());
+  verifyStatus(/* operation= */ "disabling test mode", status);
+  std::cout << "Test mode is disabled" << std::endl;
+}
+
+void getAllPreloadedNanoappIds() {
+  std::vector<int64_t> appIds{};
+  verifyStatus("get preloaded nanoapp ids",
+               getContextHub()->getPreloadedNanoappIds(kContextHubId, &appIds));
+  for (const auto &appId : appIds) {
+    std::cout << "0x" << std::hex << appId << std::endl;
+  }
 }
 
 // Please keep Command in alphabetical order
@@ -524,9 +563,11 @@
   enableSetting,
   enableTestMode,
   getContextHubs,
+  getPreloadedNanoappIds,
   list,
   load,
   query,
+  registerCallback,
   sendMessage,
   unload,
   unsupported
@@ -534,34 +575,137 @@
 
 struct CommandInfo {
   Command cmd;
-  u_int8_t numofArgs;  // including cmd;
+  u_int8_t numOfArgs;  // including cmd;
+  std::string argsFormat;
+  std::string usage;
 };
 
-Command parseCommand(const std::vector<std::string> &cmdLine) {
-  std::map<std::string, CommandInfo> commandMap{
-      {"connect", {connect, 1}},
-      {"connectEndpoint", {connectEndpoint, 2}},
-      {"disableSetting", {disableSetting, 2}},
-      {"disableTestMode", {disableTestMode, 1}},
-      {"disconnectEndpoint", {disconnectEndpoint, 2}},
-      {"enableSetting", {enableSetting, 2}},
-      {"enableTestMode", {enableTestMode, 1}},
-      {"getContextHubs", {getContextHubs, 1}},
-      {"list", {list, 2}},
-      {"load", {load, 2}},
-      {"query", {query, 1}},
-      {"sendMessage", {sendMessage, 4}},
-      {"unload", {unload, 2}},
-  };
-  if (cmdLine.empty() || commandMap.find(cmdLine[0]) == commandMap.end()) {
+const std::map<std::string, CommandInfo> kAllCommands{
+    {"connect",
+     {.cmd = connect,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "connect to HAL using hal_client library and keep the session "
+               "alive while user can execute other commands. Use 'exit' to "
+               "quit the session."}},
+    {"connectEndpoint",
+     {.cmd = connectEndpoint,
+      .numOfArgs = 2,
+      .argsFormat = "<HEX_ENDPOINT_ID>",
+      .usage =
+          "associate an endpoint with the current client and notify HAL."}},
+    {"disableSetting",
+     {.cmd = disableSetting,
+      .numOfArgs = 2,
+      .argsFormat = "<SETTING>",
+      .usage = "disable a setting identified by a number defined in "
+               "android/hardware/contexthub/Setting.aidl."}},
+    {"disableTestMode",
+     {.cmd = disableTestMode,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "disable test mode."}},
+    {"disconnectEndpoint",
+     {.cmd = disconnectEndpoint,
+      .numOfArgs = 2,
+      .argsFormat = "<HEX_ENDPOINT_ID>",
+      .usage = "remove an endpoint with the current client and notify HAL."}},
+    {"enableSetting",
+     {.cmd = enableSetting,
+      .numOfArgs = 2,
+      .argsFormat = "<SETTING>",
+      .usage = "enable a setting identified by a number defined in "
+               "android/hardware/contexthub/Setting.aidl."}},
+    {"enableTestMode",
+     {.cmd = enableTestMode,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "enable test mode."}},
+    {"getContextHubs",
+     {.cmd = getContextHubs,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "get all the context hubs."}},
+    {"getPreloadedNanoappIds",
+     {.cmd = getPreloadedNanoappIds,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "get a list of ids for the preloaded nanoapps."}},
+    {"list",
+     {.cmd = list,
+      .numOfArgs = 2,
+      .argsFormat = "</PATH/TO/NANOAPPS>",
+      .usage = "list all the nanoapps' header info in the path."}},
+    {"load",
+     {.cmd = load,
+      .numOfArgs = 2,
+      .argsFormat = "<APP_NAME | /PATH/TO/APP_NAME>",
+      .usage = "load the nanoapp specified by the name. If an absolute path is "
+               "not provided the default locations are searched."}},
+    {"query",
+     {.cmd = query,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "show all loaded nanoapps (system apps excluded)."}},
+    {"registerCallback",
+     {.cmd = registerCallback,
+      .numOfArgs = 1,
+      .argsFormat = "",
+      .usage = "register a callback for the current client."}},
+    {"sendMessage",
+     {.cmd = sendMessage,
+      .numOfArgs = 4,
+      .argsFormat = "<HEX_ENDPOINT_ID> <HEX_NANOAPP_ID | APP_NAME | "
+                    "/PATH/TO/APP_NAME> <HEX_PAYLOAD>",
+      .usage = "send a payload to a nanoapp. If an absolute path is not "
+               "provided the default locations are searched."}},
+    {"unload",
+     {.cmd = unload,
+      .numOfArgs = 2,
+      .argsFormat = "<HEX_NANOAPP_ID | APP_NAME | /PATH/TO/APP_NAME>",
+      .usage = "unload the nanoapp specified by either the nanoapp id or the "
+               "app name. If an absolute path is not provided the default "
+               "locations are searched."}},
+};
+
+void fillSupportedCommandMap(
+    const std::unordered_set<std::string> &supportedCommands,
+    std::map<std::string, CommandInfo> &supportedCommandMap) {
+  std::copy_if(kAllCommands.begin(), kAllCommands.end(),
+               std::inserter(supportedCommandMap, supportedCommandMap.begin()),
+               [&](auto const &kv_pair) {
+                 return supportedCommands.find(kv_pair.first) !=
+                        supportedCommands.end();
+               });
+}
+
+void printUsage(const std::map<std::string, CommandInfo> &supportedCommands) {
+  constexpr uint32_t kCommandLength = 40;
+  std::cout << std::left << "Usage: COMMAND [ARGUMENTS]" << std::endl;
+  for (auto const &kv_pair : supportedCommands) {
+    std::string cmdLine = kv_pair.first + " " + kv_pair.second.argsFormat;
+    std::cout << std::setw(kCommandLength) << cmdLine;
+    if (cmdLine.size() > kCommandLength) {
+      std::cout << std::endl << std::string(kCommandLength, ' ');
+    }
+    std::cout << " - " + kv_pair.second.usage << std::endl;
+  }
+  std::cout << std::endl;
+}
+
+Command parseCommand(
+    const std::vector<std::string> &cmdLine,
+    const std::map<std::string, CommandInfo> &supportedCommandMap) {
+  if (cmdLine.empty() ||
+      supportedCommandMap.find(cmdLine[0]) == supportedCommandMap.end()) {
     return unsupported;
   }
-  auto cmdInfo = commandMap.at(cmdLine[0]);
-  return cmdLine.size() == cmdInfo.numofArgs ? cmdInfo.cmd : unsupported;
+  auto cmdInfo = supportedCommandMap.at(cmdLine[0]);
+  return cmdLine.size() == cmdInfo.numOfArgs ? cmdInfo.cmd : unsupported;
 }
 
 void executeCommand(std::vector<std::string> cmdLine) {
-  switch (parseCommand(cmdLine)) {
+  switch (parseCommand(cmdLine, kAllCommands)) {
     case connectEndpoint: {
       onEndpointConnected(cmdLine[1]);
       break;
@@ -590,6 +734,10 @@
       getAllContextHubs();
       break;
     }
+    case getPreloadedNanoappIds: {
+      getAllPreloadedNanoappIds();
+      break;
+    }
     case list: {
       std::map<std::string, NanoAppBinaryHeader> nanoapps{};
       readNanoappHeaders(nanoapps, cmdLine[1]);
@@ -607,6 +755,10 @@
       queryNanoapps();
       break;
     }
+    case registerCallback: {
+      registerHostCallback();
+      break;
+    }
     case sendMessage: {
       sendMessageToNanoapp(cmdLine[1], cmdLine[2], cmdLine[3]);
       break;
@@ -616,7 +768,7 @@
       break;
     }
     default:
-      std::cout << kUsage;
+      printUsage(kAllCommands);
   }
 }
 
@@ -642,22 +794,63 @@
 }
 
 void connectToHal() {
-  auto hub = getContextHub();
-  std::cout << "Connected to context hub." << std::endl;
+  if (gCallback == nullptr) {
+    gCallback = ContextHubCallback::make<ContextHubCallback>();
+  }
+  std::unique_ptr<HalClient> halClient = HalClient::create(gCallback);
+  if (halClient == nullptr) {
+    LOGE("Failed to init the connection to HAL.");
+    return;
+  }
+  std::unordered_set<std::string> supportedCommands = {
+      "connectEndpoint", "disconnectEndpoint", "query", "sendMessage"};
+  std::map<std::string, CommandInfo> supportedCommandMap{};
+  fillSupportedCommandMap(supportedCommands, supportedCommandMap);
+
   while (true) {
     auto cmdLine = getCommandLine();
     if (cmdLine.empty()) {
       continue;
     }
-    if (cmdLine.size() == 1 && cmdLine[0] == "connect") {
-      std::cout << "Already in a live session." << std::endl;
-      continue;
-    }
     if (cmdLine.size() == 1 && cmdLine[0] == "exit") {
       break;
     }
     try {
-      executeCommand(cmdLine);
+      switch (parseCommand(cmdLine, supportedCommandMap)) {
+        case connectEndpoint: {
+          HostEndpointInfo info =
+              createHostEndpointInfo(/* hexEndpointId= */ cmdLine[1]);
+          verifyStatus(/* operation= */ "connect endpoint",
+                       halClient->connectEndpoint(info));
+          break;
+        }
+
+        case query: {
+          verifyStatusAndSignal(/* operation= */ "querying nanoapps",
+                                halClient->queryNanoapps(),
+                                gCallback->promise.get_future());
+          break;
+        }
+
+        case disconnectEndpoint: {
+          uint16_t hostEndpointId =
+              verifyAndConvertEndpointHexId(/* number= */ cmdLine[1]);
+          verifyStatus(/* operation= */ "disconnect endpoint",
+                       halClient->disconnectEndpoint(hostEndpointId));
+          break;
+        }
+        case sendMessage: {
+          ContextHubMessage message = createContextHubMessage(
+              /* hexHostEndpointId= */ cmdLine[1],
+              /* appIdOrName= */ cmdLine[2], /* hexPayload= */ cmdLine[3]);
+          verifyStatusAndSignal(
+              /* operation= */ "sending a message to " + cmdLine[2],
+              halClient->sendMessage(message), gCallback->promise.get_future());
+          break;
+        }
+        default:
+          printUsage(supportedCommandMap);
+      }
     } catch (std::system_error &e) {
       std::cerr << e.what() << std::endl;
     }
@@ -673,15 +866,15 @@
   for (int i = 1; i < argc; i++) {
     cmdLine.emplace_back(argv[i]);
   }
-  if (cmdLine.size() == 1 && cmdLine[0] == "connect") {
-    connectToHal();
-    return 0;
-  }
   try {
+    if (cmdLine.size() == 1 && cmdLine[0] == "connect") {
+      connectToHal();
+      return 0;
+    }
     executeCommand(cmdLine);
   } catch (std::system_error &e) {
     std::cerr << e.what() << std::endl;
     return -1;
   }
   return 0;
-}
\ No newline at end of file
+}
diff --git a/host/common/daemon_base.cc b/host/common/daemon_base.cc
index 48737f0..b7681cb 100644
--- a/host/common/daemon_base.cc
+++ b/host/common/daemon_base.cc
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// TODO(b/298459533): metrics_reporter_in_the_daemon ramp up -> remove old
+// code
+
 #include <signal.h>
 #include <cstdlib>
 #include <fstream>
@@ -25,12 +28,9 @@
 #include "chre_host/napp_header.h"
 
 #ifdef CHRE_DAEMON_METRIC_ENABLED
+#include <android_chre_flags.h>
 #include <chre_atoms_log.h>
 #include <system/chre/core/chre_metrics.pb.h>
-
-using ::aidl::android::frameworks::stats::IStats;
-using ::aidl::android::frameworks::stats::VendorAtom;
-using ::aidl::android::frameworks::stats::VendorAtomValue;
 #endif  // CHRE_DAEMON_METRIC_ENABLED
 
 // Aliased for consistency with the way these symbols are referenced in
@@ -40,6 +40,17 @@
 namespace android {
 namespace chre {
 
+#ifdef CHRE_DAEMON_METRIC_ENABLED
+using ::aidl::android::frameworks::stats::IStats;
+using ::aidl::android::frameworks::stats::VendorAtom;
+using ::aidl::android::frameworks::stats::VendorAtomValue;
+
+using ::android::chre::Atoms::CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED;
+using ::android::chre::Atoms::CHRE_PAL_OPEN_FAILED;
+using ::android::chre::Atoms::ChrePalOpenFailed;
+using ::android::chre::flags::metrics_reporter_in_the_daemon;
+#endif  // CHRE_DAEMON_METRIC_ENABLED
+
 namespace {
 
 void signalHandler(void *ctx) {
@@ -153,46 +164,66 @@
   const std::vector<int8_t> &encodedMetric = metricMsg->encoded_metric;
 
   switch (metricMsg->id) {
-    case Atoms::CHRE_PAL_OPEN_FAILED: {
+    case CHRE_PAL_OPEN_FAILED: {
       metrics::ChrePalOpenFailed metric;
       if (!metric.ParseFromArray(encodedMetric.data(), encodedMetric.size())) {
         LOGE("Failed to parse metric data");
       } else {
-        std::vector<VendorAtomValue> values(2);
-        values[0].set<VendorAtomValue::intValue>(metric.pal());
-        values[1].set<VendorAtomValue::intValue>(metric.type());
-        const VendorAtom atom{
-            .atomId = Atoms::CHRE_PAL_OPEN_FAILED,
-            .values{std::move(values)},
-        };
-        reportMetric(atom);
+        if (metrics_reporter_in_the_daemon()) {
+          ChrePalOpenFailed::ChrePalType pal =
+              static_cast<ChrePalOpenFailed::ChrePalType>(metric.pal());
+          ChrePalOpenFailed::Type type =
+              static_cast<ChrePalOpenFailed::Type>(metric.type());
+          if (!mMetricsReporter.logPalOpenFailed(pal, type)) {
+            LOGE("Could not log the PAL open failed metric");
+          }
+        } else {
+          std::vector<VendorAtomValue> values(2);
+          values[0].set<VendorAtomValue::intValue>(metric.pal());
+          values[1].set<VendorAtomValue::intValue>(metric.type());
+          const VendorAtom atom{
+              .atomId = Atoms::CHRE_PAL_OPEN_FAILED,
+              .values{std::move(values)},
+          };
+          reportMetric(atom);
+        }
       }
       break;
     }
-    case Atoms::CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED: {
+    case CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED: {
       metrics::ChreEventQueueSnapshotReported metric;
       if (!metric.ParseFromArray(encodedMetric.data(), encodedMetric.size())) {
         LOGE("Failed to parse metric data");
       } else {
-        std::vector<VendorAtomValue> values(6);
-        values[0].set<VendorAtomValue::intValue>(
-            metric.snapshot_chre_get_time_ms());
-        values[1].set<VendorAtomValue::intValue>(metric.max_event_queue_size());
-        values[2].set<VendorAtomValue::intValue>(
-            metric.mean_event_queue_size());
-        values[3].set<VendorAtomValue::intValue>(metric.num_dropped_events());
-        // Last two values are not currently populated and will be implemented
-        // later. To avoid confusion of the interpretation, we use UINT32_MAX
-        // as a placeholder value.
-        values[4].set<VendorAtomValue::intValue>(
-            UINT32_MAX);  // max_queue_delay_us
-        values[5].set<VendorAtomValue::intValue>(
-            UINT32_MAX);  // mean_queue_delay_us
-        const VendorAtom atom{
-            .atomId = Atoms::CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED,
-            .values{std::move(values)},
-        };
-        reportMetric(atom);
+        if (metrics_reporter_in_the_daemon()) {
+          if (!mMetricsReporter.logEventQueueSnapshotReported(
+                  metric.snapshot_chre_get_time_ms(),
+                  metric.max_event_queue_size(), metric.mean_event_queue_size(),
+                  metric.num_dropped_events())) {
+            LOGE("Could not log the event queue snapshot metric");
+          }
+        } else {
+          std::vector<VendorAtomValue> values(6);
+          values[0].set<VendorAtomValue::intValue>(
+              metric.snapshot_chre_get_time_ms());
+          values[1].set<VendorAtomValue::intValue>(
+              metric.max_event_queue_size());
+          values[2].set<VendorAtomValue::intValue>(
+              metric.mean_event_queue_size());
+          values[3].set<VendorAtomValue::intValue>(metric.num_dropped_events());
+          // Last two values are not currently populated and will be implemented
+          // later. To avoid confusion of the interpretation, we use UINT32_MAX
+          // as a placeholder value.
+          values[4].set<VendorAtomValue::intValue>(
+              UINT32_MAX);  // max_queue_delay_us
+          values[5].set<VendorAtomValue::intValue>(
+              UINT32_MAX);  // mean_queue_delay_us
+          const VendorAtom atom{
+              .atomId = Atoms::CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED,
+              .values{std::move(values)},
+          };
+          reportMetric(atom);
+        }
       }
       break;
     }
diff --git a/host/common/fbs_daemon_base.cc b/host/common/fbs_daemon_base.cc
index 94f0770..b41926f 100644
--- a/host/common/fbs_daemon_base.cc
+++ b/host/common/fbs_daemon_base.cc
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// TODO(b/298459533): metrics_reporter_in_the_daemon ramp up -> remove old
+// code
+
 #include <cstdlib>
 #include <fstream>
 
@@ -26,11 +29,8 @@
 #ifdef CHRE_DAEMON_METRIC_ENABLED
 #include <aidl/android/frameworks/stats/IStats.h>
 #include <android/binder_manager.h>
+#include <android_chre_flags.h>
 #include <chre_atoms_log.h>
-
-using ::aidl::android::frameworks::stats::IStats;
-using ::aidl::android::frameworks::stats::VendorAtom;
-using ::aidl::android::frameworks::stats::VendorAtomValue;
 #endif  // CHRE_DAEMON_METRIC_ENABLED
 
 // Aliased for consistency with the way these symbols are referenced in
@@ -40,6 +40,14 @@
 namespace android {
 namespace chre {
 
+#ifdef CHRE_DAEMON_METRIC_ENABLED
+using ::aidl::android::frameworks::stats::IStats;
+using ::aidl::android::frameworks::stats::VendorAtom;
+using ::aidl::android::frameworks::stats::VendorAtomValue;
+using ::android::chre::Atoms::ChreHalNanoappLoadFailed;
+using ::android::chre::flags::metrics_reporter_in_the_daemon;
+#endif  // CHRE_DAEMON_METRIC_ENABLED
+
 bool FbsDaemonBase::sendNanoappLoad(uint64_t appId, uint32_t appVersion,
                                     uint32_t appTargetApiVersion,
                                     const std::string &appBinaryName,
@@ -145,6 +153,9 @@
         fbs::UnPackMessageContainer(messageBuffer);
     handleNanConfigurationRequest(
         container->message.AsNanConfigurationRequest());
+  } else if (messageType == fbs::ChreMessage::NanoappInstanceIdInfo) {
+    // TODO(b/242760291): Use this info to map nanoapp log detokenizers with
+    // instance ID in log message parser.
   } else if (hostClientId == kHostClientIdDaemon) {
     handleDaemonMessage(messageBuffer);
   } else if (hostClientId == ::chre::kHostClientIdUnspecified) {
@@ -176,18 +187,27 @@
              mPreloadedNanoappPendingTransactions.front().transactionId);
 
 #ifdef CHRE_DAEMON_METRIC_ENABLED
-        std::vector<VendorAtomValue> values(3);
-        values[0].set<VendorAtomValue::longValue>(
-            mPreloadedNanoappPendingTransactions.front().nanoappId);
-        values[1].set<VendorAtomValue::intValue>(
-            Atoms::ChreHalNanoappLoadFailed::TYPE_PRELOADED);
-        values[2].set<VendorAtomValue::intValue>(
-            Atoms::ChreHalNanoappLoadFailed::REASON_ERROR_GENERIC);
-        const VendorAtom atom{
-            .atomId = Atoms::CHRE_HAL_NANOAPP_LOAD_FAILED,
-            .values{std::move(values)},
-        };
-        reportMetric(atom);
+        if (metrics_reporter_in_the_daemon()) {
+          if (!mMetricsReporter.logNanoappLoadFailed(
+                  mPreloadedNanoappPendingTransactions.front().nanoappId,
+                  ChreHalNanoappLoadFailed::TYPE_PRELOADED,
+                  ChreHalNanoappLoadFailed::REASON_ERROR_GENERIC)) {
+            LOGE("Could not log the nanoapp load failed metric");
+          }
+        } else {
+          std::vector<VendorAtomValue> values(3);
+          values[0].set<VendorAtomValue::longValue>(
+              mPreloadedNanoappPendingTransactions.front().nanoappId);
+          values[1].set<VendorAtomValue::intValue>(
+              Atoms::ChreHalNanoappLoadFailed::TYPE_PRELOADED);
+          values[2].set<VendorAtomValue::intValue>(
+              Atoms::ChreHalNanoappLoadFailed::REASON_ERROR_GENERIC);
+          const VendorAtom atom{
+              .atomId = Atoms::CHRE_HAL_NANOAPP_LOAD_FAILED,
+              .values{std::move(values)},
+          };
+          reportMetric(atom);
+        }
 #endif  // CHRE_DAEMON_METRIC_ENABLED
       }
       mPreloadedNanoappPendingTransactions.pop();
diff --git a/host/common/fragmented_load_transaction.cc b/host/common/fragmented_load_transaction.cc
index c3c26d3..42c910d 100644
--- a/host/common/fragmented_load_transaction.cc
+++ b/host/common/fragmented_load_transaction.cc
@@ -49,9 +49,8 @@
 FragmentedLoadTransaction::FragmentedLoadTransaction(
     uint32_t transactionId, uint64_t appId, uint32_t appVersion,
     uint32_t appFlags, uint32_t targetApiVersion,
-    const std::vector<uint8_t> &appBinary, size_t fragmentSize) {
-  mTransactionId = transactionId;
-
+    const std::vector<uint8_t> &appBinary, size_t fragmentSize)
+    : mTransactionId(transactionId), mNanoappId(appId) {
   // Start with fragmentId at 1 since 0 is used to indicate
   // legacy behavior at CHRE
   size_t fragmentId = 1;
@@ -64,7 +63,7 @@
           getSubVector(appBinary, byteIndex, fragmentSize));
     } else {
       mFragmentRequests.emplace_back(
-          fragmentId++, transactionId,
+          fragmentId++, transactionId, appId,
           getSubVector(appBinary, byteIndex, fragmentSize));
     }
 
diff --git a/host/common/hal_client.cc b/host/common/hal_client.cc
new file mode 100644
index 0000000..6a1c0bf
--- /dev/null
+++ b/host/common/hal_client.cc
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 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 LOG_TAG
+#define LOG_TAG "CHRE.HAL.CLIENT"
+#endif
+
+#include "chre_host/hal_client.h"
+
+#include <utils/SystemClock.h>
+
+#include <cinttypes>
+#include <thread>
+
+namespace android::chre {
+
+using ::aidl::android::hardware::contexthub::IContextHub;
+using ::aidl::android::hardware::contexthub::IContextHubCallback;
+using ::ndk::ScopedAStatus;
+
+std::unique_ptr<HalClient> HalClient::create(
+    const std::shared_ptr<IContextHubCallback> &callback,
+    int32_t contextHubId) {
+  if (callback == nullptr) {
+    LOGE("Callback function must not be null.");
+    return nullptr;
+  }
+  auto halClient =
+      std::unique_ptr<HalClient>(new HalClient(callback, contextHubId));
+  if (halClient->initConnection() != HalError::SUCCESS) {
+    return nullptr;
+  }
+  return halClient;
+}
+
+HalError HalClient::initConnection() {
+  std::lock_guard<std::shared_mutex> lockGuard{mConnectionLock};
+
+  if (mContextHub != nullptr) {
+    LOGW("Context hub is already connected.");
+    return HalError::SUCCESS;
+  }
+
+  // Wait to connect to the service. Note that we don't do local retries because
+  // we're relying on the internal retries in AServiceManager_waitForService().
+  // If HAL service has just restarted, it can take a few seconds to connect.
+  ndk::SpAIBinder binder{
+      AServiceManager_waitForService(kAidlServiceName.c_str())};
+  if (binder.get() == nullptr) {
+    return HalError::BINDER_CONNECTION_FAILED;
+  }
+
+  // Link the death recipient to handle the binder disconnection event.
+  if (AIBinder_linkToDeath(binder.get(), mDeathRecipient.get(), this) !=
+      STATUS_OK) {
+    LOGE("Failed to link the binder death recipient");
+    return HalError::LINK_DEATH_RECIPIENT_FAILED;
+  }
+
+  // Retrieve a handle of context hub service.
+  mContextHub = IContextHub::fromBinder(binder);
+  if (mContextHub == nullptr) {
+    LOGE("Got null context hub from the binder connection");
+    return HalError::NULL_CONTEXT_HUB_FROM_BINDER;
+  }
+
+  // Register an IContextHubCallback.
+  ScopedAStatus status =
+      mContextHub->registerCallback(kDefaultContextHubId, mCallback);
+  if (!status.isOk()) {
+    LOGE("Unable to register callback: %s", status.getDescription().c_str());
+    return HalError::CALLBACK_REGISTRATION_FAILED;
+  }
+  LOGI("Successfully connected to HAL.");
+  return HalError::SUCCESS;
+}
+
+void HalClient::onHalDisconnected(void *cookie) {
+  LOGW("CHRE HAL is disconnected. Reconnecting...");
+  int64_t startTime = ::android::elapsedRealtime();
+  auto *halClient = static_cast<HalClient *>(cookie);
+  {
+    std::lock_guard<std::shared_mutex> lock(halClient->mConnectionLock);
+    halClient->mContextHub = nullptr;
+  }
+
+  HalError result = halClient->initConnection();
+  uint64_t duration = ::android::elapsedRealtime() - startTime;
+  if (result != HalError::SUCCESS) {
+    LOGE("Failed to fully reconnect to HAL after %" PRIu64
+         "ms, HalErrorCode: %" PRIi32,
+         duration, result);
+    return;
+  }
+  tryReconnectEndpoints(halClient);
+  LOGI("Reconnected to HAL after %" PRIu64 "ms", duration);
+}
+
+ScopedAStatus HalClient::connectEndpoint(
+    const HostEndpointInfo &hostEndpointInfo) {
+  HostEndpointId endpointId = hostEndpointInfo.hostEndpointId;
+  if (isEndpointConnected(endpointId)) {
+    // Connecting the endpoint again even though it is already connected to let
+    // HAL and/or CHRE be the single place to control the behavior.
+    LOGW("Endpoint id %" PRIu16 " is already connected.", endpointId);
+  }
+  ScopedAStatus result = callIfConnected(
+      [&]() { return mContextHub->onHostEndpointConnected(hostEndpointInfo); });
+  if (result.isOk()) {
+    insertConnectedEndpoint(hostEndpointInfo);
+  } else {
+    LOGE("Failed to connect the endpoint id %" PRIu16,
+         hostEndpointInfo.hostEndpointId);
+  }
+  return result;
+}
+
+ScopedAStatus HalClient::disconnectEndpoint(HostEndpointId hostEndpointId) {
+  if (!isEndpointConnected(hostEndpointId)) {
+    // Disconnecting the endpoint again even though it is already disconnected
+    // to let HAL and/or CHRE be the single place to control the behavior.
+    LOGW("Endpoint id %" PRIu16 " is already disconnected.", hostEndpointId);
+  }
+  ScopedAStatus result = callIfConnected([&]() {
+    return mContextHub->onHostEndpointDisconnected(hostEndpointId);
+  });
+  if (result.isOk()) {
+    removeConnectedEndpoint(hostEndpointId);
+  } else {
+    LOGE("Failed to disconnect the endpoint id %" PRIu16, hostEndpointId);
+  }
+  return result;
+}
+
+ScopedAStatus HalClient::sendMessage(const ContextHubMessage &message) {
+  uint16_t hostEndpointId = message.hostEndPoint;
+  if (!isEndpointConnected(hostEndpointId)) {
+    // For now this is still allowed but in the future
+    // HalError::UNEXPECTED_ENDPOINT_STATE will be returned.
+    LOGW("Endpoint id %" PRIu16
+         " is unknown or disconnected. Message sending will be skipped in the "
+         "future.");
+  }
+  return callIfConnected(
+      [&]() { return mContextHub->sendMessageToHub(mContextHubId, message); });
+}
+
+void HalClient::tryReconnectEndpoints(HalClient *halClient) {
+  std::lock_guard<std::shared_mutex> lock(halClient->mStateLock);
+  for (const auto &[endpointId, endpointInfo] :
+       halClient->mConnectedEndpoints) {
+    if (!halClient
+             ->callIfConnected([&]() {
+               return halClient->mContextHub->onHostEndpointConnected(
+                   endpointInfo);
+             })
+             .isOk()) {
+      LOGE("Failed to set up the connected state for endpoint %" PRIu16
+           " after HAL restarts.",
+           endpointId);
+      halClient->mConnectedEndpoints.erase(endpointId);
+    } else {
+      LOGI("Reconnected endpoint %" PRIu16 " to CHRE HAL.", endpointId);
+    }
+  }
+}
+}  // namespace android::chre
\ No newline at end of file
diff --git a/host/common/host_protocol_host.cc b/host/common/host_protocol_host.cc
index 2493206..a24ec94 100644
--- a/host/common/host_protocol_host.cc
+++ b/host/common/host_protocol_host.cc
@@ -250,5 +250,10 @@
   finalize(builder, fbs::ChreMessage::NanConfigurationUpdate, message.Union());
 }
 
+void HostProtocolHost::encodePulseRequest(FlatBufferBuilder &builder) {
+  auto message = fbs::CreatePulseRequest(builder);
+  finalize(builder, fbs::ChreMessage::PulseRequest, message.Union());
+}
+
 }  // namespace chre
 }  // namespace android
diff --git a/host/common/include/chre_host/daemon_base.h b/host/common/include/chre_host/daemon_base.h
index c0c6e73..453a866 100644
--- a/host/common/include/chre_host/daemon_base.h
+++ b/host/common/include/chre_host/daemon_base.h
@@ -25,10 +25,14 @@
  * implement.
  */
 
+// TODO(b/298459533): metrics_reporter_in_the_daemon ramp up -> remove old
+// code
+
 #include <atomic>
 #include <csignal>
 #include <cstdint>
 #include <map>
+#include <mutex>
 #include <queue>
 #include <string>
 #include <thread>
@@ -40,6 +44,8 @@
 #ifdef CHRE_DAEMON_METRIC_ENABLED
 #include <aidl/android/frameworks/stats/IStats.h>
 #include <android/binder_manager.h>
+
+#include "chre_host/metrics_reporter.h"
 #endif  // CHRE_DAEMON_METRIC_ENABLED
 
 namespace android {
@@ -230,16 +236,15 @@
 #ifdef CHRE_LOG_ATOM_EXTENSION_ENABLED
   /**
    * Handles additional metrics that aren't logged by the common CHRE code.
-   *
    */
   virtual void handleVendorMetricLog(
       const ::chre::fbs::MetricLogT *metric_msg) = 0;
 #endif  // CHRE_LOG_ATOM_EXTENSION_ENABLED
 
   /**
-   * Create and report CHRE vendor atom and send it to stats_client
+   * Create and report CHRE vendor atom and send it to stats_client.
    *
-   * @param atom the vendor atom to be reported
+   * @param atom the vendor atom to be reported.
    */
   void reportMetric(const aidl::android::frameworks::stats::VendorAtom &atom);
 #endif  // CHRE_DAEMON_METRIC_ENABLED
@@ -263,6 +268,10 @@
   //! Server used to communicate with daemon clients
   SocketServer mServer;
 
+#ifdef CHRE_DAEMON_METRIC_ENABLED
+  android::chre::MetricsReporter mMetricsReporter;
+#endif  // CHRE_DAEMON_METRIC_ENABLED
+
  private:
   LogMessageParser mLogger;
 
diff --git a/host/common/include/chre_host/fragmented_load_transaction.h b/host/common/include/chre_host/fragmented_load_transaction.h
index f0b9ff4..1d60b3e 100644
--- a/host/common/include/chre_host/fragmented_load_transaction.h
+++ b/host/common/include/chre_host/fragmented_load_transaction.h
@@ -50,8 +50,8 @@
   std::vector<uint8_t> binary;
 
   FragmentedLoadRequest(size_t fragmentId, uint32_t transactionId,
-                        const std::vector<uint8_t> &binary)
-      : FragmentedLoadRequest(fragmentId, transactionId, 0, 0, 0, 0, 0,
+                        uint64_t appId, const std::vector<uint8_t> &binary)
+      : FragmentedLoadRequest(fragmentId, transactionId, appId, 0, 0, 0, 0,
                               binary) {}
 
   FragmentedLoadRequest(size_t fragmentId, uint32_t transactionId,
@@ -115,10 +115,15 @@
     return mTransactionId;
   }
 
+  uint64_t getNanoappId() const {
+    return mNanoappId;
+  }
+
  private:
   std::vector<FragmentedLoadRequest> mFragmentRequests;
   size_t mCurrentRequestIndex = 0;
   uint32_t mTransactionId;
+  uint64_t mNanoappId;
 
   static constexpr size_t kDefaultFragmentSize =
       CHRE_HOST_DEFAULT_FRAGMENT_SIZE;
diff --git a/host/common/include/chre_host/generated/host_messages_generated.h b/host/common/include/chre_host/generated/host_messages_generated.h
index 9b80d2e..262bb3f 100644
--- a/host/common/include/chre_host/generated/host_messages_generated.h
+++ b/host/common/include/chre_host/generated/host_messages_generated.h
@@ -45,6 +45,10 @@
 struct LoadNanoappResponseBuilder;
 struct LoadNanoappResponseT;
 
+struct NanoappInstanceIdInfo;
+struct NanoappInstanceIdInfoBuilder;
+struct NanoappInstanceIdInfoT;
+
 struct UnloadNanoappRequest;
 struct UnloadNanoappRequestBuilder;
 struct UnloadNanoappRequestT;
@@ -129,6 +133,14 @@
 struct DebugConfigurationBuilder;
 struct DebugConfigurationT;
 
+struct PulseRequest;
+struct PulseRequestBuilder;
+struct PulseRequestT;
+
+struct PulseResponse;
+struct PulseResponseBuilder;
+struct PulseResponseT;
+
 struct HostAddress;
 
 struct MessageContainer;
@@ -340,11 +352,14 @@
   NanConfigurationRequest = 26,
   NanConfigurationUpdate = 27,
   DebugConfiguration = 28,
+  PulseRequest = 29,
+  PulseResponse = 30,
+  NanoappInstanceIdInfo = 31,
   MIN = NONE,
-  MAX = DebugConfiguration
+  MAX = NanoappInstanceIdInfo
 };
 
-inline const ChreMessage (&EnumValuesChreMessage())[29] {
+inline const ChreMessage (&EnumValuesChreMessage())[32] {
   static const ChreMessage values[] = {
     ChreMessage::NONE,
     ChreMessage::NanoappMessage,
@@ -374,13 +389,16 @@
     ChreMessage::BatchedMetricLog,
     ChreMessage::NanConfigurationRequest,
     ChreMessage::NanConfigurationUpdate,
-    ChreMessage::DebugConfiguration
+    ChreMessage::DebugConfiguration,
+    ChreMessage::PulseRequest,
+    ChreMessage::PulseResponse,
+    ChreMessage::NanoappInstanceIdInfo
   };
   return values;
 }
 
 inline const char * const *EnumNamesChreMessage() {
-  static const char * const names[30] = {
+  static const char * const names[33] = {
     "NONE",
     "NanoappMessage",
     "HubInfoRequest",
@@ -410,13 +428,16 @@
     "NanConfigurationRequest",
     "NanConfigurationUpdate",
     "DebugConfiguration",
+    "PulseRequest",
+    "PulseResponse",
+    "NanoappInstanceIdInfo",
     nullptr
   };
   return names;
 }
 
 inline const char *EnumNameChreMessage(ChreMessage e) {
-  if (flatbuffers::IsOutRange(e, ChreMessage::NONE, ChreMessage::DebugConfiguration)) return "";
+  if (flatbuffers::IsOutRange(e, ChreMessage::NONE, ChreMessage::NanoappInstanceIdInfo)) return "";
   const size_t index = static_cast<size_t>(e);
   return EnumNamesChreMessage()[index];
 }
@@ -537,6 +558,18 @@
   static const ChreMessage enum_value = ChreMessage::DebugConfiguration;
 };
 
+template<> struct ChreMessageTraits<chre::fbs::PulseRequest> {
+  static const ChreMessage enum_value = ChreMessage::PulseRequest;
+};
+
+template<> struct ChreMessageTraits<chre::fbs::PulseResponse> {
+  static const ChreMessage enum_value = ChreMessage::PulseResponse;
+};
+
+template<> struct ChreMessageTraits<chre::fbs::NanoappInstanceIdInfo> {
+  static const ChreMessage enum_value = ChreMessage::NanoappInstanceIdInfo;
+};
+
 struct ChreMessageUnion {
   ChreMessage type;
   void *value;
@@ -793,6 +826,30 @@
     return type == ChreMessage::DebugConfiguration ?
       reinterpret_cast<const chre::fbs::DebugConfigurationT *>(value) : nullptr;
   }
+  chre::fbs::PulseRequestT *AsPulseRequest() {
+    return type == ChreMessage::PulseRequest ?
+      reinterpret_cast<chre::fbs::PulseRequestT *>(value) : nullptr;
+  }
+  const chre::fbs::PulseRequestT *AsPulseRequest() const {
+    return type == ChreMessage::PulseRequest ?
+      reinterpret_cast<const chre::fbs::PulseRequestT *>(value) : nullptr;
+  }
+  chre::fbs::PulseResponseT *AsPulseResponse() {
+    return type == ChreMessage::PulseResponse ?
+      reinterpret_cast<chre::fbs::PulseResponseT *>(value) : nullptr;
+  }
+  const chre::fbs::PulseResponseT *AsPulseResponse() const {
+    return type == ChreMessage::PulseResponse ?
+      reinterpret_cast<const chre::fbs::PulseResponseT *>(value) : nullptr;
+  }
+  chre::fbs::NanoappInstanceIdInfoT *AsNanoappInstanceIdInfo() {
+    return type == ChreMessage::NanoappInstanceIdInfo ?
+      reinterpret_cast<chre::fbs::NanoappInstanceIdInfoT *>(value) : nullptr;
+  }
+  const chre::fbs::NanoappInstanceIdInfoT *AsNanoappInstanceIdInfo() const {
+    return type == ChreMessage::NanoappInstanceIdInfo ?
+      reinterpret_cast<const chre::fbs::NanoappInstanceIdInfoT *>(value) : nullptr;
+  }
 };
 
 bool VerifyChreMessage(flatbuffers::Verifier &verifier, const void *obj, ChreMessage type);
@@ -2026,6 +2083,80 @@
 
 flatbuffers::Offset<LoadNanoappResponse> CreateLoadNanoappResponse(flatbuffers::FlatBufferBuilder &_fbb, const LoadNanoappResponseT *_o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
 
+struct NanoappInstanceIdInfoT : public flatbuffers::NativeTable {
+  typedef NanoappInstanceIdInfo TableType;
+  uint32_t instance_id;
+  uint64_t app_id;
+  NanoappInstanceIdInfoT()
+      : instance_id(0),
+        app_id(0) {
+  }
+};
+
+struct NanoappInstanceIdInfo FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+  typedef NanoappInstanceIdInfoT NativeTableType;
+  typedef NanoappInstanceIdInfoBuilder Builder;
+  enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+    VT_INSTANCE_ID = 4,
+    VT_APP_ID = 6
+  };
+  uint32_t instance_id() const {
+    return GetField<uint32_t>(VT_INSTANCE_ID, 0);
+  }
+  bool mutate_instance_id(uint32_t _instance_id) {
+    return SetField<uint32_t>(VT_INSTANCE_ID, _instance_id, 0);
+  }
+  uint64_t app_id() const {
+    return GetField<uint64_t>(VT_APP_ID, 0);
+  }
+  bool mutate_app_id(uint64_t _app_id) {
+    return SetField<uint64_t>(VT_APP_ID, _app_id, 0);
+  }
+  bool Verify(flatbuffers::Verifier &verifier) const {
+    return VerifyTableStart(verifier) &&
+           VerifyField<uint32_t>(verifier, VT_INSTANCE_ID) &&
+           VerifyField<uint64_t>(verifier, VT_APP_ID) &&
+           verifier.EndTable();
+  }
+  NanoappInstanceIdInfoT *UnPack(const flatbuffers::resolver_function_t *_resolver = nullptr) const;
+  void UnPackTo(NanoappInstanceIdInfoT *_o, const flatbuffers::resolver_function_t *_resolver = nullptr) const;
+  static flatbuffers::Offset<NanoappInstanceIdInfo> Pack(flatbuffers::FlatBufferBuilder &_fbb, const NanoappInstanceIdInfoT* _o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
+};
+
+struct NanoappInstanceIdInfoBuilder {
+  typedef NanoappInstanceIdInfo Table;
+  flatbuffers::FlatBufferBuilder &fbb_;
+  flatbuffers::uoffset_t start_;
+  void add_instance_id(uint32_t instance_id) {
+    fbb_.AddElement<uint32_t>(NanoappInstanceIdInfo::VT_INSTANCE_ID, instance_id, 0);
+  }
+  void add_app_id(uint64_t app_id) {
+    fbb_.AddElement<uint64_t>(NanoappInstanceIdInfo::VT_APP_ID, app_id, 0);
+  }
+  explicit NanoappInstanceIdInfoBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+        : fbb_(_fbb) {
+    start_ = fbb_.StartTable();
+  }
+  NanoappInstanceIdInfoBuilder &operator=(const NanoappInstanceIdInfoBuilder &);
+  flatbuffers::Offset<NanoappInstanceIdInfo> Finish() {
+    const auto end = fbb_.EndTable(start_);
+    auto o = flatbuffers::Offset<NanoappInstanceIdInfo>(end);
+    return o;
+  }
+};
+
+inline flatbuffers::Offset<NanoappInstanceIdInfo> CreateNanoappInstanceIdInfo(
+    flatbuffers::FlatBufferBuilder &_fbb,
+    uint32_t instance_id = 0,
+    uint64_t app_id = 0) {
+  NanoappInstanceIdInfoBuilder builder_(_fbb);
+  builder_.add_app_id(app_id);
+  builder_.add_instance_id(instance_id);
+  return builder_.Finish();
+}
+
+flatbuffers::Offset<NanoappInstanceIdInfo> CreateNanoappInstanceIdInfo(flatbuffers::FlatBufferBuilder &_fbb, const NanoappInstanceIdInfoT *_o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
+
 struct UnloadNanoappRequestT : public flatbuffers::NativeTable {
   typedef UnloadNanoappRequest TableType;
   uint32_t transaction_id;
@@ -2760,7 +2891,8 @@
   /// uint8_t                 - Log metadata, encoded as follows:
   ///                           [EI(Upper nibble) | Level(Lower nibble)]
   ///                            * Log Type
-  ///                              (0 = No encoding, 1 = Tokenized log, 2 = BT snoop log)
+  ///                              (0 = No encoding, 1 = Tokenized log,
+  ///                               2 = BT snoop log, 3 = Nanoapp Tokenized log)
   ///                            * LogBuffer log level (1 = error, 2 = warn,
   ///                                                   3 = info,  4 = debug,
   ///                                                   5 = verbose)
@@ -2772,7 +2904,7 @@
   ///   terminated string (eg: pass to string manipulation functions, get its
   ///   size via strlen(), etc.).
   ///
-  /// * Encoded logs: The first byte of the log buffer indicates the size of
+  /// * Tokenized logs: The first byte of the log buffer indicates the size of
   ///   the actual encoded data to follow. For example, if a tokenized log of
   ///   size 24 bytes were to be represented, a buffer of size 25 bytes would
   ///   be needed to encode this as: [Size(1B) | Data(24B)]. A decoder would
@@ -2786,6 +2918,15 @@
   ///   size 24 bytes were to be represented, a buffer of size 26 bytes would
   ///   be needed to encode this as: [Direction(1B) | Size(1B) | Data(24B)].
   ///
+  /// * Tokenized nanoapp logs: This log type is specifically for nanoapps with
+  ///   tokenized logs enabled. Similar to tokenized logs, the first byte is the
+  ///   size of the tokenized log data at the end. The next two bytes is the instance
+  ///   ID of the nanoapp which sends this tokenized log message. This instance ID
+  ///   will be used to map to the corresponding detokenizer in the log message parser.
+  ///   For example, if a nanoapp tokenized log of size 24 bytes were to be sent,
+  ///   a buffer of size 27 bytes would be needed to encode this as:
+  ///   [Size(1B) | InstanceId (2B) | Data(24B)].
+  ///
   /// This pattern repeats until the end of the buffer for multiple log
   /// messages. The last byte will always be a null-terminator. There are no
   /// padding bytes between these fields. Treat this like a packed struct and be
@@ -3479,6 +3620,90 @@
 
 flatbuffers::Offset<DebugConfiguration> CreateDebugConfiguration(flatbuffers::FlatBufferBuilder &_fbb, const DebugConfigurationT *_o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
 
+struct PulseRequestT : public flatbuffers::NativeTable {
+  typedef PulseRequest TableType;
+  PulseRequestT() {
+  }
+};
+
+struct PulseRequest FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+  typedef PulseRequestT NativeTableType;
+  typedef PulseRequestBuilder Builder;
+  bool Verify(flatbuffers::Verifier &verifier) const {
+    return VerifyTableStart(verifier) &&
+           verifier.EndTable();
+  }
+  PulseRequestT *UnPack(const flatbuffers::resolver_function_t *_resolver = nullptr) const;
+  void UnPackTo(PulseRequestT *_o, const flatbuffers::resolver_function_t *_resolver = nullptr) const;
+  static flatbuffers::Offset<PulseRequest> Pack(flatbuffers::FlatBufferBuilder &_fbb, const PulseRequestT* _o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
+};
+
+struct PulseRequestBuilder {
+  typedef PulseRequest Table;
+  flatbuffers::FlatBufferBuilder &fbb_;
+  flatbuffers::uoffset_t start_;
+  explicit PulseRequestBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+        : fbb_(_fbb) {
+    start_ = fbb_.StartTable();
+  }
+  PulseRequestBuilder &operator=(const PulseRequestBuilder &);
+  flatbuffers::Offset<PulseRequest> Finish() {
+    const auto end = fbb_.EndTable(start_);
+    auto o = flatbuffers::Offset<PulseRequest>(end);
+    return o;
+  }
+};
+
+inline flatbuffers::Offset<PulseRequest> CreatePulseRequest(
+    flatbuffers::FlatBufferBuilder &_fbb) {
+  PulseRequestBuilder builder_(_fbb);
+  return builder_.Finish();
+}
+
+flatbuffers::Offset<PulseRequest> CreatePulseRequest(flatbuffers::FlatBufferBuilder &_fbb, const PulseRequestT *_o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
+
+struct PulseResponseT : public flatbuffers::NativeTable {
+  typedef PulseResponse TableType;
+  PulseResponseT() {
+  }
+};
+
+struct PulseResponse FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+  typedef PulseResponseT NativeTableType;
+  typedef PulseResponseBuilder Builder;
+  bool Verify(flatbuffers::Verifier &verifier) const {
+    return VerifyTableStart(verifier) &&
+           verifier.EndTable();
+  }
+  PulseResponseT *UnPack(const flatbuffers::resolver_function_t *_resolver = nullptr) const;
+  void UnPackTo(PulseResponseT *_o, const flatbuffers::resolver_function_t *_resolver = nullptr) const;
+  static flatbuffers::Offset<PulseResponse> Pack(flatbuffers::FlatBufferBuilder &_fbb, const PulseResponseT* _o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
+};
+
+struct PulseResponseBuilder {
+  typedef PulseResponse Table;
+  flatbuffers::FlatBufferBuilder &fbb_;
+  flatbuffers::uoffset_t start_;
+  explicit PulseResponseBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+        : fbb_(_fbb) {
+    start_ = fbb_.StartTable();
+  }
+  PulseResponseBuilder &operator=(const PulseResponseBuilder &);
+  flatbuffers::Offset<PulseResponse> Finish() {
+    const auto end = fbb_.EndTable(start_);
+    auto o = flatbuffers::Offset<PulseResponse>(end);
+    return o;
+  }
+};
+
+inline flatbuffers::Offset<PulseResponse> CreatePulseResponse(
+    flatbuffers::FlatBufferBuilder &_fbb) {
+  PulseResponseBuilder builder_(_fbb);
+  return builder_.Finish();
+}
+
+flatbuffers::Offset<PulseResponse> CreatePulseResponse(flatbuffers::FlatBufferBuilder &_fbb, const PulseResponseT *_o, const flatbuffers::rehasher_function_t *_rehasher = nullptr);
+
 struct MessageContainerT : public flatbuffers::NativeTable {
   typedef MessageContainer TableType;
   chre::fbs::ChreMessageUnion message;
@@ -3589,6 +3814,15 @@
   const chre::fbs::DebugConfiguration *message_as_DebugConfiguration() const {
     return message_type() == chre::fbs::ChreMessage::DebugConfiguration ? static_cast<const chre::fbs::DebugConfiguration *>(message()) : nullptr;
   }
+  const chre::fbs::PulseRequest *message_as_PulseRequest() const {
+    return message_type() == chre::fbs::ChreMessage::PulseRequest ? static_cast<const chre::fbs::PulseRequest *>(message()) : nullptr;
+  }
+  const chre::fbs::PulseResponse *message_as_PulseResponse() const {
+    return message_type() == chre::fbs::ChreMessage::PulseResponse ? static_cast<const chre::fbs::PulseResponse *>(message()) : nullptr;
+  }
+  const chre::fbs::NanoappInstanceIdInfo *message_as_NanoappInstanceIdInfo() const {
+    return message_type() == chre::fbs::ChreMessage::NanoappInstanceIdInfo ? static_cast<const chre::fbs::NanoappInstanceIdInfo *>(message()) : nullptr;
+  }
   void *mutable_message() {
     return GetPointer<void *>(VT_MESSAGE);
   }
@@ -3729,6 +3963,18 @@
   return message_as_DebugConfiguration();
 }
 
+template<> inline const chre::fbs::PulseRequest *MessageContainer::message_as<chre::fbs::PulseRequest>() const {
+  return message_as_PulseRequest();
+}
+
+template<> inline const chre::fbs::PulseResponse *MessageContainer::message_as<chre::fbs::PulseResponse>() const {
+  return message_as_PulseResponse();
+}
+
+template<> inline const chre::fbs::NanoappInstanceIdInfo *MessageContainer::message_as<chre::fbs::NanoappInstanceIdInfo>() const {
+  return message_as_NanoappInstanceIdInfo();
+}
+
 struct MessageContainerBuilder {
   typedef MessageContainer Table;
   flatbuffers::FlatBufferBuilder &fbb_;
@@ -4100,6 +4346,35 @@
       _fragment_id);
 }
 
+inline NanoappInstanceIdInfoT *NanoappInstanceIdInfo::UnPack(const flatbuffers::resolver_function_t *_resolver) const {
+  std::unique_ptr<chre::fbs::NanoappInstanceIdInfoT> _o = std::unique_ptr<chre::fbs::NanoappInstanceIdInfoT>(new NanoappInstanceIdInfoT());
+  UnPackTo(_o.get(), _resolver);
+  return _o.release();
+}
+
+inline void NanoappInstanceIdInfo::UnPackTo(NanoappInstanceIdInfoT *_o, const flatbuffers::resolver_function_t *_resolver) const {
+  (void)_o;
+  (void)_resolver;
+  { auto _e = instance_id(); _o->instance_id = _e; }
+  { auto _e = app_id(); _o->app_id = _e; }
+}
+
+inline flatbuffers::Offset<NanoappInstanceIdInfo> NanoappInstanceIdInfo::Pack(flatbuffers::FlatBufferBuilder &_fbb, const NanoappInstanceIdInfoT* _o, const flatbuffers::rehasher_function_t *_rehasher) {
+  return CreateNanoappInstanceIdInfo(_fbb, _o, _rehasher);
+}
+
+inline flatbuffers::Offset<NanoappInstanceIdInfo> CreateNanoappInstanceIdInfo(flatbuffers::FlatBufferBuilder &_fbb, const NanoappInstanceIdInfoT *_o, const flatbuffers::rehasher_function_t *_rehasher) {
+  (void)_rehasher;
+  (void)_o;
+  struct _VectorArgs { flatbuffers::FlatBufferBuilder *__fbb; const NanoappInstanceIdInfoT* __o; const flatbuffers::rehasher_function_t *__rehasher; } _va = { &_fbb, _o, _rehasher}; (void)_va;
+  auto _instance_id = _o->instance_id;
+  auto _app_id = _o->app_id;
+  return chre::fbs::CreateNanoappInstanceIdInfo(
+      _fbb,
+      _instance_id,
+      _app_id);
+}
+
 inline UnloadNanoappRequestT *UnloadNanoappRequest::UnPack(const flatbuffers::resolver_function_t *_resolver) const {
   std::unique_ptr<chre::fbs::UnloadNanoappRequestT> _o = std::unique_ptr<chre::fbs::UnloadNanoappRequestT>(new UnloadNanoappRequestT());
   UnPackTo(_o.get(), _resolver);
@@ -4661,6 +4936,52 @@
       _health_monitor_failure_crash);
 }
 
+inline PulseRequestT *PulseRequest::UnPack(const flatbuffers::resolver_function_t *_resolver) const {
+  std::unique_ptr<chre::fbs::PulseRequestT> _o = std::unique_ptr<chre::fbs::PulseRequestT>(new PulseRequestT());
+  UnPackTo(_o.get(), _resolver);
+  return _o.release();
+}
+
+inline void PulseRequest::UnPackTo(PulseRequestT *_o, const flatbuffers::resolver_function_t *_resolver) const {
+  (void)_o;
+  (void)_resolver;
+}
+
+inline flatbuffers::Offset<PulseRequest> PulseRequest::Pack(flatbuffers::FlatBufferBuilder &_fbb, const PulseRequestT* _o, const flatbuffers::rehasher_function_t *_rehasher) {
+  return CreatePulseRequest(_fbb, _o, _rehasher);
+}
+
+inline flatbuffers::Offset<PulseRequest> CreatePulseRequest(flatbuffers::FlatBufferBuilder &_fbb, const PulseRequestT *_o, const flatbuffers::rehasher_function_t *_rehasher) {
+  (void)_rehasher;
+  (void)_o;
+  struct _VectorArgs { flatbuffers::FlatBufferBuilder *__fbb; const PulseRequestT* __o; const flatbuffers::rehasher_function_t *__rehasher; } _va = { &_fbb, _o, _rehasher}; (void)_va;
+  return chre::fbs::CreatePulseRequest(
+      _fbb);
+}
+
+inline PulseResponseT *PulseResponse::UnPack(const flatbuffers::resolver_function_t *_resolver) const {
+  std::unique_ptr<chre::fbs::PulseResponseT> _o = std::unique_ptr<chre::fbs::PulseResponseT>(new PulseResponseT());
+  UnPackTo(_o.get(), _resolver);
+  return _o.release();
+}
+
+inline void PulseResponse::UnPackTo(PulseResponseT *_o, const flatbuffers::resolver_function_t *_resolver) const {
+  (void)_o;
+  (void)_resolver;
+}
+
+inline flatbuffers::Offset<PulseResponse> PulseResponse::Pack(flatbuffers::FlatBufferBuilder &_fbb, const PulseResponseT* _o, const flatbuffers::rehasher_function_t *_rehasher) {
+  return CreatePulseResponse(_fbb, _o, _rehasher);
+}
+
+inline flatbuffers::Offset<PulseResponse> CreatePulseResponse(flatbuffers::FlatBufferBuilder &_fbb, const PulseResponseT *_o, const flatbuffers::rehasher_function_t *_rehasher) {
+  (void)_rehasher;
+  (void)_o;
+  struct _VectorArgs { flatbuffers::FlatBufferBuilder *__fbb; const PulseResponseT* __o; const flatbuffers::rehasher_function_t *__rehasher; } _va = { &_fbb, _o, _rehasher}; (void)_va;
+  return chre::fbs::CreatePulseResponse(
+      _fbb);
+}
+
 inline MessageContainerT *MessageContainer::UnPack(const flatbuffers::resolver_function_t *_resolver) const {
   std::unique_ptr<chre::fbs::MessageContainerT> _o = std::unique_ptr<chre::fbs::MessageContainerT>(new MessageContainerT());
   UnPackTo(_o.get(), _resolver);
@@ -4810,6 +5131,18 @@
       auto ptr = reinterpret_cast<const chre::fbs::DebugConfiguration *>(obj);
       return verifier.VerifyTable(ptr);
     }
+    case ChreMessage::PulseRequest: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseRequest *>(obj);
+      return verifier.VerifyTable(ptr);
+    }
+    case ChreMessage::PulseResponse: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseResponse *>(obj);
+      return verifier.VerifyTable(ptr);
+    }
+    case ChreMessage::NanoappInstanceIdInfo: {
+      auto ptr = reinterpret_cast<const chre::fbs::NanoappInstanceIdInfo *>(obj);
+      return verifier.VerifyTable(ptr);
+    }
     default: return true;
   }
 }
@@ -4940,6 +5273,18 @@
       auto ptr = reinterpret_cast<const chre::fbs::DebugConfiguration *>(obj);
       return ptr->UnPack(resolver);
     }
+    case ChreMessage::PulseRequest: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseRequest *>(obj);
+      return ptr->UnPack(resolver);
+    }
+    case ChreMessage::PulseResponse: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseResponse *>(obj);
+      return ptr->UnPack(resolver);
+    }
+    case ChreMessage::NanoappInstanceIdInfo: {
+      auto ptr = reinterpret_cast<const chre::fbs::NanoappInstanceIdInfo *>(obj);
+      return ptr->UnPack(resolver);
+    }
     default: return nullptr;
   }
 }
@@ -5058,6 +5403,18 @@
       auto ptr = reinterpret_cast<const chre::fbs::DebugConfigurationT *>(value);
       return CreateDebugConfiguration(_fbb, ptr, _rehasher).Union();
     }
+    case ChreMessage::PulseRequest: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseRequestT *>(value);
+      return CreatePulseRequest(_fbb, ptr, _rehasher).Union();
+    }
+    case ChreMessage::PulseResponse: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseResponseT *>(value);
+      return CreatePulseResponse(_fbb, ptr, _rehasher).Union();
+    }
+    case ChreMessage::NanoappInstanceIdInfo: {
+      auto ptr = reinterpret_cast<const chre::fbs::NanoappInstanceIdInfoT *>(value);
+      return CreateNanoappInstanceIdInfo(_fbb, ptr, _rehasher).Union();
+    }
     default: return 0;
   }
 }
@@ -5176,6 +5533,18 @@
       value = new chre::fbs::DebugConfigurationT(*reinterpret_cast<chre::fbs::DebugConfigurationT *>(u.value));
       break;
     }
+    case ChreMessage::PulseRequest: {
+      value = new chre::fbs::PulseRequestT(*reinterpret_cast<chre::fbs::PulseRequestT *>(u.value));
+      break;
+    }
+    case ChreMessage::PulseResponse: {
+      value = new chre::fbs::PulseResponseT(*reinterpret_cast<chre::fbs::PulseResponseT *>(u.value));
+      break;
+    }
+    case ChreMessage::NanoappInstanceIdInfo: {
+      value = new chre::fbs::NanoappInstanceIdInfoT(*reinterpret_cast<chre::fbs::NanoappInstanceIdInfoT *>(u.value));
+      break;
+    }
     default:
       break;
   }
@@ -5323,6 +5692,21 @@
       delete ptr;
       break;
     }
+    case ChreMessage::PulseRequest: {
+      auto ptr = reinterpret_cast<chre::fbs::PulseRequestT *>(value);
+      delete ptr;
+      break;
+    }
+    case ChreMessage::PulseResponse: {
+      auto ptr = reinterpret_cast<chre::fbs::PulseResponseT *>(value);
+      delete ptr;
+      break;
+    }
+    case ChreMessage::NanoappInstanceIdInfo: {
+      auto ptr = reinterpret_cast<chre::fbs::NanoappInstanceIdInfoT *>(value);
+      delete ptr;
+      break;
+    }
     default: break;
   }
   value = nullptr;
diff --git a/host/common/include/chre_host/hal_client.h b/host/common/include/chre_host/hal_client.h
new file mode 100644
index 0000000..bc6db3b
--- /dev/null
+++ b/host/common/include/chre_host/hal_client.h
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 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 CHRE_HOST_HAL_CLIENT_H_
+#define CHRE_HOST_HAL_CLIENT_H_
+
+#include <cinttypes>
+#include <memory>
+#include <shared_mutex>
+#include <unordered_map>
+#include <vector>
+
+#include <aidl/android/hardware/contexthub/BnContextHubCallback.h>
+#include <aidl/android/hardware/contexthub/ContextHubMessage.h>
+#include <aidl/android/hardware/contexthub/HostEndpointInfo.h>
+#include <aidl/android/hardware/contexthub/IContextHub.h>
+#include <aidl/android/hardware/contexthub/IContextHubCallback.h>
+#include <aidl/android/hardware/contexthub/NanoappBinary.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+#include "chre_host/log.h"
+#include "host/hal_generic/common/hal_error.h"
+
+namespace android::chre {
+
+using ::aidl::android::hardware::contexthub::AsyncEventType;
+using ::aidl::android::hardware::contexthub::BnContextHubCallback;
+using ::aidl::android::hardware::contexthub::ContextHubInfo;
+using ::aidl::android::hardware::contexthub::ContextHubMessage;
+using ::aidl::android::hardware::contexthub::HostEndpointInfo;
+using ::aidl::android::hardware::contexthub::IContextHub;
+using ::aidl::android::hardware::contexthub::IContextHubCallback;
+using ::aidl::android::hardware::contexthub::IContextHubDefault;
+using ::aidl::android::hardware::contexthub::MessageDeliveryStatus;
+using ::aidl::android::hardware::contexthub::NanoappBinary;
+using ::aidl::android::hardware::contexthub::NanoappInfo;
+using ::aidl::android::hardware::contexthub::NanSessionRequest;
+using ::aidl::android::hardware::contexthub::Setting;
+using ::ndk::ScopedAStatus;
+
+/**
+ * A class exposing CHRE HAL APIs to clients and taking care of binder
+ * (re)connection.
+ *
+ * <p>This class also maintains a set of connected host endpoints, using which
+ * it is enforced that a message can only be sent to/from an endpoint id that is
+ * already connected to HAL.
+ *
+ * <p>When the binder connection to HAL is disconnected HalClient will have a
+ * death recipient re-establish the connection and reconnect the previously
+ * connected endpoints. In a rare case that CHRE also restarts at the same time,
+ * a client should rely on IContextHubCallback.handleContextHubAsyncEvent() to
+ * handle the RESTARTED event which is a signal that CHRE is up running.
+ *
+ * TODO(b/297912356): The name of this class is the same as an internal struct
+ *   used by HalClientManager. Consider rename the latter one to avoid confusion
+ *
+ */
+class HalClient {
+ public:
+  static constexpr int32_t kDefaultContextHubId = 0;
+
+  /**
+   * Create a HalClient unique pointer used to communicate with CHRE HAL.
+   *
+   * @param callback a non-null callback.
+   * @param contextHubId context hub id that only 0 is supported at this moment.
+   *
+   * @return null pointer if the creation fails.
+   */
+  static std::unique_ptr<HalClient> create(
+      const std::shared_ptr<IContextHubCallback> &callback,
+      int32_t contextHubId = kDefaultContextHubId);
+
+  ScopedAStatus queryNanoapps() {
+    return callIfConnected(
+        [&]() { return mContextHub->queryNanoapps(mContextHubId); });
+  }
+
+  ScopedAStatus sendMessage(const ContextHubMessage &message);
+
+  ScopedAStatus connectEndpoint(const HostEndpointInfo &hostEndpointInfo);
+
+  ScopedAStatus disconnectEndpoint(char16_t hostEndpointId);
+
+ protected:
+  class HalClientCallback : public BnContextHubCallback {
+   public:
+    explicit HalClientCallback(
+        const std::shared_ptr<IContextHubCallback> &callback,
+        HalClient *halClient)
+        : mCallback(callback), mHalClient(halClient) {}
+
+    ScopedAStatus handleNanoappInfo(
+        const std::vector<NanoappInfo> &appInfo) override {
+      return mCallback->handleNanoappInfo(appInfo);
+    }
+
+    ScopedAStatus handleContextHubMessage(
+        const ContextHubMessage &msg,
+        const std::vector<std::string> &msgContentPerms) override {
+      return mCallback->handleContextHubMessage(msg, msgContentPerms);
+    }
+
+    ScopedAStatus handleContextHubAsyncEvent(AsyncEventType event) override {
+      if (event == AsyncEventType::RESTARTED) {
+        LOGW("CHRE has restarted. Reconnecting endpoints.");
+        tryReconnectEndpoints(mHalClient);
+      }
+      return mCallback->handleContextHubAsyncEvent(event);
+    }
+
+    ScopedAStatus handleTransactionResult(int32_t transactionId,
+                                          bool success) override {
+      return mCallback->handleTransactionResult(transactionId, success);
+    }
+
+    ScopedAStatus handleNanSessionRequest(
+        const NanSessionRequest &request) override {
+      return mCallback->handleNanSessionRequest(request);
+    }
+
+    ScopedAStatus handleMessageDeliveryStatus(
+        char16_t hostEndPointId,
+        const MessageDeliveryStatus &messageDeliveryStatus) override {
+      return mCallback->handleMessageDeliveryStatus(hostEndPointId,
+                                                    messageDeliveryStatus);
+    }
+
+    ScopedAStatus getUuid(std::array<uint8_t, 16> *outUuid) override {
+      return mCallback->getUuid(outUuid);
+    }
+
+    ScopedAStatus getName(std::string *outName) override {
+      return mCallback->getName(outName);
+    }
+
+   private:
+    std::shared_ptr<IContextHubCallback> mCallback;
+    HalClient *mHalClient;
+  };
+
+  explicit HalClient(const std::shared_ptr<IContextHubCallback> &callback,
+                     int32_t contextHubId = kDefaultContextHubId)
+      : mContextHubId(contextHubId) {
+    mCallback = ndk::SharedRefBase::make<HalClientCallback>(callback, this);
+    ABinderProcess_startThreadPool();
+    mDeathRecipient = ndk::ScopedAIBinder_DeathRecipient(
+        AIBinder_DeathRecipient_new(onHalDisconnected));
+  }
+
+  /**
+   * Initializes the connection to CHRE HAL.
+   */
+  HalError initConnection();
+
+  using HostEndpointId = char16_t;
+
+  const std::string kAidlServiceName =
+      std::string() + IContextHub::descriptor + "/default";
+
+  /** The callback for a disconnected HAL binder connection. */
+  static void onHalDisconnected(void *cookie);
+
+  /** Reconnect previously connected endpoints after CHRE or HAL restarts. */
+  static void tryReconnectEndpoints(HalClient *halClient);
+
+  ScopedAStatus callIfConnected(const std::function<ScopedAStatus()> &func) {
+    std::shared_lock<std::shared_mutex> sharedLock(mConnectionLock);
+    if (mContextHub == nullptr) {
+      return fromHalError(HalError::BINDER_DISCONNECTED);
+    }
+    return func();
+  }
+
+  bool isEndpointConnected(HostEndpointId hostEndpointId) {
+    std::shared_lock<std::shared_mutex> lock(mStateLock);
+    return mConnectedEndpoints.find(hostEndpointId) !=
+           mConnectedEndpoints.end();
+  }
+
+  void insertConnectedEndpoint(const HostEndpointInfo &hostEndpointInfo) {
+    std::lock_guard<std::shared_mutex> lock(mStateLock);
+    mConnectedEndpoints[hostEndpointInfo.hostEndpointId] = hostEndpointInfo;
+  }
+
+  void removeConnectedEndpoint(HostEndpointId hostEndpointId) {
+    std::lock_guard<std::shared_mutex> lock(mStateLock);
+    mConnectedEndpoints.erase(hostEndpointId);
+  }
+
+  void clearConnectedEndpoints() {
+    std::lock_guard<std::shared_mutex> lock(mStateLock);
+    mConnectedEndpoints.clear();
+  }
+
+  static ScopedAStatus fromHalError(HalError errorCode) {
+    return errorCode == HalError::SUCCESS
+               ? ScopedAStatus::ok()
+               : ScopedAStatus::fromServiceSpecificError(
+                     static_cast<int32_t>(errorCode));
+  }
+
+  // Multi-contextHub is not supported at this moment.
+  int32_t mContextHubId;
+
+  // The lock guarding mConnectedEndpoints.
+  std::shared_mutex mStateLock;
+  std::unordered_map<HostEndpointId, HostEndpointInfo> mConnectedEndpoints{};
+
+  // The lock guarding mContextHub.
+  std::shared_mutex mConnectionLock;
+  std::shared_ptr<IContextHub> mContextHub;
+
+  // Handler of the binder disconnection event with HAL.
+  ndk::ScopedAIBinder_DeathRecipient mDeathRecipient;
+
+  std::shared_ptr<HalClientCallback> mCallback;
+};
+
+}  // namespace android::chre
+#endif  // CHRE_HOST_HAL_CLIENT_H_
\ No newline at end of file
diff --git a/host/common/include/chre_host/host_protocol_host.h b/host/common/include/chre_host/host_protocol_host.h
index 29c1f8b..35f1ced 100644
--- a/host/common/include/chre_host/host_protocol_host.h
+++ b/host/common/include/chre_host/host_protocol_host.h
@@ -99,6 +99,14 @@
                                     IChreMessageHandlers &handlers);
 
   /**
+   * Encodes a message requesting pulse from CHRE
+   *
+   * @param builder A newly constructed FlatBufferBuilder that will be used to
+   *        construct the message
+   */
+  static void encodePulseRequest(flatbuffers::FlatBufferBuilder &builder);
+
+  /**
    * Encodes a message requesting hub information from CHRE
    *
    * @param builder A newly constructed FlatBufferBuilder that will be used to
diff --git a/host/common/include/chre_host/log.h b/host/common/include/chre_host/log.h
index a247e87..937d368 100644
--- a/host/common/include/chre_host/log.h
+++ b/host/common/include/chre_host/log.h
@@ -23,25 +23,35 @@
 
 #include <log/log.h>
 
-/**
- * Logs a message to both logcat and stdout. Don't use this directly; prefer one
- * of LOGE, LOGW, etc. to populate the level. Use LOG_PRI directly rather than
- * ALOG to avoid misinterpreting LOG_* macros that may be incorrectly evaluated.
- *
- * @param level log level to pass to ALOG (LOG_ERROR, LOG_WARN, etc.)
- * @param stream output stream to print to (e.g. stdout)
- * @param format printf-style format string
- */
-#define CHRE_LOG(level, stream, format, ...)                                   \
-  do {                                                                         \
-    LOG_PRI(ANDROID_##level, LOG_TAG, format, ##__VA_ARGS__);                  \
-    fprintf(stream, "%s:%d: " format "\n", __func__, __LINE__, ##__VA_ARGS__); \
-  } while (0)
+namespace android::chre {
 
-#define LOGE(format, ...) CHRE_LOG(LOG_ERROR, stderr, format, ##__VA_ARGS__)
-#define LOGW(format, ...) CHRE_LOG(LOG_WARN, stdout, format, ##__VA_ARGS__)
-#define LOGI(format, ...) CHRE_LOG(LOG_INFO, stdout, format, ##__VA_ARGS__)
-#define LOGD(format, ...) CHRE_LOG(LOG_DEBUG, stdout, format, ##__VA_ARGS__)
+/**
+ * Logs a message to both logcat and stdout/stderr. Don't use this directly;
+ * prefer one of LOGE, LOGW, etc.
+ *
+ * @param level  android log level, e.g., ANDROID_LOG_ERROR
+ * @param stream output stream to print to, e.g., stdout
+ * @param format printf-style format string
+ * @param func   the function name included in the log, e.g., __func__
+ * @param line   line number included in the log
+ */
+void outputHostLog(int priority, FILE *stream, const char *format,
+                   const char *func, unsigned int line, ...);
+
+}  // namespace android::chre
+
+#define LOGE(format, ...)                                                     \
+  ::android::chre::outputHostLog(ANDROID_LOG_ERROR, stderr, format, __func__, \
+                                 __LINE__, ##__VA_ARGS__)
+#define LOGW(format, ...)                                                    \
+  ::android::chre::outputHostLog(ANDROID_LOG_WARN, stdout, format, __func__, \
+                                 __LINE__, ##__VA_ARGS__)
+#define LOGI(format, ...)                                                    \
+  ::android::chre::outputHostLog(ANDROID_LOG_INFO, stdout, format, __func__, \
+                                 __LINE__, ##__VA_ARGS__)
+#define LOGD(format, ...)                                                     \
+  ::android::chre::outputHostLog(ANDROID_LOG_DEBUG, stdout, format, __func__, \
+                                 __LINE__, ##__VA_ARGS__)
 
 #if LOG_NDEBUG
 __attribute__((format(printf, 1, 2))) inline void chreLogNull(
@@ -49,7 +59,9 @@
 
 #define LOGV(format, ...) chreLogNull(format, ##__VA_ARGS__)
 #else
-#define LOGV(format, ...) CHRE_LOG(LOG_VERBOSE, stdout, format, ##__VA_ARGS__)
+#define LOGV(format, ...)                                             \
+  ::android::chre::outputHostLog(ANDROID_LOG_VERBOSE, stdout, format, \
+                                 __func__, __LINE__, ##__VA_ARGS__)
 #endif
 
 /**
diff --git a/host/common/include/chre_host/metrics_reporter.h b/host/common/include/chre_host/metrics_reporter.h
new file mode 100644
index 0000000..ea1ddfb
--- /dev/null
+++ b/host/common/include/chre_host/metrics_reporter.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 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 CHRE_HOST_METRICS_REPORTER_H_
+#define CHRE_HOST_METRICS_REPORTER_H_
+
+#include <aidl/android/frameworks/stats/IStats.h>
+#include <chre_atoms_log.h>
+#include <mutex>
+
+namespace android::chre {
+
+class MetricsReporter {
+ public:
+  MetricsReporter() = default;
+  ~MetricsReporter() = default;
+
+  MetricsReporter(const MetricsReporter &) = delete;
+  MetricsReporter &operator=(const MetricsReporter &) = delete;
+
+  /**
+   * Creates and reports CHRE vendor atom and send it to stats_client.
+   *
+   * @param atom the vendor atom to be reported
+   * @return true if the metric was successfully reported, false otherwise.
+   */
+  bool reportMetric(const aidl::android::frameworks::stats::VendorAtom &atom);
+
+  /**
+   * Reports an AP Wakeup caused by a nanoapp.
+   *
+   * @return whether the operation was successful.
+   */
+  bool logApWakeupOccurred(uint64_t nanoappId);
+
+  /**
+   * Reports a nanoapp load failed metric.
+   *
+   * @return whether the operation was successful.
+   */
+  bool logNanoappLoadFailed(
+      uint64_t nanoappId,
+      android::chre::Atoms::ChreHalNanoappLoadFailed::Type type,
+      android::chre::Atoms::ChreHalNanoappLoadFailed::Reason reason);
+
+  /**
+   * Reports a PAL open failed metric.
+   *
+   * @return whether the operation was successful.
+   */
+  bool logPalOpenFailed(
+      android::chre::Atoms::ChrePalOpenFailed::ChrePalType pal,
+      android::chre::Atoms::ChrePalOpenFailed::Type type);
+
+  /**
+   * Reports a event queue snapshot reported metric.
+   *
+   * @return whether the operation was successful.
+   */
+  bool logEventQueueSnapshotReported(int32_t snapshotChreGetTimeMs,
+                                     int32_t max_event_queue_size,
+                                     int32_t mean_event_queue_size,
+                                     int32_t num_dropped_events);
+
+  /**
+   * Called when the binder dies for the stats service.
+   */
+  void onBinderDied();
+
+ private:
+  /**
+   * Connects to the stats service or return nullptr if it cannot connect.
+   * This also adds a binder death handler to the service that will call
+   * onBinderDied on this.
+   *
+   * @return the stats service
+   */
+  std::shared_ptr<aidl::android::frameworks::stats::IStats> getStatsService();
+
+  std::mutex mStatsServiceMutex;
+  std::shared_ptr<aidl::android::frameworks::stats::IStats> mStatsService =
+      nullptr;
+};
+
+}  // namespace android::chre
+
+#endif  // CHRE_HOST_METRICS_REPORTER_H_
\ No newline at end of file
diff --git a/host/common/include/chre_host/pigweed/hal_channel_output.h b/host/common/include/chre_host/pigweed/hal_channel_output.h
new file mode 100644
index 0000000..f190297
--- /dev/null
+++ b/host/common/include/chre_host/pigweed/hal_channel_output.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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 CHRE_PIGWEED_HAL_CHANNEL_OUTPUT_H_
+#define CHRE_PIGWEED_HAL_CHANNEL_OUTPUT_H_
+
+#include <cstdint>
+
+#include "chre/util/non_copyable.h"
+#include "chre_host/socket_client.h"
+#include "pw_rpc/channel.h"
+#include "pw_span/span.h"
+
+namespace android::chre {
+
+/**
+ * RPC Channel Output for native vendor processes.
+ */
+class HalChannelOutput : public ::chre::NonCopyable,
+                         public pw::rpc::ChannelOutput {
+ public:
+  HalChannelOutput(android::chre::SocketClient &client, uint32_t hostEndpointId,
+                   uint64_t serverNanoappId, size_t maxMessageLen)
+      : pw::rpc::ChannelOutput("CHRE"),
+        mServerNanoappId(serverNanoappId),
+        mHostEndpointId(hostEndpointId),
+        mMaxMessageLen(maxMessageLen),
+        mSocketClient(client) {}
+
+  size_t MaximumTransmissionUnit() override;
+
+  pw::Status Send(pw::span<const std::byte> buffer) override;
+
+ private:
+  // Padding used to encode nanoapp messages.
+  static constexpr size_t kFlatBufferPadding = 80;
+
+  const uint64_t mServerNanoappId;
+  const uint32_t mHostEndpointId;
+  const size_t mMaxMessageLen;
+  SocketClient &mSocketClient;
+};
+
+}  // namespace android::chre
+
+#endif  // CHRE_PIGWEED_HAL_CHANNEL_OUTPUT_H_
\ No newline at end of file
diff --git a/host/common/include/chre_host/pigweed/hal_rpc_client.h b/host/common/include/chre_host/pigweed/hal_rpc_client.h
new file mode 100644
index 0000000..c8cdee6
--- /dev/null
+++ b/host/common/include/chre_host/pigweed/hal_rpc_client.h
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 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 CHRE_PIGWEED_HAL_RPC_CLIENT_H_
+#define CHRE_PIGWEED_HAL_RPC_CLIENT_H_
+
+#include <chrono>
+#include <condition_variable>
+#include <cstdint>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string_view>
+
+#include <android-base/thread_annotations.h>
+
+#include "chre/util/pigweed/rpc_common.h"
+#include "chre_host/pigweed/hal_channel_output.h"
+
+#include "chre/util/macros.h"
+#include "chre/util/non_copyable.h"
+#include "chre_host/host_protocol_host.h"
+#include "chre_host/log.h"
+#include "chre_host/pigweed/hal_channel_output.h"
+#include "chre_host/socket_client.h"
+#include "pw_rpc/client.h"
+
+namespace android::chre {
+
+/**
+ * RPC client helper to use with native vendor processes.
+ */
+class HalRpcClient : public ::chre::NonCopyable {
+ public:
+  /**
+   * Creates a RPC client helper.
+   *
+   * This method connects to the socket blocks until the initialization is
+   * complete.
+   *
+   # @param appName Name of the app.
+   * @param client A SocketClient that must not be already connected.
+   * @param socketCallbacks The callbacks to call on SocketClient events.
+   * @param hostEndpointId The host endpoint ID for the app.
+   * @param serverNanoappId The ID of the nanoapp providing the service.
+   * @return A pointer to a HalRpcClient. nullptr on error.
+   */
+  static std::unique_ptr<HalRpcClient> createClient(
+      std::string_view appName, SocketClient &client,
+      sp<SocketClient::ICallbacks> socketCallbacks, uint16_t hostEndpointId,
+      uint64_t serverNanoappId);
+
+  ~HalRpcClient() {
+    close();
+  }
+
+  /**
+   * Closes the RPC client and de-allocate resources.
+   *
+   * Note: This method is called from the destructor. However it can also be
+   * called explicitly.
+   */
+  void close();
+
+  /**
+   * Returns a service client.
+   *
+   * NOTE: The template parameter must be set to the Pigweed client type,
+   *       i.e. pw::rpc::pw_rpc::nanopb::<ServiceName>::Client
+
+   * @return The service client. It has no value on errors.
+   */
+  template <typename T>
+  std::optional<T> get();
+
+  /**
+   * Returns whether the server nanoapp supports the service.
+   *
+   * Also returns false when the nanoapp is not loaded.
+   *
+   * @return whether the service is published by the server.
+   */
+  bool hasService(uint64_t id, uint32_t version);
+
+ private:
+  /** Timeout for the requests to the daemon */
+  static constexpr auto kRequestTimeout = std::chrono::milliseconds(2000);
+
+  class Callbacks : public SocketClient::ICallbacks,
+                    public IChreMessageHandlers {
+   public:
+    Callbacks(HalRpcClient *client,
+              sp<SocketClient::ICallbacks> socketCallbacks)
+        : mClient(client), mSocketCallbacks(socketCallbacks) {}
+
+    /** Socket callbacks. */
+    void onMessageReceived(const void *data, size_t length) override;
+    void onConnected() override;
+    void onConnectionAborted() override;
+    void onDisconnected() override;
+
+    /** Message handlers. */
+    void handleNanoappMessage(
+        const ::chre::fbs::NanoappMessageT &message) override;
+    void handleHubInfoResponse(
+        const ::chre::fbs::HubInfoResponseT &response) override;
+    void handleNanoappListResponse(
+        const ::chre::fbs::NanoappListResponseT &response) override;
+
+   private:
+    HalRpcClient *mClient;
+    sp<SocketClient::ICallbacks> mSocketCallbacks;
+  };
+
+  HalRpcClient(std::string_view appName, SocketClient &client,
+               uint16_t hostEndpointId, uint64_t serverNanoappId)
+      : mServerNanoappId(serverNanoappId),
+        mHostEndpointId(hostEndpointId),
+        mAppName(appName),
+        mSocketClient(client) {}
+
+  /**
+   * Initializes the RPC client helper.
+   *
+   * @param socketCallbacks The callbacks to call on SocketClient events.
+   * @return Whether the initialization was successful.
+   */
+  bool init(sp<SocketClient::ICallbacks> socketCallbacks);
+
+  /** @return the Pigweed RPC channel ID */
+  uint32_t GetChannelId() {
+    return ::chre::kChannelIdHostClient | mHostEndpointId;
+  }
+
+  /**
+   * Notifies CHRE that the host endpoint has connected.
+   *
+   * Needed as the RPC Server helper will retrieve the host endpoint metadata
+   * when servicing a request.
+   */
+  bool notifyEndpointConnected();
+
+  /** Notifies CHRE that the host endpoint has disconnected. */
+  bool notifyEndpointDisconnected();
+
+  /** Query CHRE to retrieve the maximum message length. */
+  bool retrieveMaxMessageLen();
+
+  /** Query CHRE to retrieve the services published by the server nanoapp. */
+  bool retrieveServices();
+
+  const uint64_t mServerNanoappId;
+  const uint16_t mHostEndpointId;
+  const std::string mAppName;
+  SocketClient &mSocketClient;
+  std::unique_ptr<HalChannelOutput> mChannelOutput;
+  pw::rpc::Client mRpcClient;
+  bool mIsChannelOpened = false;
+
+  /** Request Hub Info. */
+  std::mutex mHubInfoMutex;
+  size_t mMaxMessageLen GUARDED_BY(mHubInfoMutex);
+  std::condition_variable mHubInfoCond;
+
+  /** Request Nanoapps. */
+  std::mutex mNanoappMutex;
+  std::vector<::chre::fbs::NanoappRpcServiceT> mServices
+      GUARDED_BY(mNanoappMutex);
+  std::condition_variable mNanoappCond;
+};
+
+template <typename T>
+std::optional<T> HalRpcClient::get() {
+  if (mChannelOutput == nullptr) {
+    LOGE("No channel output");
+    return {};
+  }
+
+  if (!mIsChannelOpened) {
+    mRpcClient.OpenChannel(GetChannelId(), *mChannelOutput);
+    mIsChannelOpened = true;
+  }
+
+  return T(mRpcClient, GetChannelId());
+}
+
+}  // namespace android::chre
+
+#endif  // CHRE_PIGWEED_HAL_RPC_CLIENT_H_
\ No newline at end of file
diff --git a/host/common/include/chre_host/preloaded_nanoapp_loader.h b/host/common/include/chre_host/preloaded_nanoapp_loader.h
index a605ae7..3a3a2fc 100644
--- a/host/common/include/chre_host/preloaded_nanoapp_loader.h
+++ b/host/common/include/chre_host/preloaded_nanoapp_loader.h
@@ -18,10 +18,12 @@
 #define CHRE_HOST_PRELOADED_NANOAPP_LOADER_H_
 
 #include <android/binder_to_string.h>
+#include <atomic>
 #include <cstdint>
 #include <mutex>
 #include <optional>
 #include <string>
+#include <unordered_set>
 #include <utility>
 
 #include "chre_connection.h"
@@ -46,7 +48,7 @@
  public:
   explicit PreloadedNanoappLoader(ChreConnection *connection,
                                   std::string configPath)
-      : mConnection(connection), mConfigPath(std::move(configPath)){};
+      : mConnection(connection), mConfigPath(std::move(configPath)) {}
 
   ~PreloadedNanoappLoader() = default;
   /**
@@ -60,8 +62,14 @@
    * ]}
    *
    * The napp_header and so files will both be used.
+   *
+   * @param selectedNanoappIds only nanoapp ids in this set will be loaded if it
+   * is set. Otherwise the default value means every preloaded nanoapp will be
+   * loaded.
    */
-  void loadPreloadedNanoapps();
+  bool loadPreloadedNanoapps(
+      const std::optional<const std::unordered_set<uint64_t>>
+          &selectedNanoappIds = std::nullopt);
 
   /** Callback function to handle the response from CHRE. */
   bool onLoadNanoappResponse(const ::chre::fbs::LoadNanoappResponseT &response,
@@ -72,35 +80,28 @@
   /** Returns true if the loading is ongoing. */
   [[nodiscard]] bool isPreloadOngoing() const {
     return mIsPreloadingOngoing;
-  };
+  }
 
  private:
+  /** Tracks the transaction state of the ongoing nanoapp loading */
+  struct Transaction {
+    uint32_t transactionId;
+    size_t fragmentId;
+  };
+
   /** Timeout value of waiting for the response of a fragmented load */
   static constexpr auto kTimeoutInMs = std::chrono::milliseconds(2000);
 
   /**
-   * Loads a preloaded nanoapp given a filename.
-   *
-   * This function allows the transaction to complete before the nanoapp starts
-   * so the server can start serving requests as soon as possible.
-   *
-   * @param directory The directory to load the nanoapp from.
-   * @param name The filename of the nanoapp to load.
-   * @param transactionId The transaction ID to use when loading the app.
-   */
-  void loadPreloadedNanoapp(const std::string &directory,
-                            const std::string &name, uint32_t transactionId);
-
-  /**
    * Loads a preloaded nanoapp.
    *
-   * @param header The nanoapp header binary blob.
-   * @param nanoapp The nanoapp binary.
+   * @param appHeader The nanoapp header binary blob.
+   * @param nanoappFileName The nanoapp binary file name.
    * @param transactionId The transaction ID identifying this load transaction.
    * @return true if successful, false otherwise.
    */
-  bool loadNanoapp(const std::vector<uint8_t> &header,
-                   const std::vector<uint8_t> &nanoapp, uint32_t transactionId);
+  bool loadNanoapp(const NanoAppBinaryHeader *appHeader,
+                   const std::string &nanoappFileName, uint32_t transactionId);
 
   /**
    * Chunks the nanoapp binary into fragments and load each fragment
@@ -123,11 +124,6 @@
   [[nodiscard]] bool verifyFragmentLoadResponse(
       const ::chre::fbs::LoadNanoappResponseT &response) const;
 
-  /** Tracks the transaction state of the ongoing nanoapp loading */
-  struct Transaction {
-    uint32_t transactionId;
-    size_t fragmentId;
-  };
   Transaction mPreloadedNanoappPendingTransaction{0, 0};
 
   /** The value of this promise carries the result in the load response. */
@@ -136,7 +132,7 @@
   /** The mutex used to guard states change for preloading. */
   std::mutex mPreloadedNanoappsMutex;
 
-  bool mIsPreloadingOngoing = false;
+  std::atomic_bool mIsPreloadingOngoing = false;
 
   ChreConnection *mConnection;
   std::string mConfigPath;
diff --git a/host/common/include/chre_host/socket_server.h b/host/common/include/chre_host/socket_server.h
index 6a19ecb..6e5b840 100644
--- a/host/common/include/chre_host/socket_server.h
+++ b/host/common/include/chre_host/socket_server.h
@@ -28,8 +28,7 @@
 #include <android-base/macros.h>
 #include <cutils/sockets.h>
 
-namespace android {
-namespace chre {
+namespace android::chre {
 
 class SocketServer {
  public:
@@ -81,7 +80,7 @@
    */
   bool sendToClientById(const void *data, size_t length, uint16_t clientId);
 
-  void shutdownServer() {
+  static void shutdownServer() {
     sSignalReceived = true;
   }
 
@@ -93,8 +92,18 @@
       static_cast<int>(kMaxActiveClients);
   static constexpr size_t kMaxPacketSize = 1024 * 1024;
 
+  // This is the same value as defined in
+  // host/hal_generic/common/hal_client_id.h. It is redefined here to avoid
+  // adding dependency path at multiple places for such a temporary change,
+  // which will be removed after migrating generic HAL to multiclient HAL.
+  static constexpr uint16_t kMaxHalClientId = 0x1ff;
+
   int mSockFd = INVALID_SOCKET;
-  uint16_t mNextClientId = 1;
+  // Socket client id and Hal client id are using the same field in the fbs
+  // message. To keep their id range disjoint enables message routing for both
+  // at the same time. There are 0xffff - 0x01ff = 0xfe00 (65024) socket
+  // client ids to use, which should be more than enough.
+  uint16_t mNextClientId = kMaxHalClientId + 1;
   // TODO: std::vector-ify this
   struct pollfd mPollFds[1 + kMaxActiveClients] = {};
 
@@ -116,16 +125,19 @@
   ClientMessageCallback mClientMessageCallback;
 
   void acceptClientConnection();
+
   void disconnectClient(int clientSocket);
+
   void handleClientData(int clientSocket);
+
   bool sendToClientSocket(const void *data, size_t length, int clientSocket,
                           uint16_t clientId);
+
   void serviceSocket();
 
   static std::atomic<bool> sSignalReceived;
 };
 
-}  // namespace chre
-}  // namespace android
+}  // namespace android::chre
 
 #endif  // CHRE_HOST_SOCKET_SERVER_H_
diff --git a/host/common/include/chre_host/time_syncer.h b/host/common/include/chre_host/time_syncer.h
index 9518ea2..f73e8ec 100644
--- a/host/common/include/chre_host/time_syncer.h
+++ b/host/common/include/chre_host/time_syncer.h
@@ -26,11 +26,9 @@
 
 using hardware::contexthub::common::implementation::ChreConnection;
 
-/** The class synchronizes time between the Context hub and Android. */
-class TimeSyncer {
+/** The functions synchronizing time between the Context hub and Android. */
+class TimeSyncer final {
  public:
-  explicit TimeSyncer(ChreConnection *connection) : mConnection(connection) {}
-
   /**
    * Sends time sync message to Context hub and retries numRetries times until
    * success.
@@ -40,7 +38,9 @@
    *
    * @return true if success, false otherwise.
    */
-  bool sendTimeSyncWithRetry(size_t numOfRetries, useconds_t retryDelayUs);
+  static bool sendTimeSyncWithRetry(ChreConnection *connection,
+                                    size_t numOfRetries,
+                                    useconds_t retryDelayUs);
 
   /**
    * Sends a time sync message to Context hub for once.
@@ -50,10 +50,10 @@
    *
    * @return true if success, false otherwise.
    */
-  bool sendTimeSync();
+  static bool sendTimeSync(ChreConnection *connection);
 
  private:
-  ChreConnection *mConnection;
+  TimeSyncer() = default;
 };
 
 }  // namespace android::chre
diff --git a/host/common/log.cc b/host/common/log.cc
new file mode 100644
index 0000000..8f3249f
--- /dev/null
+++ b/host/common/log.cc
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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 <cstdio>
+
+#include "chre_host/log.h"
+
+namespace android::chre {
+
+void outputHostLog(int priority, FILE *stream, const char *format,
+                   const char *func, unsigned int line, ...) {
+  va_list args;
+  va_start(args, line);
+  LOG_PRI_VA(priority, LOG_TAG, format, args);
+  va_end(args);
+  va_start(args, line);
+  fprintf(stream, "%s:%d: ", func, line);
+  vfprintf(stream, format, args);
+  fprintf(stream, "\n");
+  fflush(stream);  // flush the buffer to print out the log immediately
+  va_end(args);
+}
+
+}  // namespace android::chre
\ No newline at end of file
diff --git a/host/common/metrics_reporter.cc b/host/common/metrics_reporter.cc
new file mode 100644
index 0000000..490c80a
--- /dev/null
+++ b/host/common/metrics_reporter.cc
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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 "chre_host/metrics_reporter.h"
+
+#include <android/binder_manager.h>
+#include <chre_atoms_log.h>
+#include <chre_host/log.h>
+#include <limits>
+#include <mutex>
+
+namespace android::chre {
+
+using ::aidl::android::frameworks::stats::IStats;
+using ::aidl::android::frameworks::stats::VendorAtom;
+using ::aidl::android::frameworks::stats::VendorAtomValue;
+using ::android::chre::Atoms::CHRE_AP_WAKE_UP_OCCURRED;
+using ::android::chre::Atoms::CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED;
+using ::android::chre::Atoms::CHRE_HAL_NANOAPP_LOAD_FAILED;
+using ::android::chre::Atoms::CHRE_PAL_OPEN_FAILED;
+using ::android::chre::Atoms::ChreHalNanoappLoadFailed;
+using ::android::chre::Atoms::ChrePalOpenFailed;
+
+std::shared_ptr<IStats> MetricsReporter::getStatsService() {
+  const std::string statsServiceName =
+      std::string(IStats::descriptor).append("/default");
+  if (!AServiceManager_isDeclared(statsServiceName.c_str())) {
+    LOGE("Stats service is not declared.");
+    return nullptr;
+  }
+
+  ndk::SpAIBinder statsServiceBinder =
+      ndk::SpAIBinder(AServiceManager_waitForService(statsServiceName.c_str()));
+  if (statsServiceBinder.get() == nullptr) {
+    LOGE("Failed to get the IStats service binder");
+    return nullptr;
+  }
+
+  binder_status_t status = AIBinder_linkToDeath(
+      statsServiceBinder.get(), AIBinder_DeathRecipient_new([](void *cookie) {
+        MetricsReporter *metricsReporter =
+            static_cast<MetricsReporter *>(cookie);
+        metricsReporter->onBinderDied();
+      }),
+      this);
+  if (status != STATUS_OK) {
+    LOGE("Failed to link to death the stats service binder");
+    return nullptr;
+  }
+
+  std::shared_ptr<IStats> statsService = IStats::fromBinder(statsServiceBinder);
+  if (statsService == nullptr) {
+    LOGE("Failed to get IStats service");
+    return nullptr;
+  }
+  return statsService;
+}
+
+bool MetricsReporter::reportMetric(const VendorAtom &atom) {
+  ndk::ScopedAStatus ret;
+  {
+    std::lock_guard<std::mutex> lock(mStatsServiceMutex);
+    if (mStatsService == nullptr) {
+      mStatsService = getStatsService();
+      if (mStatsService == nullptr) {
+        return false;
+      }
+    }
+
+    ret = mStatsService->reportVendorAtom(atom);
+  }
+
+  if (!ret.isOk()) {
+    LOGE("Failed to report vendor atom");
+  }
+  return ret.isOk();
+}
+
+bool MetricsReporter::logApWakeupOccurred(uint64_t nanoappId) {
+  std::vector<VendorAtomValue> values(1);
+  values[0].set<VendorAtomValue::longValue>(nanoappId);
+
+  const VendorAtom atom{
+      .atomId = CHRE_AP_WAKE_UP_OCCURRED,
+      .values{std::move(values)},
+  };
+
+  return reportMetric(atom);
+}
+
+bool MetricsReporter::logNanoappLoadFailed(
+    uint64_t nanoappId, ChreHalNanoappLoadFailed::Type type,
+    ChreHalNanoappLoadFailed::Reason reason) {
+  std::vector<VendorAtomValue> values(3);
+  values[0].set<VendorAtomValue::longValue>(nanoappId);
+  values[1].set<VendorAtomValue::intValue>(type);
+  values[2].set<VendorAtomValue::intValue>(reason);
+
+  const VendorAtom atom{
+      .atomId = CHRE_HAL_NANOAPP_LOAD_FAILED,
+      .values{std::move(values)},
+  };
+
+  return reportMetric(atom);
+}
+
+bool MetricsReporter::logPalOpenFailed(ChrePalOpenFailed::ChrePalType pal,
+                                       ChrePalOpenFailed::Type type) {
+  std::vector<VendorAtomValue> values(2);
+  values[0].set<VendorAtomValue::intValue>(pal);
+  values[1].set<VendorAtomValue::intValue>(type);
+
+  const VendorAtom atom{
+      .atomId = CHRE_PAL_OPEN_FAILED,
+      .values{std::move(values)},
+  };
+
+  return reportMetric(atom);
+}
+
+bool MetricsReporter::logEventQueueSnapshotReported(
+    int32_t snapshotChreGetTimeMs, int32_t maxEventQueueSize,
+    int32_t meanEventQueueSize, int32_t numDroppedEvents) {
+  std::vector<VendorAtomValue> values(6);
+  values[0].set<VendorAtomValue::intValue>(snapshotChreGetTimeMs);
+  values[1].set<VendorAtomValue::intValue>(maxEventQueueSize);
+  values[2].set<VendorAtomValue::intValue>(meanEventQueueSize);
+  values[3].set<VendorAtomValue::intValue>(numDroppedEvents);
+
+  // TODO(b/298459533): Implement these two values
+  // Last two values are not currently populated and will be implemented
+  // later. To avoid confusion of the interpretation, we use UINT32_MAX
+  // as a placeholder value.
+  values[4].set<VendorAtomValue::longValue>(
+      std::numeric_limits<int64_t>::max());
+  values[5].set<VendorAtomValue::longValue>(
+      std::numeric_limits<int64_t>::max());
+
+  const VendorAtom atom{
+      .atomId = CHRE_EVENT_QUEUE_SNAPSHOT_REPORTED,
+      .values{std::move(values)},
+  };
+
+  return reportMetric(atom);
+}
+
+void MetricsReporter::onBinderDied() {
+  LOGI("MetricsReporter: stats service died - reconnecting");
+
+  std::lock_guard<std::mutex> lock(mStatsServiceMutex);
+  mStatsService.reset();
+  mStatsService = getStatsService();
+}
+
+}  // namespace android::chre
diff --git a/host/common/pigweed/hal_channel_output.cc b/host/common/pigweed/hal_channel_output.cc
new file mode 100644
index 0000000..f61d719
--- /dev/null
+++ b/host/common/pigweed/hal_channel_output.cc
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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 "chre_host/pigweed/hal_channel_output.h"
+
+#include <cstdint>
+
+#include "chre/event.h"
+#include "chre/util/pigweed/rpc_common.h"
+#include "chre_host/host_protocol_host.h"
+#include "chre_host/log.h"
+#include "chre_host/socket_client.h"
+#include "pw_rpc/channel.h"
+#include "pw_span/span.h"
+
+namespace android::chre {
+
+using ::flatbuffers::FlatBufferBuilder;
+
+size_t HalChannelOutput::MaximumTransmissionUnit() {
+  return mMaxMessageLen > kFlatBufferPadding
+             ? mMaxMessageLen - kFlatBufferPadding
+             : 0;
+}
+
+pw::Status HalChannelOutput::Send(pw::span<const std::byte> buffer) {
+  if (buffer.size() > 0) {
+    FlatBufferBuilder builder(buffer.size() + kFlatBufferPadding);
+
+    HostProtocolHost::encodeNanoappMessage(
+        builder, mServerNanoappId, CHRE_MESSAGE_TYPE_RPC, mHostEndpointId,
+        buffer.data(), buffer.size());
+
+    if (!mSocketClient.sendMessage(builder.GetBufferPointer(),
+                                   builder.GetSize())) {
+      LOGE("Failed to send message");
+      return PW_STATUS_UNKNOWN;
+    }
+  }
+
+  return PW_STATUS_OK;
+}
+
+}  // namespace android::chre
\ No newline at end of file
diff --git a/host/common/pigweed/hal_rpc_client.cc b/host/common/pigweed/hal_rpc_client.cc
new file mode 100644
index 0000000..d9a9146
--- /dev/null
+++ b/host/common/pigweed/hal_rpc_client.cc
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 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 "chre_host/pigweed/hal_rpc_client.h"
+
+#include <cstdint>
+#include <memory>
+
+#include "chre/event.h"
+#include "chre/util/pigweed/rpc_common.h"
+#include "chre_host/host_protocol_host.h"
+#include "chre_host/log.h"
+#include "chre_host/pigweed/hal_channel_output.h"
+
+namespace android::chre {
+
+using ::chre::fbs::HubInfoResponseT;
+using ::chre::fbs::NanoappListEntryT;
+using ::chre::fbs::NanoappListResponseT;
+using ::chre::fbs::NanoappMessageT;
+using ::chre::fbs::NanoappRpcServiceT;
+using ::flatbuffers::FlatBufferBuilder;
+
+std::unique_ptr<HalRpcClient> HalRpcClient::createClient(
+    std::string_view appName, SocketClient &client,
+    sp<SocketClient::ICallbacks> socketCallbacks, uint16_t hostEndpointId,
+    uint64_t serverNanoappId) {
+  auto rpcClient = std::unique_ptr<HalRpcClient>(
+      new HalRpcClient(appName, client, hostEndpointId, serverNanoappId));
+
+  if (!rpcClient->init(socketCallbacks)) {
+    return nullptr;
+  }
+
+  return rpcClient;
+}
+
+bool HalRpcClient::hasService(uint64_t id, uint32_t version) {
+  std::lock_guard lock(mNanoappMutex);
+  for (const NanoappRpcServiceT &service : mServices) {
+    if (service.id == id && service.version == version) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+void HalRpcClient::close() {
+  if (mIsChannelOpened) {
+    mRpcClient.CloseChannel(GetChannelId());
+    mIsChannelOpened = false;
+  }
+  if (mSocketClient.isConnected()) {
+    notifyEndpointDisconnected();
+    mSocketClient.disconnect();
+  }
+}
+
+// Private methods
+
+bool HalRpcClient::init(sp<SocketClient::ICallbacks> socketCallbacks) {
+  if (mSocketClient.isConnected()) {
+    LOGE("Already connected to socket");
+    return false;
+  }
+
+  auto callbacks = sp<Callbacks>::make(this, socketCallbacks);
+
+  if (!mSocketClient.connect("chre", callbacks)) {
+    LOGE("Couldn't connect to socket");
+    return false;
+  }
+
+  bool success = true;
+
+  if (!notifyEndpointConnected()) {
+    LOGE("Failed to notify connection");
+    success = false;
+  } else if (!retrieveMaxMessageLen()) {
+    LOGE("Failed to retrieve the max message length");
+    success = false;
+  } else if (!retrieveServices()) {
+    LOGE("Failed to retrieve the services");
+    success = false;
+  }
+
+  if (!success) {
+    mSocketClient.disconnect();
+    return false;
+  }
+
+  {
+    std::lock_guard lock(mHubInfoMutex);
+    mChannelOutput = std::make_unique<HalChannelOutput>(
+        mSocketClient, mHostEndpointId, mServerNanoappId, mMaxMessageLen);
+  }
+
+  return true;
+}
+
+bool HalRpcClient::notifyEndpointConnected() {
+  FlatBufferBuilder builder(64);
+  HostProtocolHost::encodeHostEndpointConnected(
+      builder, mHostEndpointId, CHRE_HOST_ENDPOINT_TYPE_NATIVE, mAppName,
+      /* attributionTag= */ "");
+  return mSocketClient.sendMessage(builder.GetBufferPointer(),
+                                   builder.GetSize());
+}
+
+bool HalRpcClient::notifyEndpointDisconnected() {
+  FlatBufferBuilder builder(64);
+  HostProtocolHost::encodeHostEndpointDisconnected(builder, mHostEndpointId);
+  return mSocketClient.sendMessage(builder.GetBufferPointer(),
+                                   builder.GetSize());
+}
+
+bool HalRpcClient::retrieveMaxMessageLen() {
+  FlatBufferBuilder builder(64);
+  HostProtocolHost::encodeHubInfoRequest(builder);
+  if (!mSocketClient.sendMessage(builder.GetBufferPointer(),
+                                 builder.GetSize())) {
+    return false;
+  }
+
+  std::unique_lock lock(mHubInfoMutex);
+  std::cv_status status = mHubInfoCond.wait_for(lock, kRequestTimeout);
+
+  return status != std::cv_status::timeout;
+}
+
+bool HalRpcClient::retrieveServices() {
+  FlatBufferBuilder builder(64);
+  HostProtocolHost::encodeNanoappListRequest(builder);
+
+  if (!mSocketClient.sendMessage(builder.GetBufferPointer(),
+                                 builder.GetSize())) {
+    return false;
+  }
+
+  std::unique_lock lock(mNanoappMutex);
+  std::cv_status status = mNanoappCond.wait_for(lock, kRequestTimeout);
+
+  return status != std::cv_status::timeout;
+}
+
+// Socket callbacks.
+
+void HalRpcClient::Callbacks::onMessageReceived(const void *data,
+                                                size_t length) {
+  if (!android::chre::HostProtocolHost::decodeMessageFromChre(data, length,
+                                                              *this)) {
+    LOGE("Failed to decode message");
+  }
+  mSocketCallbacks->onMessageReceived(data, length);
+}
+
+void HalRpcClient::Callbacks::onConnected() {
+  mSocketCallbacks->onConnected();
+}
+
+void HalRpcClient::Callbacks::onConnectionAborted() {
+  mSocketCallbacks->onConnectionAborted();
+}
+
+void HalRpcClient::Callbacks::onDisconnected() {
+  // Close connections on CHRE reset.
+  mClient->close();
+  mSocketCallbacks->onDisconnected();
+}
+
+// Message handlers.
+
+void HalRpcClient::Callbacks::handleNanoappMessage(
+    const NanoappMessageT &message) {
+  if (message.message_type == CHRE_MESSAGE_TYPE_RPC) {
+    pw::span packet(reinterpret_cast<const std::byte *>(message.message.data()),
+                    message.message.size());
+
+    if (message.app_id == mClient->mServerNanoappId) {
+      pw::Status status = mClient->mRpcClient.ProcessPacket(packet);
+      if (status != pw::OkStatus()) {
+        LOGE("Failed to process the packet");
+      }
+    }
+  }
+}
+
+void HalRpcClient::Callbacks::handleHubInfoResponse(
+    const HubInfoResponseT &response) {
+  {
+    std::lock_guard lock(mClient->mHubInfoMutex);
+    mClient->mMaxMessageLen = response.max_msg_len;
+  }
+  mClient->mHubInfoCond.notify_all();
+}
+
+void HalRpcClient::Callbacks::handleNanoappListResponse(
+    const NanoappListResponseT &response) {
+  for (const std::unique_ptr<NanoappListEntryT> &nanoapp : response.nanoapps) {
+    if (nanoapp->app_id == mClient->mServerNanoappId) {
+      std::lock_guard lock(mClient->mNanoappMutex);
+      mClient->mServices.clear();
+      mClient->mServices.reserve(nanoapp->rpc_services.size());
+      for (const std::unique_ptr<NanoappRpcServiceT> &service :
+           nanoapp->rpc_services) {
+        mClient->mServices.push_back(*service);
+      }
+    }
+  }
+
+  mClient->mNanoappCond.notify_all();
+}
+
+}  // namespace android::chre
\ No newline at end of file
diff --git a/host/common/preloaded_nanoapp_loader.cc b/host/common/preloaded_nanoapp_loader.cc
index b6fb892..6739d5d 100644
--- a/host/common/preloaded_nanoapp_loader.cc
+++ b/host/common/preloaded_nanoapp_loader.cc
@@ -28,82 +28,108 @@
 using android::chre::readFileContents;
 using android::hardware::contexthub::common::implementation::kHalId;
 
+namespace {
+
+bool getNanoappHeaderFromFile(const char *headerFileName,
+                              std::vector<uint8_t> &headerBuffer) {
+  if (!readFileContents(headerFileName, headerBuffer)) {
+    LOGE("Failed to read header file for nanoapp %s", headerFileName);
+    return false;
+  }
+  if (headerBuffer.size() != sizeof(NanoAppBinaryHeader)) {
+    LOGE("Nanoapp binary's header size is incorrect");
+    return false;
+  }
+  return true;
+}
+
+inline bool shouldSkipNanoapp(
+    std::optional<const std::unordered_set<uint64_t>> selectedNanoappIds,
+    uint64_t appId) {
+  return selectedNanoappIds.has_value() &&
+         selectedNanoappIds->find(appId) == selectedNanoappIds->end();
+}
+}  // namespace
+
 void PreloadedNanoappLoader::getPreloadedNanoappIds(
     std::vector<uint64_t> &out_preloadedNanoappIds) {
   std::vector<std::string> nanoappNames;
   std::string directory;
   out_preloadedNanoappIds.clear();
-  bool success =
-      getPreloadedNanoappsFromConfigFile(mConfigPath, directory, nanoappNames);
-  if (!success) {
+  if (!getPreloadedNanoappsFromConfigFile(mConfigPath, directory,
+                                          nanoappNames)) {
     LOGE("Failed to parse preloaded nanoapps config file");
   }
   for (const std::string &nanoappName : nanoappNames) {
-    std::string headerFile = directory + "/" + nanoappName + ".napp_header";
+    std::string headerFileName = directory + "/" + nanoappName + ".napp_header";
     std::vector<uint8_t> headerBuffer;
-    if (!readFileContents(headerFile.c_str(), headerBuffer)) {
-      LOGE("Cannot read header file: %s", headerFile.c_str());
+    if (!getNanoappHeaderFromFile(headerFileName.c_str(), headerBuffer)) {
+      LOGE("Failed to parse the nanoapp header for %s", headerFileName.c_str());
       continue;
     }
-    if (headerBuffer.size() != sizeof(NanoAppBinaryHeader)) {
-      LOGE("Header size mismatch");
-      continue;
-    }
-    const auto *appHeader =
+    auto header =
         reinterpret_cast<const NanoAppBinaryHeader *>(headerBuffer.data());
-    out_preloadedNanoappIds.emplace_back(appHeader->appId);
+    out_preloadedNanoappIds.emplace_back(header->appId);
   }
 }
 
-void PreloadedNanoappLoader::loadPreloadedNanoapps() {
+bool PreloadedNanoappLoader::loadPreloadedNanoapps(
+    const std::optional<const std::unordered_set<uint64_t>>
+        &selectedNanoappIds) {
   std::string directory;
   std::vector<std::string> nanoapps;
-
-  bool success =
-      getPreloadedNanoappsFromConfigFile(mConfigPath, directory, nanoapps);
-  if (!success) {
+  if (!getPreloadedNanoappsFromConfigFile(mConfigPath, directory, nanoapps)) {
     LOGE("Failed to load any preloaded nanoapp");
-  } else {
-    mIsPreloadingOngoing = true;
-    for (uint32_t i = 0; i < nanoapps.size(); ++i) {
-      loadPreloadedNanoapp(directory, nanoapps[i], i);
-    }
-    mIsPreloadingOngoing = false;
-  }
-}
-
-void PreloadedNanoappLoader::loadPreloadedNanoapp(const std::string &directory,
-                                                  const std::string &name,
-                                                  uint32_t transactionId) {
-  std::vector<uint8_t> headerBuffer;
-  std::vector<uint8_t> nanoappBuffer;
-
-  std::string headerFilename = directory + "/" + name + ".napp_header";
-  std::string nanoappFilename = directory + "/" + name + ".so";
-
-  if (!readFileContents(headerFilename.c_str(), headerBuffer) ||
-      !readFileContents(nanoappFilename.c_str(), nanoappBuffer) ||
-      !loadNanoapp(headerBuffer, nanoappBuffer, transactionId)) {
-    LOGE("Failed to load nanoapp: '%s'", name.c_str());
-  }
-}
-
-bool PreloadedNanoappLoader::loadNanoapp(const std::vector<uint8_t> &header,
-                                         const std::vector<uint8_t> &nanoapp,
-                                         uint32_t transactionId) {
-  if (header.size() != sizeof(NanoAppBinaryHeader)) {
-    LOGE("Nanoapp binary's header size is incorrect");
     return false;
   }
-  const auto *appHeader =
-      reinterpret_cast<const NanoAppBinaryHeader *>(header.data());
+  if (mIsPreloadingOngoing.exchange(true)) {
+    LOGE("Preloading is ongoing. A new request shouldn't happen.");
+    return false;
+  }
+  bool success = true;
+  for (uint32_t i = 0; i < nanoapps.size(); ++i) {
+    std::string headerFilename = directory + "/" + nanoapps[i] + ".napp_header";
+    std::string nanoappFilename = directory + "/" + nanoapps[i] + ".so";
+    // parse the header
+    std::vector<uint8_t> headerBuffer;
+    if (!getNanoappHeaderFromFile(headerFilename.c_str(), headerBuffer)) {
+      LOGE("Failed to parse the nanoapp header for %s",
+           nanoappFilename.c_str());
+      success = false;
+      continue;
+    }
+    const auto header =
+        reinterpret_cast<const NanoAppBinaryHeader *>(headerBuffer.data());
+    // check if the app should be skipped
+    if (shouldSkipNanoapp(selectedNanoappIds, header->appId)) {
+      LOGI("Loading of %s is skipped.", headerFilename.c_str());
+      continue;
+    }
+    // load the binary
+    if (!loadNanoapp(header, nanoappFilename, i)) {
+      success = false;
+    }
+  }
+  mIsPreloadingOngoing.store(false);
+  return success;
+}
 
+bool PreloadedNanoappLoader::loadNanoapp(const NanoAppBinaryHeader *appHeader,
+                                         const std::string &nanoappFileName,
+                                         uint32_t transactionId) {
+  // parse the binary
+  std::vector<uint8_t> nanoappBuffer;
+  if (!readFileContents(nanoappFileName.c_str(), nanoappBuffer)) {
+    LOGE("Unable to read %s.", nanoappFileName.c_str());
+    return false;
+  }
   // Build the target API version from major and minor.
   uint32_t targetApiVersion = (appHeader->targetChreApiMajorVersion << 24) |
                               (appHeader->targetChreApiMinorVersion << 16);
   return sendFragmentedLoadAndWaitForEachResponse(
       appHeader->appId, appHeader->appVersion, appHeader->flags,
-      targetApiVersion, nanoapp.data(), nanoapp.size(), transactionId);
+      targetApiVersion, nanoappBuffer.data(), nanoappBuffer.size(),
+      transactionId);
 }
 
 bool PreloadedNanoappLoader::sendFragmentedLoadAndWaitForEachResponse(
diff --git a/host/common/socket_client.cc b/host/common/socket_client.cc
index a8c18d9..cf17207 100644
--- a/host/common/socket_client.cc
+++ b/host/common/socket_client.cc
@@ -177,6 +177,10 @@
       ssize_t bytesReceived = recv(mSockFd, buffer, sizeof(buffer), 0);
       if (bytesReceived < 0) {
         LOG_ERROR("Exiting RX thread", errno);
+        if (!mGracefulShutdown) {
+          LOGI("Force onDisconnected");
+          mCallbacks->onDisconnected();
+        }
         break;
       } else if (bytesReceived == 0) {
         if (!mGracefulShutdown) {
diff --git a/host/common/socket_server.cc b/host/common/socket_server.cc
index e5403a2..1c2681a 100644
--- a/host/common/socket_server.cc
+++ b/host/common/socket_server.cc
@@ -19,6 +19,7 @@
 #include <poll.h>
 
 #include <cassert>
+#include <cerrno>
 #include <cinttypes>
 #include <csignal>
 #include <cstdlib>
@@ -172,6 +173,9 @@
   if (packetSize < 0) {
     LOGE("Couldn't get packet from client %" PRIu16 ": %s", clientId,
          strerror(errno));
+    if (ENOTCONN == errno) {
+      disconnectClient(clientSocket);
+    }
   } else if (packetSize == 0) {
     LOGI("Client %" PRIu16 " disconnected", clientId);
     disconnectClient(clientSocket);
diff --git a/host/common/test/chre_test_client.cc b/host/common/test/chre_test_client.cc
index 57da705..397c66c 100644
--- a/host/common/test/chre_test_client.cc
+++ b/host/common/test/chre_test_client.cc
@@ -14,6 +14,21 @@
  * limitations under the License.
  */
 
+#include <inttypes.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <chrono>
+#include <condition_variable>
+#include <fstream>
+#include <future>
+#include <mutex>
+#include <sstream>
+#include <thread>
+
+#include <cutils/sockets.h>
+#include <utils/StrongPointer.h>
+
 #include "chre/util/nanoapp/app_id.h"
 #include "chre/util/system/napp_header_utils.h"
 #include "chre_host/file_stream.h"
@@ -22,18 +37,6 @@
 #include "chre_host/napp_header.h"
 #include "chre_host/socket_client.h"
 
-#include <inttypes.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-
-#include <fstream>
-#include <future>
-#include <sstream>
-#include <thread>
-
-#include <cutils/sockets.h>
-#include <utils/StrongPointer.h>
-
 /**
  * @file
  * A test utility that connects to the CHRE daemon that runs on the apps
@@ -47,6 +50,7 @@
  */
 
 using android::sp;
+using android::chre::FragmentedLoadRequest;
 using android::chre::FragmentedLoadTransaction;
 using android::chre::getStringFromByteVector;
 using android::chre::HostProtocolHost;
@@ -62,15 +66,34 @@
 
 namespace {
 
-//! The host endpoint we use when sending; set to CHRE_HOST_ENDPOINT_UNSPECIFIED
-//! Other clients below the HAL may use a value above 0x8000 to enable unicast
-//! messaging (currently requires internal coordination to avoid conflict;
-//! in the future these should be assigned by the daemon).
-constexpr uint16_t kHostEndpoint = 0xfffe;
+//! The host endpoint we use when sending; Clients may use a value above
+//! 0x8000 to enable unicast messaging (currently requires internal coordination
+//! to avoid conflict).
+constexpr uint16_t kHostEndpoint = 0x8002;
 
 constexpr uint32_t kDefaultAppVersion = 1;
 constexpr uint32_t kDefaultApiVersion = 0x01000000;
 
+// Timeout for loading a nanoapp fragment.
+static constexpr auto kFragmentTimeout = std::chrono::milliseconds(2000);
+
+enum class LoadingStatus {
+  kLoading,
+  kSuccess,
+  kError,
+};
+
+// State of a nanoapp fragment.
+struct FragmentStatus {
+  size_t id;
+  LoadingStatus loadStatus;
+};
+
+// State of the current nanoapp fragment.
+std::mutex gFragmentMutex;
+std::condition_variable gFragmentCondVar;
+FragmentStatus gFragmentStatus;
+
 class SocketCallbacks : public SocketClient::ICallbacks,
                         public IChreMessageHandlers {
  public:
@@ -127,8 +150,21 @@
 
   void handleLoadNanoappResponse(
       const fbs::LoadNanoappResponseT &response) override {
-    LOGI("Got load nanoapp response, transaction ID 0x%" PRIx32 " result %d",
-         response.transaction_id, response.success);
+    LOGI("Got load nanoapp response, transaction ID 0x%" PRIx32
+         " fragment %" PRIx32 " result %d",
+         response.transaction_id, response.fragment_id, response.success);
+
+    {
+      std::lock_guard lock(gFragmentMutex);
+      if (response.fragment_id != gFragmentStatus.id) {
+        gFragmentStatus.loadStatus = LoadingStatus::kError;
+      } else {
+        gFragmentStatus.loadStatus =
+            response.success ? LoadingStatus::kSuccess : LoadingStatus::kError;
+      }
+    }
+
+    gFragmentCondVar.notify_all();
   }
 
   void handleUnloadNanoappResponse(
@@ -187,20 +223,48 @@
 void sendNanoappLoad(SocketClient &client, uint64_t appId, uint32_t appVersion,
                      uint32_t apiVersion, uint32_t appFlags,
                      const std::vector<uint8_t> &binary) {
-  // Perform loading with 1 fragment for simplicity
-  FlatBufferBuilder builder(binary.size() + 128);
   FragmentedLoadTransaction transaction = FragmentedLoadTransaction(
-      1 /* transactionId */, appId, appVersion, appFlags, apiVersion, binary,
-      binary.size() /* fragmentSize */);
-  HostProtocolHost::encodeFragmentedLoadNanoappRequest(
-      builder, transaction.getNextRequest());
+      1 /* transactionId */, appId, appVersion, appFlags, apiVersion, binary);
 
-  LOGI("Sending load nanoapp request (%" PRIu32
-       " bytes total w/%zu bytes of "
-       "payload)",
-       builder.GetSize(), binary.size());
-  if (!client.sendMessage(builder.GetBufferPointer(), builder.GetSize())) {
-    LOGE("Failed to send message");
+  bool success = true;
+  while (!transaction.isComplete()) {
+    const FragmentedLoadRequest &request = transaction.getNextRequest();
+    LOGI("Loading nanoapp fragment %zu", request.fragmentId);
+
+    FlatBufferBuilder builder(request.binary.size() + 128);
+    HostProtocolHost::encodeFragmentedLoadNanoappRequest(builder, request);
+
+    std::unique_lock lock(gFragmentMutex);
+    gFragmentStatus = {.id = request.fragmentId,
+                       .loadStatus = LoadingStatus::kLoading};
+    lock.unlock();
+
+    if (!client.sendMessage(builder.GetBufferPointer(), builder.GetSize())) {
+      LOGE("Failed to send fragment");
+      success = false;
+      break;
+    }
+
+    lock.lock();
+    std::cv_status status = gFragmentCondVar.wait_for(lock, kFragmentTimeout);
+
+    if (status == std::cv_status::timeout) {
+      success = false;
+      LOGE("Timeout loading the fragment");
+      break;
+    }
+
+    if (gFragmentStatus.loadStatus != LoadingStatus::kSuccess) {
+      LOGE("Error loading the fragment");
+      success = false;
+      break;
+    }
+  }
+
+  if (success) {
+    LOGI("Nanoapp loaded successfully");
+  } else {
+    LOGE("Error loading the nanoapp");
   }
 }
 
diff --git a/host/common/test/chre_test_rpc.cc b/host/common/test/chre_test_rpc.cc
new file mode 100644
index 0000000..64ad98c
--- /dev/null
+++ b/host/common/test/chre_test_rpc.cc
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 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 <inttypes.h>
+#include <utils/StrongPointer.h>
+#include <chrono>
+#include <cstdint>
+#include <future>
+#include <thread>
+
+#include "chre/util/nanoapp/app_id.h"
+#include "chre_host/host_protocol_host.h"
+#include "chre_host/log.h"
+#include "chre_host/pigweed/hal_rpc_client.h"
+#include "chre_host/socket_client.h"
+#include "rpc_world.pb.h"
+#include "rpc_world.rpc.pb.h"
+
+/**
+ * @file
+ * Test RPC by calling a service provided by the rpc_world nanoapp.
+ *
+ * Usage:
+ * 1. Compile and push the rpc_world nanoapp to the device.
+ *
+ * 2. Load the nanoapp:
+ *    adb shell chre_test_client load_with_header \
+ *      /vendor/etc/chre/rpc_world.napp_header \
+ *      /vendor/etc/chre/rpc_world.so
+ *
+ * 3. Build this test and push it to the device:
+ *    m chre_test_rpc
+ *    adb push \
+ *      out/target/product/<product>/vendor/bin/chre_test_rpc \
+ *      /vendor/bin/chre_test_rpc
+ *
+ * 4. Launch the test:
+ *    adb shell chre_test_rpc
+ */
+
+namespace {
+
+using ::android::sp;
+using ::android::chre::HalRpcClient;
+using ::android::chre::HostProtocolHost;
+using ::android::chre::IChreMessageHandlers;
+using ::android::chre::SocketClient;
+using ::flatbuffers::FlatBufferBuilder;
+
+// Aliased for consistency with the way these symbols are referenced in
+// CHRE-side code
+namespace fbs = ::chre::fbs;
+
+constexpr uint16_t kHostEndpoint = 0x8006;
+
+constexpr uint32_t kRequestNumber = 10;
+std::promise<uint32_t> gResponsePromise;
+
+class SocketCallbacks : public SocketClient::ICallbacks,
+                        public IChreMessageHandlers {
+ public:
+  void onMessageReceived(const void *data, size_t length) override {
+    if (!HostProtocolHost::decodeMessageFromChre(data, length, *this)) {
+      LOGE("Failed to decode message");
+    }
+  }
+
+  void handleNanoappListResponse(
+      const fbs::NanoappListResponseT &response) override {
+    LOGI("Got nanoapp list response with %zu apps", response.nanoapps.size());
+  }
+};
+
+}  // namespace
+
+void incrementResponse(const chre_rpc_NumberMessage &response,
+                       pw::Status status) {
+  if (status.ok()) {
+    gResponsePromise.set_value(response.number);
+  } else {
+    LOGE("Increment failed with status %d", static_cast<int>(status.code()));
+  }
+}
+
+int main(int argc, char *argv[]) {
+  UNUSED_VAR(argc);
+  UNUSED_VAR(argv);
+
+  SocketClient socketClient;
+
+  auto callbacks = sp<SocketCallbacks>::make();
+
+  std::unique_ptr<HalRpcClient> rpcClient =
+      HalRpcClient::createClient("chre_test_rpc", socketClient, callbacks,
+                                 kHostEndpoint, chre::kRpcWorldAppId);
+
+  if (rpcClient == nullptr) {
+    LOGE("Failed to create the RPC client");
+    return -1;
+  }
+
+  if (!rpcClient->hasService(/* id= */ 0xca8f7150a3f05847,
+                             /* version= */ 0x01020034)) {
+    LOGE("RpcWorld service not found");
+    return -1;
+  }
+
+  auto client =
+      rpcClient->get<chre::rpc::pw_rpc::nanopb::RpcWorldService::Client>();
+
+  chre_rpc_NumberMessage incrementRequest;
+  incrementRequest.number = kRequestNumber;
+
+  pw::rpc::NanopbUnaryReceiver<chre_rpc_NumberMessage> call =
+      client->Increment(incrementRequest, incrementResponse);
+
+  if (!call.active()) {
+    LOGE("Failed to call the service");
+    return -1;
+  }
+
+  std::future<uint32_t> response = gResponsePromise.get_future();
+
+  if (response.wait_for(std::chrono::seconds(2)) != std::future_status::ready) {
+    LOGE("No response received from RPC");
+  } else {
+    const uint32_t value = response.get();
+    LOGI("The RPC service says %" PRIu32 " + 1 = %" PRIu32, kRequestNumber,
+         value);
+  }
+
+  rpcClient->close();
+
+  return 0;
+}
diff --git a/host/common/test/power_test/chre_power_test_client.cc b/host/common/test/power_test/chre_power_test_client.cc
index 344944d..1251e99 100644
--- a/host/common/test/power_test/chre_power_test_client.cc
+++ b/host/common/test/power_test/chre_power_test_client.cc
@@ -145,7 +145,10 @@
 
 namespace {
 
-constexpr uint16_t kHostEndpoint = 0xfffd;
+//! The host endpoint we use when sending; Clients may use a value above
+//! 0x8000 to enable unicast messaging (currently requires internal coordination
+//! to avoid conflict).
+constexpr uint16_t kHostEndpoint = 0x8003;
 
 constexpr uint64_t kPowerTestAppId = 0x012345678900000f;
 constexpr uint64_t kPowerTestTcmAppId = 0x0123456789000010;
diff --git a/host/common/time_syncer.cc b/host/common/time_syncer.cc
index e6d2792..41028b9 100644
--- a/host/common/time_syncer.cc
+++ b/host/common/time_syncer.cc
@@ -20,33 +20,33 @@
 
 namespace android::chre {
 // TODO(b/247124878): Can we add a static assert to make sure these functions
-//  are not called when mConnection->isTimeSyncNeeded() returns false?
-bool TimeSyncer::sendTimeSync() {
-  if (!mConnection->isTimeSyncNeeded()) {
+//  are not called when connection->isTimeSyncNeeded() returns false?
+bool TimeSyncer::sendTimeSync(ChreConnection *connection) {
+  if (!connection->isTimeSyncNeeded()) {
     LOGW("Platform doesn't require time sync. Ignore the request.");
     return true;
   }
   int64_t timeOffsetUs = 0;
-  if (!mConnection->getTimeOffset(&timeOffsetUs)) {
+  if (!connection->getTimeOffset(&timeOffsetUs)) {
     LOGE("Failed to get time offset.");
     return false;
   }
   flatbuffers::FlatBufferBuilder builder(64);
   // clientId doesn't matter for time sync request so the default id is used.
   HostProtocolHost::encodeTimeSyncMessage(builder, timeOffsetUs);
-  return mConnection->sendMessage(builder.GetBufferPointer(),
-                                  builder.GetSize());
+  return connection->sendMessage(builder.GetBufferPointer(), builder.GetSize());
 }
 
-bool TimeSyncer::sendTimeSyncWithRetry(size_t numOfRetries,
+bool TimeSyncer::sendTimeSyncWithRetry(ChreConnection *connection,
+                                       size_t numOfRetries,
                                        useconds_t retryDelayUs) {
-  if (!mConnection->isTimeSyncNeeded()) {
+  if (!connection->isTimeSyncNeeded()) {
     LOGW("Platform doesn't require time sync. Ignore the request.");
     return true;
   }
   bool success = false;
   while (!success && (numOfRetries-- > 0)) {
-    success = sendTimeSync();
+    success = sendTimeSync(connection);
     if (!success) {
       usleep(retryDelayUs);
     }
diff --git a/host/hal_generic/Android.bp b/host/hal_generic/Android.bp
index 1a956d7..6b01356 100644
--- a/host/hal_generic/Android.bp
+++ b/host/hal_generic/Android.bp
@@ -28,22 +28,31 @@
 
 cc_binary {
     name: "android.hardware.contexthub-service.generic",
-    defaults: ["hidl_defaults"],
+    defaults: ["chre_aidl_hal_generic_defaults"],
     vendor: true,
     relative_install_path: "hw",
+    srcs: [":hal_aidl_generic_srcs", "aidl/service.cc"],
+    init_rc: ["aidl/android.hardware.contexthub-service.generic.rc"],
+    vintf_fragments: ["aidl/android.hardware.contexthub-service.generic.xml"],
+    visibility: ["//visibility:public"],
+}
+
+filegroup {
+    name: "hal_aidl_generic_srcs",
     srcs: [
         "aidl/generic_context_hub_aidl.cc",
-        "aidl/service.cc",
         "common/hal_chre_socket_connection.cc",
         "common/permissions_util.cc",
     ],
+}
+
+cc_defaults {
+    name: "chre_aidl_hal_generic_defaults",
+    vendor: true,
     include_dirs: [
+        "system/chre/host/hal_generic/common/",
         "system/chre/util/include",
     ],
-    local_include_dirs: [
-        "common/",
-    ],
-    init_rc: ["aidl/android.hardware.contexthub-service.generic.rc"],
     cflags: [
         "-Wall",
         "-Werror",
@@ -51,6 +60,9 @@
         "-DCHRE_HAL_SOCKET_METRICS_ENABLED",
         "-DCHRE_IS_HOST_BUILD",
     ],
+    header_libs: [
+        "chre_api",
+    ],
     shared_libs: [
         "android.frameworks.stats-V1-ndk",
         "libcutils",
@@ -60,17 +72,24 @@
         "libutils",
         "libbase",
         "libbinder_ndk",
-        "android.hardware.contexthub-V2-ndk",
+        "android.hardware.contexthub-V3-ndk",
         "chremetrics-cpp",
         "chre_atoms_log",
-    ],
-    header_libs: [
-        "chre_api",
+        "chre_metrics_reporter",
+        "server_configurable_flags",
     ],
     static_libs: [
         "chre_client",
         "chre_config_util",
         "event_logger",
+        "chre_flags_c_lib",
     ],
-    vintf_fragments: ["aidl/android.hardware.contexthub-service.generic.xml"],
+}
+
+cc_library_headers {
+    name: "chre_aidl_hal_generic",
+    vendor: true,
+    export_include_dirs: [
+        "aidl",
+    ],
 }
diff --git a/host/hal_generic/aidl/android.hardware.contexthub-service.generic.xml b/host/hal_generic/aidl/android.hardware.contexthub-service.generic.xml
index 930f672..2f8ddc8 100644
--- a/host/hal_generic/aidl/android.hardware.contexthub-service.generic.xml
+++ b/host/hal_generic/aidl/android.hardware.contexthub-service.generic.xml
@@ -1,7 +1,7 @@
 <manifest version="1.0" type="device">
     <hal format="aidl">
         <name>android.hardware.contexthub</name>
-        <version>2</version>
+        <version>3</version>
         <interface>
             <name>IContextHub</name>
             <instance>default</instance>
diff --git a/host/hal_generic/aidl/generic_context_hub_aidl.cc b/host/hal_generic/aidl/generic_context_hub_aidl.cc
index dae0d6f..16c5c3c 100644
--- a/host/hal_generic/aidl/generic_context_hub_aidl.cc
+++ b/host/hal_generic/aidl/generic_context_hub_aidl.cc
@@ -51,6 +51,7 @@
 constexpr char kPreloadedNanoappsConfigPath[] =
     "/vendor/etc/chre/preloaded_nanoapps.json";
 constexpr std::chrono::duration kTestModeTimeout = std::chrono::seconds(10);
+constexpr uint16_t kMaxValidHostEndPointId = 0x7fff;
 
 /*
  * The starting transaction ID for internal transactions. We choose
@@ -124,6 +125,9 @@
 
     hub.supportedPermissions = kSupportedPermissions;
 
+    // TODO(b/312417087): Implement reliable message support
+    hub.supportsReliableMessages = false;
+
     out_contextHubInfos->push_back(hub);
   }
 
@@ -296,6 +300,13 @@
   return enable ? enableTestMode() : disableTestMode();
 }
 
+ScopedAStatus ContextHub::sendMessageDeliveryStatusToHub(
+    int32_t /* contextHubId */,
+    const MessageDeliveryStatus & /* messageDeliveryStatus */) {
+  // TODO(b/312417087): Implement reliable message support
+  return ndk::ScopedAStatus::ok();
+}
+
 ScopedAStatus ContextHub::onHostEndpointConnected(
     const HostEndpointInfo &in_info) {
   std::lock_guard<std::mutex> lock(mConnectedHostEndpointsMutex);
@@ -345,6 +356,11 @@
 void ContextHub::onNanoappMessage(const ::chre::fbs::NanoappMessageT &message) {
   std::lock_guard<std::mutex> lock(mCallbackMutex);
   if (mCallback != nullptr) {
+    if (message.host_endpoint > kMaxValidHostEndPointId &&
+        message.host_endpoint != CHRE_HOST_ENDPOINT_BROADCAST) {
+      return;
+    }
+
     mEventLogger.logMessageFromNanoapp(message);
     ContextHubMessage outMessage;
     outMessage.nanoappId = message.app_id;
diff --git a/host/hal_generic/aidl/generic_context_hub_aidl.h b/host/hal_generic/aidl/generic_context_hub_aidl.h
index 9c12c16..024cb1e 100644
--- a/host/hal_generic/aidl/generic_context_hub_aidl.h
+++ b/host/hal_generic/aidl/generic_context_hub_aidl.h
@@ -79,6 +79,9 @@
   ::ndk::ScopedAStatus sendMessageToHub(
       int32_t contextHubId, const ContextHubMessage &message) override;
   ::ndk::ScopedAStatus setTestMode(bool enable) override;
+  ::ndk::ScopedAStatus sendMessageDeliveryStatusToHub(
+      int32_t contextHubId,
+      const MessageDeliveryStatus &messageDeliveryStatus) override;
   ::ndk::ScopedAStatus onHostEndpointConnected(
       const HostEndpointInfo &in_info) override;
   ::ndk::ScopedAStatus onHostEndpointDisconnected(
diff --git a/host/hal_generic/common/hal_chre_socket_connection.cc b/host/hal_generic/common/hal_chre_socket_connection.cc
index b113e44..432a36e 100644
--- a/host/hal_generic/common/hal_chre_socket_connection.cc
+++ b/host/hal_generic/common/hal_chre_socket_connection.cc
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+// TODO(b/298459533): remove_ap_wakeup_metric_report_limit ramp up -> remove old
+// code
+
 #define LOG_TAG "ContextHubHal"
 #define LOG_NDEBUG 1
 
@@ -22,8 +25,13 @@
 #include <log/log.h>
 
 #ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
+// TODO(b/298459533): Remove these when the flag_log_nanoapp_load_metrics flag
+// is cleaned up
 #include <aidl/android/frameworks/stats/IStats.h>
 #include <android/binder_manager.h>
+#include <android_chre_flags.h>
+// TODO(b/298459533): Remove end
+
 #include <chre_atoms_log.h>
 #include <utils/SystemClock.h>
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
@@ -34,15 +42,25 @@
 namespace common {
 namespace implementation {
 
-using chre::FragmentedLoadRequest;
-using chre::FragmentedLoadTransaction;
-using chre::HostProtocolHost;
-using flatbuffers::FlatBufferBuilder;
+using ::android::chre::FragmentedLoadRequest;
+using ::android::chre::FragmentedLoadTransaction;
+using ::android::chre::HostProtocolHost;
+using ::flatbuffers::FlatBufferBuilder;
 
 #ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
+// TODO(b/298459533): Remove these when the flag_log_nanoapp_load_metrics flag
+// is cleaned up
 using ::aidl::android::frameworks::stats::IStats;
 using ::aidl::android::frameworks::stats::VendorAtom;
 using ::aidl::android::frameworks::stats::VendorAtomValue;
+using ::android::chre::Atoms::CHRE_AP_WAKE_UP_OCCURRED;
+using ::android::chre::Atoms::CHRE_HAL_NANOAPP_LOAD_FAILED;
+using ::android::chre::flags::flag_log_nanoapp_load_metrics;
+using ::android::chre::flags::remove_ap_wakeup_metric_report_limit;
+// TODO(b/298459533): Remove end
+
+using ::android::chre::MetricsReporter;
+using ::android::chre::Atoms::ChreHalNanoappLoadFailed;
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
 
 HalChreSocketConnection::HalChreSocketConnection(
@@ -182,7 +200,9 @@
     HalChreSocketConnection &parent, IChreSocketCallback *callback)
     : mParent(parent), mCallback(callback) {
 #ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
-  mLastClearedTimestamp = elapsedRealtime();
+  if (!remove_ap_wakeup_metric_report_limit()) {
+    mLastClearedTimestamp = elapsedRealtime();
+  }
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
 }
 
@@ -217,25 +237,35 @@
     // check and update the 24hour timer
     std::lock_guard<std::mutex> lock(mNanoappWokeApCountMutex);
     long nanoappId = message.app_id;
-    long timeElapsed = elapsedRealtime() - mLastClearedTimestamp;
-    if (timeElapsed > kOneDayinMillis) {
-      mNanoappWokeUpCount = 0;
-      mLastClearedTimestamp = elapsedRealtime();
+
+    if (!remove_ap_wakeup_metric_report_limit()) {
+      long timeElapsed = elapsedRealtime() - mLastClearedTimestamp;
+      if (timeElapsed > kOneDayinMillis) {
+        mNanoappWokeUpCount = 0;
+        mLastClearedTimestamp = elapsedRealtime();
+      }
+
+      mNanoappWokeUpCount++;
     }
 
-    // update and report the AP woke up metric
-    mNanoappWokeUpCount++;
-    if (mNanoappWokeUpCount < kMaxDailyReportedApWakeUp) {
-      // create and report the vendor atom
-      std::vector<VendorAtomValue> values(1);
-      values[0].set<VendorAtomValue::longValue>(nanoappId);
+    if (remove_ap_wakeup_metric_report_limit() ||
+        mNanoappWokeUpCount < kMaxDailyReportedApWakeUp) {
+      if (flag_log_nanoapp_load_metrics()) {
+        if (!mParent.mMetricsReporter.logApWakeupOccurred(nanoappId)) {
+          ALOGE("Could not log AP Wakeup metric");
+        }
+      } else {
+        // create and report the vendor atom
+        std::vector<VendorAtomValue> values(1);
+        values[0].set<VendorAtomValue::longValue>(nanoappId);
 
-      const VendorAtom atom{
-          .atomId = chre::Atoms::CHRE_AP_WAKE_UP_OCCURRED,
-          .values{std::move(values)},
-      };
+        const VendorAtom atom{
+            .atomId = CHRE_AP_WAKE_UP_OCCURRED,
+            .values{std::move(values)},
+        };
 
-      mParent.reportMetric(atom);
+        mParent.reportMetric(atom);
+      }
     }
   }
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
@@ -293,6 +323,19 @@
         }
       } else {
         success = response.success;
+
+#ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
+        if (!success) {
+          if (flag_log_nanoapp_load_metrics()) {
+            if (!mParent.mMetricsReporter.logNanoappLoadFailed(
+                    transaction.getNanoappId(),
+                    ChreHalNanoappLoadFailed::TYPE_DYNAMIC,
+                    ChreHalNanoappLoadFailed::REASON_ERROR_GENERIC)) {
+              ALOGE("Could not log the nanoapp load failed metric");
+            }
+          }
+        }
+#endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
       }
 
       if (!continueLoadRequest) {
@@ -347,19 +390,27 @@
           request.fragmentId);
 
 #ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
-    // create and report the vendor atom
-    std::vector<VendorAtomValue> values(3);
-    values[0].set<VendorAtomValue::longValue>(request.appId);
-    values[1].set<VendorAtomValue::intValue>(
-        chre::Atoms::ChreHalNanoappLoadFailed::TYPE_DYNAMIC);
-    values[2].set<VendorAtomValue::intValue>(
-        chre::Atoms::ChreHalNanoappLoadFailed::REASON_ERROR_GENERIC);
+    if (flag_log_nanoapp_load_metrics()) {
+      if (!mMetricsReporter.logNanoappLoadFailed(
+              request.appId, ChreHalNanoappLoadFailed::TYPE_DYNAMIC,
+              ChreHalNanoappLoadFailed::REASON_CONNECTION_ERROR)) {
+        ALOGE("Could not log the nanoapp load failed metric");
+      }
+    } else {
+      // create and report the vendor atom
+      std::vector<VendorAtomValue> values(3);
+      values[0].set<VendorAtomValue::longValue>(request.appId);
+      values[1].set<VendorAtomValue::intValue>(
+          ChreHalNanoappLoadFailed::TYPE_DYNAMIC);
+      values[2].set<VendorAtomValue::intValue>(
+          ChreHalNanoappLoadFailed::REASON_ERROR_GENERIC);
 
-    const VendorAtom atom{
-        .atomId = chre::Atoms::CHRE_HAL_NANOAPP_LOAD_FAILED,
-        .values{std::move(values)},
-    };
-    reportMetric(atom);
+      const VendorAtom atom{
+          .atomId = CHRE_HAL_NANOAPP_LOAD_FAILED,
+          .values{std::move(values)},
+      };
+      reportMetric(atom);
+    }
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
 
   } else {
@@ -371,6 +422,8 @@
 }
 
 #ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
+// TODO(b/298459533): Remove this the flag_log_nanoapp_load_metrics flag is
+// cleaned up
 void HalChreSocketConnection::reportMetric(const VendorAtom atom) {
   const std::string statsServiceName =
       std::string(IStats::descriptor).append("/default");
@@ -391,6 +444,7 @@
     ALOGE("Failed to report vendor atom");
   }
 }
+// TODO(b/298459533): Remove end
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
 
 }  // namespace implementation
diff --git a/host/hal_generic/common/hal_chre_socket_connection.h b/host/hal_generic/common/hal_chre_socket_connection.h
index b3bb294..417638c 100644
--- a/host/hal_generic/common/hal_chre_socket_connection.h
+++ b/host/hal_generic/common/hal_chre_socket_connection.h
@@ -26,7 +26,9 @@
 
 #ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
 #include <aidl/android/frameworks/stats/IStats.h>
-#endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
+
+#include "chre_host/metrics_reporter.h"
+#endif  //  CHRE_HAL_SOCKET_METRICS_ENABLED
 
 namespace android {
 namespace hardware {
@@ -182,6 +184,10 @@
   std::optional<chre::FragmentedLoadTransaction> mPendingLoadTransaction;
   std::mutex mPendingLoadTransactionMutex;
 
+#ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
+  android::chre::MetricsReporter mMetricsReporter;
+#endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
+
   /**
    * Checks to see if a load response matches the currently pending
    * fragmented load transaction. mPendingLoadTransactionMutex must
@@ -206,14 +212,17 @@
   bool sendFragmentedLoadNanoAppRequest(
       chre::FragmentedLoadTransaction &transaction);
 
+#ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
+  // TODO(b/298459533): Remove this when the flag_log_nanoapp_load_metrics flag
+  // is cleaned up
   /**
    * Create and report CHRE vendor atom and send it to stats_client
    *
    * @param atom the vendor atom to be reported
    */
-#ifdef CHRE_HAL_SOCKET_METRICS_ENABLED
   void reportMetric(const aidl::android::frameworks::stats::VendorAtom atom);
 #endif  // CHRE_HAL_SOCKET_METRICS_ENABLED
+  // TODO(b/298459533): Remove end
 };
 
 }  // namespace implementation
diff --git a/host/hal_generic/common/hal_client_id.h b/host/hal_generic/common/hal_client_id.h
index fb6e2a5..778a59d 100644
--- a/host/hal_generic/common/hal_client_id.h
+++ b/host/hal_generic/common/hal_client_id.h
@@ -17,6 +17,7 @@
 #ifndef ANDROID_HARDWARE_CONTEXTHUB_COMMON_HAL_CLIENT_ID_H_
 #define ANDROID_HARDWARE_CONTEXTHUB_COMMON_HAL_CLIENT_ID_H_
 
+#include <cstdint>
 #include <limits>
 
 namespace android::hardware::contexthub::common::implementation {
@@ -29,9 +30,6 @@
 /** Max number of HAL clients supported. */
 constexpr uint16_t kMaxNumOfHalClients = kMaxHalClientId - 1;
 
-/** The default HAL client id indicating the id is not assigned. */
-constexpr HalClientId kDefaultHalClientId = 0;
-
 /**
  * The HAL client id indicating the message is actually sent to the HAL itself.
  */
diff --git a/host/hal_generic/common/hal_client_manager.cc b/host/hal_generic/common/hal_client_manager.cc
index 39ce4c9..4658c83 100644
--- a/host/hal_generic/common/hal_client_manager.cc
+++ b/host/hal_generic/common/hal_client_manager.cc
@@ -14,197 +14,246 @@
  * limitations under the License.
  */
 #include "hal_client_manager.h"
+
+#include <fstream>
+
 #include <aidl/android/hardware/contexthub/AsyncEventType.h>
 #include <android-base/strings.h>
+#include <android_chre_flags.h>
 #include <json/json.h>
 #include <utils/SystemClock.h>
-#include <fstream>
 
 namespace android::hardware::contexthub::common::implementation {
 
-using aidl::android::hardware::contexthub::AsyncEventType;
-using aidl::android::hardware::contexthub::ContextHubMessage;
-using aidl::android::hardware::contexthub::HostEndpointInfo;
-using aidl::android::hardware::contexthub::IContextHubCallback;
+using ::aidl::android::hardware::contexthub::AsyncEventType;
+using ::aidl::android::hardware::contexthub::ContextHubMessage;
+using ::aidl::android::hardware::contexthub::HostEndpointInfo;
+using ::aidl::android::hardware::contexthub::IContextHubCallback;
+using ::android::chre::flags::context_hub_callback_uuid_enabled;
+
+using HalClient = HalClientManager::HalClient;
 
 namespace {
-bool getClientMappingsFromFile(const char *filePath, Json::Value &mappings) {
+bool getClientMappingsFromFile(const std::string &filePath,
+                               Json::Value &mappings) {
   std::fstream file(filePath);
   Json::CharReaderBuilder builder;
   return file.good() &&
          Json::parseFromStream(builder, file, &mappings, /* errs= */ nullptr);
 }
+
+std::string getUuid(const std::shared_ptr<IContextHubCallback> &callback) {
+  std::array<uint8_t, 16> uuidBytes{};
+  callback->getUuid(&uuidBytes);
+  std::ostringstream oStringStream;
+  char buffer[3]{};
+  for (const uint8_t &byte : uuidBytes) {
+    snprintf(buffer, sizeof(buffer), "%02x", static_cast<int>(byte));
+    oStringStream << buffer;
+  }
+  return oStringStream.str();
+}
+
+std::string getName(const std::shared_ptr<IContextHubCallback> &callback) {
+  std::string name;
+  callback->getName(&name);
+  return name;
+}
+
+bool isCallbackV3Enabled(const std::shared_ptr<IContextHubCallback> &callback) {
+  int32_t callbackVersion;
+  callback->getInterfaceVersion(&callbackVersion);
+  return callbackVersion >= 3 && context_hub_callback_uuid_enabled();
+}
+
 }  // namespace
 
-std::optional<HalClientId> HalClientManager::createClientIdLocked(
-    const std::string &processName) {
-  if (mPIdsToClientIds.size() > kMaxNumOfHalClients ||
-      mNextClientId > kMaxHalClientId) {
-    LOGE("Too many HAL clients registered which should never happen.");
-    return std::nullopt;
+HalClient *HalClientManager::getClientByField(
+    const std::function<bool(const HalClient &client)> &fieldMatcher) {
+  for (HalClient &client : mClients) {
+    if (fieldMatcher(client)) {
+      return &client;
+    }
   }
-  if (mProcessNamesToClientIds.find(processName) !=
-      mProcessNamesToClientIds.end()) {
-    return mProcessNamesToClientIds[processName];
+  return nullptr;
+}
+
+HalClient *HalClientManager::getClientByClientIdLocked(HalClientId clientId) {
+  return getClientByField([&clientId](const HalClient &client) {
+    return client.clientId == clientId;
+  });
+}
+
+HalClient *HalClientManager::getClientByUuidLocked(const std::string &uuid) {
+  return getClientByField(
+      [&uuid](const HalClient &client) { return client.uuid == uuid; });
+}
+
+HalClient *HalClientManager::getClientByProcessIdLocked(pid_t pid) {
+  return getClientByField(
+      [&pid](const HalClient &client) { return client.pid == pid; });
+}
+
+bool HalClientManager::updateNextClientIdLocked() {
+  std::unordered_set<HalClientId> usedClientIds{};
+  for (const HalClient &client : mClients) {
+    usedClientIds.insert(client.clientId);
   }
+  for (int i = 0; i < kMaxNumOfHalClients; i++) {
+    mNextClientId = (mNextClientId + 1) % kMaxHalClientId;
+    if (mNextClientId != ::chre::kHostClientIdUnspecified &&
+        mReservedClientIds.find(mNextClientId) == mReservedClientIds.end() &&
+        usedClientIds.find(mNextClientId) == usedClientIds.end()) {
+      // Found a client id that is not reserved nor used.
+      return true;
+    }
+  }
+  LOGE("Unable to find the next available client id");
+  mNextClientId = ::chre::kHostClientIdUnspecified;
+  return false;
+}
+
+bool HalClientManager::createClientLocked(
+    const std::string &uuid, pid_t pid,
+    const std::shared_ptr<IContextHubCallback> &callback,
+    void *deathRecipientCookie) {
+  if (mClients.size() > kMaxNumOfHalClients ||
+      mNextClientId == ::chre::kHostClientIdUnspecified) {
+    LOGE("Too many HAL clients (%zu) registered which should never happen.",
+         mClients.size());
+    return false;
+  }
+  std::string name{"undefined"};
+  if (isCallbackV3Enabled(callback)) {
+    name = getName(callback);
+  }
+  mClients.emplace_back(uuid, name, mNextClientId, pid, callback,
+                        deathRecipientCookie);
   // Update the json list with the new mapping
-  mProcessNamesToClientIds.emplace(processName, mNextClientId);
   Json::Value mappings;
-  for (const auto &[name, clientId] : mProcessNamesToClientIds) {
+  for (const auto &client : mClients) {
     Json::Value mapping;
-    mapping[kJsonProcessName] = name;
-    mapping[kJsonClientId] = clientId;
+    mapping[kJsonUuid] = client.uuid;
+    mapping[kJsonName] = client.name;
+    mapping[kJsonClientId] = client.clientId;
     mappings.append(mapping);
   }
   // write to the file; Create the file if it doesn't exist
   Json::StreamWriterBuilder factory;
   std::unique_ptr<Json::StreamWriter> const writer(factory.newStreamWriter());
-  std::ofstream fileStream(kClientMappingFilePath);
+  std::ofstream fileStream(mClientMappingFilePath);
   writer->write(mappings, &fileStream);
   fileStream << std::endl;
-  return {mNextClientId++};
+  updateNextClientIdLocked();
+  return true;
 }
 
-HalClientId HalClientManager::getClientId() {
-  pid_t pid = AIBinder_getCallingPid();
+HalClientId HalClientManager::getClientId(pid_t pid) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (isKnownPIdLocked(pid)) {
-    return mPIdsToClientIds[pid];
+  const HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
+    LOGE("Failed to find the client id for pid %d", pid);
+    return ::chre::kHostClientIdUnspecified;
   }
-  LOGE("Failed to find the client id for pid %d", pid);
-  return kDefaultHalClientId;
+  return client->clientId;
 }
 
 std::shared_ptr<IContextHubCallback> HalClientManager::getCallback(
     HalClientId clientId) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (isAllocatedClientIdLocked(clientId)) {
-    return mClientIdsToClientInfo.at(clientId).callback;
+  const HalClient *client = getClientByClientIdLocked(clientId);
+  if (client == nullptr) {
+    LOGE("Failed to find the callback for the client id %" PRIu16, clientId);
+    return nullptr;
   }
-  LOGE("Failed to find the callback for the client id %" PRIu16, clientId);
-  return nullptr;
+  return client->callback;
 }
 
 bool HalClientManager::registerCallback(
-    const std::shared_ptr<IContextHubCallback> &callback,
-    const ndk::ScopedAIBinder_DeathRecipient &deathRecipient,
-    void *deathRecipientCookie) {
-  pid_t pid = AIBinder_getCallingPid();
-  const std::lock_guard<std::mutex> lock(mLock);
-  if (AIBinder_linkToDeath(callback->asBinder().get(), deathRecipient.get(),
-                           deathRecipientCookie) != STATUS_OK) {
-    LOGE("Failed to link client binder to death recipient.");
-    return false;
-  }
-  if (isKnownPIdLocked(pid)) {
-    LOGW("The pid %d has already registered. Overriding its callback.", pid);
-    return overrideCallbackLocked(pid, callback, deathRecipient,
-                                  deathRecipientCookie);
-  }
-  std::string processName = getProcessName(pid);
-  std::optional<HalClientId> clientIdOptional =
-      createClientIdLocked(processName);
-  if (clientIdOptional == std::nullopt) {
-    LOGE("Failed to generate a valid client id for process %s",
-         processName.c_str());
-    return false;
-  }
-  HalClientId clientId = clientIdOptional.value();
-  if (mClientIdsToClientInfo.find(clientId) != mClientIdsToClientInfo.end()) {
-    LOGE("Process %s already has a connection to HAL.", processName.c_str());
-    return false;
-  }
-  mPIdsToClientIds[pid] = clientId;
-  mClientIdsToClientInfo.emplace(clientId,
-                                 HalClientInfo(callback, deathRecipientCookie));
-  if (mFrameworkServiceClientId == kDefaultHalClientId &&
-      processName == kSystemServerName) {
-    mFrameworkServiceClientId = clientId;
-  }
-  return true;
-}
-
-bool HalClientManager::overrideCallbackLocked(
     pid_t pid, const std::shared_ptr<IContextHubCallback> &callback,
-    const ndk::ScopedAIBinder_DeathRecipient &deathRecipient,
     void *deathRecipientCookie) {
-  LOGI("Overriding the callback for pid %d", pid);
-  HalClientInfo &clientInfo =
-      mClientIdsToClientInfo.at(mPIdsToClientIds.at(pid));
-  if (AIBinder_unlinkToDeath(clientInfo.callback->asBinder().get(),
-                             deathRecipient.get(),
-                             clientInfo.deathRecipientCookie) != STATUS_OK) {
-    LOGE("Unable to unlink the old callback for pid %d", pid);
-    return false;
+  const std::lock_guard<std::mutex> lock(mLock);
+  HalClient *client = getClientByProcessIdLocked(pid);
+  if (client != nullptr) {
+    LOGW("The pid %d has already registered. Overriding its callback.", pid);
+    if (!mDeadClientUnlinker(client->callback, client->deathRecipientCookie)) {
+      LOGE("Unable to unlink the old callback for pid %d", pid);
+      return false;
+    }
+    client->callback.reset();
+    client->callback = callback;
+    client->deathRecipientCookie = deathRecipientCookie;
+    return true;
   }
-  clientInfo.callback.reset();
-  clientInfo.callback = callback;
-  clientInfo.deathRecipientCookie = deathRecipientCookie;
-  return true;
+
+  std::string uuid;
+  if (isCallbackV3Enabled(callback)) {
+    uuid = getUuid(callback);
+  } else {
+    uuid = getUuidLocked();
+  }
+
+  client = getClientByUuidLocked(uuid);
+  if (client != nullptr) {
+    if (client->pid != HalClient::PID_UNSET) {
+      // A client is trying to connect to HAL from a different process. But the
+      // previous connection is still active because otherwise the pid will be
+      // cleared in handleClientDeath().
+      LOGE("Client (uuid=%s) already has a connection to HAL.", uuid.c_str());
+      return false;
+    }
+    // For a known client the previous assigned clientId will be reused.
+    client->reset(/* processId= */ pid,
+                  /* contextHubCallback= */ callback,
+                  /* cookie= */ deathRecipientCookie);
+    return true;
+  }
+  return createClientLocked(uuid, pid, callback, deathRecipientCookie);
 }
 
-void HalClientManager::handleClientDeath(
-    pid_t pid, const ndk::ScopedAIBinder_DeathRecipient &deathRecipient) {
+void HalClientManager::handleClientDeath(pid_t pid) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE("Failed to locate the dead pid %d", pid);
     return;
   }
-  HalClientId clientId = mPIdsToClientIds[pid];
-  mPIdsToClientIds.erase(mPIdsToClientIds.find(pid));
-  if (!isAllocatedClientIdLocked(clientId)) {
-    LOGE("Failed to locate the dead client id %" PRIu16, clientId);
-    return;
-  }
 
-  for (const auto &[procName, id] : mProcessNamesToClientIds) {
-    if (id == clientId && procName == kSystemServerName) {
-      LOGE("System server is disconnected");
-      mIsFirstClient = true;
-    }
-  }
-
-  HalClientInfo &clientInfo = mClientIdsToClientInfo.at(clientId);
-  if (AIBinder_unlinkToDeath(clientInfo.callback->asBinder().get(),
-                             deathRecipient.get(),
-                             clientInfo.deathRecipientCookie) != STATUS_OK) {
+  if (!mDeadClientUnlinker(client->callback, client->deathRecipientCookie)) {
     LOGE("Unable to unlink the old callback for pid %d in death handler", pid);
   }
-  clientInfo.callback.reset();
+  client->reset(/* processId= */ HalClient::PID_UNSET,
+                /* contextHubCallback= */ nullptr, /* cookie= */ nullptr);
+
   if (mPendingLoadTransaction.has_value() &&
-      mPendingLoadTransaction->clientId == clientId) {
+      mPendingLoadTransaction->clientId == client->clientId) {
     mPendingLoadTransaction.reset();
   }
   if (mPendingUnloadTransaction.has_value() &&
-      mPendingUnloadTransaction->clientId == clientId) {
+      mPendingUnloadTransaction->clientId == client->clientId) {
     mPendingLoadTransaction.reset();
   }
-  mClientIdsToClientInfo.erase(clientId);
-  if (mFrameworkServiceClientId == clientId) {
-    mFrameworkServiceClientId = kDefaultHalClientId;
-  }
   LOGI("Process %" PRIu32 " is disconnected from HAL.", pid);
 }
 
 bool HalClientManager::registerPendingLoadTransaction(
-    std::unique_ptr<chre::FragmentedLoadTransaction> transaction) {
+    pid_t pid, std::unique_ptr<chre::FragmentedLoadTransaction> transaction) {
   if (transaction->isComplete()) {
     LOGW("No need to register a completed load transaction.");
     return false;
   }
-  pid_t pid = AIBinder_getCallingPid();
 
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  const HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE("Unknown HAL client when registering its pending load transaction.");
     return false;
   }
-  auto clientId = mPIdsToClientIds[pid];
-  if (!isNewTransactionAllowedLocked(clientId)) {
+  if (!isNewTransactionAllowedLocked(client->clientId)) {
     return false;
   }
   mPendingLoadTransaction.emplace(
-      clientId, /* registeredTimeMs= */ android::elapsedRealtime(),
+      client->clientId, /* registeredTimeMs= */ android::elapsedRealtime(),
       /* currentFragmentId= */ 0, std::move(transaction));
   return true;
 }
@@ -228,19 +277,18 @@
 }
 
 bool HalClientManager::registerPendingUnloadTransaction(
-    uint32_t transactionId) {
-  pid_t pid = AIBinder_getCallingPid();
+    pid_t pid, uint32_t transactionId) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  const HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE("Unknown HAL client when registering its pending unload transaction.");
     return false;
   }
-  auto clientId = mPIdsToClientIds[pid];
-  if (!isNewTransactionAllowedLocked(clientId)) {
+  if (!isNewTransactionAllowedLocked(client->clientId)) {
     return false;
   }
   mPendingUnloadTransaction.emplace(
-      clientId, transactionId,
+      client->clientId, transactionId,
       /* registeredTimeMs= */ android::elapsedRealtime());
   return true;
 }
@@ -293,79 +341,94 @@
   return true;
 }
 
-bool HalClientManager::registerEndpointId(const HostEndpointId &endpointId) {
-  pid_t pid = AIBinder_getCallingPid();
+bool HalClientManager::registerEndpointId(pid_t pid,
+                                          const HostEndpointId &endpointId) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE(
         "Unknown HAL client (pid %d). Register the callback before registering "
         "an endpoint.",
         pid);
     return false;
   }
-  HalClientId clientId = mPIdsToClientIds[pid];
-  if (!isValidEndpointId(clientId, endpointId)) {
+  if (!isValidEndpointId(client, endpointId)) {
     LOGE("Endpoint id %" PRIu16 " from process %d is out of range.", endpointId,
          pid);
     return false;
   }
-  if (mClientIdsToClientInfo[clientId].endpointIds.find(endpointId) !=
-      mClientIdsToClientInfo[clientId].endpointIds.end()) {
+  if (client->endpointIds.find(endpointId) != client->endpointIds.end()) {
     LOGW("The endpoint %" PRIu16 " is already connected.", endpointId);
     return false;
   }
-  mClientIdsToClientInfo[clientId].endpointIds.insert(endpointId);
+  client->endpointIds.insert(endpointId);
   LOGI("Endpoint id %" PRIu16 " is connected to client %" PRIu16, endpointId,
-       clientId);
+       client->clientId);
   return true;
 }
 
-bool HalClientManager::removeEndpointId(const HostEndpointId &endpointId) {
-  pid_t pid = AIBinder_getCallingPid();
+bool HalClientManager::removeEndpointId(pid_t pid,
+                                        const HostEndpointId &endpointId) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE(
         "Unknown HAL client (pid %d). A callback should have been registered "
         "before removing an endpoint.",
         pid);
     return false;
   }
-  HalClientId clientId = mPIdsToClientIds[pid];
-  if (!isValidEndpointId(clientId, endpointId)) {
+  if (!isValidEndpointId(client, endpointId)) {
     LOGE("Endpoint id %" PRIu16 " from process %d is out of range.", endpointId,
          pid);
     return false;
   }
-  if (mClientIdsToClientInfo[clientId].endpointIds.find(endpointId) ==
-      mClientIdsToClientInfo[clientId].endpointIds.end()) {
+  if (client->endpointIds.find(endpointId) == client->endpointIds.end()) {
     LOGW("The endpoint %" PRIu16 " is not connected.", endpointId);
     return false;
   }
-  mClientIdsToClientInfo[clientId].endpointIds.erase(endpointId);
+  client->endpointIds.erase(endpointId);
   LOGI("Endpoint id %" PRIu16 " is removed from client %" PRIu16, endpointId,
-       clientId);
+       client->clientId);
   return true;
 }
 
 std::shared_ptr<IContextHubCallback> HalClientManager::getCallbackForEndpoint(
-    const HostEndpointId &endpointId) {
+    const HostEndpointId mutatedEndpointId) {
   const std::lock_guard<std::mutex> lock(mLock);
-  HalClientId clientId = getClientIdFromEndpointId(endpointId);
-  if (!isAllocatedClientIdLocked(clientId)) {
+  HalClient *client;
+  if (mutatedEndpointId & kVendorEndpointIdBitMask) {
+    HalClientId clientId =
+        mutatedEndpointId >> kNumOfBitsForEndpointId & kMaxHalClientId;
+    client = getClientByClientIdLocked(clientId);
+  } else {
+    client = getClientByUuidLocked(kSystemServerUuid);
+  }
+
+  HostEndpointId originalEndpointId =
+      convertToOriginalEndpointId(mutatedEndpointId);
+  if (client == nullptr) {
     LOGE("Unknown endpoint id %" PRIu16 ". Please register the callback first.",
-         endpointId);
+         originalEndpointId);
     return nullptr;
   }
-  return mClientIdsToClientInfo[clientId].callback;
+  if (client->endpointIds.find(originalEndpointId) ==
+      client->endpointIds.end()) {
+    LOGW(
+        "Received a message from CHRE for an unknown or disconnected endpoint "
+        "id %" PRIu16,
+        originalEndpointId);
+  }
+  return client->callback;
 }
 
 void HalClientManager::sendMessageForAllCallbacks(
     const ContextHubMessage &message,
     const std::vector<std::string> &messageParams) {
   const std::lock_guard<std::mutex> lock(mLock);
-  for (const auto &[_, clientInfo] : mClientIdsToClientInfo) {
-    if (clientInfo.callback != nullptr) {
-      clientInfo.callback->handleContextHubMessage(message, messageParams);
+  for (const auto &client : mClients) {
+    if (client.callback != nullptr) {
+      client.callback->handleContextHubMessage(message, messageParams);
     }
   }
 }
@@ -373,30 +436,26 @@
 const std::unordered_set<HostEndpointId>
     *HalClientManager::getAllConnectedEndpoints(pid_t pid) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  const HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE("Unknown HAL client with pid %d", pid);
     return nullptr;
   }
-  HalClientId clientId = mPIdsToClientIds[pid];
-  if (mClientIdsToClientInfo.find(clientId) == mClientIdsToClientInfo.end()) {
-    LOGE("Can't find any information for client id %" PRIu16, clientId);
-    return nullptr;
-  }
-  return &mClientIdsToClientInfo[clientId].endpointIds;
+  return &(client->endpointIds);
 }
 
 bool HalClientManager::mutateEndpointIdFromHostIfNeeded(
-    const pid_t &pid, HostEndpointId &endpointId) {
+    pid_t pid, HostEndpointId &endpointId) {
   const std::lock_guard<std::mutex> lock(mLock);
-  if (!isKnownPIdLocked(pid)) {
+  const HalClient *client = getClientByProcessIdLocked(pid);
+  if (client == nullptr) {
     LOGE("Unknown HAL client with pid %d", pid);
     return false;
   }
   // no need to mutate client id for framework service
-  if (mPIdsToClientIds[pid] != mFrameworkServiceClientId) {
-    HalClientId clientId = mPIdsToClientIds[pid];
+  if (client->uuid != kSystemServerUuid) {
     endpointId = kVendorEndpointIdBitMask |
-                 clientId << kNumOfBitsForEndpointId | endpointId;
+                 client->clientId << kNumOfBitsForEndpointId | endpointId;
   }
   return true;
 }
@@ -409,31 +468,35 @@
   return endpointId;
 }
 
-HalClientManager::HalClientManager() {
+HalClientManager::HalClientManager(
+    DeadClientUnlinker deadClientUnlinker,
+    const std::string &clientIdMappingFilePath,
+    const std::unordered_set<HalClientId> &reservedClientIds) {
+  mDeadClientUnlinker = std::move(deadClientUnlinker);
+  mClientMappingFilePath = clientIdMappingFilePath;
+  mReservedClientIds = reservedClientIds;
   // Parses the file to construct a mapping from process names to client ids.
   Json::Value mappings;
-  if (!getClientMappingsFromFile(kClientMappingFilePath, mappings)) {
+  if (!getClientMappingsFromFile(mClientMappingFilePath, mappings)) {
     // TODO(b/247124878): When the device was firstly booted up the file doesn't
     //   exist which is expected. Consider to create a default file to avoid
     //   confusions.
-    LOGW("Unable to find and read %s.", kClientMappingFilePath);
-    return;
-  }
-  for (int i = 0; i < mappings.size(); i++) {
-    Json::Value mapping = mappings[i];
-    if (!mapping.isMember(kJsonClientId) ||
-        !mapping.isMember(kJsonProcessName)) {
-      LOGE("Unable to find expected key name for the entry %d", i);
-      continue;
-    }
-    std::string processName = mapping[kJsonProcessName].asString();
-    auto clientId = static_cast<HalClientId>(mapping[kJsonClientId].asUInt());
-    mProcessNamesToClientIds[processName] = clientId;
-    // mNextClientId should always hold the next available client id
-    if (mNextClientId <= clientId) {
-      mNextClientId = clientId + 1;
+    LOGW("Unable to find and read %s.", mClientMappingFilePath.c_str());
+  } else {
+    for (int i = 0; i < mappings.size(); i++) {
+      Json::Value mapping = mappings[i];
+      if (!mapping.isMember(kJsonClientId) || !mapping.isMember(kJsonUuid) ||
+          !mapping.isMember(kJsonName)) {
+        LOGE("Unable to find expected key name for the entry %d", i);
+        continue;
+      }
+      std::string uuid = mapping[kJsonUuid].asString();
+      std::string name = mapping[kJsonName].asString();
+      auto clientId = static_cast<HalClientId>(mapping[kJsonClientId].asUInt());
+      mClients.emplace_back(uuid, name, clientId);
     }
   }
+  updateNextClientIdLocked();
 }
 
 bool HalClientManager::isPendingLoadTransactionMatchedLocked(
@@ -490,15 +553,14 @@
     const std::lock_guard<std::mutex> lock(mLock);
     mPendingLoadTransaction.reset();
     mPendingUnloadTransaction.reset();
-    for (auto &[_, clientInfo] : mClientIdsToClientInfo) {
-      clientInfo.endpointIds.clear();
+    for (HalClient &client : mClients) {
+      client.endpointIds.clear();
     }
   }
   // Incurs callbacks without holding the lock to avoid deadlocks.
-  for (auto &[_, clientInfo] : mClientIdsToClientInfo) {
-    if (clientInfo.callback != nullptr) {
-      clientInfo.callback->handleContextHubAsyncEvent(
-          AsyncEventType::RESTARTED);
+  for (const HalClient &client : mClients) {
+    if (client.callback != nullptr) {
+      client.callback->handleContextHubAsyncEvent(AsyncEventType::RESTARTED);
     }
   }
 }
diff --git a/host/hal_generic/common/hal_client_manager.h b/host/hal_generic/common/hal_client_manager.h
index 66ca859..80f2089 100644
--- a/host/hal_generic/common/hal_client_manager.h
+++ b/host/hal_generic/common/hal_client_manager.h
@@ -16,17 +16,21 @@
 #ifndef ANDROID_HARDWARE_CONTEXTHUB_COMMON_HAL_CLIENT_MANAGER_H_
 #define ANDROID_HARDWARE_CONTEXTHUB_COMMON_HAL_CLIENT_MANAGER_H_
 
-#include <aidl/android/hardware/contexthub/ContextHubMessage.h>
-#include <aidl/android/hardware/contexthub/IContextHub.h>
-#include <aidl/android/hardware/contexthub/IContextHubCallback.h>
-#include <chre_host/fragmented_load_transaction.h>
-#include <chre_host/preloaded_nanoapp_loader.h>
+#include "chre/platform/shared/host_protocol_common.h"
+#include "chre_host/fragmented_load_transaction.h"
+#include "chre_host/log.h"
+#include "chre_host/preloaded_nanoapp_loader.h"
+#include "hal_client_id.h"
+
 #include <sys/types.h>
 #include <cstddef>
 #include <unordered_map>
 #include <unordered_set>
-#include "chre_host/log.h"
-#include "hal_client_id.h"
+#include <utility>
+
+#include <aidl/android/hardware/contexthub/ContextHubMessage.h>
+#include <aidl/android/hardware/contexthub/IContextHub.h>
+#include <aidl/android/hardware/contexthub/IContextHubCallback.h>
 
 using aidl::android::hardware::contexthub::ContextHubMessage;
 using aidl::android::hardware::contexthub::HostEndpointInfo;
@@ -42,16 +46,21 @@
  * A HAL client is defined as a user calling the IContextHub API. The main
  * purpose of this class are:
  *   - to assign a unique HalClientId identifying each client;
- *   - to maintain a mapping between client ids and HalClientInfos;
- *   - to maintain a mapping between client ids and their endpoint ids.
+ *   - to maintain a mapping between a HAL client and its states defined in
+ *     HalClient;
+ *   - to track the ongoing load/unload transactions
  *
- * There are two types of ids HalClientManager will track, host endpoint id and
- * client id. A host endpoint id, which is defined at
- * hardware/interfaces/contexthub/aidl/android/hardware/contexthub/ContextHubMessage.aidl,
- * identifies a host app that communicates with a HAL client. A client id
- * identifies a HAL client, which is the layer beneath the host apps, such as
- * ContextHubService. Multiple apps with different host endpoint IDs can have
- * the same client ID.
+ * There are 3 types of ids HalClientManager will track: client uuid, HAL client
+ * id and host endpoint id.
+ *   - A uuid uniquely identifies a client when it registers its callback.
+ *     After a callback is registered, a HAL client id is created and will be
+ *     used to identify the client in the following API calls from/to it
+ *   - A client id identifies a HAL client, which is the layer beneath the host
+ *     apps, such as ContextHubService. Multiple apps with different host
+ *     endpoint IDs can have the same client ID.
+ *   - A host endpoint id, which is defined at
+ *     hardware/interfaces/contexthub/aidl/android/hardware/contexthub/ContextHubMessage.aidl,
+ *     identifies a host app that communicates with a HAL client.
  *
  * For a host endpoint connected to ContextHubService, its endpoint id is kept
  *in the form below during the communication with CHRE.
@@ -77,7 +86,57 @@
  */
 class HalClientManager {
  public:
-  HalClientManager();
+  struct HalClient {
+    static constexpr pid_t PID_UNSET = 0;
+
+    explicit HalClient(const std::string &uuid, const std::string &name,
+                       const HalClientId clientId)
+        : HalClient(uuid, name, clientId, /* pid= */ PID_UNSET,
+                    /* callback= */ nullptr,
+                    /* deathRecipientCookie= */ nullptr) {}
+
+    explicit HalClient(std::string uuid, std::string name,
+                       const HalClientId clientId, pid_t pid,
+                       const std::shared_ptr<IContextHubCallback> &callback,
+                       void *deathRecipientCookie)
+        : uuid{std::move(uuid)},
+          name{std::move(name)},
+          clientId{clientId},
+          pid{pid},
+          callback{callback},
+          deathRecipientCookie{deathRecipientCookie} {}
+
+    /** Resets the client's fields except uuid and clientId. */
+    void reset(pid_t processId,
+               const std::shared_ptr<IContextHubCallback> &contextHubCallback,
+               void *cookie) {
+      pid = processId;
+      callback = contextHubCallback;
+      deathRecipientCookie = cookie;
+      endpointIds.clear();
+    }
+
+    const std::string uuid;
+    std::string name;
+    const HalClientId clientId;
+    pid_t pid{};
+    std::shared_ptr<IContextHubCallback> callback{};
+    // cookie is used by the death recipient's linked callback
+    void *deathRecipientCookie{};
+    std::unordered_set<HostEndpointId> endpointIds{};
+  };
+
+  // The endpoint id is from a vendor client if the highest bit is set to 1.
+  static constexpr HostEndpointId kVendorEndpointIdBitMask = 0x8000;
+  static constexpr uint8_t kNumOfBitsForEndpointId = 6;
+
+  using DeadClientUnlinker = std::function<bool(
+      const std::shared_ptr<IContextHubCallback> &callback, void *cookie)>;
+
+  explicit HalClientManager(
+      DeadClientUnlinker deadClientUnlinker,
+      const std::string &clientIdMappingFilePath,
+      const std::unordered_set<HalClientId> &reservedClientIds = {});
   virtual ~HalClientManager() = default;
 
   /** Disable copy constructor and copy assignment to avoid duplicates. */
@@ -87,14 +146,16 @@
   /**
    * Gets the client id allocated to the current HAL client.
    *
-   * The current HAL client is identified by its process id, which is retrieved
-   * by calling AIBinder_getCallingPid(). If the process doesn't have any client
-   * id assigned, HalClientManager will create one mapped to its process id.
+   * The current HAL client is identified by its process id. If the process
+   * doesn't have any client id assigned, HalClientManager will create one
+   * mapped to its process id.
    *
-   * @return client id assigned to the calling process, or kDefaultHalClientId
-   * if the process id is not found.
+   * @param pid process id of the current client
+   *
+   * @return client id assigned to the calling process, or
+   * ::chre::kHostClientIdUnspecified if the process id is not found.
    */
-  HalClientId getClientId();
+  HalClientId getClientId(pid_t pid);
 
   /**
    * Gets the callback for the current HAL client identified by the clientId.
@@ -109,16 +170,15 @@
    * client id. @p deathRecipient and @p deathRecipientCookie are used to unlink
    * the previous registered callback for the same client, if any.
    *
+   * @param pid process id of the current client
    * @param callback a function incurred to handle the client death event.
-   * @param deathRecipient a handle on the death notification.
    * @param deathRecipientCookie the data used by the callback.
    *
    * @return true if success, otherwise false.
    */
-  bool registerCallback(
-      const std::shared_ptr<IContextHubCallback> &callback,
-      const ndk::ScopedAIBinder_DeathRecipient &deathRecipient,
-      void *deathRecipientCookie);
+  bool registerCallback(pid_t pid,
+                        const std::shared_ptr<IContextHubCallback> &callback,
+                        void *deathRecipientCookie);
 
   /**
    * Registers a FragmentedLoadTransaction for the current HAL client.
@@ -126,10 +186,13 @@
    * At this moment only one active transaction, either load or unload, is
    * supported.
    *
+   * @param pid process id of the current client
+   * @param transaction the transaction being registered
+   *
    * @return true if success, otherwise false.
    */
   bool registerPendingLoadTransaction(
-      std::unique_ptr<FragmentedLoadTransaction> transaction);
+      pid_t pid, std::unique_ptr<chre::FragmentedLoadTransaction> transaction);
 
   /**
    * Returns true if the load transaction matches the arguments provided.
@@ -166,9 +229,12 @@
    * At this moment only one active transaction, either load or unload, is
    * supported.
    *
+   * @param pid process id of the current client
+   * @param transaction the transaction being registered
+   *
    * @return true if success, otherwise false.
    */
-  bool registerPendingUnloadTransaction(uint32_t transactionId);
+  bool registerPendingUnloadTransaction(pid_t pid, uint32_t transactionId);
 
   /**
    * Clears the pending unload transaction.
@@ -188,31 +254,39 @@
   /**
    * Registers an endpoint id when it is connected to HAL.
    *
+   * @param pid process id of the current HAL client
+   * @param endpointId the endpointId being registered
+   *
    * @return true if success, otherwise false.
    */
-  bool registerEndpointId(const HostEndpointId &endpointId);
+  bool registerEndpointId(pid_t pid, const HostEndpointId &endpointId);
 
   /**
    * Removes an endpoint id when it is disconnected to HAL.
    *
+   * @param pid process id of the current HAL client
+   * @param endpointId the endpointId being registered
+   *
    * @return true if success, otherwise false.
    */
-  bool removeEndpointId(const HostEndpointId &endpointId);
+  bool removeEndpointId(pid_t pid, const HostEndpointId &endpointId);
 
   /**
    * Mutates the endpoint id if the hal client is not the framework service.
    *
+   * @param pid process id of the current HAL client
+   * @param endpointId the endpointId being registered
+   *
    * @return true if success, otherwise false.
    */
-  bool mutateEndpointIdFromHostIfNeeded(const pid_t &pid,
-                                        HostEndpointId &endpointId);
+  bool mutateEndpointIdFromHostIfNeeded(pid_t pid, HostEndpointId &endpointId);
 
   /** Returns the original endpoint id sent by the host client. */
   static HostEndpointId convertToOriginalEndpointId(
       const HostEndpointId &endpointId);
 
   /**
-   * Gets all the connected endpoints for the client identified by the pid.
+   * Gets all the connected endpoints for the client identified by the @p pid.
    *
    * @return the pointer to the endpoint id set if the client is identifiable,
    * otherwise nullptr.
@@ -225,45 +299,28 @@
       const std::vector<std::string> &messageParams);
 
   std::shared_ptr<IContextHubCallback> getCallbackForEndpoint(
-      const HostEndpointId &endpointId);
+      HostEndpointId mutatedEndpointId);
 
   /**
    * Handles the client death event.
    *
    * @param pid of the client that loses the binder connection to the HAL.
-   * @param deathRecipient to be unlinked with the client's callback
    */
-  void handleClientDeath(
-      pid_t pid, const ndk::ScopedAIBinder_DeathRecipient &deathRecipient);
+  void handleClientDeath(pid_t pid);
 
   /** Handles CHRE restart event. */
   void handleChreRestart();
 
  protected:
-  static constexpr char kSystemServerName[] = "system_server";
-  static constexpr char kClientMappingFilePath[] =
-      "/data/vendor/chre/chre_hal_clients.json";
+  static constexpr char kSystemServerUuid[] =
+      "9a17008d6bf1445a90116d21bd985b6c";
+  static constexpr char kVendorClientUuid[] = "vendor-client";
   static constexpr char kJsonClientId[] = "ClientId";
-  static constexpr char kJsonProcessName[] = "ProcessName";
+  static constexpr char kJsonUuid[] = "uuid";
+  static constexpr char kJsonName[] = "name";
   static constexpr int64_t kTransactionTimeoutThresholdMs = 5000;  // 5 seconds
-  static constexpr uint8_t kNumOfBitsForEndpointId = 6;
   static constexpr HostEndpointId kMaxVendorEndpointId =
       (1 << kNumOfBitsForEndpointId) - 1;
-  // The endpoint id is from a vendor client if the highest bit is set to 1.
-  static constexpr HostEndpointId kVendorEndpointIdBitMask = 0x8000;
-
-  struct HalClientInfo {
-    explicit HalClientInfo(const std::shared_ptr<IContextHubCallback> &callback,
-                           void *cookie) {
-      this->callback = callback;
-      this->deathRecipientCookie = cookie;
-    }
-    HalClientInfo() = default;
-    std::shared_ptr<IContextHubCallback> callback;
-    // cookie is used by the death recipient's linked callback
-    void *deathRecipientCookie{};
-    std::unordered_set<HostEndpointId> endpointIds{};
-  };
 
   struct PendingTransaction {
     PendingTransaction(HalClientId clientId, uint32_t transactionId,
@@ -310,10 +367,17 @@
    *
    * mLock must be held when this function is called.
    *
-   * @param processName the process name of the client
    */
-  virtual std::optional<HalClientId> createClientIdLocked(
-      const std::string &processName);
+  bool createClientLocked(const std::string &uuid, pid_t pid,
+                          const std::shared_ptr<IContextHubCallback> &callback,
+                          void *deathRecipientCookie);
+
+  /**
+   * Update @p mNextClientId to be the next available one.
+   *
+   * @return true if success, otherwise false.
+   */
+  bool updateNextClientIdLocked();
 
   /**
    * Returns true if @p clientId and @p transactionId match the
@@ -355,84 +419,48 @@
    */
   bool isNewTransactionAllowedLocked(HalClientId clientId);
 
-  /**
-   * Returns true if the clientId is being used.
-   *
-   * mLock must be held when this function is called.
-   */
-  inline bool isAllocatedClientIdLocked(HalClientId clientId) {
-    return mClientIdsToClientInfo.find(clientId) !=
-           mClientIdsToClientInfo.end();
-  }
-
-  /**
-   * Returns true if the pid is being used to identify a client.
-   *
-   * mLock must be held when this function is called.
-   */
-  inline bool isKnownPIdLocked(pid_t pid) {
-    return mPIdsToClientIds.find(pid) != mPIdsToClientIds.end();
-  }
-
   /** Returns true if the endpoint id is within the accepted range. */
-  [[nodiscard]] inline bool isValidEndpointId(
-      const HalClientId &clientId, const HostEndpointId &endpointId) const {
-    if (clientId != mFrameworkServiceClientId) {
-      return endpointId <= kMaxVendorEndpointId;
-    }
-    return true;
+  [[nodiscard]] static inline bool isValidEndpointId(
+      const HalClient *client, const HostEndpointId &endpointId) {
+    return client->uuid == kSystemServerUuid ||
+           endpointId <= kMaxVendorEndpointId;
   }
 
-  /**
-   * Overrides the old callback registered with the client.
-   *
-   * @return true if success, otherwise false
-   */
-  bool overrideCallbackLocked(
-      pid_t pid, const std::shared_ptr<IContextHubCallback> &callback,
-      const ndk::ScopedAIBinder_DeathRecipient &deathRecipient,
-      void *deathRecipientCookie);
-
-  /**
-   * Extracts the client id from the endpoint id.
-   *
-   * @param endpointId the endpoint id received from CHRE, before any conversion
-   */
-  [[nodiscard]] inline HalClientId getClientIdFromEndpointId(
-      const HostEndpointId &endpointId) const {
-    if (endpointId & kVendorEndpointIdBitMask) {
-      return endpointId >> kNumOfBitsForEndpointId & kMaxHalClientId;
-    }
-    return mFrameworkServiceClientId;
+  // TODO(b/290375569): isSystemServerConnectedLocked() and getUuidLocked() are
+  //   temporary solutions to get a pseudo-uuid. Remove these two functions when
+  //   flag context_hub_callback_uuid_enabled is ramped up.
+  inline bool isSystemServerConnectedLocked() {
+    HalClient *client = getClientByUuidLocked(kSystemServerUuid);
+    return client != nullptr && client->pid != 0;
+  }
+  inline std::string getUuidLocked() {
+    return isSystemServerConnectedLocked() ? kVendorClientUuid
+                                           : kSystemServerUuid;
   }
 
-  std::string getProcessName(pid_t /*pid*/) {
-    // TODO(b/274597758): this is a temporary solution that should be updated
-    //   after b/274597758 is resolved.
-    if (mIsFirstClient) {
-      mIsFirstClient = false;
-      return kSystemServerName;
-    }
-    return "the_vendor_client";
-  }
+  HalClient *getClientByField(
+      const std::function<bool(const HalClient &client)> &fieldMatcher);
 
-  bool mIsFirstClient = true;
+  HalClient *getClientByClientIdLocked(HalClientId clientId);
+
+  HalClient *getClientByUuidLocked(const std::string &uuid);
+
+  HalClient *getClientByProcessIdLocked(pid_t pid);
+
+  DeadClientUnlinker mDeadClientUnlinker{};
+
+  std::string mClientMappingFilePath;
 
   // next available client id
-  HalClientId mNextClientId = kDefaultHalClientId + 1;
-  // framework service client id
-  HalClientId mFrameworkServiceClientId = kDefaultHalClientId;
+  HalClientId mNextClientId = ::chre::kHostClientIdUnspecified;
+
+  // reserved client ids that will not be used
+  std::unordered_set<HalClientId> mReservedClientIds;
 
   // The lock guarding the access to clients' states and pending transactions
   std::mutex mLock;
 
-  // Map from process name to client id which stays consistent with the file
-  // stored at kClientMappingFilePath
-  std::unordered_map<std::string, HalClientId> mProcessNamesToClientIds{};
-  // Map from pids to client ids
-  std::unordered_map<pid_t, HalClientId> mPIdsToClientIds{};
-  // Map from client ids to ClientInfos
-  std::unordered_map<HalClientId, HalClientInfo> mClientIdsToClientInfo{};
+  std::vector<HalClient> mClients;
 
   // States tracking pending transactions
   std::optional<PendingLoadTransaction> mPendingLoadTransaction = std::nullopt;
diff --git a/host/hal_generic/common/hal_error.h b/host/hal_generic/common/hal_error.h
new file mode 100644
index 0000000..8dc437a
--- /dev/null
+++ b/host/hal_generic/common/hal_error.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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 CHRE_HOST_HAL_ERROR_H_
+#define CHRE_HOST_HAL_ERROR_H_
+
+#include <cinttypes>
+
+namespace android::chre {
+
+enum class HalError {
+  SUCCESS = 0,
+
+  // Hal service errors
+  OPERATION_FAILED = -1,
+  INVALID_RESULT = -2,
+  INVALID_ARGUMENT = -3,
+
+  // Hal client errors
+  BINDER_CONNECTION_FAILED = -100,
+  BINDER_DISCONNECTED = -101,
+  NULL_CONTEXT_HUB_FROM_BINDER = -102,
+  LINK_DEATH_RECIPIENT_FAILED = -103,
+  CALLBACK_REGISTRATION_FAILED = -104,
+  UNEXPECTED_ENDPOINT_STATE = -105,
+};
+
+}  // namespace android::chre
+#endif  // CHRE_HOST_HAL_ERROR_H_
\ No newline at end of file
diff --git a/host/hal_generic/common/multi_client_context_hub_base.cc b/host/hal_generic/common/multi_client_context_hub_base.cc
index 950c09f..0c257c3 100644
--- a/host/hal_generic/common/multi_client_context_hub_base.cc
+++ b/host/hal_generic/common/multi_client_context_hub_base.cc
@@ -15,14 +15,15 @@
  */
 
 #include "multi_client_context_hub_base.h"
+
 #include <chre/platform/shared/host_protocol_common.h>
 #include <chre_host/generated/host_messages_generated.h>
 #include <chre_host/log.h>
 #include "chre/event.h"
 #include "chre_host/config_util.h"
-#include "chre_host/file_stream.h"
 #include "chre_host/fragmented_load_transaction.h"
 #include "chre_host/host_protocol_host.h"
+#include "hal_error.h"
 #include "permissions_util.h"
 
 namespace android::hardware::contexthub::common::implementation {
@@ -37,12 +38,8 @@
 
 // timeout for calling getContextHubs(), which is synchronous
 constexpr auto kHubInfoQueryTimeout = std::chrono::seconds(5);
-
-enum class HalErrorCode : int32_t {
-  OPERATION_FAILED = -1,
-  INVALID_RESULT = -2,
-  INVALID_ARGUMENT = -3,
-};
+// timeout for enable/disable test mode, which is synchronous
+constexpr std::chrono::duration ktestModeTimeOut = std::chrono::seconds(5);
 
 bool isValidContextHubId(uint32_t hubId) {
   if (hubId != kDefaultHubId) {
@@ -89,16 +86,34 @@
 }
 
 // functions that help to generate ScopedAStatus from different values.
-inline ScopedAStatus fromServiceError(HalErrorCode errorCode) {
+inline ScopedAStatus fromServiceError(HalError errorCode) {
   return ScopedAStatus::fromServiceSpecificError(
       static_cast<int32_t>(errorCode));
 }
 inline ScopedAStatus fromResult(bool result) {
   return result ? ScopedAStatus::ok()
-                : fromServiceError(HalErrorCode::OPERATION_FAILED);
+                : fromServiceError(HalError::OPERATION_FAILED);
 }
 }  // anonymous namespace
 
+MultiClientContextHubBase::MultiClientContextHubBase() {
+  mDeathRecipient = ndk::ScopedAIBinder_DeathRecipient(
+      AIBinder_DeathRecipient_new(onClientDied));
+  AIBinder_DeathRecipient_setOnUnlinked(
+      mDeathRecipient.get(), /*onUnlinked= */ [](void *cookie) {
+        LOGI("Callback is unlinked. Releasing the death recipient cookie.");
+        delete static_cast<HalDeathRecipientCookie *>(cookie);
+      });
+  mDeadClientUnlinker =
+      [&deathRecipient = mDeathRecipient](
+          const std::shared_ptr<IContextHubCallback> &callback,
+          void *deathRecipientCookie) {
+        return AIBinder_unlinkToDeath(callback->asBinder().get(),
+                                      deathRecipient.get(),
+                                      deathRecipientCookie) == STATUS_OK;
+      };
+}
+
 ScopedAStatus MultiClientContextHubBase::getContextHubs(
     std::vector<ContextHubInfo> *contextHubInfos) {
   std::unique_lock<std::mutex> lock(mHubInfoMutex);
@@ -108,7 +123,7 @@
     HostProtocolHost::encodeHubInfoRequest(builder);
     if (!mConnection->sendMessage(builder)) {
       LOGE("Failed to send a message to CHRE to get context hub info.");
-      return fromServiceError(HalErrorCode::OPERATION_FAILED);
+      return fromServiceError(HalError::OPERATION_FAILED);
     }
     mHubInfoCondition.wait_for(lock, kHubInfoQueryTimeout,
                                [this]() { return mContextHubInfo != nullptr; });
@@ -119,7 +134,7 @@
   }
   LOGE("Unable to get a valid context hub info for PID %d",
        AIBinder_getCallingPid());
-  return fromServiceError(HalErrorCode::INVALID_RESULT);
+  return fromServiceError(HalError::INVALID_RESULT);
 }
 
 ScopedAStatus MultiClientContextHubBase::loadNanoapp(
@@ -135,20 +150,25 @@
       transactionId, appBinary.nanoappId, appBinary.nanoappVersion,
       appBinary.flags, targetApiVersion, appBinary.customBinary,
       mConnection->getLoadFragmentSizeBytes());
+  pid_t pid = AIBinder_getCallingPid();
   if (!mHalClientManager->registerPendingLoadTransaction(
-          std::move(transaction))) {
+          pid, std::move(transaction))) {
     return fromResult(false);
   }
-  auto clientId = mHalClientManager->getClientId();
+  auto clientId = mHalClientManager->getClientId(pid);
   auto request = mHalClientManager->getNextFragmentedLoadRequest();
 
   if (request.has_value() &&
       sendFragmentedLoadRequest(clientId, request.value())) {
+    mEventLogger.logNanoappLoad(appBinary, /* success= */ true);
     return ScopedAStatus::ok();
   }
   LOGE("Failed to send the first load request for nanoapp 0x%" PRIx64,
        appBinary.nanoappId);
   mHalClientManager->resetPendingLoadTransaction();
+  // TODO(b/284481035): The result should be logged after the async response is
+  //  received.
+  mEventLogger.logNanoappLoad(appBinary, /* success= */ false);
   return fromResult(false);
 }
 
@@ -168,10 +188,12 @@
   if (!isValidContextHubId(contextHubId)) {
     return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
   }
-  if (!mHalClientManager->registerPendingUnloadTransaction(transactionId)) {
+  pid_t pid = AIBinder_getCallingPid();
+  if (!mHalClientManager->registerPendingUnloadTransaction(pid,
+                                                           transactionId)) {
     return fromResult(false);
   }
-  HalClientId clientId = mHalClientManager->getClientId();
+  HalClientId clientId = mHalClientManager->getClientId(pid);
   flatbuffers::FlatBufferBuilder builder(64);
   HostProtocolHost::encodeUnloadNanoappRequest(
       builder, transactionId, appId, /* allowSystemNanoappUnload= */ false);
@@ -182,6 +204,9 @@
   if (!result) {
     mHalClientManager->resetPendingUnloadTransaction(clientId, transactionId);
   }
+  // TODO(b/284481035): The result should be logged after the async response is
+  //  received.
+  mEventLogger.logNanoappUnload(appId, result);
   return fromResult(result);
 }
 
@@ -255,9 +280,9 @@
   }
   flatbuffers::FlatBufferBuilder builder(64);
   HostProtocolHost::encodeNanoappListRequest(builder);
-  HostProtocolHost::mutateHostClientId(builder.GetBufferPointer(),
-                                       builder.GetSize(),
-                                       mHalClientManager->getClientId());
+  HostProtocolHost::mutateHostClientId(
+      builder.GetBufferPointer(), builder.GetSize(),
+      mHalClientManager->getClientId(AIBinder_getCallingPid()));
   return fromResult(mConnection->sendMessage(builder));
 }
 
@@ -291,14 +316,20 @@
     LOGE("Callback of context hub HAL must not be null");
     return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
   }
-  // If everything is successful cookie will be released by the callback of
-  // binder unlinking (callback overridden).
-  auto *cookie = new HalDeathRecipientCookie(this, AIBinder_getCallingPid());
-  if (!mHalClientManager->registerCallback(callback, mDeathRecipient, cookie)) {
-    LOGE("Unable to register the callback");
+  pid_t pid = AIBinder_getCallingPid();
+  auto *cookie = new HalDeathRecipientCookie(this, pid);
+  if (AIBinder_linkToDeath(callback->asBinder().get(), mDeathRecipient.get(),
+                           cookie) != STATUS_OK) {
+    LOGE("Failed to link a client binder (pid=%d) to the death recipient", pid);
     delete cookie;
     return fromResult(false);
   }
+  // If AIBinder_linkToDeath is successful the cookie will be released by the
+  // callback of binder unlinking (callback overridden).
+  if (!mHalClientManager->registerCallback(pid, callback, cookie)) {
+    LOGE("Unable to register a client (pid=%d) callback", pid);
+    return fromResult(false);
+  }
   return ScopedAStatus::ok();
 }
 
@@ -316,7 +347,9 @@
   HostProtocolHost::encodeNanoappMessage(
       builder, message.nanoappId, message.messageType, hostEndpointId,
       message.messageBody.data(), message.messageBody.size());
-  return fromResult(mConnection->sendMessage(builder));
+  bool success = mConnection->sendMessage(builder);
+  mEventLogger.logMessageToNanoapp(message, success);
+  return fromResult(success);
 }
 
 ScopedAStatus MultiClientContextHubBase::onHostEndpointConnected(
@@ -334,14 +367,14 @@
       break;
     default:
       LOGE("Unsupported host endpoint type %" PRIu32, info.type);
-      return fromServiceError(HalErrorCode::INVALID_ARGUMENT);
+      return fromServiceError(HalError::INVALID_ARGUMENT);
   }
 
   uint16_t endpointId = info.hostEndpointId;
-  if (!mHalClientManager->registerEndpointId(info.hostEndpointId) ||
-      !mHalClientManager->mutateEndpointIdFromHostIfNeeded(
-          AIBinder_getCallingPid(), endpointId)) {
-    return fromServiceError(HalErrorCode::INVALID_ARGUMENT);
+  pid_t pid = AIBinder_getCallingPid();
+  if (!mHalClientManager->registerEndpointId(pid, info.hostEndpointId) ||
+      !mHalClientManager->mutateEndpointIdFromHostIfNeeded(pid, endpointId)) {
+    return fromServiceError(HalError::INVALID_ARGUMENT);
   }
   flatbuffers::FlatBufferBuilder builder(64);
   HostProtocolHost::encodeHostEndpointConnected(
@@ -353,10 +386,11 @@
 ScopedAStatus MultiClientContextHubBase::onHostEndpointDisconnected(
     char16_t in_hostEndpointId) {
   HostEndpointId hostEndpointId = in_hostEndpointId;
+  pid_t pid = AIBinder_getCallingPid();
   bool isSuccessful = false;
-  if (mHalClientManager->removeEndpointId(hostEndpointId) &&
-      mHalClientManager->mutateEndpointIdFromHostIfNeeded(
-          AIBinder_getCallingPid(), hostEndpointId)) {
+  if (mHalClientManager->removeEndpointId(pid, hostEndpointId) &&
+      mHalClientManager->mutateEndpointIdFromHostIfNeeded(pid,
+                                                          hostEndpointId)) {
     flatbuffers::FlatBufferBuilder builder(64);
     HostProtocolHost::encodeHostEndpointDisconnected(builder, hostEndpointId);
     isSuccessful = mConnection->sendMessage(builder);
@@ -373,9 +407,65 @@
   return ndk::ScopedAStatus::ok();
 }
 
-ScopedAStatus MultiClientContextHubBase::setTestMode(bool /*enable*/) {
-  // To be implemented.
-  return ScopedAStatus::ok();
+ScopedAStatus MultiClientContextHubBase::setTestMode(bool enable) {
+  return fromResult(enable ? enableTestMode() : disableTestMode());
+}
+
+ScopedAStatus MultiClientContextHubBase::sendMessageDeliveryStatusToHub(
+    int32_t /* contextHubId */,
+    const MessageDeliveryStatus & /* messageDeliveryStatus */) {
+  // TODO(b/312417087): Implement reliable message support - transaction status
+  return ndk::ScopedAStatus::ok();
+}
+
+bool MultiClientContextHubBase::enableTestMode() {
+  std::unique_lock<std::mutex> lock(mTestModeMutex);
+  if (mIsTestModeEnabled) {
+    return true;
+  }
+  mTestModeNanoapps.reset();
+  if (!queryNanoapps(kDefaultHubId).isOk()) {
+    LOGE("Failed to get a list of loaded nanoapps.");
+    mTestModeNanoapps.emplace();
+    return false;
+  }
+  mEnableTestModeCv.wait_for(lock, ktestModeTimeOut,
+                             [&]() { return mTestModeNanoapps.has_value(); });
+  for (const auto &appId : *mTestModeNanoapps) {
+    if (!unloadNanoapp(kDefaultHubId, appId, mTestModeTransactionId).isOk()) {
+      LOGE("Failed to unload nanoapp 0x%" PRIx64 " to enable the test mode.",
+           appId);
+      return false;
+    }
+    mTestModeSyncUnloadResult.reset();
+    mEnableTestModeCv.wait_for(lock, ktestModeTimeOut, [&]() {
+      return mTestModeSyncUnloadResult.has_value();
+    });
+    if (!*mTestModeSyncUnloadResult) {
+      LOGE("Failed to unload nanoapp 0x%" PRIx64 " to enable the test mode.",
+           appId);
+      return false;
+    }
+  }
+  mIsTestModeEnabled = true;
+  return true;
+}
+
+bool MultiClientContextHubBase::disableTestMode() {
+  std::unique_lock<std::mutex> lock(mTestModeMutex);
+  if (!mIsTestModeEnabled) {
+    return true;
+  }
+  if (mTestModeNanoapps.has_value() && !mTestModeNanoapps->empty()) {
+    if (!mPreloadedNanoappLoader->loadPreloadedNanoapps(*mTestModeNanoapps)) {
+      LOGE("Failed to reload the nanoapps to disable the test mode.");
+      return false;
+    }
+  }
+  mTestModeNanoapps.emplace();
+  mTestModeTransactionId = static_cast<int32_t>(kDefaultTestModeTransactionId);
+  mIsTestModeEnabled = false;
+  return true;
 }
 
 void MultiClientContextHubBase::handleMessageFromChre(
@@ -402,6 +492,14 @@
       onNanoappLoadResponse(*message.AsLoadNanoappResponse(), clientId);
       break;
     }
+    case fbs::ChreMessage::TimeSyncRequest: {
+      if (mConnection->isTimeSyncNeeded()) {
+        TimeSyncer::sendTimeSync(mConnection.get());
+      } else {
+        LOGW("Received an unexpected time sync request from CHRE.");
+      }
+      break;
+    }
     case fbs::ChreMessage::UnloadNanoappResponse: {
       onNanoappUnloadResponse(*message.AsUnloadNanoappResponse(), clientId);
       break;
@@ -410,6 +508,14 @@
       onNanoappMessage(*message.AsNanoappMessage());
       break;
     }
+    case fbs::ChreMessage::DebugDumpData: {
+      onDebugDumpData(*message.AsDebugDumpData());
+      break;
+    }
+    case fbs::ChreMessage::DebugDumpResponse: {
+      onDebugDumpComplete(*message.AsDebugDumpResponse());
+      break;
+    }
     default:
       LOGW("Got unexpected message type %" PRIu8,
            static_cast<uint8_t>(message.type));
@@ -432,9 +538,32 @@
   mContextHubInfo->chreApiMinorVersion = extractChreApiMinorVersion(version);
   mContextHubInfo->chrePatchVersion = extractChrePatchVersion(version);
   mContextHubInfo->supportedPermissions = kSupportedPermissions;
+
+  // TODO(b/312417087): Implement reliable message support
+  mContextHubInfo->supportsReliableMessages = false;
   mHubInfoCondition.notify_all();
 }
 
+void MultiClientContextHubBase::onDebugDumpData(
+    const ::chre::fbs::DebugDumpDataT &data) {
+  auto str = std::string(reinterpret_cast<const char *>(data.debug_str.data()),
+                         data.debug_str.size());
+  debugDumpAppend(str);
+}
+
+void MultiClientContextHubBase::onDebugDumpComplete(
+    const ::chre::fbs::DebugDumpResponseT &response) {
+  if (!response.success) {
+    LOGE("Dumping debug information fails");
+  }
+  if (checkDebugFd()) {
+    const std::string &dump = mEventLogger.dump();
+    writeToDebugFile(dump.c_str());
+    writeToDebugFile("\n-- End of CHRE/ASH debug info --\n");
+  }
+  debugDumpComplete();
+}
+
 void MultiClientContextHubBase::onNanoappListResponse(
     const fbs::NanoappListResponseT &response, HalClientId clientId) {
   std::shared_ptr<IContextHubCallback> callback =
@@ -463,6 +592,17 @@
     appInfo.rpcServices = rpcServices;
     appInfoList.push_back(appInfo);
   }
+  {
+    std::unique_lock<std::mutex> lock(mTestModeMutex);
+    if (!mTestModeNanoapps.has_value()) {
+      mTestModeNanoapps.emplace();
+      for (const auto &appInfo : appInfoList) {
+        mTestModeNanoapps->insert(appInfo.nanoappId);
+      }
+      mEnableTestModeCv.notify_all();
+    }
+  }
+
   callback->handleNanoappInfo(appInfoList);
 }
 
@@ -517,6 +657,14 @@
     const fbs::UnloadNanoappResponseT &response, HalClientId clientId) {
   if (mHalClientManager->resetPendingUnloadTransaction(
           clientId, response.transaction_id)) {
+    {
+      std::unique_lock<std::mutex> lock(mTestModeMutex);
+      if (response.transaction_id == mTestModeTransactionId) {
+        mTestModeSyncUnloadResult.emplace(response.success);
+        mEnableTestModeCv.notify_all();
+        return;
+      }
+    }
     if (auto callback = mHalClientManager->getCallback(clientId);
         callback != nullptr) {
       callback->handleTransactionResult(response.transaction_id,
@@ -527,6 +675,7 @@
 
 void MultiClientContextHubBase::onNanoappMessage(
     const ::chre::fbs::NanoappMessageT &message) {
+  mEventLogger.logMessageFromNanoapp(message);
   ContextHubMessage outMessage;
   outMessage.nanoappId = message.app_id;
   outMessage.hostEndPoint = message.host_endpoint;
@@ -567,11 +716,32 @@
       mConnection->sendMessage(builder);
     }
   }
-  mHalClientManager->handleClientDeath(clientPid, mDeathRecipient);
+  mHalClientManager->handleClientDeath(clientPid);
 }
 
 void MultiClientContextHubBase::onChreRestarted() {
   mIsWifiAvailable.reset();
+  mEventLogger.logContextHubRestart();
   mHalClientManager->handleChreRestart();
 }
+
+binder_status_t MultiClientContextHubBase::dump(int fd,
+                                                const char ** /* args */,
+                                                uint32_t /* numArgs */) {
+  // debugDumpStart waits for the dump to finish before returning.
+  debugDumpStart(fd);
+  return STATUS_OK;
+}
+
+bool MultiClientContextHubBase::requestDebugDump() {
+  flatbuffers::FlatBufferBuilder builder;
+  HostProtocolHost::encodeDebugDumpRequest(builder);
+  return mConnection->sendMessage(builder);
+}
+
+void MultiClientContextHubBase::writeToDebugFile(const char *str) {
+  if (!::android::base::WriteStringToFd(std::string(str), getDebugFd())) {
+    LOGW("Failed to write %zu bytes to debug dump fd", strlen(str));
+  }
+}
 }  // namespace android::hardware::contexthub::common::implementation
diff --git a/host/hal_generic/common/multi_client_context_hub_base.h b/host/hal_generic/common/multi_client_context_hub_base.h
index 8fa00e5..7d92e71 100644
--- a/host/hal_generic/common/multi_client_context_hub_base.h
+++ b/host/hal_generic/common/multi_client_context_hub_base.h
@@ -17,16 +17,15 @@
 #ifndef ANDROID_HARDWARE_CONTEXTHUB_COMMON_MULTICLIENTS_HAL_BASE_H_
 #define ANDROID_HARDWARE_CONTEXTHUB_COMMON_MULTICLIENTS_HAL_BASE_H_
 
-#ifndef LOG_TAG
-#define LOG_TAG "CHRE.HAL"
-#endif
-
 #include <aidl/android/hardware/contexthub/BnContextHub.h>
 #include <chre_host/generated/host_messages_generated.h>
 
 #include "chre_connection_callback.h"
 #include "chre_host/napp_header.h"
 #include "chre_host/preloaded_nanoapp_loader.h"
+#include "chre_host/time_syncer.h"
+#include "debug_dump_helper.h"
+#include "event_logger.h"
 #include "hal_client_id.h"
 #include "hal_client_manager.h"
 
@@ -37,29 +36,22 @@
 using ::ndk::ScopedAStatus;
 
 /**
- * The base class of multiclients HAL.
+ * The base class of multiclient HAL.
  *
  * A subclass should initiate mConnection, mHalClientManager and
  * mPreloadedNanoappLoader in its constructor.
- *
- * TODO(b/247124878): A few things are pending:
- *   - Some APIs of IContextHub are not implemented yet;
- *   - onHostEndpointConnected/Disconnected now returns an error if the endpoint
- *     id is illegal or already connected/disconnected. The doc of
- *     IContextHub.aidl should be updated accordingly.
- *   - registerCallback() can fail if mHalClientManager sees an error during
- *     registration. The doc of IContextHub.aidl should be updated accordingly.
- *   - Involve EventLogger to log API calls;
- *   - extends DebugDumpHelper to ease debugging
  */
 class MultiClientContextHubBase
     : public BnContextHub,
       public ::android::hardware::contexthub::common::implementation::
-          ChreConnectionCallback {
+          ChreConnectionCallback,
+      public ::android::hardware::contexthub::DebugDumpHelper {
  public:
   /** The entry point of death recipient for a disconnected client. */
   static void onClientDied(void *cookie);
 
+  MultiClientContextHubBase();
+
   // functions implementing IContextHub
   ScopedAStatus getContextHubs(
       std::vector<ContextHubInfo> *contextHubInfos) override;
@@ -86,12 +78,20 @@
   ScopedAStatus onNanSessionStateChanged(
       const NanSessionStateUpdate &in_update) override;
   ScopedAStatus setTestMode(bool enable) override;
+  ScopedAStatus sendMessageDeliveryStatusToHub(
+      int32_t contextHubId,
+      const MessageDeliveryStatus &messageDeliveryStatus) override;
 
   // The callback function implementing ChreConnectionCallback
   void handleMessageFromChre(const unsigned char *messageBuffer,
                              size_t messageLen) override;
   void onChreRestarted() override;
 
+  // The functions for dumping debug information
+  binder_status_t dump(int fd, const char **args, uint32_t numArgs) override;
+  bool requestDebugDump() override;
+  void writeToDebugFile(const char *str) override;
+
  protected:
   // The data needed by the death client to clear states of a client.
   struct HalDeathRecipientCookie {
@@ -102,7 +102,15 @@
       this->clientPid = pid;
     }
   };
-  MultiClientContextHubBase() = default;
+
+  static constexpr uint32_t kDefaultTestModeTransactionId = 0x80000000;
+
+  void tryTimeSync(size_t numOfRetries, useconds_t retryDelayUs) {
+    if (mConnection->isTimeSyncNeeded()) {
+      TimeSyncer::sendTimeSyncWithRetry(mConnection.get(), numOfRetries,
+                                        retryDelayUs);
+    }
+  }
 
   bool sendFragmentedLoadRequest(HalClientId clientId,
                                  FragmentedLoadRequest &fragmentedLoadRequest);
@@ -117,14 +125,21 @@
       const ::chre::fbs::UnloadNanoappResponseT &response,
       HalClientId clientId);
   void onNanoappMessage(const ::chre::fbs::NanoappMessageT &message);
-
+  void onDebugDumpData(const ::chre::fbs::DebugDumpDataT &data);
+  void onDebugDumpComplete(
+      const ::chre::fbs::DebugDumpResponseT & /* response */);
   void handleClientDeath(pid_t pid);
 
+  bool enableTestMode();
+  bool disableTestMode();
+
   inline bool isSettingEnabled(Setting setting) {
     return mSettingEnabled.find(setting) != mSettingEnabled.end() &&
            mSettingEnabled[setting];
   }
 
+  HalClientManager::DeadClientUnlinker mDeadClientUnlinker;
+
   // HAL is the unique owner of the communication channel to CHRE.
   std::unique_ptr<ChreConnection> mConnection{};
 
@@ -151,6 +166,18 @@
   // A mutex to synchronize access to the list of preloaded nanoapp IDs.
   std::mutex mPreloadedNanoappIdsMutex;
   std::optional<std::vector<uint64_t>> mPreloadedNanoappIds{};
+
+  // test mode settings
+  std::mutex mTestModeMutex;
+  std::condition_variable mEnableTestModeCv;
+  bool mIsTestModeEnabled = false;
+  std::optional<bool> mTestModeSyncUnloadResult = std::nullopt;
+  std::optional<std::unordered_set<uint64_t>> mTestModeNanoapps =
+      std::unordered_set<uint64_t>{};
+  int32_t mTestModeTransactionId =
+      static_cast<int32_t>(kDefaultTestModeTransactionId);
+
+  EventLogger mEventLogger;
 };
 }  // namespace android::hardware::contexthub::common::implementation
 #endif  // ANDROID_HARDWARE_CONTEXTHUB_COMMON_MULTICLIENTS_HAL_BASE_H_
diff --git a/host/test/hal_generic/common/hal_client_manager_test.cc b/host/test/hal_generic/common/hal_client_manager_test.cc
new file mode 100644
index 0000000..b8e7a5f
--- /dev/null
+++ b/host/test/hal_generic/common/hal_client_manager_test.cc
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2023 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 <array>
+#include <chrono>
+#include <cstdlib>
+#include <fstream>
+#include <thread>
+
+#include <json/json.h>
+
+#include <aidl/android/hardware/contexthub/BnContextHubCallback.h>
+#include <aidl/android/hardware/contexthub/IContextHub.h>
+#include <aidl/android/hardware/contexthub/NanoappBinary.h>
+#include "chre/platform/shared/host_protocol_common.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "hal_client_manager.h"
+
+namespace android::hardware::contexthub::common::implementation {
+
+namespace {
+using aidl::android::hardware::contexthub::AsyncEventType;
+using aidl::android::hardware::contexthub::BnContextHubCallback;
+using aidl::android::hardware::contexthub::ContextHubMessage;
+using aidl::android::hardware::contexthub::MessageDeliveryStatus;
+using aidl::android::hardware::contexthub::NanoappInfo;
+using aidl::android::hardware::contexthub::NanSessionRequest;
+
+using ndk::ScopedAStatus;
+
+using ::testing::_;
+using ::testing::ByMove;
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::SizeIs;
+using ::testing::UnorderedElementsAre;
+
+using HalClient = HalClientManager::HalClient;
+
+constexpr pid_t kSystemServerPid = 1000;
+// The uuid assigned to ContextHubService
+const std::string kSystemServerUuid = "9a17008d6bf1445a90116d21bd985b6c";
+
+constexpr pid_t kVendorPid = 1001;
+const std::string kVendorUuid = "6e406b36cf4f4c0d8183db3708f45d8f";
+
+const std::string kClientIdMappingFilePath = "./chre_hal_clients.json";
+const std::string kClientName = "HalClientManagerTest";
+
+class ContextHubCallbackForTest : public BnContextHubCallback {
+ public:
+  explicit ContextHubCallbackForTest(const std::string &uuid) {
+    assert(uuid.length() == 32);  // 2 digits for one bytes x 16 bytes
+    for (int i = 0; i < 16; i++) {
+      mUuid[i] = strtol(uuid.substr(i * 2, 2).c_str(), /* end_ptr= */ nullptr,
+                        /* base= */ 16);
+    }
+    ON_CALL(*this, handleContextHubAsyncEvent(_))
+        .WillByDefault(Return(ByMove(ScopedAStatus::ok())));
+  }
+  ScopedAStatus handleNanoappInfo(
+      const std::vector<NanoappInfo> & /*appInfo*/) override {
+    return ScopedAStatus::ok();
+  }
+
+  ScopedAStatus handleContextHubMessage(
+      const ContextHubMessage & /*message*/,
+      const std::vector<std::string> & /*msgContentPerms*/) override {
+    return ScopedAStatus::ok();
+  }
+
+  MOCK_METHOD(ScopedAStatus, handleContextHubAsyncEvent, (AsyncEventType event),
+              (override));
+
+  // Called after loading/unloading a nanoapp.
+  ScopedAStatus handleTransactionResult(int32_t /*transactionId*/,
+                                        bool /*success*/) override {
+    return ScopedAStatus::ok();
+  }
+
+  ScopedAStatus handleNanSessionRequest(
+      const NanSessionRequest & /* request */) override {
+    return ScopedAStatus::ok();
+  }
+
+  ScopedAStatus handleMessageDeliveryStatus(
+      char16_t /* hostEndPointId */,
+      const MessageDeliveryStatus & /* messageDeliveryStatus */) override {
+    return ScopedAStatus::ok();
+  }
+
+  ScopedAStatus getUuid(std::array<uint8_t, 16> *out_uuid) override {
+    *out_uuid = mUuid;
+    return ScopedAStatus::ok();
+  }
+
+  ScopedAStatus getName(std::string *out_name) override {
+    *out_name = kClientName;
+    return ScopedAStatus::ok();
+  }
+
+ private:
+  const std::string kClientName = "HalClientManagerUnitTest";
+  std::array<uint8_t, 16> mUuid{};
+};
+
+class HalClientManagerForTest : public HalClientManager {
+ public:
+  HalClientManagerForTest(
+      DeadClientUnlinker deadClientUnlinker,
+      const std::string &clientIdMappingFilePath,
+      const std::unordered_set<HalClientId> &reservedClientIds = {})
+      : HalClientManager(std::move(deadClientUnlinker), clientIdMappingFilePath,
+                         reservedClientIds) {}
+
+  const std::vector<HalClient> getClients() {
+    return mClients;
+  }
+
+  bool createClientForTest(const std::string &uuid, pid_t pid) {
+    // No need to hold the lock during a unit test which is single-threaded
+    std::shared_ptr<ContextHubCallbackForTest> callback =
+        ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+            kSystemServerUuid);
+    return createClientLocked(uuid, pid, callback,
+                              /* deathRecipientCookie= */ nullptr);
+  }
+
+  HalClientId getNextClientId() {
+    return mNextClientId;
+  }
+
+  static int64_t getTransactionTimeoutSeconds() {
+    return kTransactionTimeoutThresholdMs / 1000;
+  }
+
+  static const char *getClientIdTag() {
+    return kJsonClientId;
+  }
+
+  static const char *getUuidTag() {
+    return kJsonUuid;
+  }
+
+  static const char *getNameTag() {
+    return kJsonName;
+  }
+};
+
+class HalClientManagerTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    // Clears out the mapping file content
+    std::ofstream file(kClientIdMappingFilePath);
+    ASSERT_TRUE(file.good());
+  }
+  void TearDown() override {}
+};
+
+auto mockDeadClientUnlinker =
+    [](const std::shared_ptr<IContextHubCallback> & /*callback*/,
+       void * /*deathRecipientCookie*/) { return true; };
+
+std::unique_ptr<FragmentedLoadTransaction> createLoadTransaction(
+    uint32_t transactionId) {
+  uint64_t appId = 0x476f6f676cabcdef;
+  uint32_t appVersion = 2;
+  uint32_t appFlags = 3;
+  uint32_t targetApiVersion = 4;
+  std::vector<uint8_t> binary = {0xf0, 0xf1};
+  return std::make_unique<FragmentedLoadTransaction>(
+      transactionId, appId, appVersion, appFlags, targetApiVersion, binary,
+      /* fragmentSize= */ 2048);
+}
+
+TEST_F(HalClientManagerTest, ClientIdMappingFile) {
+  HalClientId systemClientId = 100;
+  {
+    // Write systemClientId into the mapping file
+    Json::Value mappings;
+    Json::Value mapping;
+    mapping[HalClientManagerForTest::getClientIdTag()] = systemClientId;
+    mapping[HalClientManagerForTest::getUuidTag()] = kSystemServerUuid;
+    mapping[HalClientManagerForTest::getNameTag()] = kClientName;
+    mappings.append(mapping);
+    Json::StreamWriterBuilder factory;
+    std::unique_ptr<Json::StreamWriter> const writer(factory.newStreamWriter());
+    std::ofstream fileStream(kClientIdMappingFilePath);
+    writer->write(mappings, &fileStream);
+    fileStream << std::endl;
+  }
+
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  EXPECT_TRUE(
+      halClientManager->registerCallback(kSystemServerPid, callback,
+                                         /* deathRecipientCookie= */ nullptr));
+
+  std::vector<HalClient> clients = halClientManager->getClients();
+  const HalClient &client = clients.front();
+  EXPECT_THAT(clients, SizeIs(1));
+  EXPECT_THAT(client.endpointIds, IsEmpty());
+  EXPECT_EQ(client.callback, callback);
+  EXPECT_EQ(client.uuid, kSystemServerUuid);
+  EXPECT_EQ(client.pid, kSystemServerPid);
+  // The client id allocated should be the one specified in the mapping file
+  EXPECT_EQ(client.clientId, systemClientId);
+}
+
+TEST_F(HalClientManagerTest, CallbackRegistryBasic) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+
+  EXPECT_TRUE(
+      halClientManager->registerCallback(kSystemServerPid, callback,
+                                         /* deathRecipientCookie= */ nullptr));
+
+  std::vector<HalClient> clients = halClientManager->getClients();
+  const HalClient &client = clients.front();
+
+  EXPECT_THAT(clients, SizeIs(1));
+  EXPECT_THAT(client.endpointIds, IsEmpty());
+  EXPECT_EQ(client.callback, callback);
+  EXPECT_EQ(client.uuid, kSystemServerUuid);
+  EXPECT_EQ(client.pid, kSystemServerPid);
+  EXPECT_NE(client.clientId, ::chre::kHostClientIdUnspecified);
+}
+
+TEST_F(HalClientManagerTest, CallbackRegistryTwiceFromSameClient) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callbackA =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  std::shared_ptr<ContextHubCallbackForTest> callbackB =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+
+  EXPECT_TRUE(
+      halClientManager->registerCallback(kSystemServerPid, callbackA,
+                                         /* deathRecipientCookie= */ nullptr));
+  EXPECT_THAT(halClientManager->getClients(), SizeIs(1));
+  EXPECT_EQ(halClientManager->getClients().front().callback, callbackA);
+  // Same client can override its callback
+  EXPECT_TRUE(
+      halClientManager->registerCallback(kSystemServerPid, callbackB,
+                                         /* deathRecipientCookie= */ nullptr));
+  EXPECT_THAT(halClientManager->getClients(), SizeIs(1));
+  EXPECT_EQ(halClientManager->getClients().front().callback, callbackB);
+}
+
+TEST_F(HalClientManagerTest, CallbackRetrievalByEndpoint) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> systemCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  std::shared_ptr<ContextHubCallbackForTest> vendorCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(kVendorUuid);
+  uint16_t vendorEndpointId = 1;
+  uint16_t systemServerEndpointId = 1;
+
+  // Register the callbacks and endpoint ids
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, systemCallback, /* deathRecipientCookie= */ nullptr));
+  EXPECT_TRUE(halClientManager->registerEndpointId(kSystemServerPid,
+                                                   systemServerEndpointId));
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kVendorPid, vendorCallback, /* deathRecipientCookie= */ nullptr));
+  EXPECT_TRUE(
+      halClientManager->registerEndpointId(kVendorPid, vendorEndpointId));
+
+  // Though endpoint ids have the same value, they should be mutated before
+  // getting sent to CHRE and mapped to different callbacks
+  EXPECT_TRUE(halClientManager->mutateEndpointIdFromHostIfNeeded(
+      kVendorPid, vendorEndpointId));
+  EXPECT_TRUE(halClientManager->mutateEndpointIdFromHostIfNeeded(
+      kSystemServerPid, systemServerEndpointId));
+  EXPECT_EQ(halClientManager->getCallbackForEndpoint(vendorEndpointId),
+            vendorCallback);
+  EXPECT_EQ(halClientManager->getCallbackForEndpoint(systemServerEndpointId),
+            systemCallback);
+}
+
+TEST_F(HalClientManagerTest, ClientCreation) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  int uuid = 1;
+  int pid = 1;
+  for (int i = 0; i < kMaxNumOfHalClients; i++, uuid++, pid++) {
+    EXPECT_TRUE(
+        halClientManager->createClientForTest(std::to_string(uuid), pid));
+  }
+  // if max number of clients are reached no more client can be created
+  EXPECT_FALSE(
+      halClientManager->createClientForTest(std::to_string(uuid), pid));
+  // mNextClientId is reset to ::chre::kHostClientIdUnspecified when new client
+  // is not accepted
+  EXPECT_EQ(halClientManager->getNextClientId(),
+            ::chre::kHostClientIdUnspecified);
+}
+
+TEST_F(HalClientManagerTest, ClientCreationWithReservedClientId) {
+  std::unordered_set<HalClientId> reservedClientIds{
+      ::chre::kHostClientIdUnspecified + 1, 64};
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath, reservedClientIds);
+  int uuid = 1;
+  int pid = 1;
+  for (int i = 0; i < kMaxNumOfHalClients - reservedClientIds.size();
+       i++, uuid++, pid++) {
+    EXPECT_TRUE(
+        halClientManager->createClientForTest(std::to_string(uuid), pid));
+  }
+  // if max number of clients are reached no more client can be created
+  EXPECT_FALSE(
+      halClientManager->createClientForTest(std::to_string(uuid), pid));
+  // mNextClientId is reset to ::chre::kHostClientIdUnspecified when new client
+  // is not accepted
+  EXPECT_EQ(halClientManager->getNextClientId(),
+            ::chre::kHostClientIdUnspecified);
+  // Verify that every reserved client id is not used:
+  for (const HalClient &client : halClientManager->getClients()) {
+    EXPECT_EQ(reservedClientIds.find(client.clientId), reservedClientIds.end());
+  }
+}
+
+TEST_F(HalClientManagerTest, TransactionRegistryAndOverridden) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, callback, /* deathRecipientCookie= */ nullptr));
+
+  EXPECT_TRUE(halClientManager->registerPendingLoadTransaction(
+      kSystemServerPid, createLoadTransaction(/* transactionId= */ 1)));
+
+  // Immediate transaction overridden is not allowed as each transaction is
+  // given a certain amount of time to finish
+  EXPECT_FALSE(halClientManager->registerPendingLoadTransaction(
+      kSystemServerPid, createLoadTransaction(/* transactionId= */ 2)));
+
+  // Wait until the transaction is timed out to override it
+  std::this_thread::sleep_for(std::chrono::seconds(
+      HalClientManagerForTest::getTransactionTimeoutSeconds()));
+  EXPECT_TRUE(halClientManager->registerPendingLoadTransaction(
+      kSystemServerPid, createLoadTransaction(/* transactionId= */ 3)));
+}
+
+TEST_F(HalClientManagerTest, TransactionRegistryLoadAndUnload) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, callback, /* deathRecipientCookie= */ nullptr));
+
+  EXPECT_TRUE(halClientManager->registerPendingUnloadTransaction(
+      kSystemServerPid, /* transactionId= */ 1));
+
+  // Load and unload transaction can't coexist because unloading a nanoapp that
+  // is being loaded can cause problems.
+  EXPECT_FALSE(halClientManager->registerPendingLoadTransaction(
+      kSystemServerPid, createLoadTransaction(/* transactionId= */ 2)));
+
+  // Clears out the pending unload transaction to register a new one.
+  halClientManager->resetPendingUnloadTransaction(
+      halClientManager->getClientId(kSystemServerPid), /* transactionId= */ 1);
+  EXPECT_TRUE(halClientManager->registerPendingLoadTransaction(
+      kSystemServerPid, createLoadTransaction(/* transactionId= */ 2)));
+}
+
+TEST_F(HalClientManagerTest, EndpointRegistry) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> systemCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  std::shared_ptr<ContextHubCallbackForTest> vendorCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(kVendorUuid);
+
+  halClientManager->registerCallback(kSystemServerPid, systemCallback,
+                                     /* deathRecipientCookie= */ nullptr);
+  halClientManager->registerCallback(kVendorPid, vendorCallback,
+                                     /* deathRecipientCookie= */ nullptr);
+
+  std::vector<HalClient> clients = halClientManager->getClients();
+  EXPECT_THAT(clients, SizeIs(2));
+  // only system server can register endpoint ids > 63.
+
+  EXPECT_TRUE(halClientManager->registerEndpointId(kSystemServerPid,
+                                                   /* endpointId= */ 64));
+  EXPECT_TRUE(halClientManager->registerEndpointId(kVendorPid,
+                                                   /*endpointId= */ 63));
+  EXPECT_FALSE(halClientManager->registerEndpointId(kVendorPid,
+                                                    /* endpointId= */ 64));
+}
+
+TEST_F(HalClientManagerTest, EndpointIdMutationForVendorClient) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> vendorCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(kVendorUuid);
+  std::shared_ptr<ContextHubCallbackForTest> systemCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  const uint16_t originalEndpointId = 10;  // 0b1010;
+  uint16_t mutatedEndpointId = originalEndpointId;
+
+  // Register the system callback
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, systemCallback, /* deathRecipientCookie= */ nullptr));
+  // Register the vendor callback
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kVendorPid, vendorCallback, /* deathRecipientCookie= */ nullptr));
+
+  // Mutate endpoint id from host to CHRE
+  EXPECT_TRUE(halClientManager->mutateEndpointIdFromHostIfNeeded(
+      kVendorPid, mutatedEndpointId));
+  HalClientId clientId = halClientManager->getClientId(kVendorPid);
+  EXPECT_EQ(mutatedEndpointId, 0x8000 | clientId << 6 | originalEndpointId);
+
+  // Mutate endpoint id from CHRE to Host
+  EXPECT_EQ(halClientManager->convertToOriginalEndpointId(mutatedEndpointId),
+            originalEndpointId);
+}
+
+TEST_F(HalClientManagerTest, EndpointIdMutationForSystemServer) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  const uint16_t originalEndpointId = 100;
+  uint16_t mutatedEndpointId = originalEndpointId;
+
+  // Register the callback
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, callback, /* deathRecipientCookie= */ nullptr));
+
+  // Endpoint id from the system server shouldn't be mutated
+  EXPECT_TRUE(halClientManager->mutateEndpointIdFromHostIfNeeded(
+      kSystemServerPid, mutatedEndpointId));
+  EXPECT_EQ(mutatedEndpointId, originalEndpointId);
+  EXPECT_EQ(halClientManager->convertToOriginalEndpointId(mutatedEndpointId),
+            originalEndpointId);
+}
+
+TEST_F(HalClientManagerTest, EndpointIdUnknownFromChre) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> vendorCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(kVendorUuid);
+  std::shared_ptr<ContextHubCallbackForTest> systemCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  const HostEndpointId originalEndpointId = 0x10;  // unregistered endpoint id
+  HostEndpointId mutatedEndpointId = originalEndpointId;
+
+  // Register the callback
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, systemCallback, /* deathRecipientCookie= */ nullptr));
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kVendorPid, vendorCallback, /* deathRecipientCookie= */ nullptr));
+
+  // As long as a client's callback is registered, hal_client_manager won't
+  // block message exchanged from/to the client even if the endpoint id is
+  // not registered. The enforcement of endpoint id registration is done on the
+  // client side (contextHubService, library, etc.).
+  EXPECT_TRUE(halClientManager->mutateEndpointIdFromHostIfNeeded(
+      kVendorPid, mutatedEndpointId));
+  EXPECT_NE(mutatedEndpointId, originalEndpointId);
+  EXPECT_EQ(halClientManager->convertToOriginalEndpointId(mutatedEndpointId),
+            originalEndpointId);
+  EXPECT_EQ(halClientManager->getCallbackForEndpoint(mutatedEndpointId),
+            vendorCallback);
+}
+
+TEST_F(HalClientManagerTest, handleDeathClient) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> callback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  halClientManager->registerCallback(kSystemServerPid, callback,
+                                     /* deathRecipientCookie= */ nullptr);
+  halClientManager->registerEndpointId(kSystemServerPid, /* endpointId= */ 10);
+
+  halClientManager->handleClientDeath(kSystemServerPid);
+
+  const std::vector<HalClient> &clients = halClientManager->getClients();
+  EXPECT_THAT(clients, SizeIs(1));
+  const HalClient &client = clients.front();
+  EXPECT_EQ(client.callback, nullptr);
+  EXPECT_EQ(client.pid, HalClient::PID_UNSET);
+  EXPECT_EQ(client.uuid, kSystemServerUuid);
+  EXPECT_NE(client.clientId, ::chre::kHostClientIdUnspecified);
+  EXPECT_THAT(client.endpointIds, IsEmpty());
+}
+
+TEST_F(HalClientManagerTest, handleChreRestartForConnectedClientsOnly) {
+  auto halClientManager = std::make_unique<HalClientManagerForTest>(
+      mockDeadClientUnlinker, kClientIdMappingFilePath);
+  std::shared_ptr<ContextHubCallbackForTest> vendorCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(kVendorUuid);
+  std::shared_ptr<ContextHubCallbackForTest> systemCallback =
+      ContextHubCallbackForTest::make<ContextHubCallbackForTest>(
+          kSystemServerUuid);
+  // Register the system callback
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kSystemServerPid, systemCallback, /* deathRecipientCookie= */ nullptr));
+  // Register the vendor callback
+  EXPECT_TRUE(halClientManager->registerCallback(
+      kVendorPid, vendorCallback, /* deathRecipientCookie= */ nullptr));
+
+  // Only connected clients' handleContextHubAsyncEvent should be called.
+  EXPECT_CALL(*systemCallback,
+              handleContextHubAsyncEvent(AsyncEventType::RESTARTED));
+  EXPECT_CALL(*vendorCallback,
+              handleContextHubAsyncEvent(AsyncEventType::RESTARTED))
+      .Times(0);
+
+  // Disconnect the vendor client and handle CHRE restart for the system server
+  halClientManager->handleClientDeath(kVendorPid);
+  halClientManager->handleChreRestart();
+}
+
+}  // namespace
+}  // namespace android::hardware::contexthub::common::implementation
diff --git a/host/test/hal_generic/common/hal_client_test.cc b/host/test/hal_generic/common/hal_client_test.cc
new file mode 100644
index 0000000..eac5bd8
--- /dev/null
+++ b/host/test/hal_generic/common/hal_client_test.cc
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 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 "chre_host/hal_client.h"
+#include "host/hal_generic/common/hal_error.h"
+
+#include <unordered_set>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include <aidl/android/hardware/contexthub/IContextHub.h>
+
+namespace android::chre {
+
+namespace {
+using ::aidl::android::hardware::contexthub::ContextHubMessage;
+using ::aidl::android::hardware::contexthub::HostEndpointInfo;
+using ::aidl::android::hardware::contexthub::IContextHub;
+using ::aidl::android::hardware::contexthub::IContextHubCallbackDefault;
+using ::aidl::android::hardware::contexthub::IContextHubDefault;
+
+using ::ndk::ScopedAStatus;
+
+using ::testing::_;
+using ::testing::ByMove;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAre;
+
+using HostEndpointId = char16_t;
+constexpr HostEndpointId kEndpointId = 0x10;
+
+class HalClientForTest : public HalClient {
+ public:
+  HalClientForTest(const std::shared_ptr<IContextHub> &contextHub,
+                   const std::vector<HostEndpointId> &connectedEndpoints,
+                   const std::shared_ptr<IContextHubCallback> &callback =
+                       ndk::SharedRefBase::make<IContextHubCallbackDefault>())
+      : HalClient(callback) {
+    mContextHub = contextHub;
+    for (const HostEndpointId &endpointId : connectedEndpoints) {
+      mConnectedEndpoints[endpointId] = {.hostEndpointId = endpointId};
+    }
+  }
+
+  std::unordered_set<HostEndpointId> getConnectedEndpointIds() {
+    std::unordered_set<HostEndpointId> result{};
+    for (const auto &[endpointId, unusedEndpointInfo] : mConnectedEndpoints) {
+      result.insert(endpointId);
+    }
+    return result;
+  }
+
+  HalClientCallback *getClientCallback() {
+    return mCallback.get();
+  }
+};
+
+class MockContextHub : public IContextHubDefault {
+ public:
+  MOCK_METHOD(ScopedAStatus, onHostEndpointConnected,
+              (const HostEndpointInfo &info), (override));
+  MOCK_METHOD(ScopedAStatus, onHostEndpointDisconnected,
+              (HostEndpointId endpointId), (override));
+  MOCK_METHOD(ScopedAStatus, queryNanoapps, (int32_t icontextHubId),
+              (override));
+  MOCK_METHOD(ScopedAStatus, sendMessageToHub,
+              (int32_t contextHubId, const ContextHubMessage &message),
+              (override));
+};
+
+}  // namespace
+
+TEST(HalClientTest, EndpointConnectionBasic) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+  const HostEndpointInfo kInfo = {
+      .hostEndpointId = kEndpointId,
+      .type = HostEndpointInfo::Type::NATIVE,
+      .packageName = "HalClientTest",
+      .attributionTag{},
+  };
+
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub, std::vector<HostEndpointId>{});
+  EXPECT_THAT(halClient->getConnectedEndpointIds(), IsEmpty());
+
+  EXPECT_CALL(*mockContextHub,
+              onHostEndpointConnected(
+                  Field(&HostEndpointInfo::hostEndpointId, kEndpointId)))
+      .WillOnce(Return(ScopedAStatus::ok()));
+
+  halClient->connectEndpoint(kInfo);
+  EXPECT_THAT(halClient->getConnectedEndpointIds(),
+              UnorderedElementsAre(kEndpointId));
+}
+
+TEST(HalClientTest, EndpointConnectionMultipleRequests) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+  const HostEndpointInfo kInfo = {
+      .hostEndpointId = kEndpointId,
+      .type = HostEndpointInfo::Type::NATIVE,
+      .packageName = "HalClientTest",
+      .attributionTag{},
+  };
+
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub, std::vector<HostEndpointId>{});
+  EXPECT_THAT(halClient->getConnectedEndpointIds(), IsEmpty());
+
+  // multiple requests are tolerated
+  EXPECT_CALL(*mockContextHub,
+              onHostEndpointConnected(
+                  Field(&HostEndpointInfo::hostEndpointId, kEndpointId)))
+      .WillOnce(Return(ScopedAStatus::ok()))
+      .WillOnce(Return(ScopedAStatus::ok()));
+
+  halClient->connectEndpoint(kInfo);
+  halClient->connectEndpoint(kInfo);
+
+  EXPECT_THAT(halClient->getConnectedEndpointIds(),
+              UnorderedElementsAre(kEndpointId));
+}
+
+TEST(HalClientTest, EndpointDisconnectionBasic) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub, std::vector<HostEndpointId>{kEndpointId});
+
+  EXPECT_THAT(halClient->getConnectedEndpointIds(),
+              UnorderedElementsAre(kEndpointId));
+
+  EXPECT_CALL(*mockContextHub, onHostEndpointDisconnected(kEndpointId))
+      .WillOnce(Return(ScopedAStatus::ok()));
+  halClient->disconnectEndpoint(kEndpointId);
+
+  EXPECT_THAT(halClient->getConnectedEndpointIds(), IsEmpty());
+}
+
+TEST(HalClientTest, EndpointDisconnectionMultipleRequest) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub, std::vector<HostEndpointId>{kEndpointId});
+  EXPECT_THAT(halClient->getConnectedEndpointIds(),
+              UnorderedElementsAre(kEndpointId));
+
+  EXPECT_CALL(*mockContextHub, onHostEndpointDisconnected(kEndpointId))
+      .WillOnce(Return(ScopedAStatus::ok()))
+      .WillOnce(Return(ScopedAStatus::ok()));
+
+  halClient->disconnectEndpoint(kEndpointId);
+  halClient->disconnectEndpoint(kEndpointId);
+
+  EXPECT_THAT(halClient->getConnectedEndpointIds(), IsEmpty());
+}
+
+TEST(HalClientTest, SendMessageBasic) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+  const ContextHubMessage contextHubMessage = {
+      .nanoappId = 0xbeef,
+      .hostEndPoint = kEndpointId,
+      .messageBody = {},
+      .permissions = {},
+  };
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub, std::vector<HostEndpointId>{kEndpointId});
+
+  EXPECT_CALL(*mockContextHub, sendMessageToHub(_, _))
+      .WillOnce(Return(ScopedAStatus::ok()));
+  halClient->sendMessage(contextHubMessage);
+}
+
+TEST(HalClientTest, QueryNanoapp) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub, std::vector<HostEndpointId>{});
+
+  EXPECT_CALL(*mockContextHub, queryNanoapps(HalClient::kDefaultContextHubId));
+  halClient->queryNanoapps();
+}
+
+TEST(HalClientTest, HandleChreRestart) {
+  auto mockContextHub = ndk::SharedRefBase::make<MockContextHub>();
+
+  auto halClient = std::make_unique<HalClientForTest>(
+      mockContextHub,
+      std::vector<HostEndpointId>{kEndpointId, kEndpointId + 1});
+
+  EXPECT_CALL(*mockContextHub, onHostEndpointConnected(
+                                   Field(&HostEndpointInfo::hostEndpointId, _)))
+      .WillOnce(Return(ScopedAStatus::ok()))
+      .WillOnce(Return(ScopedAStatus::ok()));
+
+  halClient->getClientCallback()->handleContextHubAsyncEvent(
+      AsyncEventType::RESTARTED);
+  EXPECT_THAT(halClient->getConnectedEndpointIds(),
+              UnorderedElementsAre(kEndpointId, kEndpointId + 1));
+}
+}  // namespace android::chre
diff --git a/host/tinysys/hal/Android.bp b/host/tinysys/hal/Android.bp
index cde85a0..c7ba06a 100644
--- a/host/tinysys/hal/Android.bp
+++ b/host/tinysys/hal/Android.bp
@@ -34,7 +34,7 @@
         "tinysys_chre_connection.cc",
         "tinysys_context_hub.cc",
         ":st_hal_lpma_handler",
-        ":contexthub_generic_aidl_hal_core",
+        ":contexthub_hal_core",
     ],
     include_dirs: [
         "system/chre/util/include/",
@@ -54,7 +54,7 @@
     ],
     shared_libs: [
         "android.media.soundtrigger.types-V1-ndk",
-        "android.hardware.contexthub-V2-ndk",
+        "android.hardware.contexthub-V3-ndk",
         "android.hardware.soundtrigger3-V1-ndk",
         "libcutils",
         "liblog",
@@ -63,6 +63,7 @@
         "libbinder_ndk",
         "libpower",
         "libjsoncpp",
+        "server_configurable_flags",
     ],
     header_libs: [
         "chre_api",
@@ -71,6 +72,7 @@
     ],
     static_libs: [
         "chre_client",
+        "chre_flags_c_lib",
         "event_logger",
         "pw_varint",
         "pw_detokenizer",
diff --git a/host/tinysys/hal/android.hardware.contexthub-service.tinysys.xml b/host/tinysys/hal/android.hardware.contexthub-service.tinysys.xml
index 54d4592..db2e2d7 100644
--- a/host/tinysys/hal/android.hardware.contexthub-service.tinysys.xml
+++ b/host/tinysys/hal/android.hardware.contexthub-service.tinysys.xml
@@ -1,7 +1,7 @@
 <manifest version="1.0" type="device">
     <hal format="aidl">
         <name>android.hardware.contexthub</name>
-        <version>2</version>
+        <version>3</version>
         <fqname>IContextHub/default</fqname>
     </hal>
 </manifest>
diff --git a/host/tinysys/hal/tinysys_chre_connection.cc b/host/tinysys/hal/tinysys_chre_connection.cc
index 3be581c..c560d7b 100644
--- a/host/tinysys/hal/tinysys_chre_connection.cc
+++ b/host/tinysys/hal/tinysys_chre_connection.cc
@@ -21,6 +21,7 @@
 
 #include <hardware_legacy/power.h>
 #include <sys/ioctl.h>
+#include <utils/SystemClock.h>
 #include <cerrno>
 #include <thread>
 
@@ -131,10 +132,13 @@
            chreNextState);
     }
     if (chreCurrentState == SCP_CHRE_STOP && chreNextState == SCP_CHRE_START) {
-      // TODO(b/277128368): We should have an explicit indication from CHRE for
-      // restart recovery.
-      LOGW("SCP restarted. Give it 5s for recovery before notifying clients");
-      std::this_thread::sleep_for(std::chrono::milliseconds(5000));
+      int64_t startTime = ::android::elapsedRealtime();
+      // Though usually CHRE is recovered within 1s after SCP is up, in a corner
+      // case it can go beyond 5s. Wait for 10s to cover more extreme cases.
+      chreConnection->waitChreBackOnline(
+          /* timeoutMs= */ std::chrono::milliseconds(10000));
+      LOGW("SCP restarted! CHRE recover time: %" PRIu64 "ms.",
+           ::android::elapsedRealtime() - startTime);
       chreConnection->getCallback()->onChreRestarted();
     }
     chreCurrentState = chreNextState;
@@ -206,6 +210,10 @@
       chreConnection->getLpmaHandler()->enable(/* enabled= */ false);
       break;
     }
+    case fbs::ChreMessage::PulseResponse: {
+      chreConnection->notifyChreBackOnline();
+      break;
+    }
     case fbs::ChreMessage::MetricLog:
     case fbs::ChreMessage::NanConfigurationRequest:
     case fbs::ChreMessage::TimeSyncRequest:
diff --git a/host/tinysys/hal/tinysys_chre_connection.h b/host/tinysys/hal/tinysys_chre_connection.h
index e42b0c4..32974e0 100644
--- a/host/tinysys/hal/tinysys_chre_connection.h
+++ b/host/tinysys/hal/tinysys_chre_connection.h
@@ -20,6 +20,7 @@
 #include "chre_connection.h"
 #include "chre_connection_callback.h"
 #include "chre_host/fragmented_load_transaction.h"
+#include "chre_host/host_protocol_host.h"
 #include "chre_host/log.h"
 #include "chre_host/log_message_parser.h"
 #include "chre_host/st_hal_lpma_handler.h"
@@ -35,6 +36,7 @@
 namespace aidl::android::hardware::contexthub {
 
 using namespace ::android::hardware::contexthub::common::implementation;
+using ::android::chre::HostProtocolHost;
 
 /** A class handling message transmission between context hub HAL and CHRE. */
 // TODO(b/267188769): We should add comments explaining how IPI works.
@@ -67,6 +69,27 @@
 
   bool sendMessage(void *data, size_t length) override;
 
+  void waitChreBackOnline(std::chrono::milliseconds timeoutMs) {
+    flatbuffers::FlatBufferBuilder builder(48);
+    HostProtocolHost::encodePulseRequest(builder);
+
+    std::unique_lock<std::mutex> lock(mChrePulseMutex);
+    // reset mIsChreRecovered before sending a PulseRequest message
+    mIsChreBackOnline = false;
+    sendMessage(builder.GetBufferPointer(), builder.GetSize());
+    mChrePulseCondition.wait_for(
+        lock, timeoutMs,
+        [&isChreBackOnline = mIsChreBackOnline] { return isChreBackOnline; });
+  }
+
+  void notifyChreBackOnline() {
+    {
+      std::unique_lock<std::mutex> lock(mChrePulseMutex);
+      mIsChreBackOnline = true;
+    }
+    mChrePulseCondition.notify_all();
+  }
+
   inline ChreConnectionCallback *getCallback() {
     return mCallback;
   }
@@ -191,6 +214,12 @@
 
   // For messages sent to CHRE
   SynchronousMessageQueue mQueue;
+
+  // Mutex and CV are used to get PulseResponse from CHRE synchronously.
+  std::mutex mChrePulseMutex;
+  std::condition_variable mChrePulseCondition;
+  bool mIsChreBackOnline =
+      false;  // set to true after CHRE recovers from a restart
 };
 }  // namespace aidl::android::hardware::contexthub
 
diff --git a/host/tinysys/hal/tinysys_context_hub.cc b/host/tinysys/hal/tinysys_context_hub.cc
index 2f54aba..2e290c6 100644
--- a/host/tinysys/hal/tinysys_context_hub.cc
+++ b/host/tinysys/hal/tinysys_context_hub.cc
@@ -18,28 +18,21 @@
 
 namespace aidl::android::hardware::contexthub {
 TinysysContextHub::TinysysContextHub() {
-  mDeathRecipient = ndk::ScopedAIBinder_DeathRecipient(
-      AIBinder_DeathRecipient_new(onClientDied));
-  AIBinder_DeathRecipient_setOnUnlinked(
-      mDeathRecipient.get(), [](void *cookie) {
-        LOGI("Callback is unlinked. Releasing the death recipient cookie.");
-        delete static_cast<HalDeathRecipientCookie *>(cookie);
-      });
   mConnection = std::make_unique<TinysysChreConnection>(this);
-  mHalClientManager = std::make_unique<HalClientManager>();
+  mHalClientManager = std::make_unique<HalClientManager>(
+      mDeadClientUnlinker, kClientIdMappingFilePath);
   mPreloadedNanoappLoader = std::make_unique<PreloadedNanoappLoader>(
       mConnection.get(), kPreloadedNanoappsConfigPath);
   if (mConnection->init()) {
-    if (!kPreloadedNanoappsConfigPath.empty()) {
-      mPreloadedNanoappLoader->loadPreloadedNanoapps();
-    }
+    mPreloadedNanoappLoader->loadPreloadedNanoapps();
+  } else {
+    LOGE("Failed to initialize the connection to CHRE. Restart.");
+    exit(-1);
   }
 }
 
 void TinysysContextHub::onChreRestarted() {
-  if (!kPreloadedNanoappsConfigPath.empty()) {
-    mPreloadedNanoappLoader->loadPreloadedNanoapps();
-  }
+  mPreloadedNanoappLoader->loadPreloadedNanoapps();
   MultiClientContextHubBase::onChreRestarted();
 }
 }  // namespace aidl::android::hardware::contexthub
\ No newline at end of file
diff --git a/host/tinysys/hal/tinysys_context_hub.h b/host/tinysys/hal/tinysys_context_hub.h
index 4be2d93..2fbf1bb 100644
--- a/host/tinysys/hal/tinysys_context_hub.h
+++ b/host/tinysys/hal/tinysys_context_hub.h
@@ -36,6 +36,8 @@
   void onChreRestarted() override;
   const std::string kPreloadedNanoappsConfigPath =
       "/vendor/etc/chre/preloaded_nanoapps.json";
+  const std::string kClientIdMappingFilePath =
+      "/data/vendor/chre/chre_hal_clients.json";
 };
 }  // namespace aidl::android::hardware::contexthub
 #endif  // ANDROID_HARDWARE_CONTEXTHUB_AIDL_CONTEXTHUB_H
diff --git a/java/test/audio_concurrency/Android.bp b/java/test/audio_concurrency/Android.bp
index 526c67e..50b09a2 100644
--- a/java/test/audio_concurrency/Android.bp
+++ b/java/test/audio_concurrency/Android.bp
@@ -34,5 +34,5 @@
         "chre-test-utils",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/audio_concurrency/src/com/google/android/chre/test/audioconcurrency/ContextHubAudioConcurrencyTestExecutor.java b/java/test/audio_concurrency/src/com/google/android/chre/test/audioconcurrency/ContextHubAudioConcurrencyTestExecutor.java
index b534586..b2aae5e 100644
--- a/java/test/audio_concurrency/src/com/google/android/chre/test/audioconcurrency/ContextHubAudioConcurrencyTestExecutor.java
+++ b/java/test/audio_concurrency/src/com/google/android/chre/test/audioconcurrency/ContextHubAudioConcurrencyTestExecutor.java
@@ -25,6 +25,8 @@
 import android.media.AudioFormat;
 import android.media.AudioRecord;
 import android.media.MediaRecorder;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.util.Log;
 
 import com.google.android.chre.nanoapp.proto.ChreAudioConcurrencyTest;
@@ -65,6 +67,8 @@
 
     private boolean mInitialized = false;
 
+    private boolean mVerifyAudioGaps = false;
+
     private final AtomicBoolean mChreAudioEnabled = new AtomicBoolean(false);
 
     private final AtomicReference<ChreTestCommon.TestResult> mTestResult = new AtomicReference<>();
@@ -132,21 +136,24 @@
         Assert.assertFalse("init() must not be invoked when already initialized", mInitialized);
         ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHubInfo, mNanoAppBinary);
 
+        mVerifyAudioGaps = shouldVerifyAudioGaps();
         mInitialized = true;
     }
 
     /**
      * Runs the test.
      */
-    public void run() {
+    public void run() throws InterruptedException {
         // Send a message to the nanoapp to enable CHRE audio
         mCountDownLatch = new CountDownLatch(1);
-        sendTestCommandMessage(ChreAudioConcurrencyTest.TestCommand.Step.ENABLE_AUDIO);
-        try {
-            mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
+        if (mVerifyAudioGaps) {
+            sendTestCommandMessage(
+                    ChreAudioConcurrencyTest.TestCommand.Step.ENABLE_AUDIO_WITH_GAP_VERIFICATION);
+        } else {
+            sendTestCommandMessage(ChreAudioConcurrencyTest.TestCommand.Step.ENABLE_AUDIO);
         }
+        boolean success = mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: ENABLE_AUDIO", success);
         Assert.assertTrue("Failed to enable CHRE audio",
                 mChreAudioEnabled.get() || mTestResult.get() != null);
 
@@ -155,11 +162,8 @@
         // Send a message to the nanoapp to verify that CHRE audio resumes
         mCountDownLatch = new CountDownLatch(1);
         sendTestCommandMessage(ChreAudioConcurrencyTest.TestCommand.Step.VERIFY_AUDIO_RESUME);
-        try {
-            mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
-        }
+        success = mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: VERIFY_AUDIO_RESUME", success);
 
         if (mTestResult.get() == null) {
             Assert.fail("No test result received");
@@ -231,4 +235,14 @@
             mCountDownLatch.countDown();
         }
     }
+
+    /**
+     * Returns whether we should verify audio gaps. This is only supported on devices
+     * that are currently running Android U or later and were shipped with Android U
+     * or later.
+     */
+    private boolean shouldVerifyAudioGaps() {
+        return VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE &&
+               VERSION.DEVICE_INITIAL_SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE;
+    }
 }
diff --git a/java/test/ble_concurrency/src/com/google/android/chre/test/bleconcurrency/ContextHubBleConcurrencyTestExecutor.java b/java/test/ble_concurrency/src/com/google/android/chre/test/bleconcurrency/ContextHubBleConcurrencyTestExecutor.java
index b961f64..cd1db4d 100644
--- a/java/test/ble_concurrency/src/com/google/android/chre/test/bleconcurrency/ContextHubBleConcurrencyTestExecutor.java
+++ b/java/test/ble_concurrency/src/com/google/android/chre/test/bleconcurrency/ContextHubBleConcurrencyTestExecutor.java
@@ -16,85 +16,18 @@
 
 package com.google.android.chre.test.bleconcurrency;
 
-import static com.google.common.truth.Truth.assertThat;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
 import android.hardware.location.NanoAppBinary;
 
-import com.google.android.chre.test.chqts.ContextHubChreApiTestExecutor;
-import com.google.android.utils.chre.ChreApiTestUtil;
-import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ByteString;
-
-import org.junit.Assert;
-
-import java.util.HexFormat;
-import java.util.List;
-
-import dev.chre.rpc.proto.ChreApiTest;
+import com.google.android.chre.test.chqts.ContextHubBleTestExecutor;
 
 /**
  * A class that can execute the CHRE BLE concurrency test.
  */
-public class ContextHubBleConcurrencyTestExecutor extends ContextHubChreApiTestExecutor {
+public class ContextHubBleConcurrencyTestExecutor extends ContextHubBleTestExecutor {
     private static final String TAG = "ContextHubBleConcurrencyTestExecutor";
 
-    /**
-     * The delay to report results in milliseconds.
-     */
-    private static final int REPORT_DELAY_MS = 0;
-
-    /**
-     * The RSSI threshold for the BLE scan filter.
-     */
-    private static final int RSSI_THRESHOLD = -128;
-
-    /**
-     * The advertisement type for service data.
-     */
-    private static final int CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 = 0x16;
-
-    /**
-     * CHRE BLE capabilities and filter capabilities.
-     */
-    private static final int CHRE_BLE_CAPABILITIES_SCAN = 1 << 0;
-    private static final int CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA = 1 << 7;
-
-    private BluetoothLeScanner mBluetoothLeScanner = null;
-
-    private final ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onBatchScanResults(List<ScanResult> results) {
-            // do nothing
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-            Assert.fail("Failed to start a BLE scan on the host");
-        }
-
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-            // do nothing
-        }
-    };
-
     public ContextHubBleConcurrencyTestExecutor(NanoAppBinary nanoapp) {
         super(nanoapp);
-
-        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
-        assertThat(bluetoothManager).isNotNull();
-        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
-        if (bluetoothAdapter != null) {
-            mBluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
-        }
     }
 
     /**
@@ -109,143 +42,6 @@
     }
 
     /**
-     * Generates a BLE scan filter that filters only for the known Google beacons:
-     * Google Eddystone and Nearby Fastpair.
-     */
-    private static ChreApiTest.ChreBleScanFilter getDefaultScanFilter() {
-        ChreApiTest.ChreBleGenericFilter eddystoneFilter =
-                ChreApiTest.ChreBleGenericFilter.newBuilder()
-                        .setType(CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16)
-                        .setLength(2)
-                        .setData(ByteString.copyFrom(HexFormat.of().parseHex("AAFE")))
-                        .setMask(ByteString.copyFrom(HexFormat.of().parseHex("FFFF")))
-                        .build();
-        ChreApiTest.ChreBleGenericFilter nearbyFastpairFilter =
-                ChreApiTest.ChreBleGenericFilter.newBuilder()
-                        .setType(CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16)
-                        .setLength(2)
-                        .setData(ByteString.copyFrom(HexFormat.of().parseHex("2CFE")))
-                        .setMask(ByteString.copyFrom(HexFormat.of().parseHex("FFFF")))
-                        .build();
-
-        return ChreApiTest.ChreBleScanFilter.newBuilder()
-                .setRssiThreshold(RSSI_THRESHOLD)
-                .setScanFilterCount(2)
-                .addScanFilters(eddystoneFilter)
-                .addScanFilters(nearbyFastpairFilter)
-                .build();
-    }
-
-    /**
-     * Generates a BLE scan filter that filters only for the known Google beacons:
-     * Google Eddystone and Nearby Fastpair. We specify the filter data in (little-endian) LE
-     * here as the CHRE code will take BE input and transform it to LE.
-     */
-    private static List<ScanFilter> getDefaultScanFilterHost() {
-        assertThat(CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16)
-                .isEqualTo(ScanRecord.DATA_TYPE_SERVICE_DATA_16_BIT);
-
-        ScanFilter scanFilter = new ScanFilter.Builder()
-                .setAdvertisingDataTypeWithData(
-                        ScanRecord.DATA_TYPE_SERVICE_DATA_16_BIT,
-                        ByteString.copyFrom(HexFormat.of().parseHex("AAFE")).toByteArray(),
-                        ByteString.copyFrom(HexFormat.of().parseHex("FFFF")).toByteArray())
-                .build();
-        ScanFilter scanFilter2 = new ScanFilter.Builder()
-                .setAdvertisingDataTypeWithData(
-                        ScanRecord.DATA_TYPE_SERVICE_DATA_16_BIT,
-                        ByteString.copyFrom(HexFormat.of().parseHex("2CFE")).toByteArray(),
-                        ByteString.copyFrom(HexFormat.of().parseHex("FFFF")).toByteArray())
-                .build();
-
-        return ImmutableList.of(scanFilter, scanFilter2);
-    }
-
-    /**
-     * Starts a BLE scan and asserts it was started successfully in a synchronous manner.
-     * This waits for the event to be received and returns the status in the event.
-     *
-     * @param scanFilter                The scan filter.
-     */
-    private void chreBleStartScanSync(ChreApiTest.ChreBleScanFilter scanFilter) throws Exception {
-        ChreApiTest.ChreBleStartScanAsyncInput.Builder inputBuilder =
-                ChreApiTest.ChreBleStartScanAsyncInput.newBuilder()
-                        .setMode(ChreApiTest.ChreBleScanMode.CHRE_BLE_SCAN_MODE_FOREGROUND)
-                        .setReportDelayMs(REPORT_DELAY_MS)
-                        .setHasFilter(scanFilter != null);
-        if (scanFilter != null) {
-            inputBuilder.setFilter(scanFilter);
-        }
-
-        ChreApiTestUtil util = new ChreApiTestUtil();
-        List<ChreApiTest.GeneralSyncMessage> response =
-                util.callServerStreamingRpcMethodSync(getRpcClient(),
-                        "chre.rpc.ChreApiTestService.ChreBleStartScanSync",
-                        inputBuilder.build());
-        assertThat(response).isNotEmpty();
-        for (ChreApiTest.GeneralSyncMessage status: response) {
-            assertThat(status.getStatus()).isTrue();
-        }
-    }
-
-    /**
-     * Stops a BLE scan and asserts it was started successfully in a synchronous manner.
-     * This waits for the event to be received and returns the status in the event.
-     */
-    private void chreBleStopScanSync() throws Exception {
-        ChreApiTestUtil util = new ChreApiTestUtil();
-        List<ChreApiTest.GeneralSyncMessage> response =
-                util.callServerStreamingRpcMethodSync(getRpcClient(),
-                        "chre.rpc.ChreApiTestService.ChreBleStopScanSync");
-        assertThat(response).isNotEmpty();
-        for (ChreApiTest.GeneralSyncMessage status: response) {
-            assertThat(status.getStatus()).isTrue();
-        }
-    }
-
-    /**
-     * Returns true if the required BLE capabilities and filter capabilities exist,
-     * otherwise returns false.
-     */
-    private boolean doesNecessaryBleCapabilitiesExist() throws Exception {
-        if (mBluetoothLeScanner == null) {
-            return false;
-        }
-
-        ChreApiTest.Capabilities capabilitiesResponse =
-                ChreApiTestUtil.callUnaryRpcMethodSync(getRpcClient(),
-                        "chre.rpc.ChreApiTestService.ChreBleGetCapabilities");
-        int capabilities = capabilitiesResponse.getCapabilities();
-        if ((capabilities & CHRE_BLE_CAPABILITIES_SCAN) != 0) {
-            ChreApiTest.Capabilities filterCapabilitiesResponse =
-                    ChreApiTestUtil.callUnaryRpcMethodSync(getRpcClient(),
-                            "chre.rpc.ChreApiTestService.ChreBleGetFilterCapabilities");
-            int filterCapabilities = filterCapabilitiesResponse.getCapabilities();
-            return (filterCapabilities & CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA) != 0;
-        }
-        return false;
-    }
-
-    /**
-     * Starts a BLE scan on the host side with known Google beacon filters.
-     */
-    private void startBleScanOnHost() {
-        ScanSettings scanSettings = new ScanSettings.Builder()
-                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
-                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-                .build();
-        mBluetoothLeScanner.startScan(getDefaultScanFilterHost(),
-                scanSettings, mScanCallback);
-    }
-
-    /**
-     * Stops a BLE scan on the host side.
-     */
-    private void stopBleScanOnHost() {
-        mBluetoothLeScanner.stopScan(mScanCallback);
-    }
-
-    /**
      * Tests with the host starting scanning first.
      */
     private void testHostScanFirst() throws Exception {
diff --git a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubBleTestExecutor.java b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubBleTestExecutor.java
new file mode 100644
index 0000000..cdb0e98
--- /dev/null
+++ b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubBleTestExecutor.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2023 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.chre.test.chqts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertisingSet;
+import android.bluetooth.le.AdvertisingSetCallback;
+import android.bluetooth.le.AdvertisingSetParameters;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.hardware.location.NanoAppBinary;
+import android.os.ParcelUuid;
+
+import com.google.android.utils.chre.ChreApiTestUtil;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
+import org.junit.Assert;
+
+import java.util.HexFormat;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.chre.rpc.proto.ChreApiTest;
+
+/**
+ * A class that contains common BLE functionality using the CHRE API Test nanoapp.
+ */
+public class ContextHubBleTestExecutor extends ContextHubChreApiTestExecutor {
+    private static final String TAG = "ContextHubBleTestExecutor";
+
+    /**
+     * The Base UUID is used for calculating 128-bit UUIDs from "short UUIDs" (16- and 32-bit).
+     *
+     * @see {https://www.bluetooth.com/specifications/assigned-numbers/service-discovery}
+     */
+    public static final UUID BASE_UUID = UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
+
+    /**
+     * Used for UUID conversion. This is the bit index where the 16-bit UUID is inserted.
+     */
+    private static final int BIT_INDEX_OF_16_BIT_UUID = 32;
+
+    /**
+     * The ID for the Google Eddystone beacon.
+     */
+    public static final UUID EDDYSTONE_UUID = to128BitUuid((short) 0xFEAA);
+
+    /**
+     * The delay to report results in milliseconds.
+     */
+    private static final int REPORT_DELAY_MS = 0;
+
+    /**
+     * The RSSI threshold for the BLE scan filter.
+     */
+    private static final int RSSI_THRESHOLD = -128;
+
+    /**
+     * The advertisement type for service data.
+     */
+    public static final int CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16 = 0x16;
+
+    /**
+     * The BLE advertisement event ID.
+     */
+    public static final int CHRE_EVENT_BLE_ADVERTISEMENT = 0x0350 + 1;
+
+    /**
+     * CHRE BLE capabilities and filter capabilities.
+     */
+    public static final int CHRE_BLE_CAPABILITIES_SCAN = 1 << 0;
+    public static final int CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA = 1 << 7;
+
+    private BluetoothAdapter mBluetoothAdapter = null;
+    private BluetoothLeAdvertiser mBluetoothLeAdvertiser = null;
+    private BluetoothLeScanner mBluetoothLeScanner = null;
+
+    /**
+     * The signal for advertising start.
+     */
+    private CountDownLatch mAdvertisingStartLatch = new CountDownLatch(1);
+
+    /**
+     * The signal for advertising stop.
+     */
+    private CountDownLatch mAdvertisingStopLatch = new CountDownLatch(1);
+
+    /**
+     * Indicates whether the device is advertising or not.
+     */
+    private AtomicBoolean mIsAdvertising = new AtomicBoolean();
+
+    /**
+     * Callback for BLE scans.
+     */
+    private final ScanCallback mScanCallback = new ScanCallback() {
+        @Override
+        public void onBatchScanResults(List<ScanResult> results) {
+            // do nothing
+        }
+
+        @Override
+        public void onScanFailed(int errorCode) {
+            Assert.fail("Failed to start a BLE scan on the host");
+        }
+
+        @Override
+        public void onScanResult(int callbackType, ScanResult result) {
+            // do nothing
+        }
+    };
+
+    /**
+     * The BLE advertising callback. It updates the mIsAdvertising state
+     * and notifies any waiting threads that the state of advertising
+     * has changed.
+     */
+    private AdvertisingSetCallback mAdvertisingSetCallback = new AdvertisingSetCallback() {
+        @Override
+        public void onAdvertisingSetStarted(AdvertisingSet advertisingSet,
+                int txPower, int status) {
+            mIsAdvertising.set(true);
+            mAdvertisingStartLatch.countDown();
+        }
+
+        @Override
+        public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) {
+            // do nothing
+        }
+
+        @Override
+        public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) {
+            // do nothing
+        }
+
+        @Override
+        public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) {
+            mIsAdvertising.set(false);
+            mAdvertisingStopLatch.countDown();
+        }
+    };
+
+    public ContextHubBleTestExecutor(NanoAppBinary nanoapp) {
+        super(nanoapp);
+
+        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
+        assertThat(bluetoothManager).isNotNull();
+        mBluetoothAdapter = bluetoothManager.getAdapter();
+        if (mBluetoothAdapter != null) {
+            mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
+            mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
+        }
+    }
+
+    /**
+     * Generates a BLE scan filter that filters only for the known Google beacons:
+     * Google Eddystone and Nearby Fastpair.
+     *
+     * @param useEddystone          if true, filter for Google Eddystone.
+     * @param useNearbyFastpair     if true, filter for Nearby Fastpair.
+     */
+    public static ChreApiTest.ChreBleScanFilter getDefaultScanFilter(boolean useEddystone,
+            boolean useNearbyFastpair) {
+        ChreApiTest.ChreBleScanFilter.Builder builder =
+                ChreApiTest.ChreBleScanFilter.newBuilder()
+                        .setRssiThreshold(RSSI_THRESHOLD);
+
+        if (useEddystone) {
+            ChreApiTest.ChreBleGenericFilter eddystoneFilter =
+                    ChreApiTest.ChreBleGenericFilter.newBuilder()
+                            .setType(CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16)
+                            .setLength(2)
+                            .setData(ByteString.copyFrom(HexFormat.of().parseHex("AAFE")))
+                            .setMask(ByteString.copyFrom(HexFormat.of().parseHex("FFFF")))
+                            .build();
+            builder = builder.addScanFilters(eddystoneFilter);
+        }
+
+        if (useNearbyFastpair) {
+            ChreApiTest.ChreBleGenericFilter nearbyFastpairFilter =
+                    ChreApiTest.ChreBleGenericFilter.newBuilder()
+                            .setType(CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16)
+                            .setLength(2)
+                            .setData(ByteString.copyFrom(HexFormat.of().parseHex("2CFE")))
+                            .setMask(ByteString.copyFrom(HexFormat.of().parseHex("FFFF")))
+                            .build();
+            builder = builder.addScanFilters(nearbyFastpairFilter);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Generates a BLE scan filter that filters only for the known Google beacons:
+     * Google Eddystone and Nearby Fastpair.
+     */
+    public static ChreApiTest.ChreBleScanFilter getDefaultScanFilter() {
+        return getDefaultScanFilter(true /* useEddystone */, true /* useNearbyFastpair */);
+    }
+
+    /**
+     * Generates a BLE scan filter that filters only for Google Eddystone.
+     */
+    public static ChreApiTest.ChreBleScanFilter getGoogleEddystoneScanFilter() {
+        return getDefaultScanFilter(true /* useEddystone */, false /* useNearbyFastpair */);
+    }
+
+    /**
+     * Generates a BLE scan filter that filters only for the known Google beacons:
+     * Google Eddystone and Nearby Fastpair. We specify the filter data in (little-endian) LE
+     * here as the CHRE code will take BE input and transform it to LE.
+     */
+    public static List<ScanFilter> getDefaultScanFilterHost() {
+        assertThat(CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16)
+                .isEqualTo(ScanRecord.DATA_TYPE_SERVICE_DATA_16_BIT);
+
+        ScanFilter scanFilter = new ScanFilter.Builder()
+                .setAdvertisingDataTypeWithData(
+                        ScanRecord.DATA_TYPE_SERVICE_DATA_16_BIT,
+                        ByteString.copyFrom(HexFormat.of().parseHex("AAFE")).toByteArray(),
+                        ByteString.copyFrom(HexFormat.of().parseHex("FFFF")).toByteArray())
+                .build();
+        ScanFilter scanFilter2 = new ScanFilter.Builder()
+                .setAdvertisingDataTypeWithData(
+                        ScanRecord.DATA_TYPE_SERVICE_DATA_16_BIT,
+                        ByteString.copyFrom(HexFormat.of().parseHex("2CFE")).toByteArray(),
+                        ByteString.copyFrom(HexFormat.of().parseHex("FFFF")).toByteArray())
+                .build();
+
+        return ImmutableList.of(scanFilter, scanFilter2);
+    }
+
+    /**
+     * Starts a BLE scan and asserts it was started successfully in a synchronous manner.
+     * This waits for the event to be received and returns the status in the event.
+     *
+     * @param scanFilter                The scan filter.
+     */
+    public void chreBleStartScanSync(ChreApiTest.ChreBleScanFilter scanFilter) throws Exception {
+        ChreApiTest.ChreBleStartScanAsyncInput.Builder inputBuilder =
+                ChreApiTest.ChreBleStartScanAsyncInput.newBuilder()
+                        .setMode(ChreApiTest.ChreBleScanMode.CHRE_BLE_SCAN_MODE_FOREGROUND)
+                        .setReportDelayMs(REPORT_DELAY_MS)
+                        .setHasFilter(scanFilter != null);
+        if (scanFilter != null) {
+            inputBuilder.setFilter(scanFilter);
+        }
+
+        ChreApiTestUtil util = new ChreApiTestUtil();
+        List<ChreApiTest.GeneralSyncMessage> response =
+                util.callServerStreamingRpcMethodSync(getRpcClient(),
+                        "chre.rpc.ChreApiTestService.ChreBleStartScanSync",
+                        inputBuilder.build());
+        assertThat(response).isNotEmpty();
+        for (ChreApiTest.GeneralSyncMessage status: response) {
+            assertThat(status.getStatus()).isTrue();
+        }
+    }
+
+    /**
+     * Stops a BLE scan and asserts it was started successfully in a synchronous manner.
+     * This waits for the event to be received and returns the status in the event.
+     */
+    public void chreBleStopScanSync() throws Exception {
+        ChreApiTestUtil util = new ChreApiTestUtil();
+        List<ChreApiTest.GeneralSyncMessage> response =
+                util.callServerStreamingRpcMethodSync(getRpcClient(),
+                        "chre.rpc.ChreApiTestService.ChreBleStopScanSync");
+        assertThat(response).isNotEmpty();
+        for (ChreApiTest.GeneralSyncMessage status: response) {
+            assertThat(status.getStatus()).isTrue();
+        }
+    }
+
+    /**
+     * Returns true if the required BLE capabilities and filter capabilities exist,
+     * otherwise returns false.
+     */
+    public boolean doesNecessaryBleCapabilitiesExist() throws Exception {
+        if (mBluetoothLeScanner == null) {
+            return false;
+        }
+
+        ChreApiTest.Capabilities capabilitiesResponse =
+                ChreApiTestUtil.callUnaryRpcMethodSync(getRpcClient(),
+                        "chre.rpc.ChreApiTestService.ChreBleGetCapabilities");
+        int capabilities = capabilitiesResponse.getCapabilities();
+        if ((capabilities & CHRE_BLE_CAPABILITIES_SCAN) != 0) {
+            ChreApiTest.Capabilities filterCapabilitiesResponse =
+                    ChreApiTestUtil.callUnaryRpcMethodSync(getRpcClient(),
+                            "chre.rpc.ChreApiTestService.ChreBleGetFilterCapabilities");
+            int filterCapabilities = filterCapabilitiesResponse.getCapabilities();
+            return (filterCapabilities & CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA) != 0;
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the required BLE capabilities and filter capabilities exist
+     * on the AP, otherwise returns false.
+     */
+    public boolean doesNecessaryBleCapabilitiesExistOnTheAP() throws Exception {
+        return mBluetoothLeAdvertiser != null;
+    }
+
+    /**
+     * Starts a BLE scan on the host side with known Google beacon filters.
+     */
+    public void startBleScanOnHost() {
+        ScanSettings scanSettings = new ScanSettings.Builder()
+                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+                .build();
+        mBluetoothLeScanner.startScan(getDefaultScanFilterHost(),
+                scanSettings, mScanCallback);
+    }
+
+    /**
+     * Stops a BLE scan on the host side.
+     */
+    public void stopBleScanOnHost() {
+        mBluetoothLeScanner.stopScan(mScanCallback);
+    }
+
+    /**
+     * Starts broadcasting the Google Eddystone beacon from the AP.
+     */
+    public void startBleAdvertisingGoogleEddystone() throws InterruptedException {
+        if (mIsAdvertising.get()) {
+            return;
+        }
+
+        AdvertisingSetParameters parameters = (new AdvertisingSetParameters.Builder())
+                .setLegacyMode(true)
+                .setConnectable(false)
+                .setInterval(AdvertisingSetParameters.INTERVAL_HIGH)
+                .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_HIGH)
+                .build();
+
+        AdvertiseData data = new AdvertiseData.Builder()
+                .addServiceData(new ParcelUuid(EDDYSTONE_UUID), new byte[] {0})
+                .setIncludeDeviceName(false)
+                .setIncludeTxPowerLevel(true)
+                .build();
+
+        mBluetoothLeAdvertiser.startAdvertisingSet(parameters, data,
+                null, null, null, mAdvertisingSetCallback);
+        mAdvertisingStartLatch.await();
+        assertThat(mIsAdvertising.get()).isTrue();
+    }
+
+    /**
+     * Stops advertising Google Eddystone from the AP.
+     */
+    public void stopBleAdvertisingGoogleEddystone() throws InterruptedException {
+        if (!mIsAdvertising.get()) {
+            return;
+        }
+
+        mBluetoothLeAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
+        mAdvertisingStopLatch.await();
+        assertThat(mIsAdvertising.get()).isFalse();
+    }
+
+    /**
+     * Converts short UUID to 128 bit UUID.
+     */
+    private static UUID to128BitUuid(short shortUuid) {
+        return new UUID(
+                ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID)
+                        | BASE_UUID.getMostSignificantBits(),
+                        BASE_UUID.getLeastSignificantBits());
+    }
+}
diff --git a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubChreApiTestExecutor.java b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubChreApiTestExecutor.java
index 28b363f..b3c2336 100644
--- a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubChreApiTestExecutor.java
+++ b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubChreApiTestExecutor.java
@@ -42,7 +42,6 @@
  */
 public class ContextHubChreApiTestExecutor extends ContextHubClientCallback {
     private final List<NanoAppBinary> mNanoAppBinaries;
-    private final List<Long> mNanoAppIds = new ArrayList<Long>();
     private final ContextHubClient mContextHubClient;
     private final AtomicBoolean mChreReset = new AtomicBoolean(false);
     protected final Context mContext = InstrumentationRegistry.getTargetContext();
@@ -64,7 +63,6 @@
         mContextHubClient = mContextHubManager.createClient(mContextHub, this);
 
         for (NanoAppBinary nanoapp: nanoapps) {
-            mNanoAppIds.add(nanoapp.getNanoAppId());
             Service chreApiService = ChreApiTestUtil.getChreApiService();
             mRpcClients.add(new ChreRpcClient(
                     mContextHubManager, mContextHub, nanoapp.getNanoAppId(),
@@ -79,7 +77,6 @@
 
     /** Should be invoked before run() is invoked to set up the test, e.g. in a @Before method. */
     public void init() {
-        mContextHubManager.enableTestMode();
         for (NanoAppBinary nanoapp: mNanoAppBinaries) {
             ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHub, nanoapp);
         }
@@ -91,10 +88,10 @@
             Assert.fail("CHRE reset during the test");
         }
 
-        for (Long nanoappId: mNanoAppIds) {
-            ChreTestUtil.unloadNanoAppAssertSuccess(mContextHubManager, mContextHub, nanoappId);
+        for (NanoAppBinary nanoapp: mNanoAppBinaries) {
+            ChreTestUtil.unloadNanoAppAssertSuccess(mContextHubManager, mContextHub,
+                    nanoapp.getNanoAppId());
         }
-        mContextHubManager.disableTestMode();
         mContextHubClient.close();
     }
 
diff --git a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubGeneralTestExecutor.java b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubGeneralTestExecutor.java
index 10680f2..355d0ce 100644
--- a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubGeneralTestExecutor.java
+++ b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubGeneralTestExecutor.java
@@ -15,6 +15,12 @@
  */
 package com.google.android.chre.test.chqts;
 
+import android.app.Instrumentation;
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.hardware.location.ContextHubClient;
 import android.hardware.location.ContextHubClientCallback;
 import android.hardware.location.ContextHubInfo;
@@ -25,6 +31,7 @@
 import android.util.Log;
 
 import com.google.android.utils.chre.ChreTestUtil;
+import com.google.android.utils.chre.SettingsUtil;
 
 import org.junit.Assert;
 
@@ -74,6 +81,28 @@
 
     private long mThreadId;
 
+    private final Instrumentation mInstrumentation =
+            androidx.test.platform.app.InstrumentationRegistry.getInstrumentation();
+
+    private final Context mContext = mInstrumentation.getTargetContext();
+
+    private final SettingsUtil mSettingsUtil;
+
+    private boolean mInitialBluetoothEnabled = false;
+
+    public static class BluetoothUpdateListener {
+        public CountDownLatch mBluetoothLatch = new CountDownLatch(1);
+
+        public BroadcastReceiver mBluetoothUpdateReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+                    mBluetoothLatch.countDown();
+                }
+            }
+        };
+    }
+
     /**
      * A container class to describe a general_test nanoapp.
      */
@@ -145,6 +174,7 @@
         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
             mNanoAppIdSet.add(test.getNanoAppBinary().getNanoAppId());
         }
+        mSettingsUtil = new SettingsUtil(mContext);
     }
 
     @Override
@@ -200,6 +230,9 @@
         mContextHubClient = mContextHubManager.createClient(mContextHubInfo, this);
 
         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
+            if (test.getTestName() == ContextHubTestConstants.TestNames.BASIC_BLE_TEST) {
+                handleBleTestSetup();
+            }
             if (test.loadAtInit()) {
                 ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHubInfo,
                         test.getNanoAppBinary());
@@ -209,10 +242,33 @@
         mErrorString.set(null);
     }
 
+    private void handleBleTestSetup() {
+        mInitialBluetoothEnabled = mSettingsUtil.isBluetoothEnabled();
+        Log.i(TAG, "Initial bluetooth setting enabled: " + mInitialBluetoothEnabled);
+        if (mInitialBluetoothEnabled) {
+            return;
+        }
+        BluetoothUpdateListener bluetoothUpdateListener = new BluetoothUpdateListener();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        mContext.registerReceiver(bluetoothUpdateListener.mBluetoothUpdateReceiver, filter);
+        mSettingsUtil.setBluetooth(true /* enable */);
+        try {
+            bluetoothUpdateListener.mBluetoothLatch.await(10, TimeUnit.SECONDS);
+            Assert.assertTrue(mSettingsUtil.isBluetoothEnabled());
+            Log.i(TAG, "Bluetooth enabled successfully");
+            // Wait a few seconds to ensure setting is propagated to CHRE path
+            // TODO(b/302018530): Remove Thread.sleep calls for CHRE settings propagation
+            Thread.sleep(2000);
+        } catch (InterruptedException e) {
+            Assert.fail(e.getMessage());
+        }
+    }
+
     /**
      * Run the test.
      */
-    public void run(long timeoutSeconds) {
+    public void run(long timeoutSeconds) throws InterruptedException {
         mThreadId = Thread.currentThread().getId();
 
         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
@@ -222,13 +278,7 @@
             }
         }
 
-        boolean success = false;
-        try {
-            success = mCountDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
-        }
-
+        boolean success = mCountDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
         Assert.assertTrue("Test timed out", success);
     }
 
@@ -248,6 +298,10 @@
         // TODO: If the nanoapp aborted (i.e. test failed), wait for CHRE reset or nanoapp abort
         // callback, and otherwise assert unload success.
         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
+            if (test.getTestName() == ContextHubTestConstants.TestNames.BASIC_BLE_TEST
+                    && !mInitialBluetoothEnabled) {
+                mSettingsUtil.setBluetooth(mInitialBluetoothEnabled);
+            }
             ChreTestUtil.unloadNanoApp(mContextHubManager, mContextHubInfo,
                     test.getNanoAppBinary().getNanoAppId());
         }
diff --git a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubHostEndpointInfoTestExecutor.java b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubHostEndpointInfoTestExecutor.java
index 7290ab8..2843cb4 100644
--- a/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubHostEndpointInfoTestExecutor.java
+++ b/java/test/chqts/src/com/google/android/chre/test/chqts/ContextHubHostEndpointInfoTestExecutor.java
@@ -19,30 +19,43 @@
 import android.hardware.location.ContextHubClient;
 import android.hardware.location.NanoAppBinary;
 
+import androidx.annotation.NonNull;
+
 import com.google.android.utils.chre.ChreApiTestUtil;
 
 import org.junit.Assert;
 
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
 import dev.chre.rpc.proto.ChreApiTest;
 
 /**
  * Test to ensure CHRE HostEndpoint related API works as expected.
  */
 public class ContextHubHostEndpointInfoTestExecutor extends ContextHubChreApiTestExecutor {
+    private static final int HOST_ENDPOINT_NOTIFICATION_TYPE_DISCONNECT = 0;
+    private static final int CHRE_HOST_ENDPOINT_NOTIFICATION_EVENT = 0x0008;
+
     public ContextHubHostEndpointInfoTestExecutor(NanoAppBinary nanoapp) {
         super(nanoapp);
     }
 
     /**
-     * Validates if the host endpoint info stored in ChreApiTest Nanoapp is as expected.
+     * Validates if the host endpoint info stored in ChreApiTest Nanoapp is as
+     * expected.
      *
-     * @param id the host endpoint id of the host endpoint.
-     * @param success true if we are expecting to retrieve host endpoint info by this id, otherwise
-     *     false.
+     * @param id      the host endpoint id of the host endpoint.
+     * @param success true if we are expecting to retrieve host endpoint info by
+     *                this id, otherwise false.
      */
     private void validateHostEndpointInfoById(int id, boolean success) throws Exception {
         ChreApiTest.ChreGetHostEndpointInfoInput input =
-                ChreApiTest.ChreGetHostEndpointInfoInput.newBuilder().setHostEndpointId(id).build();
+                ChreApiTest.ChreGetHostEndpointInfoInput.newBuilder()
+                .setHostEndpointId(id)
+                .build();
         ChreApiTest.ChreGetHostEndpointInfoOutput response =
                 ChreApiTestUtil.callUnaryRpcMethodSync(
                         getRpcClient(), "chre.rpc.ChreApiTestService.ChreGetHostEndpointInfo",
@@ -84,25 +97,31 @@
      * Validates if the nanoapp received a proper host endpoint notification disconnection event.
      *
      * @param id host endpoint id for the most recent disconnected host endpoint.
+     * @param events host endpoint notification events received by the nanoapp.
      */
-    private void validateLatestHostEndpointNotification(int id) throws Exception {
-        // TODO(b/274791978): Deprecate this once we can capture event in test mode.
-        ChreApiTest.RetrieveLatestDisconnectedHostEndpointEventOutput response =
-                ChreApiTestUtil.callUnaryRpcMethodSync(
-                        getRpcClient(),
-                        "chre.rpc.ChreApiTestService.RetrieveLatestDisconnectedHostEndpointEvent");
+    private void validateChreHostEndpointNotification(int id,
+            @NonNull List<ChreApiTest.GeneralEventsMessage> events) throws Exception {
+        Objects.requireNonNull(events);
         Assert.assertEquals(
                 "Should have exactly receive 1 host endpoint notification",
-                1,
-                response.getDisconnectedCount());
-        Assert.assertEquals("Host endpoint Id mismatch", response.getHostEndpointId(), id);
+                1, events.size());
+        ChreApiTest.ChreHostEndpointNotification hostEndpointNotification =
+                events.get(0).getChreHostEndpointNotification();
+        Assert.assertTrue(events.get(0).getStatus());
+        Assert.assertEquals("Host endpoint Id mismatch", id,
+                hostEndpointNotification.getHostEndpointId());
+
+        Assert.assertEquals("Not receive HOST_ENDPOINT_NOTIFICATION_TYPE_DISCONNECT",
+                HOST_ENDPOINT_NOTIFICATION_TYPE_DISCONNECT,
+                hostEndpointNotification.getNotificationType());
     }
 
     /**
      * Asks the test nanoapp to configure host endpoint notification.
      *
      * @param hostEndpointId which host endpoint to get notification.
-     * @param enable true to enable host endpoint notification, false to disable.
+     * @param enable         true to enable host endpoint notification, false to
+     *                       disable.
      */
     private void configureHostEndpointNotification(int hostEndpointId, boolean enable)
             throws Exception {
@@ -127,8 +146,15 @@
         ContextHubClient client = mContextHubManager.createClient(mContextHub, this);
         int clientHostEndpointId = client.getId();
         configureHostEndpointNotification(clientHostEndpointId, true);
+        Future<List<ChreApiTest.GeneralEventsMessage>> eventsFuture = new ChreApiTestUtil()
+                .gatherEvents(mRpcClients.get(0),
+                        CHRE_HOST_ENDPOINT_NOTIFICATION_EVENT,
+                        /* eventCount= */ 1,
+                        ChreApiTestUtil.RPC_TIMEOUT_IN_NS);
         client.close();
-        Thread.sleep(1000);
-        validateLatestHostEndpointNotification(clientHostEndpointId);
+        List<ChreApiTest.GeneralEventsMessage> events =
+                        eventsFuture.get(2 * ChreApiTestUtil.RPC_TIMEOUT_IN_NS,
+                                TimeUnit.NANOSECONDS);
+        validateChreHostEndpointNotification(clientHostEndpointId, events);
     }
 }
diff --git a/java/test/chqts/src/com/google/android/chre/test/chqts/multidevice/ContextHubMultiDeviceBleBeaconTestExecutor.java b/java/test/chqts/src/com/google/android/chre/test/chqts/multidevice/ContextHubMultiDeviceBleBeaconTestExecutor.java
new file mode 100644
index 0000000..19f0491
--- /dev/null
+++ b/java/test/chqts/src/com/google/android/chre/test/chqts/multidevice/ContextHubMultiDeviceBleBeaconTestExecutor.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.chre.test.chqts.multidevice;
+
+import android.hardware.location.NanoAppBinary;
+
+import com.google.android.chre.test.chqts.ContextHubBleTestExecutor;
+import com.google.android.utils.chre.ChreApiTestUtil;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import dev.chre.rpc.proto.ChreApiTest;
+
+public class ContextHubMultiDeviceBleBeaconTestExecutor extends ContextHubBleTestExecutor {
+    private static final int NUM_EVENTS_TO_GATHER = 10;
+
+    private static final long TIMEOUT_IN_S = 30;
+
+    private static final long TIMEOUT_IN_NS = TIMEOUT_IN_S * 1000000000L;
+
+    public ContextHubMultiDeviceBleBeaconTestExecutor(NanoAppBinary nanoapp) {
+        super(nanoapp);
+    }
+
+    /**
+     * Gathers BLE advertisement events from the nanoapp for TIMEOUT_IN_NS or up to
+     * NUM_EVENTS_TO_GATHER events. This function returns true if all
+     * chreBleAdvertisingReport's contain advertisments for Google Eddystone and
+     * there is at least one advertisement, otherwise it returns false.
+     */
+    public boolean gatherAndVerifyChreBleAdvertisementsForGoogleEddystone() throws Exception {
+        Future<List<ChreApiTest.GeneralEventsMessage>> eventsFuture =
+                new ChreApiTestUtil().gatherEvents(
+                        mRpcClients.get(0),
+                        Arrays.asList(CHRE_EVENT_BLE_ADVERTISEMENT),
+                        NUM_EVENTS_TO_GATHER,
+                        TIMEOUT_IN_NS);
+
+        List<ChreApiTest.GeneralEventsMessage> events = eventsFuture.get();
+        if (events == null) {
+            return false;
+        }
+
+        boolean foundGoogleEddystoneBleAdvertisement = false;
+        for (ChreApiTest.GeneralEventsMessage event: events) {
+            if (!event.hasChreBleAdvertisementEvent()) {
+                continue;
+            }
+
+            ChreApiTest.ChreBleAdvertisementEvent bleAdvertisementEvent =
+                    event.getChreBleAdvertisementEvent();
+            for (int i = 0; i < bleAdvertisementEvent.getReportsCount(); ++i) {
+                ChreApiTest.ChreBleAdvertisingReport report = bleAdvertisementEvent.getReports(i);
+                byte[] data = report.getData().toByteArray();
+                if (data == null || data.length < 2) {
+                    continue;
+                }
+
+                if (!searchForGoogleEddystoneAdvertisement(data)) {
+                    return false;
+                }
+                foundGoogleEddystoneBleAdvertisement = true;
+            }
+        }
+        return foundGoogleEddystoneBleAdvertisement;
+    }
+
+    /**
+     * Starts a BLE scan with the Google Eddystone filter.
+     */
+    public void chreBleStartScanSyncWithGoogleEddystoneFilter() throws Exception {
+        chreBleStartScanSync(getGoogleEddystoneScanFilter());
+    }
+
+    /**
+     * Returns true if the data contains an advertisement for Google Eddystone,
+     * otherwise returns false.
+     */
+    private boolean searchForGoogleEddystoneAdvertisement(byte[] data) {
+        if (data.length < 2) {
+            return false;
+        }
+
+        for (int j = 0; j < data.length - 1; ++j) {
+            if (Byte.compare(data[j], (byte) 0xAA) == 0
+                    && Byte.compare(data[j + 1], (byte) 0xFE) == 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/java/test/chre_concurrency/src/com/google/android/chre/test/chreconcurrency/ContextHubChreConcurrencyTestExecutor.java b/java/test/chre_concurrency/src/com/google/android/chre/test/chreconcurrency/ContextHubChreConcurrencyTestExecutor.java
index 4610179..81460af 100644
--- a/java/test/chre_concurrency/src/com/google/android/chre/test/chreconcurrency/ContextHubChreConcurrencyTestExecutor.java
+++ b/java/test/chre_concurrency/src/com/google/android/chre/test/chreconcurrency/ContextHubChreConcurrencyTestExecutor.java
@@ -56,12 +56,18 @@
     private static final long TIMEOUT_IN_NS = 5000000000L;
 
     /**
-     * The multiplier used to allow for jitter in the timestamps of the samples. This is
+     * The multiplier used to allow for jitter in the intervals of the samples. This is
      * multiplied against the requested interval between samples to produce the final interval
      * that is compared with the real interval between samples. This allows for 5% of jitter.
      */
     private static final double JITTER_MULTIPLIER = 1.05;
 
+    /**
+     * The sum of the intervals in ns and timestamp count. Used to calculate the average interval.
+     */
+    private double mIntervalSumNs = 0;
+    private int mTimestampCount = 0;
+
     public ContextHubChreConcurrencyTestExecutor(NanoAppBinary nanoapp, NanoAppBinary nanoapp2) {
         super(Arrays.asList(nanoapp, nanoapp2));
     }
@@ -203,19 +209,30 @@
             List<ChreApiTest.GeneralEventsMessage> events,
             long requestedIntervalNs) {
         assertThat(events).isNotNull();
+
         Long lastReadingTimestamp = null;
+        mIntervalSumNs = 0;
+        mTimestampCount = 0;
         for (ChreApiTest.GeneralEventsMessage event: events) {
             Assert.assertTrue(event.getStatus());
             if (event.hasChreSensorThreeAxisData()) {
                 lastReadingTimestamp = handleChreSensorThreeAxisData(event, requestedIntervalNs,
                         lastReadingTimestamp, accelerometerHandle);
             } else if (event.hasChreSensorSamplingStatusEvent()) {
+                // The interval will change due to the sampling status event.
+                // Assert the past data was good and continue with fresh data.
+                assertAverageIntervalIsLessThanOrEqualToTheRequestedInterval(requestedIntervalNs);
+
+                mIntervalSumNs = 0;
+                mTimestampCount = 0;
                 requestedIntervalNs = handleChreSensorSamplingStatusEvent(event,
                         requestedIntervalNs, accelerometerHandle);
             } else {
                 Assert.fail("Event does not contain any requested data.");
             }
         }
+
+        assertAverageIntervalIsLessThanOrEqualToTheRequestedInterval(requestedIntervalNs);
     }
 
     /**
@@ -231,8 +248,8 @@
             long requestedIntervalNs, Long lastReadingTimestamp, int accelerometerHandle) {
         ChreApiTest.ChreSensorThreeAxisData data = event.getChreSensorThreeAxisData();
         Assert.assertEquals(data.getHeader().getSensorHandle(), accelerometerHandle);
-
         assertThat(data.getReadingsCount()).isGreaterThan(0);
+
         for (int i = 0; i < data.getReadingsCount(); ++i) {
             ChreApiTest.ChreSensorThreeAxisSampleData sampleData = data.getReadings(i);
 
@@ -243,11 +260,8 @@
                     ? data.getHeader().getBaseTimestamp()
                     : lastReadingTimestamp) + sampleData.getTimestampDelta();
             if (lastReadingTimestamp != null) {
-                Assert.assertTrue("Invalid timestamp between samples: interval: "
-                        + (readingTimestamp - lastReadingTimestamp)
-                        + ", requestedIntervalNs: " + requestedIntervalNs,
-                        readingTimestamp <= requestedIntervalNs * JITTER_MULTIPLIER
-                                + lastReadingTimestamp);
+                mIntervalSumNs += readingTimestamp - lastReadingTimestamp;
+                ++mTimestampCount;
             }
             lastReadingTimestamp = readingTimestamp;
         }
@@ -271,4 +285,22 @@
         ChreApiTest.ChreSensorSamplingStatus samplingStatus = samplingStatusEvent.getStatus();
         return samplingStatus.getEnabled() ? samplingStatus.getInterval() : requestedIntervalNs;
     }
+
+    /**
+     * Asserts that the average interval is less than the requested interval (both in ns),
+     * accounting for jitter.
+     *
+     * @param requestedIntervalNs           the requested interval in ns.
+     */
+    private void assertAverageIntervalIsLessThanOrEqualToTheRequestedInterval(
+                long requestedIntervalNs) {
+        if (mTimestampCount <= 0) {
+            return;
+        }
+
+        double averageIntervalNs = mIntervalSumNs / mTimestampCount;
+        Assert.assertTrue("Invalid average timestamp between samples: averageIntervalNs: "
+                + averageIntervalNs + ", requestedIntervalNs: " + requestedIntervalNs,
+                averageIntervalNs <= requestedIntervalNs * JITTER_MULTIPLIER);
+    }
 }
diff --git a/java/test/cross_validation/Android.bp b/java/test/cross_validation/Android.bp
index e77f999..fc073aa 100644
--- a/java/test/cross_validation/Android.bp
+++ b/java/test/cross_validation/Android.bp
@@ -36,5 +36,5 @@
         "guava",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorBase.java b/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorBase.java
index 5028b9b..c564127 100644
--- a/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorBase.java
+++ b/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorBase.java
@@ -24,6 +24,8 @@
 import android.hardware.location.NanoAppBinary;
 import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.util.Log;
 
 import com.google.android.utils.chre.ChreTestUtil;
@@ -101,7 +103,7 @@
      * @param samplingDurationInMs The amount of time in milliseconds to collect samples from AP and
      * CHRE.
      */
-    public abstract void validate() throws AssertionError;
+    public abstract void validate() throws AssertionError, InterruptedException;
 
     /**
     * Clean up resources allocated for cross validation. Subclasses should override this method and
@@ -122,6 +124,13 @@
     * with data received.
     */
     private void unloadAllNanoApps() {
+        // We only need to unload all nanoapps when the device has version < U, so the
+        // tests remain the same on those devices. On newer devices, test mode will
+        // handle this.
+        if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            return;
+        }
+
         List<NanoAppState> nanoAppStateList =
                 ChreTestUtil.queryNanoAppsAssertSuccess(mContextHubManager, mContextHubInfo);
 
diff --git a/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorSensor.java b/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorSensor.java
index d53f408..f6a4321 100644
--- a/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorSensor.java
+++ b/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorSensor.java
@@ -150,7 +150,7 @@
     }
 
     @Override
-    public void validate() throws AssertionError {
+    public void validate() throws AssertionError, InterruptedException {
         HashSet<Integer> testedSensorIndices = new HashSet<>();
         for (int i = 0; i < mSensorList.size(); i++) {
             mApDatapointsQueue.clear();
@@ -453,12 +453,9 @@
      * @param samplingDurationInMs The amount of time to wait for AP and CHRE to collected data in
      *                             ms.
      */
-    private void waitForDataSampling() throws AssertionError {
-        try {
-            mAwaitDataLatch.await(getAwaitDataTimeoutInMs(), TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail("await data latch interrupted");
-        }
+    private void waitForDataSampling() throws AssertionError, InterruptedException {
+        mAwaitDataLatch.await(getAwaitDataTimeoutInMs(), TimeUnit.MILLISECONDS);
+
         if (mErrorStr.get() != null) {
             Assert.fail(mErrorStr.get());
         }
@@ -543,21 +540,15 @@
     /**
      * Verify the CHRE sensor being evaluated is present on this device.
      */
-    private void verifyChreSensorIsPresent() {
+    private void verifyChreSensorIsPresent() throws InterruptedException {
         mCollectingData.set(true);
         sendMessageToNanoApp(makeInfoCommandMessage());
         waitForInfoResponse();
         mCollectingData.set(false);
     }
 
-    private void waitForInfoResponse() {
-        boolean success = false;
-        try {
-            success = mAwaitDataLatch.await(INFO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail("await data latch interrupted");
-        }
-
+    private void waitForInfoResponse() throws InterruptedException {
+        boolean success = mAwaitDataLatch.await(INFO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
         if (!success) {
             Assert.fail("Timed out waiting for sensor info response");
         }
diff --git a/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorWifi.java b/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorWifi.java
index 812fe3a..e9f9f92 100644
--- a/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorWifi.java
+++ b/java/test/cross_validation/src/com/google/android/chre/test/crossvalidator/ChreCrossValidatorWifi.java
@@ -41,6 +41,7 @@
 import org.junit.Assume;
 
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -104,7 +105,7 @@
         context.registerReceiver(mWifiScanReceiver, intentFilter);
     }
 
-    @Override public void validate() throws AssertionError {
+    @Override public void validate() throws AssertionError, InterruptedException {
         mCollectingData.set(true);
         sendStepStartMessage(Step.CAPABILITIES);
         waitForMessageFromNanoapp();
@@ -183,12 +184,10 @@
     /**
      * Wait for a messaage from the nanoapp.
      */
-    private void waitForMessageFromNanoapp() {
-        try {
-            mAwaitDataLatch.await(AWAIT_STEP_RESULT_MESSAGE_TIMEOUT_SEC, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail("Interrupted while awaiting " + getCurrentStepName() + " step");
-        }
+    private void waitForMessageFromNanoapp() throws InterruptedException {
+        boolean success =
+                mAwaitDataLatch.await(AWAIT_STEP_RESULT_MESSAGE_TIMEOUT_SEC, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: wait for message from nanoapp", success);
         mAwaitDataLatch = new CountDownLatch(1);
         Assert.assertTrue("Timed out while waiting for step result in " + getCurrentStepName()
                 + " step", mDidReceiveNanoAppMessage.get());
@@ -207,18 +206,31 @@
             && (capabilities.getWifiCapabilities() & WIFI_CAPABILITIES_ON_DEMAND_SCAN) != 0;
     }
 
-    private void waitForApScanResults() {
-        try {
-            mAwaitApWifiSetupScan.await(AWAIT_WIFI_SCAN_RESULT_TIMEOUT_SEC, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail("Interrupted while awaiting ap wifi scan result");
-        }
+    private void waitForApScanResults() throws InterruptedException {
+        boolean success =
+                mAwaitApWifiSetupScan.await(AWAIT_WIFI_SCAN_RESULT_TIMEOUT_SEC, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: wait for ap scan results", success);
         Assert.assertTrue("AP wifi scan result failed asynchronously", mApWifiScanSuccess.get());
     }
 
     private void sendWifiScanResultsToChre() {
         List<ScanResult> results = mWifiManager.getScanResults();
         Assert.assertTrue("No wifi scan results returned from AP", !results.isEmpty());
+
+        // CHRE does not currently support 6 GHz results, so filter these results from the list
+        int logsRemoved = 0;
+        Iterator<ScanResult> iter = results.iterator();
+        while (iter.hasNext()) {
+            ScanResult current = iter.next();
+            if (current.getBand() == ScanResult.WIFI_BAND_6_GHZ) {
+                iter.remove();
+                logsRemoved++;
+            }
+        }
+        if (logsRemoved > 0) {
+            Log.i(TAG, "Filtering out 6 GHz band scan result for CHRE, total=" + logsRemoved);
+        }
+
         for (int i = 0; i < results.size(); i++) {
             sendMessageToNanoApp(makeWifiScanResultMessage(results.get(i), results.size(), i));
         }
diff --git a/java/test/permissions/Android.bp b/java/test/permissions/Android.bp
index f8f6e02..398e78c 100644
--- a/java/test/permissions/Android.bp
+++ b/java/test/permissions/Android.bp
@@ -35,5 +35,5 @@
         "chre-test-utils",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/rpc_service/Android.bp b/java/test/rpc_service/Android.bp
index 8a6eebb..69e839a 100644
--- a/java/test/rpc_service/Android.bp
+++ b/java/test/rpc_service/Android.bp
@@ -36,5 +36,5 @@
         "pw_rpc_java_client",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/rpc_service/src/com/google/android/chre/test/rpc_service/ContextHubRpcServiceTestExecutor.java b/java/test/rpc_service/src/com/google/android/chre/test/rpc_service/ContextHubRpcServiceTestExecutor.java
index b6da0f1..78b1a1a 100644
--- a/java/test/rpc_service/src/com/google/android/chre/test/rpc_service/ContextHubRpcServiceTestExecutor.java
+++ b/java/test/rpc_service/src/com/google/android/chre/test/rpc_service/ContextHubRpcServiceTestExecutor.java
@@ -129,7 +129,6 @@
         };
 
         IntentFilter filter = new IntentFilter(ACTION);
-        mContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
         Intent intent = new Intent(ACTION).setPackage(mContext.getPackageName());
         PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
                 intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
@@ -138,6 +137,7 @@
                 pendingIntent, mNanoAppId);
 
         mRpcClient = new ChreRpcClient(contextHubClient, mNanoAppId, List.of(mEchoService));
+        mContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
 
         invokeRpc(mRpcClient);
 
diff --git a/java/test/settings/Android.bp b/java/test/settings/Android.bp
index 2f68553..4521017 100644
--- a/java/test/settings/Android.bp
+++ b/java/test/settings/Android.bp
@@ -33,5 +33,5 @@
         "chre-test-utils",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubBleSettingsTestExecutor.java b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubBleSettingsTestExecutor.java
index 4ada672..e91785d 100644
--- a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubBleSettingsTestExecutor.java
+++ b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubBleSettingsTestExecutor.java
@@ -16,8 +16,6 @@
 
 package com.google.android.chre.test.setting;
 
-import static android.Manifest.permission.BLUETOOTH_CONNECT;
-
 import android.app.Instrumentation;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
@@ -88,7 +86,7 @@
     /**
      * Should be called in a @Before method.
      */
-    public void setUp() {
+    public void setUp() throws InterruptedException {
         mInitialBluetoothEnabled = mSettingsUtil.isBluetoothEnabled();
         mInitialBluetoothScanningEnabled = mSettingsUtil.isBluetoothScanningAlwaysEnabled();
         mInitialAirplaneMode = mSettingsUtil.isAirplaneModeOn();
@@ -99,7 +97,7 @@
         mExecutor.init();
     }
 
-    public void runBleScanningTest() {
+    public void runBleScanningTest() throws InterruptedException {
         runBleScanningTest(false /* enableBluetooth */, false /* enableBluetoothScanning */);
         runBleScanningTest(true /* enableBluetooth */, false /* enableBluetoothScanning */);
         runBleScanningTest(false /* enableBluetooth */, true /* enableBluetoothScanning */);
@@ -109,7 +107,7 @@
     /**
      * Should be called in an @After method.
      */
-    public void tearDown() {
+    public void tearDown() throws InterruptedException {
         mExecutor.deinit();
         mSettingsUtil.setBluetooth(mInitialBluetoothEnabled);
         mSettingsUtil.setBluetoothScanningSettings(mInitialBluetoothScanningEnabled);
@@ -122,6 +120,12 @@
      * @param enableBluetoothScanning   if true, enable BLE scanning; false, otherwise
      */
     private void setBluetoothSettings(boolean enable, boolean enableBluetoothScanning) {
+        // Check if already in the desired state
+        if ((enable == mSettingsUtil.isBluetoothEnabled())
+                 && (enableBluetoothScanning == mSettingsUtil.isBluetoothScanningAlwaysEnabled())) {
+            return;
+        }
+
         int state = BluetoothAdapter.STATE_OFF;
         if (enable) {
             state = BluetoothAdapter.STATE_ON;
@@ -142,11 +146,11 @@
             Assert.assertTrue(bluetoothManager != null);
             BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
             Assert.assertTrue(bluetoothAdapter != null);
-            mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(BLUETOOTH_CONNECT);
             Assert.assertTrue(bluetoothAdapter.enableBLE());
         }
         try {
-            bluetoothUpdateListener.mBluetoothLatch.await(10, TimeUnit.SECONDS);
+            boolean success = bluetoothUpdateListener.mBluetoothLatch.await(10, TimeUnit.SECONDS);
+            Assert.assertTrue("Timeout waiting for signal: bluetooth update listener", success);
             Assert.assertTrue(enable == mSettingsUtil.isBluetoothEnabled());
             Assert.assertTrue(enableBluetoothScanning
                     == mSettingsUtil.isBluetoothScanningAlwaysEnabled());
@@ -167,7 +171,7 @@
      * @param enableBluetoothScanning if bluetooth scanning is always enabled
      */
     private void runBleScanningTest(boolean enableBluetooth,
-            boolean enableBluetoothScanning) {
+            boolean enableBluetoothScanning) throws InterruptedException {
         setBluetoothSettings(enableBluetooth, enableBluetoothScanning);
 
         boolean enableFeature = enableBluetooth || enableBluetoothScanning;
diff --git a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubGnssSettingsTestExecutor.java b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubGnssSettingsTestExecutor.java
index 8b49dad..e99fed1 100644
--- a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubGnssSettingsTestExecutor.java
+++ b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubGnssSettingsTestExecutor.java
@@ -48,12 +48,12 @@
         mExecutor.init();
     }
 
-    public void runGnssLocationTest() {
+    public void runGnssLocationTest() throws InterruptedException {
         runTest(ChreSettingsTest.TestCommand.Feature.GNSS_LOCATION, false /* enableFeature */);
         runTest(ChreSettingsTest.TestCommand.Feature.GNSS_LOCATION, true /* enableFeature */);
     }
 
-    public void runGnssMeasurementTest() {
+    public void runGnssMeasurementTest() throws InterruptedException {
         runTest(ChreSettingsTest.TestCommand.Feature.GNSS_MEASUREMENT, false /* enableFeature */);
         runTest(ChreSettingsTest.TestCommand.Feature.GNSS_MEASUREMENT, true /* enableFeature */);
     }
@@ -61,7 +61,7 @@
     /**
      * Should be called in an @After method.
      */
-    public void tearDown() {
+    public void tearDown() throws InterruptedException {
         mExecutor.deinit();
         mSettingsUtil.setLocationMode(mInitialLocationEnabled, 30 /* timeoutSeconds */);
     }
@@ -71,7 +71,8 @@
      * @param feature The GNSS feature to test.
      * @param enableFeature True for enable.
      */
-    private void runTest(ChreSettingsTest.TestCommand.Feature feature, boolean enableFeature) {
+    private void runTest(ChreSettingsTest.TestCommand.Feature feature, boolean enableFeature)
+            throws InterruptedException {
         mSettingsUtil.setLocationMode(enableFeature, 30 /* timeoutSeconds */);
 
         ChreSettingsTest.TestCommand.State state = enableFeature
diff --git a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubMicDisableSettingsTestExecutor.java b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubMicDisableSettingsTestExecutor.java
index 8d8c4e9..c17dd31 100644
--- a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubMicDisableSettingsTestExecutor.java
+++ b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubMicDisableSettingsTestExecutor.java
@@ -50,7 +50,7 @@
         mExecutor.init();
     }
 
-    public void runMicDisableSettingsTest() {
+    public void runMicDisableSettingsTest() throws InterruptedException {
         setMicrophoneDisableSetting(false /* disableAccess */);
         runTest(ChreSettingsTest.TestCommand.Feature.AUDIO, false /* enableFeature */);
 
@@ -87,7 +87,8 @@
      * @param feature The feature to test.
      * @param enableFeature True for enable.
      */
-    private void runTest(ChreSettingsTest.TestCommand.Feature feature, boolean enableFeature) {
+    private void runTest(ChreSettingsTest.TestCommand.Feature feature, boolean enableFeature)
+            throws InterruptedException {
         ChreSettingsTest.TestCommand.State state = enableFeature
                 ? ChreSettingsTest.TestCommand.State.ENABLED
                 : ChreSettingsTest.TestCommand.State.DISABLED;
diff --git a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubSettingsTestExecutor.java b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubSettingsTestExecutor.java
index 78a2f1b..5c9a217 100644
--- a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubSettingsTestExecutor.java
+++ b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubSettingsTestExecutor.java
@@ -138,7 +138,8 @@
      *
      * @param feature The feature to set the test up for.
      */
-    public void setupTestAssertSuccess(ChreSettingsTest.TestCommand.Feature feature) {
+    public void setupTestAssertSuccess(ChreSettingsTest.TestCommand.Feature feature)
+            throws InterruptedException {
         mTestResult.set(null);
         mTestSetupComplete.set(false);
         mCountDownLatch = new CountDownLatch(1);
@@ -155,12 +156,8 @@
             Assert.fail("Failed to send message: result = " + result);
         }
 
-        try {
-            mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
-        }
-
+        boolean success = mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: test setup", success);
         Assert.assertTrue(
                 "Failed to set up test", mTestSetupComplete.get() || mTestResult.get() != null);
     }
@@ -173,7 +170,7 @@
      */
     public void startTestAssertSuccess(
             ChreSettingsTest.TestCommand.Feature feature,
-            ChreSettingsTest.TestCommand.State state) {
+            ChreSettingsTest.TestCommand.State state) throws InterruptedException {
         mTestResult.set(null);
         mCountDownLatch = new CountDownLatch(1);
 
@@ -188,11 +185,8 @@
             Assert.fail("Failed to send message: result = " + result);
         }
 
-        try {
-            mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
-        }
+        boolean success = mCountDownLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: wait for test", success);
 
         if (mTestResult.get() == null) {
             Assert.fail("No test result received");
diff --git a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWifiSettingsTestExecutor.java b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWifiSettingsTestExecutor.java
index 50d740d..0cc477b 100644
--- a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWifiSettingsTestExecutor.java
+++ b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWifiSettingsTestExecutor.java
@@ -95,12 +95,12 @@
         mExecutor.init();
     }
 
-    public void runWifiScanningTest() {
+    public void runWifiScanningTest() throws InterruptedException {
         runWifiScanningTest(false /* enableFeature */);
         runWifiScanningTest(true /* enableFeature */);
     }
 
-    public void runWifiRangingTest() {
+    public void runWifiRangingTest() throws InterruptedException {
         runWifiRangingTest(false /* enableFeature */);
         runWifiRangingTest(true /* enableFeature */);
     }
@@ -108,7 +108,7 @@
     /**
      * Should be called in an @After method.
      */
-    public void tearDown() {
+    public void tearDown() throws InterruptedException {
         mExecutor.deinit();
         mSettingsUtil.setWifi(mInitialWifiEnabled);
         mSettingsUtil.setWifiScanningSettings(mInitialWifiScanningAlwaysEnabled);
@@ -119,7 +119,7 @@
      * Sets the WiFi scanning settings on the device.
      * @param enable true to enable WiFi settings, false to disable it.
      */
-    private void setWifiSettings(boolean enable) {
+    private void setWifiSettings(boolean enable) throws InterruptedException {
         WifiUpdateListener wifiUpdateListener = new WifiUpdateListener(enable);
         mContext.registerReceiver(
                 wifiUpdateListener.mWifiUpdateReceiver,
@@ -128,14 +128,11 @@
         mSettingsUtil.setWifiScanningSettings(enable);
         mSettingsUtil.setWifi(enable);
 
-        try {
-            wifiUpdateListener.mWifiLatch.await(30, TimeUnit.SECONDS);
+        boolean success = wifiUpdateListener.mWifiLatch.await(30, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: set wifi settings", success);
 
-            // Wait a few seconds to ensure setting is propagated to CHRE path
-            Thread.sleep(10000);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
-        }
+        // Wait a few seconds to ensure setting is propagated to CHRE path
+        Thread.sleep(10000);
 
         mContext.unregisterReceiver(wifiUpdateListener.mWifiUpdateReceiver);
     }
@@ -144,7 +141,7 @@
      * Helper function to run the test.
      * @param enableFeature True for enable.
      */
-    private void runWifiScanningTest(boolean enableFeature) {
+    private void runWifiScanningTest(boolean enableFeature) throws InterruptedException {
         setWifiSettings(enableFeature);
 
         ChreSettingsTest.TestCommand.State state = enableFeature
@@ -158,7 +155,7 @@
      * Helper function to run the test.
      * @param enableFeature True for enable.
      */
-    private void runWifiRangingTest(boolean enableFeature) {
+    private void runWifiRangingTest(boolean enableFeature) throws InterruptedException {
         if (!mSettingsUtil.isWifiEnabled() || !mSettingsUtil.isWifiScanningAlwaysEnabled()) {
             setWifiSettings(true /* enable */);
         }
diff --git a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWwanSettingsTestExecutor.java b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWwanSettingsTestExecutor.java
index e6d82e7..c7f726f 100644
--- a/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWwanSettingsTestExecutor.java
+++ b/java/test/settings/src/com/google/android/chre/test/setting/ContextHubWwanSettingsTestExecutor.java
@@ -51,7 +51,7 @@
         mExecutor.init();
     }
 
-    public void runWwanTest() {
+    public void runWwanTest() throws InterruptedException {
         runTest(false /* enableFeature */);
         runTest(true /* enableFeature */);
     }
@@ -59,7 +59,7 @@
     /**
      * Should be called in an @After method.
      */
-    public void tearDown() {
+    public void tearDown() throws InterruptedException {
         mExecutor.deinit();
         mSettingsUtil.setAirplaneMode(mInitialAirplaneMode);
     }
@@ -68,7 +68,7 @@
      * Helper function to run the test.
      * @param enableFeature True for enable.
      */
-    private void runTest(boolean enableFeature) {
+    private void runTest(boolean enableFeature) throws InterruptedException {
         boolean airplaneModeExpected = !enableFeature;
         mSettingsUtil.setAirplaneMode(airplaneModeExpected);
 
diff --git a/java/test/stress/Android.bp b/java/test/stress/Android.bp
index 7e9988c..81c1c46 100644
--- a/java/test/stress/Android.bp
+++ b/java/test/stress/Android.bp
@@ -34,5 +34,5 @@
         "chre-test-utils",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/stress/src/com/google/android/chre/test/stress/ContextHubStressTestExecutor.java b/java/test/stress/src/com/google/android/chre/test/stress/ContextHubStressTestExecutor.java
index 378640a..a907a00 100644
--- a/java/test/stress/src/com/google/android/chre/test/stress/ContextHubStressTestExecutor.java
+++ b/java/test/stress/src/com/google/android/chre/test/stress/ContextHubStressTestExecutor.java
@@ -175,7 +175,7 @@
      * @param timeout The amount of time to run the stress test.
      * @param unit    The unit for timeout.
      */
-    public void runStressTest(long timeout, TimeUnit unit) {
+    public void runStressTest(long timeout, TimeUnit unit) throws InterruptedException {
         ChreStressTest.TestCommand.Feature[] features = {
                 ChreStressTest.TestCommand.Feature.WIFI_ON_DEMAND_SCAN,
                 ChreStressTest.TestCommand.Feature.GNSS_LOCATION,
@@ -194,11 +194,8 @@
         }
 
         if (!mLoadAndStartOnly) {
-            try {
-                mCountDownLatch.await(timeout, unit);
-            } catch (InterruptedException e) {
-                Assert.fail(e.getMessage());
-            }
+            boolean success = mCountDownLatch.await(timeout, unit);
+            Assert.assertTrue("Timeout waiting for signal", success);
 
             checkTestFailure();
 
@@ -233,7 +230,7 @@
      * 4. Keep the nanoapp loaded, and then run this test.
      * 5. Unload the nanoapp after this test ends.
      */
-    public void runWifiScanMonitorRestartTest() {
+    public void runWifiScanMonitorRestartTest() throws InterruptedException {
         // Since the host connection may have reset, inform the nanoapp about this event.
         NanoAppMessage message = NanoAppMessage.createMessageToNanoApp(
                 mNanoAppId, ChreStressTest.MessageType.TEST_HOST_RESTARTED_VALUE,
@@ -246,11 +243,8 @@
                 new byte[0]);
         sendMessageToNanoApp(message);
 
-        try {
-            mCountDownLatch.await(30, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Assert.fail(e.getMessage());
-        }
+        boolean success = mCountDownLatch.await(30, TimeUnit.SECONDS);
+        Assert.assertTrue("Timeout waiting for signal: wifi scan monitor restart test", success);
 
         if ((mCapabilities.getWifi() & WIFI_CAPABILITIES_SCAN_MONITORING) != 0) {
             WifiManager manager =
@@ -262,11 +256,8 @@
             mCountDownLatch = new CountDownLatch(1);
             Assert.assertTrue(manager.startScan());
 
-            try {
-                mCountDownLatch.await(30, TimeUnit.SECONDS);
-            } catch (InterruptedException e) {
-                Assert.fail(e.getMessage());
-            }
+            success = mCountDownLatch.await(30, TimeUnit.SECONDS);
+            Assert.assertTrue("Timeout waiting for signal: trigger scan monitor", success);
             Assert.assertTrue(mWifiScanMonitorTriggered.get());
             checkTestFailure();
         }
diff --git a/java/test/utils/Android.bp b/java/test/utils/Android.bp
index 94059aa..91a6799 100644
--- a/java/test/utils/Android.bp
+++ b/java/test/utils/Android.bp
@@ -29,11 +29,12 @@
 
     static_libs: [
         "androidx.test.rules",
-        "truth",
         "chre_api_test_proto_java_lite",
         "chre_pigweed_utils",
+        "compatibility-device-util-axt",
         "pw_rpc_java_client",
+        "truth",
     ],
 
-    sdk_version: "system_current",
+    sdk_version: "test_current",
 }
diff --git a/java/test/utils/src/com/google/android/utils/chre/ChreApiTestUtil.java b/java/test/utils/src/com/google/android/utils/chre/ChreApiTestUtil.java
index 7a0be9e..d30d3ed 100644
--- a/java/test/utils/src/com/google/android/utils/chre/ChreApiTestUtil.java
+++ b/java/test/utils/src/com/google/android/utils/chre/ChreApiTestUtil.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 
 import com.google.android.chre.utils.pigweed.ChreRpcClient;
+import com.google.protobuf.Empty;
 import com.google.protobuf.MessageLite;
 
 import java.util.ArrayList;
@@ -55,6 +56,11 @@
     public static final int RPC_TIMEOUT_IN_MS = RPC_TIMEOUT_IN_SECONDS * 1000;
 
     /**
+     * The default timeout for an RPC call in nanosecond.
+     */
+    public static final long RPC_TIMEOUT_IN_NS = RPC_TIMEOUT_IN_SECONDS * 1000000000L;
+
+    /**
      * The number of threads for the executor that executes the futures.
      * We need at least 2 here. One to process the RPCs for server streaming
      * and one to process events (which has server streaming as a dependent).
@@ -140,7 +146,7 @@
 
     /**
      * Calls a server streaming RPC method with RPC_TIMEOUT_IN_SECONDS seconds of
-     * timeout with a void request.
+     * timeout with an empty request.
      *
      * @param <ResponseType>  the type of the response (proto generated type).
      * @param rpcClient       the RPC client.
@@ -155,7 +161,7 @@
         Objects.requireNonNull(rpcClient);
         Objects.requireNonNull(method);
 
-        ChreApiTest.Void request = ChreApiTest.Void.newBuilder().build();
+        Empty request = Empty.newBuilder().build();
         return callServerStreamingRpcMethodSync(rpcClient, method, request);
     }
 
@@ -266,7 +272,7 @@
     }
 
     /**
-     * Calls an RPC method with RPC_TIMEOUT_IN_SECONDS seconds of timeout with a void request.
+     * Calls an RPC method with RPC_TIMEOUT_IN_SECONDS seconds of timeout with an empty request.
      *
      * @param <ResponseType>  the type of the response (proto generated type).
      * @param rpcClient       the RPC client.
@@ -280,7 +286,7 @@
         Objects.requireNonNull(rpcClient);
         Objects.requireNonNull(method);
 
-        ChreApiTest.Void request = ChreApiTest.Void.newBuilder().build();
+        Empty request = Empty.newBuilder().build();
         return callUnaryRpcMethodSync(rpcClient, method, request);
     }
 
@@ -303,7 +309,6 @@
 
         ChreApiTest.GatherEventsInput input = ChreApiTest.GatherEventsInput.newBuilder()
                 .addAllEventTypes(eventTypes)
-                .setEventTypeCount(eventTypes.size())
                 .setEventCount(eventCount)
                 .setTimeoutInNs(timeoutInNs)
                 .build();
@@ -385,69 +390,56 @@
         return new Service("chre.rpc.ChreApiTestService",
                 Service.unaryMethod(
                         "ChreBleGetCapabilities",
-                        ChreApiTest.Void.class,
-                        ChreApiTest.Capabilities.class),
+                        Empty.parser(),
+                        ChreApiTest.Capabilities.parser()),
                 Service.unaryMethod(
                         "ChreBleGetFilterCapabilities",
-                        ChreApiTest.Void.class,
-                        ChreApiTest.Capabilities.class),
-                Service.unaryMethod(
-                        "ChreBleStartScanAsync",
-                        ChreApiTest.ChreBleStartScanAsyncInput.class,
-                        ChreApiTest.Status.class),
+                        Empty.parser(),
+                        ChreApiTest.Capabilities.parser()),
                 Service.serverStreamingMethod(
                         "ChreBleStartScanSync",
-                        ChreApiTest.ChreBleStartScanAsyncInput.class,
-                        ChreApiTest.GeneralSyncMessage.class),
-                Service.unaryMethod(
-                        "ChreBleStopScanAsync",
-                        ChreApiTest.Void.class,
-                        ChreApiTest.Status.class),
+                        ChreApiTest.ChreBleStartScanAsyncInput.parser(),
+                        ChreApiTest.GeneralSyncMessage.parser()),
                 Service.serverStreamingMethod(
                         "ChreBleStopScanSync",
-                        ChreApiTest.Void.class,
-                        ChreApiTest.GeneralSyncMessage.class),
+                        Empty.parser(),
+                        ChreApiTest.GeneralSyncMessage.parser()),
                 Service.unaryMethod(
                         "ChreSensorFindDefault",
-                        ChreApiTest.ChreSensorFindDefaultInput.class,
-                        ChreApiTest.ChreSensorFindDefaultOutput.class),
+                        ChreApiTest.ChreSensorFindDefaultInput.parser(),
+                        ChreApiTest.ChreSensorFindDefaultOutput.parser()),
                 Service.unaryMethod(
                         "ChreGetSensorInfo",
-                        ChreApiTest.ChreHandleInput.class,
-                        ChreApiTest.ChreGetSensorInfoOutput.class),
+                        ChreApiTest.ChreHandleInput.parser(),
+                        ChreApiTest.ChreGetSensorInfoOutput.parser()),
                 Service.unaryMethod(
                         "ChreGetSensorSamplingStatus",
-                        ChreApiTest.ChreHandleInput.class,
-                        ChreApiTest.ChreGetSensorSamplingStatusOutput.class),
+                        ChreApiTest.ChreHandleInput.parser(),
+                        ChreApiTest.ChreGetSensorSamplingStatusOutput.parser()),
                 Service.unaryMethod(
                         "ChreSensorConfigure",
-                        ChreApiTest.ChreSensorConfigureInput.class,
-                        ChreApiTest.Status.class),
+                        ChreApiTest.ChreSensorConfigureInput.parser(),
+                        ChreApiTest.Status.parser()),
                 Service.unaryMethod(
                         "ChreSensorConfigureModeOnly",
-                        ChreApiTest.ChreSensorConfigureModeOnlyInput.class,
-                        ChreApiTest.Status.class),
+                        ChreApiTest.ChreSensorConfigureModeOnlyInput.parser(),
+                        ChreApiTest.Status.parser()),
                 Service.unaryMethod(
                         "ChreAudioGetSource",
-                        ChreApiTest.ChreHandleInput.class,
-                        ChreApiTest.ChreAudioGetSourceOutput.class),
+                        ChreApiTest.ChreHandleInput.parser(),
+                        ChreApiTest.ChreAudioGetSourceOutput.parser()),
                 Service.unaryMethod(
                         "ChreConfigureHostEndpointNotifications",
-                        ChreApiTest.ChreConfigureHostEndpointNotificationsInput.class,
-                        ChreApiTest.Status.class),
-                Service.unaryMethod(
-                        "RetrieveLatestDisconnectedHostEndpointEvent",
-                        ChreApiTest.Void.class,
-                        ChreApiTest.RetrieveLatestDisconnectedHostEndpointEventOutput
-                                .class),
+                        ChreApiTest.ChreConfigureHostEndpointNotificationsInput.parser(),
+                        ChreApiTest.Status.parser()),
                 Service.unaryMethod(
                         "ChreGetHostEndpointInfo",
-                        ChreApiTest.ChreGetHostEndpointInfoInput.class,
-                        ChreApiTest.ChreGetHostEndpointInfoOutput.class),
+                        ChreApiTest.ChreGetHostEndpointInfoInput.parser(),
+                        ChreApiTest.ChreGetHostEndpointInfoOutput.parser()),
                 Service.serverStreamingMethod(
                         "GatherEvents",
-                        ChreApiTest.GatherEventsInput.class,
-                        ChreApiTest.GeneralEventsMessage.class));
+                        ChreApiTest.GatherEventsInput.parser(),
+                        ChreApiTest.GeneralEventsMessage.parser()));
     }
 
     /**
diff --git a/java/test/utils/src/com/google/android/utils/chre/ContextHubHostTestUtil.java b/java/test/utils/src/com/google/android/utils/chre/ContextHubHostTestUtil.java
new file mode 100644
index 0000000..ba1b8a0
--- /dev/null
+++ b/java/test/utils/src/com/google/android/utils/chre/ContextHubHostTestUtil.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2023 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.utils.chre;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+import android.hardware.location.NanoAppBinary;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.DynamicConfigDeviceSide;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class that defines various utility functions to be used by Context Hub host tests.
+ */
+public class ContextHubHostTestUtil {
+    /**
+     * The names of the dynamic configs corresponding to each test suite.
+     */
+    public static final String[] DEVICE_DYNAMIC_CONFIG_NAMES =
+            new String[] {"GtsGmscoreHostTestCases", "GtsLocationContextMultiDeviceTestCases"};
+
+    public static String multiDeviceExternalNanoappPath = null;
+
+    /**
+     * Returns the path to the directory containing the nanoapp binaries.
+     * It is the external path if passed in, otherwise the relative path
+     * to the assets directory of the context of the calling app.
+     *
+     * @param context the Context to find the asset resources
+     * @param hubInfo the ContextHubInfo describing the hub
+     * @return the path to the nanoapps
+     */
+    public static String getNanoAppBinaryPath(Context context, ContextHubInfo hubInfo) {
+        String path = getExternalNanoAppPath();
+
+        // Only check for bundled nanoapps if the test is not in debug mode
+        if (path == null) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                path = getNanoAppBinaryPathFromPlatformId(hubInfo.getChrePlatformId());
+            } else {
+                path = getNanoAppBinaryPathFromHubName(hubInfo.getName());
+            }
+
+            boolean haveNanoApps = false;
+            try {
+                haveNanoApps = context.getAssets().list(path).length > 0;
+            } catch (IOException e) {
+                Assert.fail("IOException while getting asset list at " + path);
+            }
+
+            // We anticipate this failure case (ContextHubInfo being of an unknown
+            // platform for us) much more than the case of knowing about a platform
+            // but not having a specific nanoapp built for it.  So we separate
+            // out this error check to help the user debug this case more easily.
+            Assert.assertTrue("None of the test nanoapps are available on the platform: " + path,
+                    haveNanoApps);
+        }
+        return path;
+    }
+
+    /**
+     * Waits on a CountDownLatch or assert if it timed out or was interrupted.
+     *
+     * @param latch                       the CountDownLatch
+     * @param timeout                     the timeout duration
+     * @param unit                        the timeout unit
+     * @param timeoutErrorMessage         the message to display on timeout assert
+     */
+    public static void awaitCountDownLatchAssertOnFailure(
+            CountDownLatch latch, long timeout, TimeUnit unit, String timeoutErrorMessage)
+                    throws InterruptedException {
+        boolean result = latch.await(timeout, unit);
+        Assert.assertTrue(timeoutErrorMessage, result);
+    }
+
+    /**
+     * Creates a NanoAppBinary object from the nanoapp filename.
+     *
+     * @param hub      the hub to create the binary for
+     * @param filename the filename of the nanoapp
+     * @return the NanoAppBinary object
+     */
+    public static NanoAppBinary createNanoAppBinary(ContextHubInfo hub, String filename) {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        String fullName = getNanoAppBinaryPath(context, hub) + "/" + filename;
+
+        InputStream stream = getNanoAppInputStream(context, fullName);
+        byte[] binary = null;
+        try {
+            binary = new byte[stream.available()];
+            stream.read(binary);
+        } catch (IOException e) {
+            Assert.fail("IOException while reading binary for " + fullName + ": " + e.getMessage());
+        }
+
+        return new NanoAppBinary(binary);
+    }
+
+    /**
+     * Read the nanoapp to an InputStream object.
+     *
+     * @param context   the Context to find the asset resources
+     * @param fullName  the fullName of the nanoapp
+     * @return the InputStream of the nanoapp
+     */
+    public static InputStream getNanoAppInputStream(Context context, String fullName) {
+        InputStream inputStream = null;
+        try {
+            inputStream = (getExternalNanoAppPath() == null)
+                    ? context.getAssets().open(fullName) :
+                    new FileInputStream(new File(fullName));
+        } catch (IOException e) {
+            Assert.fail("Could not find asset " + fullName + ": "
+                        + e.toString());
+        }
+        return inputStream;
+    }
+
+    /**
+     * Converts a list of integers to a byte array.
+     *
+     * @param intArray the integer values
+     * @return the byte[] array containing the integer values in byte order
+     */
+    public static byte[] intArrayToByteArray(int[] intArray) {
+        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * intArray.length);
+        for (int i = 0; i < intArray.length; i++) {
+            buffer.putInt(intArray[i]);
+        }
+
+        return buffer.array();
+    }
+
+    /**
+     * Determines if the device under test is in the allowlist to run CHQTS.
+     *
+     * The device-side test currently skips the test only if the Context Hub
+     * Service does not exist, but the Android framework currently exposes it unconditionally.
+     * This method should be used to skip the test if the test device is not in the allowlist and
+     * no Context Hub exists in order to avoid false positive failures. In the future, the framework
+     * should be modified to only expose the service if CHRE is supported on the device. This hack
+     * should then only be used on Android version that do not have that capability.
+     *
+     * @return true if the device is in the allowlist, false otherwise
+     */
+    public static boolean deviceInAllowlist() {
+        DynamicConfigDeviceSide deviceDynamicConfig = getDynamicConfig();
+        List<String> configValues = deviceDynamicConfig.getValues("chre_device_whitelist");
+        Assert.assertTrue("Could not find device allowlist from dynamic config",
+                configValues != null);
+
+        String deviceName = Build.DEVICE;
+        for (String element : configValues) {
+            if (element.equals(deviceName)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Determines if the device under test is in the denylist to NOT run CHQTS.
+     *
+     * The denylist is structured as "device,platform_id,max_chre_version", and characterizes
+     * if a device should skip running CHQTS. For platform_id, and max_chre_version, "*" denotes
+     * "matches everything".
+     *
+     * @return true if the device is in the denylist, false otherwise
+     */
+    public static boolean deviceInDenylist() {
+        DynamicConfigDeviceSide deviceDynamicConfig = getDynamicConfig();
+        List<String> configValues = deviceDynamicConfig.getValues("chre_device_blacklist");
+        Assert.assertTrue("Could not find device denylist from dynamic config",
+                configValues != null);
+
+        String deviceName = Build.DEVICE;
+        for (String element : configValues) {
+            String[] delimited = element.split(",");
+            if (delimited.length != 0 && delimited[0].equals(deviceName)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the path of the nanoapps for a CHRE implementation using the platform ID.
+     *
+     * The dynamic configuration file for GtsGmscoreHostTestCases defines the key
+     * 'chre_platform_id_nanoapp_dir_pairs', whose value is a list of strings of the form
+     * '{platformId},{assetDirectoryName}', where platformId is one defined by the Context Hub
+     * and is returned by ContextHubInfo.getChrePlatformId().
+     *
+     * @param platformId the platform ID of the hub
+     * @return the path to the nanoapps
+     */
+    private static String getNanoAppBinaryPathFromPlatformId(long platformId) {
+        DynamicConfigDeviceSide deviceDynamicConfig = getDynamicConfig();
+        List<String> configValues =
+                deviceDynamicConfig.getValues("chre_platform_id_nanoapp_dir_pairs");
+        Assert.assertTrue("Could not find nanoapp asset list from dynamic config",
+                configValues != null);
+
+        String platformIdHexString = Long.toHexString(platformId);
+        String path = null;
+        for (String element : configValues) {
+            String[] delimited = element.split(",");
+            if (delimited.length == 2 && delimited[0].equals(platformIdHexString)) {
+                path = delimited[1];
+                break;
+            }
+        }
+
+        Assert.assertTrue("Could not find known asset directory for hub with platform ID = 0x"
+                + platformIdHexString, path != null);
+        return path;
+    }
+
+    /**
+     * Returns the path for nanoapps of a CHRE implementation using the Context Hub name.
+     *
+     * The dynamic configuration file for GtsGmscoreHostTestCases defines the key
+     * 'chre_hubname_nanoapp_dir_pairs', whose value is a list of strings of the form
+     * '{hubName},{assetDirectoryName}', where hubName is one defined by the Context Hub
+     * and is returned by ContextHubInfo.getName(). This method should be used instead of
+     * getNanoAppBinaryPathFromPlatformId() prior to Android P.
+     *
+     * @param contextHubName the name of the hub
+     * @return the path to the nanoapps
+     */
+    private static String getNanoAppBinaryPathFromHubName(String contextHubName) {
+        DynamicConfigDeviceSide deviceDynamicConfig = getDynamicConfig();
+        List<String> configValues =
+                deviceDynamicConfig.getValues("chre_hubname_nanoapp_dir_pairs");
+        Assert.assertTrue("Could not find nanoapp asset list from dynamic config",
+                configValues != null);
+
+        String path = null;
+        for (String element : configValues) {
+            String[] delimited = element.split(",");
+            if (delimited.length == 2 && delimited[0].equals(contextHubName)) {
+                path = delimited[1];
+                break;
+            }
+        }
+
+        Assert.assertTrue("Could not find known asset directory for hub " + contextHubName,
+                path != null);
+        return path;
+    }
+
+    /**
+     * @return the device side dynamic config for GtsGmscoreHostTestCases or
+     *         GtsLocationContextMultiDeviceTestCases
+     */
+    private static DynamicConfigDeviceSide getDynamicConfig() {
+        DynamicConfigDeviceSide deviceDynamicConfig = null;
+        for (String deviceDynamicConfigName: DEVICE_DYNAMIC_CONFIG_NAMES) {
+            try {
+                deviceDynamicConfig = new DynamicConfigDeviceSide(deviceDynamicConfigName);
+            } catch (XmlPullParserException e) {
+                Assert.fail(e.getMessage());
+            } catch (IOException e) {
+                // Not found - try again
+            }
+        }
+
+        if (deviceDynamicConfig == null) {
+            Assert.fail("Could not get the device dynamic config.");
+        }
+        return deviceDynamicConfig;
+    }
+
+    /**
+     * Returns the path to external nanoapps. This method should be used to debug the test
+     * with custom unbundled nanoapps, and must not be used in actual certification.
+     *
+     * @return external nanoapp path, null if no externalNanoAppPath passed in
+     */
+    public static String getExternalNanoAppPath() {
+        if (multiDeviceExternalNanoappPath != null) {
+            return multiDeviceExternalNanoappPath;
+        }
+
+        Bundle extras = InstrumentationRegistry.getArguments();
+        return (extras == null) ? null : extras.getString("externalNanoAppPath");
+    }
+
+    /**
+     * Runs various checks to decide if the platform should run CHQTS.
+     *
+     * @param context The context at which the test should run.
+     * @param manager The ContextHubManager on this app.
+     */
+    public static void checkDeviceShouldRunTest(Context context, ContextHubManager manager) {
+        boolean supportsContextHub;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            supportsContextHub =
+                    context.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_CONTEXT_HUB);
+            Assert.assertTrue("ContextHubManager must be null if feature is not supported.",
+                    supportsContextHub || manager == null);
+        } else {
+            supportsContextHub = (manager != null);
+        }
+        Assume.assumeTrue("Device does not support Context Hub, skipping test", supportsContextHub);
+
+        int numContextHubs;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            numContextHubs = manager.getContextHubs().size();
+        } else {
+            int[] handles = manager.getContextHubHandles();
+            Assert.assertNotNull(handles);
+            numContextHubs = handles.length;
+        }
+
+        // Only use allowlist logic on builds that do not require the Context Hub feature flag.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            // Use allowlist on platforms that reports no Context Hubs to prevent false positive
+            // failures on devices that do not actually support CHRE.
+            Assume.assumeTrue(
+                    "Device not in allowlist and does not have Context Hub, skipping test",
+                    numContextHubs != 0 || deviceInAllowlist());
+        }
+
+        // Use a denylist on platforms that should not run CHQTS.
+        Assume.assumeTrue("Device is in denylist, skipping test", !deviceInDenylist());
+    }
+}
diff --git a/java/test/utils/src/com/google/android/utils/chre/ContextHubServiceTestHelper.java b/java/test/utils/src/com/google/android/utils/chre/ContextHubServiceTestHelper.java
index 1c0731a..18623a0 100644
--- a/java/test/utils/src/com/google/android/utils/chre/ContextHubServiceTestHelper.java
+++ b/java/test/utils/src/com/google/android/utils/chre/ContextHubServiceTestHelper.java
@@ -26,6 +26,8 @@
 import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.NanoAppBinary;
 import android.hardware.location.NanoAppState;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 
 import java.util.HashMap;
 import java.util.List;
@@ -65,8 +67,14 @@
 
     public void initAndUnloadAllNanoApps() throws InterruptedException, TimeoutException {
         init();
-        // Unload all nanoapps to ensure test starts at a clean state.
-        unloadAllNanoApps();
+
+        // We only need to unload all nanoapps when the device has version < U, so the
+        // tests remain the same on those devices. On newer devices, test mode will
+        // handle this.
+        if (VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            // Unload all nanoapps to ensure test starts at a clean state.
+            unloadAllNanoApps();
+        }
     }
 
     public void deinit() {
diff --git a/java/test/utils/src/com/google/android/utils/chre/SettingsUtil.java b/java/test/utils/src/com/google/android/utils/chre/SettingsUtil.java
index 98a2ace..54401ac 100644
--- a/java/test/utils/src/com/google/android/utils/chre/SettingsUtil.java
+++ b/java/test/utils/src/com/google/android/utils/chre/SettingsUtil.java
@@ -116,7 +116,7 @@
      * @param enable True to enable location, false to disable it.
      * @param timeoutSeconds The maximum amount of time in seconds to wait.
      */
-    public void setLocationMode(boolean enable, long timeoutSeconds) {
+    public void setLocationMode(boolean enable, long timeoutSeconds) throws InterruptedException {
         if (isLocationEnabled() != enable) {
             LocationUpdateListener listener = new LocationUpdateListener();
 
@@ -125,14 +125,11 @@
                     new IntentFilter(LocationManager.MODE_CHANGED_ACTION));
             mLocationManager.setLocationEnabledForUser(enable, UserHandle.CURRENT);
 
-            try {
-                listener.mLocationLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+            boolean success = listener.mLocationLatch.await(timeoutSeconds, TimeUnit.SECONDS);
+            Assert.assertTrue("Timeout waiting for signal: set location mode", success);
 
-                // Wait 1 additional second to make sure setting gets propagated to CHRE
-                Thread.sleep(1000);
-            } catch (InterruptedException e) {
-                Assert.fail("InterruptedException while waiting for location update");
-            }
+            // Wait 1 additional second to make sure setting gets propagated to CHRE
+            Thread.sleep(1000);
 
             Assert.assertTrue(isLocationEnabled() == enable);
 
@@ -172,7 +169,7 @@
     public boolean isBluetoothEnabled() {
         String out = ChreTestUtil.executeShellCommand(
                 mInstrumentation, "settings get global bluetooth_on");
-        return ChreTestUtil.convertToIntegerOrFail(out) > 0;
+        return ChreTestUtil.convertToIntegerOrFail(out) == 1;
     }
 
     /**
@@ -190,7 +187,7 @@
      * Sets the airplane mode on the device.
      * @param enable True to enable airplane mode, false to disable it.
      */
-    public void setAirplaneMode(boolean enable) {
+    public void setAirplaneMode(boolean enable) throws InterruptedException {
         if (isAirplaneModeOn() != enable) {
             AirplaneModeListener listener = new AirplaneModeListener();
             mContext.registerReceiver(
@@ -201,13 +198,10 @@
             ChreTestUtil.executeShellCommand(
                     mInstrumentation, "cmd connectivity airplane-mode " + value);
 
-            try {
-                listener.mAirplaneModeLatch.await(10, TimeUnit.SECONDS);
-                // Wait 1 additional second to make sure setting gets propagated to CHRE
-                Thread.sleep(1000);
-            } catch (InterruptedException e) {
-                Assert.fail(e.getMessage());
-            }
+            boolean success = listener.mAirplaneModeLatch.await(10, TimeUnit.SECONDS);
+            Assert.assertTrue("Timeout waiting for signal: set airplane mode", success);
+            // Wait 1 additional second to make sure setting gets propagated to CHRE
+            Thread.sleep(1000);
             Assert.assertTrue(isAirplaneModeOn() == enable);
             mContext.unregisterReceiver(listener.mAirplaneModeReceiver);
         }
diff --git a/java/utils/pigweed/Android.bp b/java/utils/pigweed/Android.bp
index 1559d00..77ab1cf 100644
--- a/java/utils/pigweed/Android.bp
+++ b/java/utils/pigweed/Android.bp
@@ -28,7 +28,6 @@
     srcs: ["src/**/*.java"],
 
     static_libs: [
-        "androidx.annotation_annotation",
         "guava",
         "pw_rpc_java_client",
     ],
diff --git a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreCallbackHandler.java b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreCallbackHandler.java
index b84ebad..291ebf7 100644
--- a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreCallbackHandler.java
+++ b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreCallbackHandler.java
@@ -22,9 +22,6 @@
 import android.hardware.location.ContextHubClientCallback;
 import android.hardware.location.NanoAppMessage;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 import java.util.Objects;
 
 import dev.pigweed.pw_rpc.Client;
@@ -35,16 +32,17 @@
  */
 public class ChreCallbackHandler extends ContextHubClientCallback {
     private final long mNanoappId;
-    @Nullable
+    // Nullable.
     private final ContextHubClientCallback mCallback;
     private Client mRpcClient;
     private ChreChannelOutput mChannelOutput;
 
     /**
      * @param nanoappId ID of the RPC Server nanoapp
-     * @param callback The callbacks receiving messages and life-cycle events from nanoapps
+     * @param callback  The callbacks receiving messages and life-cycle events from nanoapps,
+     *                  nullable.
      */
-    public ChreCallbackHandler(long nanoappId, @Nullable ContextHubClientCallback callback) {
+    public ChreCallbackHandler(long nanoappId, ContextHubClientCallback callback) {
         mNanoappId = nanoappId;
         mCallback = callback;
     }
@@ -52,10 +50,10 @@
     /**
      * Completes the initialization.
      *
-     * @param rpcClient The Pigweed RPC client
-     * @param channelOutput The ChannelOutput used by Pigweed
+     * @param rpcClient     The Pigweed RPC client, non null
+     * @param channelOutput The ChannelOutput used by Pigweed, non null
      */
-    public void lateInit(@NonNull Client rpcClient, @NonNull ChreChannelOutput channelOutput) {
+    public void lateInit(Client rpcClient, ChreChannelOutput channelOutput) {
         mRpcClient = Objects.requireNonNull(rpcClient);
         mChannelOutput = Objects.requireNonNull(channelOutput);
     }
diff --git a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreChannelOutput.java b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreChannelOutput.java
index 43440b5..1e79693 100644
--- a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreChannelOutput.java
+++ b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreChannelOutput.java
@@ -30,10 +30,9 @@
  */
 public class ChreChannelOutput implements Channel.Output {
     /**
-     * Random value chosen not too close to max value to try to avoid conflicts with other messages
-     * in case pw rpc isn't the only way the client chooses to communicate.
+     * Message type to use for RPC messages.
      */
-    public static final int PW_RPC_CHRE_MESSAGE_TYPE = Integer.MAX_VALUE - 10;
+    public static final int CHRE_MESSAGE_TYPE_RPC = 0x7FFFFFF5;
 
     // 1 denotes that a host endpoint is the client that created the channel.
     private static final int CHANNEL_ID_HOST_CLIENT = (1 << 16);
@@ -55,7 +54,7 @@
     @Override
     public void send(byte[] packet) throws ChannelOutputException {
         NanoAppMessage message = NanoAppMessage.createMessageToNanoApp(mNanoappId,
-                PW_RPC_CHRE_MESSAGE_TYPE, packet);
+                CHRE_MESSAGE_TYPE_RPC, packet);
         if (mAuthDenied.get()
                 || ContextHubTransaction.RESULT_SUCCESS != mClient.sendMessageToNanoApp(message)) {
             throw new ChannelOutputException();
diff --git a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreIntentHandler.java b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreIntentHandler.java
index bec78fc..91cdcf2 100644
--- a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreIntentHandler.java
+++ b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreIntentHandler.java
@@ -23,8 +23,6 @@
 import android.hardware.location.ContextHubManager;
 import android.util.Log;
 
-import androidx.annotation.NonNull;
-
 import java.util.Objects;
 
 import dev.pigweed.pw_rpc.Client;
@@ -38,13 +36,13 @@
     /**
      * Handles CHRE intents.
      *
-     * @param intent        the intent.
+     * @param intent        the intent, non null
      * @param nanoappId     ID of the RPC Server nanoapp
-     * @param rpcClient     The Pigweed RPC client
-     * @param channelOutput The ChannelOutput used by Pigweed
+     * @param rpcClient     The Pigweed RPC client, non null
+     * @param channelOutput The ChannelOutput used by Pigweed, non null
      */
-    public static void handle(@NonNull Intent intent, long nanoappId, @NonNull Client rpcClient,
-            @NonNull ChreChannelOutput channelOutput) {
+    public static void handle(Intent intent, long nanoappId, Client rpcClient,
+            ChreChannelOutput channelOutput) {
         Objects.requireNonNull(intent);
         Objects.requireNonNull(rpcClient);
         Objects.requireNonNull(channelOutput);
diff --git a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreRpcClient.java b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreRpcClient.java
index 0ba98ed..205482e 100644
--- a/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreRpcClient.java
+++ b/java/utils/pigweed/src/com/google/android/chre/utils/pigweed/ChreRpcClient.java
@@ -23,9 +23,6 @@
 import android.hardware.location.NanoAppRpcService;
 import android.hardware.location.NanoAppState;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 import java.util.List;
 import java.util.Objects;
 
@@ -40,14 +37,15 @@
  * See https://g3doc.corp.google.com/location/lbs/contexthub/g3doc/nanoapps/pw_rpc_host.md
  */
 public class ChreRpcClient {
-    @NonNull
+
+    // Non null.
     private final Client mRpcClient;
-    @NonNull
+    // Non null.
     private final Channel mChannel;
-    @NonNull
+    // Non null.
     private final ChreChannelOutput mChannelOutput;
     private final long mServerNanoappId;
-    @NonNull
+    // Non null.
     private final ContextHubClient mContextHubClient;
     private ChreIntentHandler mIntentHandler;
 
@@ -56,14 +54,16 @@
      *
      * Use this constructor for persistent clients using callbacks.
      *
-     * @param manager         The context manager used to create a client
+     * @param manager         The context manager used to create a client, non null
+     * @param info            Context hub info, non null
      * @param serverNanoappId The ID of the RPC server nanoapp
-     * @param services        The list of services provided by the server
-     * @param callback        The callbacks receiving messages and life-cycle events from nanoapps
+     * @param services        The list of services provided by the server, non null
+     * @param callback        The callbacks receiving messages and life-cycle events from nanoapps,
+     *                        nullable
      */
-    public ChreRpcClient(@NonNull ContextHubManager manager, @NonNull ContextHubInfo info,
-            long serverNanoappId, @NonNull List<Service> services,
-            @Nullable ContextHubClientCallback callback) {
+    public ChreRpcClient(ContextHubManager manager, ContextHubInfo info,
+            long serverNanoappId, List<Service> services,
+            ContextHubClientCallback callback) {
         Objects.requireNonNull(manager);
         Objects.requireNonNull(info);
         Objects.requireNonNull(services);
@@ -83,12 +83,12 @@
      *
      * handleIntent() must be called with any CHRE intent received by the BroadcastReceiver.
      *
-     * @param contextHubClient The context hub client providing the RPC server nanoapp
+     * @param contextHubClient The context hub client providing the RPC server nanoapp, non null
      * @param serverNanoappId  The ID of the RPC server nanoapp
-     * @param services         The list of services provided by the server
+     * @param services         The list of services provided by the server, non null
      */
-    public ChreRpcClient(@NonNull ContextHubClient contextHubClient, long serverNanoappId,
-            @NonNull List<Service> services) {
+    public ChreRpcClient(ContextHubClient contextHubClient, long serverNanoappId,
+            List<Service> services) {
         mContextHubClient = Objects.requireNonNull(contextHubClient);
         Objects.requireNonNull(services);
         mServerNanoappId = serverNanoappId;
@@ -124,9 +124,9 @@
     /**
      * Handles CHRE intents.
      *
-     * @param intent The CHRE intent.
+     * @param intent The CHRE intent, non null
      */
-    public void handleIntent(@NonNull Intent intent) {
+    public void handleIntent(Intent intent) {
         ChreIntentHandler.handle(intent, mServerNanoappId, mRpcClient, mChannelOutput);
     }
 
diff --git a/pal/include/chre/pal/ble.h b/pal/include/chre/pal/ble.h
index 4f27535..aa1cb90 100644
--- a/pal/include/chre/pal/ble.h
+++ b/pal/include/chre/pal/ble.h
@@ -46,9 +46,19 @@
 #define CHRE_PAL_BLE_API_V1_8 CHRE_PAL_CREATE_API_VERSION(1, 8)
 
 /**
+ * Introduced alongside CHRE API v1.8, adds flush() API.
+ */
+#define CHRE_PAL_BLE_API_V1_9 CHRE_PAL_CREATE_API_VERSION(1, 9)
+
+/**
+ * Introduced alongside CHRE API v1.9, add broadcaster address filter.
+ */
+#define CHRE_PAL_BLE_API_V1_10 CHRE_PAL_CREATE_API_VERSION(1, 10)
+
+/**
  * The version of the CHRE BLE PAL defined in this header file.
  */
-#define CHRE_PAL_BLE_API_CURRENT_VERSION CHRE_PAL_BLE_API_V1_8
+#define CHRE_PAL_BLE_API_CURRENT_VERSION CHRE_PAL_BLE_API_V1_10
 
 /**
  * The maximum amount of time allowed to elapse between the call to
@@ -123,6 +133,18 @@
   void (*readRssiCallback)(uint8_t errorCode, uint16_t handle, int8_t rssi);
 
   /**
+   * Callback used to inform CHRE of a completed flush event.
+   *
+   * @param errorCode An error code from enum chreError, with CHRE_ERROR_NONE
+   *        indicating a successful response.
+   *
+   * @see chrePalBleApi.flush
+   *
+   * @since v1.9
+   */
+  void (*flushCallback)(uint8_t errorCode);
+
+  /**
    * Sends a BT snoop log to the CHRE daemon.
    *
    * @param isTxToBtController True if the direction of the BT snoop log is Tx
@@ -202,7 +224,7 @@
    * @see chreBleStartScanAsync()
    */
   bool (*startScan)(enum chreBleScanMode mode, uint32_t reportDelayMs,
-                    const struct chreBleScanFilter *filter);
+                    const struct chreBleScanFilterV1_9 *filter);
   /**
    * Stops Bluetooth LE (BLE) scanning.
    *
@@ -243,6 +265,17 @@
    * @since v1.8
    */
   bool (*readRssi)(uint16_t connectionHandle);
+
+  /**
+   * Initiates a flush operation where all batched advertisement events will be
+   * immediately processed.
+   *
+   * @return true if the request was accepted, in which case a subsequent call
+   * to flushCallback() will be used to indicate the result of the operation.
+   *
+   * @since v1.9
+   */
+  bool (*flush)();
 };
 
 /**
diff --git a/pal/tests/src/audio_pal_impl_test.cc b/pal/tests/src/audio_pal_impl_test.cc
index 1b3c306..daf8e1e 100644
--- a/pal/tests/src/audio_pal_impl_test.cc
+++ b/pal/tests/src/audio_pal_impl_test.cc
@@ -125,8 +125,7 @@
   LockGuard<Mutex> lock(gCallbacks->mMutex);
   EXPECT_TRUE(mApi->requestAudioDataEvent(0 /*handle*/, 1000 /*numSamples*/,
                                           100 /*eventDelaysNs*/));
-  gCallbacks->mCondVarDataEvents.wait_for(
-      gCallbacks->mMutex, Nanoseconds(25 * kOneMillisecondInNanoseconds));
+  gCallbacks->mCondVarDataEvents.wait(gCallbacks->mMutex);
   ASSERT_TRUE(gCallbacks->mDataEvent.has_value());
   struct chreAudioDataEvent *event = gCallbacks->mDataEvent.value();
   EXPECT_EQ(event->handle, 0);
diff --git a/pal/tests/src/ble_pal_impl_test.cc b/pal/tests/src/ble_pal_impl_test.cc
index 0d08a87..8c53aeb 100644
--- a/pal/tests/src/ble_pal_impl_test.cc
+++ b/pal/tests/src/ble_pal_impl_test.cc
@@ -35,6 +35,7 @@
 
 using ::chre::ConditionVariable;
 using ::chre::createBleScanFilterForKnownBeacons;
+using ::chre::createBleScanFilterForKnownBeaconsV1_9;
 using ::chre::FixedSizeVector;
 using ::chre::gChrePalSystemApi;
 using ::chre::LockGuard;
@@ -53,6 +54,10 @@
 
 class Callbacks {
  public:
+  Callbacks() = delete;
+
+  explicit Callbacks(const struct chrePalBleApi *api) : mApi(api) {}
+
   void requestStateResync() {}
 
   void scanStatusChangeCallback(bool enabled, uint8_t errorCode) {
@@ -71,6 +76,8 @@
       if (mEventData.full()) {
         mCondVarEvents.notify_one();
       }
+    } else {
+      mApi->releaseAdvertisingEvent(event);
     }
   }
 
@@ -83,6 +90,9 @@
   Mutex mMutex;
   ConditionVariable mCondVarStatus;
   ConditionVariable mCondVarEvents;
+
+  //! CHRE PAL implementation API.
+  const struct chrePalBleApi *mApi;
 };
 
 UniquePtr<Callbacks> gCallbacks = nullptr;
@@ -108,10 +118,10 @@
 class PalBleTest : public testing::Test {
  protected:
   void SetUp() override {
-    gCallbacks = MakeUnique<Callbacks>();
     chre::TaskManagerSingleton::deinit();
     chre::TaskManagerSingleton::init();
     mApi = chrePalBleGetApi(CHRE_PAL_BLE_API_CURRENT_VERSION);
+    gCallbacks = MakeUnique<Callbacks>(mApi);
     ASSERT_NE(mApi, nullptr);
     EXPECT_EQ(mApi->moduleVersion, CHRE_PAL_BLE_API_CURRENT_VERSION);
     ASSERT_TRUE(mApi->open(&gChrePalSystemApi, &mPalCallbacks));
@@ -168,19 +178,21 @@
 // advertising BLE beacons with service data for either the Google eddystone
 // or fastpair UUIDs.
 TEST_F(PalBleTest, FilteredScan) {
-  struct chreBleScanFilter filter;
+  struct chreBleScanFilterV1_9 filterV1_9;
   chreBleGenericFilter uuidFilters[kNumScanFilters];
-  createBleScanFilterForKnownBeacons(filter, uuidFilters, kNumScanFilters);
-
-  EXPECT_TRUE(mApi->startScan(CHRE_BLE_SCAN_MODE_BACKGROUND,
-                              kBleBatchDurationMs, &filter));
+  createBleScanFilterForKnownBeaconsV1_9(filterV1_9, uuidFilters,
+                                         kNumScanFilters);
 
   LockGuard<Mutex> lock(gCallbacks->mMutex);
+
+  EXPECT_TRUE(mApi->startScan(CHRE_BLE_SCAN_MODE_BACKGROUND,
+                              kBleBatchDurationMs, &filterV1_9));
+
+  EXPECT_TRUE(mApi->startScan(CHRE_BLE_SCAN_MODE_AGGRESSIVE,
+                              kBleBatchDurationMs, &filterV1_9));
   gCallbacks->mCondVarStatus.wait_for(gCallbacks->mMutex, kBleStatusTimeoutNs);
-  EXPECT_TRUE(gCallbacks->mEnabled.has_value());
-  if (gCallbacks->mEnabled.has_value()) {
-    EXPECT_TRUE(gCallbacks->mEnabled.value());
-  }
+  ASSERT_TRUE(gCallbacks->mEnabled.has_value());
+  EXPECT_TRUE(gCallbacks->mEnabled.value());
 
   gCallbacks->mCondVarEvents.wait_for(gCallbacks->mMutex, kBleEventTimeoutNs);
   EXPECT_TRUE(gCallbacks->mEventData.full());
diff --git a/pal/tests/src/sensor_pal_impl_test.cc b/pal/tests/src/sensor_pal_impl_test.cc
index f76c617..6b0aefa 100644
--- a/pal/tests/src/sensor_pal_impl_test.cc
+++ b/pal/tests/src/sensor_pal_impl_test.cc
@@ -183,6 +183,8 @@
   ASSERT_TRUE(gCallbacks->mStatus.has_value());
   EXPECT_TRUE(gCallbacks->mStatus.value()->enabled);
   gApi->releaseSamplingStatusEvent(gCallbacks->mStatus.value());
+  gCallbacks->mStatus.reset();
+  gCallbacks->mStatusSensorIndex.reset();
 
   gCallbacks->mCondVarEvents.wait_for(
       gCallbacks->mMutex,
@@ -197,6 +199,22 @@
     EXPECT_EQ(threeAxisData->header.readingCount, 1);
     gApi->releaseSensorDataEvent(data);
   }
+  // Need to unlock this mutex because the following disable sensor request
+  // needs it.
+  gCallbacks->mMutex.unlock();
+
+  EXPECT_TRUE(gApi->configureSensor(
+      0 /* sensorInfoIndex */, CHRE_SENSOR_CONFIGURE_MODE_DONE,
+      kOneMillisecondInNanoseconds /* intervalNs */, 0 /* latencyNs */));
+  gCallbacks->mMutex.lock();
+  gCallbacks->mCondVarStatus.wait_for(
+      gCallbacks->mMutex,
+      Nanoseconds(kTimeoutMultiplier * kOneMillisecondInNanoseconds));
+  ASSERT_TRUE(gCallbacks->mStatusSensorIndex.has_value());
+  ASSERT_TRUE(gCallbacks->mStatus.has_value());
+  gApi->releaseSamplingStatusEvent(gCallbacks->mStatus.value());
+  gCallbacks->mStatus.reset();
+  gCallbacks->mStatusSensorIndex.reset();
 }
 
 TEST_F(PalSensorTest, DisableAContinuousSensor) {
@@ -208,11 +226,13 @@
   gCallbacks->mCondVarStatus.wait_for(
       gCallbacks->mMutex,
       Nanoseconds(kTimeoutMultiplier * kOneMillisecondInNanoseconds));
-  EXPECT_TRUE(gCallbacks->mStatusSensorIndex.has_value());
+  ASSERT_TRUE(gCallbacks->mStatusSensorIndex.has_value());
   EXPECT_EQ(gCallbacks->mStatusSensorIndex.value(), 0);
-  EXPECT_TRUE(gCallbacks->mStatus.has_value());
+  ASSERT_TRUE(gCallbacks->mStatus.has_value());
   EXPECT_FALSE(gCallbacks->mStatus.value()->enabled);
   gApi->releaseSamplingStatusEvent(gCallbacks->mStatus.value());
+  gCallbacks->mStatus.reset();
+  gCallbacks->mStatusSensorIndex.reset();
 }
 
-}  // namespace
\ No newline at end of file
+}  // namespace
diff --git a/platform/exynos/host_link.cc b/platform/exynos/host_link.cc
index 3e03bec..480e022 100644
--- a/platform/exynos/host_link.cc
+++ b/platform/exynos/host_link.cc
@@ -139,6 +139,8 @@
   // TODO(b/230134803): Implement this.
 }
 
+void HostMessageHandlers::handlePulseRequest() {}
+
 void HostMessageHandlers::sendFragmentResponse(uint16_t hostClientId,
                                                uint32_t transactionId,
                                                uint32_t fragmentId,
diff --git a/platform/include/chre/platform/assert.h b/platform/include/chre/platform/assert.h
index 218dd48..be83a36 100644
--- a/platform/include/chre/platform/assert.h
+++ b/platform/include/chre/platform/assert.h
@@ -21,19 +21,26 @@
 
 /**
  * @file
- * Defines the CHRE_ASSERT and CHRE_ASSERT_LOG macros for CHRE platforms.
- * Platforms must supply an implementation for assertCondition or use the shared
- * implementation.
+ * Includes the platform-specific header file that supplies an assertion macro.
+ * The platform header must supply the following symbol as a macro or free
+ * function:
+ *
+ *   CHRE_ASSERT(scalar expression)
+ *
+ * Where expression will be checked to be false (ie: compares equal to zero) and
+ * terminate the program if found to be the case.
  */
 
-#if defined(CHRE_ASSERTIONS_ENABLED)
+#if defined(CHRE_ASSERTIONS_ENABLED) && defined(CHRE_ASSERTIONS_DISABLED)
+#error "CHRE_ASSERT is both enabled and disabled!"
 
-#define CHRE_ASSERT(condition)               \
-  do {                                       \
-    if (!(condition)) {                      \
-      chreDoAssert(CHRE_FILENAME, __LINE__); \
-    }                                        \
-  } while (0)
+#elif defined(CHRE_ASSERTIONS_ENABLED)
+
+#include "chre/target_platform/assert.h"
+
+#ifndef CHRE_ASSERT
+#error "CHRE_ASSERT must be defined by the target platform's assert.h"
+#endif  // CHRE_ASSERT
 
 #elif defined(CHRE_ASSERTIONS_DISABLED)
 
@@ -81,20 +88,4 @@
 #define CHRE_ASSERT_IF_NOT_TEST(condition) CHRE_ASSERT(condition)
 #endif
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * Performs assertion while logging the filename and line provided.
- *
- * @param filename The filename containing the assertion being fired.
- * @param line The line that contains the assertion being fired.
- */
-void chreDoAssert(const char *filename, size_t line);
-
-#ifdef __cplusplus
-}
-#endif
-
 #endif  // CHRE_PLATFORM_ASSERT_H_
diff --git a/platform/include/chre/platform/atomic.h b/platform/include/chre/platform/atomic.h
index 7688c49..445feb9 100644
--- a/platform/include/chre/platform/atomic.h
+++ b/platform/include/chre/platform/atomic.h
@@ -53,7 +53,7 @@
    */
   operator bool() const {
     return load();
-  };
+  }
 
   /**
    * Atomically loads the current value of the atomic object.
diff --git a/platform/include/chre/platform/fatal_error.h b/platform/include/chre/platform/fatal_error.h
index 0c301db..f272eba 100644
--- a/platform/include/chre/platform/fatal_error.h
+++ b/platform/include/chre/platform/fatal_error.h
@@ -40,6 +40,9 @@
   do {                        \
     LOGE(fmt, ##__VA_ARGS__); \
     FATAL_ERROR_QUIT();       \
+    while (1) {               \
+      /* never return */      \
+    }                         \
   } while (0)
 
 /**
diff --git a/platform/include/chre/platform/platform_ble.h b/platform/include/chre/platform/platform_ble.h
index 3f9b56a..fdb495f 100644
--- a/platform/include/chre/platform/platform_ble.h
+++ b/platform/include/chre/platform/platform_ble.h
@@ -69,7 +69,7 @@
    * @return true if scan was successfully enabled.
    */
   bool startScanAsync(chreBleScanMode mode, uint32_t reportDelayMs,
-                      const struct chreBleScanFilter *filter);
+                      const struct chreBleScanFilterV1_9 *filter);
 
   /**
    * End a BLE scan asynchronously. The result is delivered through a
@@ -105,6 +105,17 @@
    * @since v1.8
    */
   bool readRssiAsync(uint16_t connectionHandle);
+
+  /**
+   * Initiates a flush operation where all batched advertisement events will be
+   * immediately processed.
+   *
+   * @return true if the request was accepted, in which case a subsequent call
+   * to flushCallback() will be used to indicate the result of the operation.
+   *
+   * @since v1.9
+   */
+  bool flushAsync();
 };
 
 }  // namespace chre
diff --git a/platform/include/chre/platform/tracing.h b/platform/include/chre/platform/tracing.h
index fc94deb..7a5559b 100644
--- a/platform/include/chre/platform/tracing.h
+++ b/platform/include/chre/platform/tracing.h
@@ -19,36 +19,90 @@
 
 #include <cstdint>
 
-/**
- * @file
- * Tracing support for CHRE.
- * Platforms must supply an implementation of the trace functions.
- */
+// Needs to be a number because it's used in STRINGIFY and as a number.
+#define CHRE_TRACE_STR_BUFFER_SIZE 11
+// Strings are placed into a buffer in the form:
+// {<1-byte str len>, str chars...}.
+// So the max string size is always one less than the total string buffer size.
+#define CHRE_TRACE_MAX_STRING_SIZE CHRE_TRACE_STR_BUFFER_SIZE - 1
 
-namespace chre {
-/**
- * Registers a nanoapp instance with the tracing infrastructure.
- *
- * @param instanceId instance ID of the nanoapp.
- * @param name name of the nanoapp.
- */
-void traceRegisterNanoapp(uint16_t instanceId, const char *name);
+// TODO(b/301497381): See if netstruct lib would be more useful here
+// Field values defined by python struct docs:
+// https://docs.python.org/3/library/struct.html.
+#define TRACE_BOOL "?"
+#define TRACE_U8 "B"
+#define TRACE_U16 "H"
+#define TRACE_U32 "L"
+#define TRACE_U64 "Q"
+#define TRACE_I8 "b"
+#define TRACE_I16 "h"
+#define TRACE_I32 "l"
+#define TRACE_I64 "q"
+#define TRACE_C "c"
+#define TRACE_S STRINGIFY(CHRE_TRACE_STR_BUFFER_SIZE) "p"
+
+// Check to make sure pointer size macro is defined.
+#ifndef __SIZEOF_POINTER__
+#error "__SIZEOF_POINTER__ macro not defined - unsupported toolchain being used"
+#else
+static_assert(sizeof(void *) == __SIZEOF_POINTER__,
+              "Size of pointer does not match __SIZEOF_POINTER__ macro");
+#endif
+
+// Check the predefined pointer size to use the most accurate size
+#if __SIZEOF_POINTER__ == 8
+#define TRACE_PTR TRACE_U64
+#elif __SIZEOF_POINTER__ == 4
+#define TRACE_PTR TRACE_U32
+#else
+#error "Pointer size is of unsupported size"
+#endif  // __SIZEOF_POINTER__ == 8 || __SIZEOF_POINTER__ == 4
+
+#ifdef CHRE_TRACING_ENABLED
+
+#include "chre/target_platform/tracing.h"
 
 /**
- * Marks the start of the nanoappHandleEvent function.
- *
- * @param instanceId instance ID of the nanoapp.
- * @param eventType event being handled.
+ * All tracing macros to be used in CHRE
  */
-void traceNanoappHandleEventStart(uint16_t instanceId, uint16_t eventType);
+#ifndef CHRE_TRACE_INSTANT
+#error "CHRE_TRACE_INSTANT must be defined by chre/target_platform/tracing.h"
+#endif
 
-/**
- * Marks the end of the nanoappHandleEvent function.
- *
- * @param instanceId instance ID of the nanoapp.
- */
-void traceNanoappHandleEventEnd(uint16_t instanceId);
+#ifndef CHRE_TRACE_START
+#error "CHRE_TRACE_START must be defined by chre/target_platform/tracing.h"
+#endif
 
-}  // namespace chre
+#ifndef CHRE_TRACE_END
+#error "CHRE_TRACE_END must be defined by chre/target_platform/tracing.h"
+#endif
+
+#ifndef CHRE_TRACE_INSTANT_DATA
+#error \
+    "CHRE_TRACE_INSTANT_DATA must be defined by chre/target_platform/tracing.h"
+#endif
+
+#ifndef CHRE_TRACE_START_DATA
+#error "CHRE_TRACE_START_DATA must be defined by chre/target_platform/tracing.h"
+#endif
+
+#ifndef CHRE_TRACE_END_DATA
+#error "CHRE_TRACE_END_DATA must be defined by chre/target_platform/tracing.h"
+#endif
+
+#else  // CHRE_TRACING_ENABLED
+
+#include "chre/util/macros.h"
+
+inline void chreTraceUnusedParams(...) {}
+
+#define CHRE_TRACE_INSTANT(...) chreTraceUnusedParams(__VA_ARGS__)
+#define CHRE_TRACE_START(...) chreTraceUnusedParams(__VA_ARGS__)
+#define CHRE_TRACE_END(...) chreTraceUnusedParams(__VA_ARGS__)
+#define CHRE_TRACE_INSTANT_DATA(...) chreTraceUnusedParams(__VA_ARGS__)
+#define CHRE_TRACE_START_DATA(...) chreTraceUnusedParams(__VA_ARGS__)
+#define CHRE_TRACE_END_DATA(...) chreTraceUnusedParams(__VA_ARGS__)
+
+#endif  // CHRE_TRACING_ENABLED
 
 #endif  // CHRE_PLATFORM_TRACING_H_
diff --git a/platform/linux/include/chre/platform/linux/task_util/task.h b/platform/linux/include/chre/platform/linux/task_util/task.h
index 858cfb7..698df3b 100644
--- a/platform/linux/include/chre/platform/linux/task_util/task.h
+++ b/platform/linux/include/chre/platform/linux/task_util/task.h
@@ -28,7 +28,7 @@
 
 /**
  * Represents a task to execute (a function to call) that can be executed once
- * or repeatedly with interval: repeatInterval in milliseconds until
+ * or repeatedly with interval: intervalOrDelay in nanoseconds until
  * cancel() is called.
  *
  * Note: The Task class is not thread-safe nor synchronized properly. It is
@@ -51,12 +51,14 @@
    * Construct a new Task object.
    *
    * @param func              the function to execute.
-   * @param repeatInterval    the interval in which to repeat execution in
-   *                          milliseconds.
+   * @param intervalOrDelay   the interval in which to repeat execution or the
+   * delay for a one-shot Task.
    * @param id                the unique ID for use with the Task Manager.
+   * @param isOneShot         if true, the task should only be executed once
+   * after a delay of intervalOrDelay.
    */
-  Task(const TaskFunction &func, std::chrono::milliseconds repeatInterval,
-       uint32_t id);
+  Task(const TaskFunction &func, std::chrono::nanoseconds intervalOrDelay,
+       uint32_t id, bool isOneShot = false);
 
   /**
    * Construct a new Task object.
@@ -68,8 +70,8 @@
   /**
    * Assignment operator.
    *
-   * @param rhs              rhs arg.
-   * @return                 this.
+   * @param rhs               rhs arg.
+   * @return                  this.
    */
   Task &operator=(const Task &rhs);
 
@@ -101,8 +103,8 @@
   /**
    * Returns true if the task has executed at least once, false if otherwise.
    *
-   * @return true         if the task has executed at least once.
-   * @return false        if the task has not executed at least once.
+   * @return true             if the task has executed at least once.
+   * @return false            if the task has not executed at least once.
    */
   inline bool hasExecuted() const {
     return mHasExecuted;
@@ -112,8 +114,8 @@
    * Returns true if the task is ready to execute (time now is >= task
    * timestamp).
    *
-   * @return true         the task can be executed.
-   * @return false        do not yet execute the task.
+   * @return true             the task can be executed.
+   * @return false            do not yet execute the task.
    */
   inline bool isReadyToExecute() const {
     return mExecutionTimestamp <= std::chrono::steady_clock::now();
@@ -121,10 +123,10 @@
 
   /**
    * Returns true if the task is a repeating task - if it has has a
-   * repeatInterval > 0.
+   * intervalOrDelay > 0.
    *
-   * @return true         if the task is a repeating task.
-   * @return false        otherwise.
+   * @return true             if the task is a repeating task.
+   * @return false            otherwise.
    */
   inline bool isRepeating() const {
     return mRepeatInterval.count() > 0;
@@ -158,7 +160,7 @@
   /**
    * The amount of time to wait in between repeating the task.
    */
-  std::chrono::milliseconds mRepeatInterval;
+  std::chrono::nanoseconds mRepeatInterval;
 
   /**
    * The function to execute.
diff --git a/platform/linux/include/chre/platform/linux/task_util/task_manager.h b/platform/linux/include/chre/platform/linux/task_util/task_manager.h
index 9710c39..ad32221 100644
--- a/platform/linux/include/chre/platform/linux/task_util/task_manager.h
+++ b/platform/linux/include/chre/platform/linux/task_util/task_manager.h
@@ -51,24 +51,29 @@
 
   /**
    * Adds a task to the queue for execution. The manager calls the function func
-   * during execution. If repeatInterval > 0, the task will repeat every
-   * repeatInterval milliseconds. If repeatInterval == 0, the task will be
-   * executed only once.
+   * during execution. If intervalOrDelay > 0 and isOneShot is false, the task
+   * will repeat every intervalOrDelay nanoseconds. If intervalOrDelay is > 0
+   * and isOneShot is true, the task will be executed only once after a delay of
+   * intervalOrDelay. If intervalOrDelay == 0, the task will be executed only
+   * once with no delay.
    *
    * @param func                     the function to call.
-   * @param repeatInterval           the interval to repeat.
+   * @param intervalOrDelay          the interval to repeat.
+   * @param isOneShot                if true, the task should be executed only
+   * once with a delay of intervalOrDelay.
    * @return                         the ID of the Task object or an empty
    * Optional<> when there is an error.
    */
   std::optional<uint32_t> addTask(
       const Task::TaskFunction &func,
-      std::chrono::milliseconds repeatInterval = std::chrono::milliseconds(0));
+      std::chrono::nanoseconds intervalOrDelay = std::chrono::nanoseconds(0),
+      bool isOneShot = false);
 
   /**
    * Cancels the task with the taskId.
    *
-   * @param taskId              the ID of the task.
-   * @return bool               success.
+   * @param taskId                   the ID of the task.
+   * @return bool                    success.
    */
   bool cancelTask(uint32_t taskId);
 
diff --git a/platform/linux/include/chre/target_platform/assert.h b/platform/linux/include/chre/target_platform/assert.h
new file mode 100644
index 0000000..76f2450
--- /dev/null
+++ b/platform/linux/include/chre/target_platform/assert.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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 PLATFORM_LINUX_INCLUDE_CHRE_TARGET_PLATFORM_ASSERT_H
+#define PLATFORM_LINUX_INCLUDE_CHRE_TARGET_PLATFORM_ASSERT_H
+
+#include "chre/platform/shared/assert_func.h"
+
+#endif // PLATFORM_LINUX_INCLUDE_CHRE_TARGET_PLATFORM_ASSERT_H
diff --git a/platform/linux/pal_audio.cc b/platform/linux/pal_audio.cc
index 3664c0d..5816232 100644
--- a/platform/linux/pal_audio.cc
+++ b/platform/linux/pal_audio.cc
@@ -31,7 +31,7 @@
  */
 namespace {
 
-using chre::TaskManagerSingleton;
+using ::chre::TaskManagerSingleton;
 
 const struct chrePalSystemApi *gSystemApi = nullptr;
 const struct chrePalAudioCallbacks *gCallbacks = nullptr;
@@ -45,6 +45,7 @@
 void stopHandle0Task() {
   if (gHandle0TaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gHandle0TaskId.value());
+    gHandle0TaskId.reset();
   }
 }
 
@@ -80,9 +81,6 @@
       static_cast<const uint8_t *>(chre::memoryAlloc(numSamples));
 
   gCallbacks->audioDataEventCallback(data.release());
-
-  // Cancel the task so this is only run once with a delay.
-  TaskManagerSingleton::get()->cancelTask(gHandle0TaskId.value());
 }
 
 bool chrePalAudioApiRequestAudioDataEvent(uint32_t handle, uint32_t numSamples,
@@ -96,8 +94,7 @@
     gIsHandle0Enabled = true;
     gHandle0TaskId = TaskManagerSingleton::get()->addTask(
         [numSamples]() { sendHandle0Events(numSamples); },
-        std::chrono::duration_cast<std::chrono::milliseconds>(
-            std::chrono::nanoseconds(eventDelayNs)));
+        std::chrono::nanoseconds(eventDelayNs), true /*isOneShot*/);
     if (!gHandle0TaskId.has_value()) {
       return false;
     }
diff --git a/platform/linux/pal_ble.cc b/platform/linux/pal_ble.cc
index bb4b144..341caea 100644
--- a/platform/linux/pal_ble.cc
+++ b/platform/linux/pal_ble.cc
@@ -24,23 +24,30 @@
 
 #include <chrono>
 #include <optional>
+#include <vector>
 
 /**
  * A simulated implementation of the BLE PAL for the linux platform.
  */
 namespace {
 
-using chre::TaskManagerSingleton;
+using ::chre::TaskManagerSingleton;
 
 const struct chrePalSystemApi *gSystemApi = nullptr;
 const struct chrePalBleCallbacks *gCallbacks = nullptr;
 
 bool gBleEnabled = false;
 bool gDelayScanStart = false;
-std::chrono::milliseconds gScanInterval(1400);
+
+std::mutex gBatchMutex;
+std::vector<struct chreBleAdvertisementEvent *> gBatchedAdEvents;
+std::chrono::time_point<std::chrono::steady_clock> gLastAdDataTimestamp;
+std::optional<uint32_t> gReportDelayMs;
+std::chrono::nanoseconds gScanInterval = std::chrono::milliseconds(1400);
 
 // Tasks for startScan, sendAdReportEvents, and stopScan.
 std::optional<uint32_t> gBleAdReportEventTaskId;
+std::optional<uint32_t> gBleFlushTaskId;
 
 void updateScanInterval(chreBleScanMode mode) {
   gScanInterval = std::chrono::milliseconds(1400);
@@ -57,6 +64,15 @@
   }
 }
 
+void flush() {
+  std::lock_guard<std::mutex> lock(gBatchMutex);
+  for (struct chreBleAdvertisementEvent *batchedEvent : gBatchedAdEvents) {
+    gCallbacks->advertisingEventCallback(batchedEvent);
+  }
+  gBatchedAdEvents.clear();
+  gLastAdDataTimestamp = std::chrono::steady_clock::now();
+}
+
 void sendAdReportEvents() {
   auto event = chre::MakeUniqueZeroFill<struct chreBleAdvertisementEvent>();
   auto report = chre::MakeUniqueZeroFill<struct chreBleAdvertisingReport>();
@@ -69,22 +85,58 @@
   report->dataLength = 2;
   event->reports = report.release();
   event->numReports = 1;
-  gCallbacks->advertisingEventCallback(event.release());
+
+  std::lock_guard<std::mutex> lock(gBatchMutex);
+  if (!gReportDelayMs.has_value() || gReportDelayMs.value() == 0) {
+    gCallbacks->advertisingEventCallback(event.release());
+  } else {
+    gBatchedAdEvents.push_back(event.release());
+  }
 }
 
 void stopAllTasks() {
   if (gBleAdReportEventTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gBleAdReportEventTaskId.value());
+    gBleAdReportEventTaskId.reset();
+  }
+
+  if (gBleFlushTaskId.has_value()) {
+    TaskManagerSingleton::get()->cancelTask(gBleFlushTaskId.value());
+    gBleFlushTaskId.reset();
   }
 }
 
 bool startScan() {
   stopAllTasks();
-  gCallbacks->scanStatusChangeCallback(true, CHRE_ERROR_NONE);
-  gBleEnabled = true;
+
+  std::lock_guard<std::mutex> lock(gBatchMutex);
+  gLastAdDataTimestamp = std::chrono::steady_clock::now();
+
   gBleAdReportEventTaskId =
       TaskManagerSingleton::get()->addTask(sendAdReportEvents, gScanInterval);
-  return gBleAdReportEventTaskId.has_value();
+  if (!gBleAdReportEventTaskId.has_value()) {
+    return false;
+  }
+
+  if (gReportDelayMs.has_value()) {
+    gBleFlushTaskId = TaskManagerSingleton::get()->addTask(
+        flush, std::chrono::duration_cast<std::chrono::nanoseconds>(
+                   std::chrono::milliseconds(gReportDelayMs.value())));
+    if (!gBleFlushTaskId.has_value()) {
+      stopAllTasks();
+      return false;
+    }
+  }
+
+  std::optional<uint32_t> callbackTaskId = TaskManagerSingleton::get()->addTask(
+      []() { gCallbacks->scanStatusChangeCallback(true, CHRE_ERROR_NONE); });
+  if (!callbackTaskId.has_value()) {
+    stopAllTasks();
+    return false;
+  }
+
+  gBleEnabled = true;
+  return true;
 }
 
 uint32_t chrePalBleGetCapabilities() {
@@ -98,20 +150,33 @@
          CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA;
 }
 
-bool chrePalBleStartScan(chreBleScanMode mode, uint32_t /* reportDelayMs */,
-                         const struct chreBleScanFilter * /* filter */) {
-  updateScanInterval(mode);
-  if (gDelayScanStart) {
-    return true;
+bool chrePalBleStartScan(chreBleScanMode mode, uint32_t reportDelayMs,
+                         const struct chreBleScanFilterV1_9 * /* filter */) {
+  {
+    std::lock_guard<std::mutex> lock(gBatchMutex);
+
+    if (gReportDelayMs.has_value()) {
+      gReportDelayMs = std::min(gReportDelayMs.value(), reportDelayMs);
+    } else {
+      gReportDelayMs = reportDelayMs;
+    }
   }
-  return startScan();
+
+  updateScanInterval(mode);
+  flush();
+  return gDelayScanStart || startScan();
 }
 
 bool chrePalBleStopScan() {
   stopAllTasks();
-  gCallbacks->scanStatusChangeCallback(false, CHRE_ERROR_NONE);
-  gBleEnabled = false;
-  return true;
+  flush();
+
+  std::optional<uint32_t> callbackTaskId = TaskManagerSingleton::get()->addTask(
+      []() { gCallbacks->scanStatusChangeCallback(false, CHRE_ERROR_NONE); });
+
+  // If the callback is successfully scheduled, then BLE is disabled.
+  gBleEnabled = !callbackTaskId.has_value();
+  return callbackTaskId.has_value();
 }
 
 void chrePalBleReleaseAdvertisingEvent(
@@ -125,12 +190,34 @@
 }
 
 bool chrePalBleReadRssi(uint16_t connectionHandle) {
-  gCallbacks->readRssiCallback(CHRE_ERROR_NONE, connectionHandle, -65);
-  return true;
+  std::optional<uint32_t> readRssiTaskId =
+      TaskManagerSingleton::get()->addTask([connectionHandle]() {
+        gCallbacks->readRssiCallback(CHRE_ERROR_NONE, connectionHandle, -65);
+      });
+
+  return readRssiTaskId.has_value();
+}
+
+bool chrePalBleFlush() {
+  std::optional<uint32_t> flushTaskId =
+      TaskManagerSingleton::get()->addTask([]() {
+        flush();
+        gCallbacks->flushCallback(CHRE_ERROR_NONE);
+      });
+
+  return flushTaskId.has_value();
 }
 
 void chrePalBleApiClose() {
   stopAllTasks();
+
+  {
+    std::lock_guard<std::mutex> lock(gBatchMutex);
+
+    for (struct chreBleAdvertisementEvent *batchedEvent : gBatchedAdEvents) {
+      chrePalBleReleaseAdvertisingEvent(batchedEvent);
+    }
+  }
 }
 
 bool chrePalBleApiOpen(const struct chrePalSystemApi *systemApi,
@@ -172,6 +259,7 @@
       .stopScan = chrePalBleStopScan,
       .releaseAdvertisingEvent = chrePalBleReleaseAdvertisingEvent,
       .readRssi = chrePalBleReadRssi,
+      .flush = chrePalBleFlush,
   };
 
   if (!CHRE_PAL_VERSIONS_ARE_COMPATIBLE(kApi.moduleVersion,
diff --git a/platform/linux/pal_gnss.cc b/platform/linux/pal_gnss.cc
index ebb40b5..ae78a64 100644
--- a/platform/linux/pal_gnss.cc
+++ b/platform/linux/pal_gnss.cc
@@ -32,7 +32,7 @@
  */
 namespace {
 
-using chre::TaskManagerSingleton;
+using ::chre::TaskManagerSingleton;
 
 const struct chrePalSystemApi *gSystemApi = nullptr;
 const struct chrePalGnssCallbacks *gCallbacks = nullptr;
@@ -40,7 +40,6 @@
 // Task to deliver asynchronous location data after a CHRE request.
 std::mutex gLocationEventsMutex;
 std::optional<uint32_t> gLocationEventsTaskId;
-std::optional<uint32_t> gLocationEventsChangeCallbackTaskId;
 uint32_t gLocationEventsMinIntervalMs = 0;
 bool gDelaySendingLocationEvents = false;
 bool gIsLocationEnabled = false;
@@ -49,7 +48,6 @@
 std::optional<uint32_t> gLocationStatusTaskId;
 
 // Task to deliver asynchronous measurement data after a CHRE request.
-std::optional<uint32_t> gMeasurementEventsChangeCallbackTaskId;
 std::optional<uint32_t> gMeasurementEventsTaskId;
 bool gIsMeasurementEnabled = false;
 
@@ -73,11 +71,12 @@
   std::lock_guard<std::mutex> lock(gLocationEventsMutex);
   if (gLocationEventsTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gLocationEventsTaskId.value());
+    gLocationEventsTaskId.reset();
   }
 
-  gLocationEventsChangeCallbackTaskId = TaskManagerSingleton::get()->addTask(
+  TaskManagerSingleton::get()->addTask(
       []() { gCallbacks->locationStatusChangeCallback(true, CHRE_ERROR_NONE); },
-      std::chrono::milliseconds(0));
+      std::chrono::nanoseconds(0), true /* isOneShot */);
 
   gLocationEventsTaskId = TaskManagerSingleton::get()->addTask(
       sendLocationEvents, std::chrono::milliseconds(minIntervalMs));
@@ -108,33 +107,28 @@
 void stopLocationTasks() {
   {
     std::lock_guard<std::mutex> lock(gLocationEventsMutex);
-    if (gLocationEventsChangeCallbackTaskId.has_value()) {
-      TaskManagerSingleton::get()->cancelTask(
-          gLocationEventsChangeCallbackTaskId.value());
-    }
 
     if (gLocationEventsTaskId.has_value()) {
       TaskManagerSingleton::get()->cancelTask(gLocationEventsTaskId.value());
+      gLocationEventsTaskId.reset();
     }
   }
 
   if (gLocationStatusTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gLocationStatusTaskId.value());
+    gLocationStatusTaskId.reset();
   }
 }
 
 void stopMeasurementTasks() {
-  if (gMeasurementEventsChangeCallbackTaskId.has_value()) {
-    TaskManagerSingleton::get()->cancelTask(
-        gMeasurementEventsChangeCallbackTaskId.value());
-  }
-
   if (gMeasurementEventsTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gMeasurementEventsTaskId.value());
+    gMeasurementEventsTaskId.reset();
   }
 
   if (gMeasurementStatusTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gMeasurementStatusTaskId.value());
+    gMeasurementStatusTaskId.reset();
   }
 }
 
@@ -150,8 +144,7 @@
   gLocationEventsMinIntervalMs = minIntervalMs;
   if (enable && !gDelaySendingLocationEvents) {
     startSendingLocationEvents(minIntervalMs);
-    if (!gLocationEventsChangeCallbackTaskId.has_value() ||
-        !gLocationEventsTaskId.has_value()) {
+    if (!gLocationEventsTaskId.has_value()) {
       return false;
     }
   } else if (!enable) {
@@ -173,14 +166,14 @@
   stopMeasurementTasks();
 
   if (enable) {
-    gMeasurementEventsChangeCallbackTaskId =
+    std::optional<uint32_t> measurementEventsChangeCallbackTaskId =
         TaskManagerSingleton::get()->addTask(
             []() {
               gCallbacks->measurementStatusChangeCallback(true,
                                                           CHRE_ERROR_NONE);
             },
-            std::chrono::milliseconds(0));
-    if (!gMeasurementEventsChangeCallbackTaskId.has_value()) {
+            std::chrono::nanoseconds(0), true /* isOneShot */);
+    if (!measurementEventsChangeCallbackTaskId.has_value()) {
       return false;
     }
 
diff --git a/platform/linux/pal_sensor.cc b/platform/linux/pal_sensor.cc
index d4df5d3..2fb2bf2 100644
--- a/platform/linux/pal_sensor.cc
+++ b/platform/linux/pal_sensor.cc
@@ -31,7 +31,7 @@
  */
 namespace {
 
-using chre::TaskManagerSingleton;
+using ::chre::TaskManagerSingleton;
 
 const struct chrePalSystemApi *gSystemApi = nullptr;
 const struct chrePalSensorCallbacks *gCallbacks = nullptr;
@@ -57,6 +57,7 @@
 void stopSensor0Task() {
   if (gSensor0TaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gSensor0TaskId.value());
+    gSensor0TaskId.reset();
   }
 }
 
@@ -127,9 +128,7 @@
     gIsSensor0Enabled = true;
     sendSensor0StatusUpdate(intervalNs, true /*enabled*/);
     gSensor0TaskId = TaskManagerSingleton::get()->addTask(
-        sendSensor0Events,
-        std::chrono::duration_cast<std::chrono::milliseconds>(
-            std::chrono::nanoseconds(intervalNs)));
+        sendSensor0Events, std::chrono::nanoseconds(intervalNs));
     return gSensor0TaskId.has_value();
   }
 
diff --git a/platform/linux/pal_wifi.cc b/platform/linux/pal_wifi.cc
index a28205c..7013b89 100644
--- a/platform/linux/pal_wifi.cc
+++ b/platform/linux/pal_wifi.cc
@@ -34,7 +34,7 @@
  */
 namespace {
 
-using chre::TaskManagerSingleton;
+using ::chre::TaskManagerSingleton;
 
 const struct chrePalSystemApi *gSystemApi = nullptr;
 const struct chrePalWifiCallbacks *gCallbacks = nullptr;
@@ -51,6 +51,9 @@
 //! Whether PAL should respond to scan request.
 std::atomic_bool gEnableScanResponse(true);
 
+//! Thread sync variable for TaskIds.
+std::mutex gRequestScanMutex;
+
 //! Task IDs for the scanning tasks
 std::optional<uint32_t> gScanMonitorTaskId;
 std::optional<uint32_t> gRequestScanTaskId;
@@ -58,10 +61,19 @@
 
 //! How long should each the PAL hold before response.
 //! Use to mimic real world hardware process time.
-std::chrono::milliseconds gAsyncRequestDelayResponseTime[chre::asBaseType(
+std::chrono::nanoseconds gAsyncRequestDelayResponseTime[chre::asBaseType(
     PalWifiAsyncRequestTypes::NUM_WIFI_REQUEST_TYPE)];
 
 void sendScanResponse() {
+  {
+    std::lock_guard<std::mutex> lock(gRequestScanMutex);
+    if (!gRequestScanTaskId.has_value()) {
+      LOGE("Sending scan response with no pending task");
+      return;
+    }
+    gRequestScanTaskId.reset();
+  }
+
   if (gEnableScanResponse) {
     auto event = chre::MakeUniqueZeroFill<struct chreWifiScanEvent>();
     auto result = chre::MakeUniqueZeroFill<struct chreWifiScanResult>();
@@ -71,9 +83,6 @@
     event->results = result.release();
     gCallbacks->scanEventCallback(event.release());
   }
-
-  // We just want to delay this task - only execute it once.
-  TaskManagerSingleton::get()->cancelTask(gRequestScanTaskId.value());
 }
 
 void sendScanMonitorResponse(bool enable) {
@@ -95,18 +104,21 @@
 void stopScanMonitorTask() {
   if (gScanMonitorTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gScanMonitorTaskId.value());
+    gScanMonitorTaskId.reset();
   }
 }
 
 void stopRequestScanTask() {
   if (gRequestScanTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gRequestScanTaskId.value());
+    gRequestScanTaskId.reset();
   }
 }
 
 void stopRequestRangingTask() {
   if (gRequestRangingTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(gRequestRangingTaskId.value());
+    gRequestRangingTaskId.reset();
   }
 }
 
@@ -125,7 +137,11 @@
 }
 
 bool chrePalWifiApiRequestScan(const struct chreWifiScanParams * /* params */) {
-  stopRequestScanTask();
+  std::lock_guard<std::mutex> lock(gRequestScanMutex);
+  if (gRequestScanTaskId.has_value()) {
+    LOGE("Requesting scan when existing scan request still in process");
+    return false;
+  }
 
   std::optional<uint32_t> requestScanTaskCallbackId =
       TaskManagerSingleton::get()->addTask([]() {
@@ -135,8 +151,10 @@
       });
   if (requestScanTaskCallbackId.has_value()) {
     gRequestScanTaskId = TaskManagerSingleton::get()->addTask(
-        sendScanResponse, gAsyncRequestDelayResponseTime[chre::asBaseType(
-                              PalWifiAsyncRequestTypes::SCAN)]);
+        sendScanResponse,
+        gAsyncRequestDelayResponseTime[chre::asBaseType(
+            PalWifiAsyncRequestTypes::SCAN)],
+        /* isOneShot= */ true);
     return gRequestScanTaskId.has_value();
   }
   return false;
@@ -257,7 +275,7 @@
 void chrePalWifiDelayResponse(PalWifiAsyncRequestTypes requestType,
                               std::chrono::seconds seconds) {
   gAsyncRequestDelayResponseTime[chre::asBaseType(requestType)] =
-      std::chrono::duration_cast<std::chrono::milliseconds>(seconds);
+      std::chrono::duration_cast<std::chrono::nanoseconds>(seconds);
 }
 
 const struct chrePalWifiApi *chrePalWifiGetApi(uint32_t requestedApiVersion) {
diff --git a/platform/linux/pal_wwan.cc b/platform/linux/pal_wwan.cc
index b7a488c..64e384c 100644
--- a/platform/linux/pal_wwan.cc
+++ b/platform/linux/pal_wwan.cc
@@ -29,7 +29,7 @@
  */
 namespace {
 
-using chre::TaskManagerSingleton;
+using ::chre::TaskManagerSingleton;
 
 const struct chrePalSystemApi *gSystemApi = nullptr;
 const struct chrePalWwanCallbacks *gCallbacks = nullptr;
@@ -63,6 +63,7 @@
 void stopCellInfoTask() {
   if (gCellInfosTaskId.has_value()) {
     TaskManagerSingleton::get()->cancelTask(*gCellInfosTaskId);
+    gCellInfosTaskId.reset();
   }
 }
 
diff --git a/platform/linux/task_util/task.cc b/platform/linux/task_util/task.cc
index f224c10..7b50f8d 100644
--- a/platform/linux/task_util/task.cc
+++ b/platform/linux/task_util/task.cc
@@ -19,16 +19,14 @@
 namespace chre {
 namespace task_manager_internal {
 
-Task::Task(const TaskFunction &func, std::chrono::milliseconds repeatInterval,
-           uint32_t id)
-    : mExecutionTimestamp(std::chrono::steady_clock::now() + repeatInterval),
-      mRepeatInterval(repeatInterval),
+Task::Task(const TaskFunction &func, std::chrono::nanoseconds intervalOrDelay,
+           uint32_t id, bool isOneShot)
+    : mExecutionTimestamp(std::chrono::steady_clock::now() + intervalOrDelay),
+      mRepeatInterval(isOneShot ? std::chrono::nanoseconds(0)
+                                : intervalOrDelay),
+      mFunc(func),
       mId(id),
-      mHasExecuted(false) {
-  if (func != nullptr) {
-    mFunc = func;
-  }
-}
+      mHasExecuted(false) {}
 
 Task::Task(const Task &rhs)
     : mExecutionTimestamp(rhs.mExecutionTimestamp),
@@ -56,7 +54,7 @@
 
 void Task::cancel() {
   std::lock_guard lock(mExecutionMutex);
-  mRepeatInterval = std::chrono::milliseconds(0);
+  mRepeatInterval = std::chrono::nanoseconds(0);
   mFunc.reset();
 }
 
diff --git a/platform/linux/task_util/task_manager.cc b/platform/linux/task_util/task_manager.cc
index b2130b0..22bfad7 100644
--- a/platform/linux/task_util/task_manager.cc
+++ b/platform/linux/task_util/task_manager.cc
@@ -46,7 +46,8 @@
 }
 
 std::optional<uint32_t> TaskManager::addTask(
-    const Task::TaskFunction &func, std::chrono::milliseconds repeatInterval) {
+    const Task::TaskFunction &func, std::chrono::nanoseconds intervalOrDelay,
+    bool isOneShot) {
   std::lock_guard<std::mutex> lock(mMutex);
   bool success = false;
 
@@ -57,7 +58,7 @@
     // select the next ID
     assert(mCurrentId < std::numeric_limits<uint32_t>::max());
     returnId = mCurrentId++;
-    Task task(func, repeatInterval, returnId);
+    Task task(func, intervalOrDelay, returnId, isOneShot);
     success = mQueue.push(task);
   }
 
diff --git a/platform/linux/tests/task_manager_test.cc b/platform/linux/tests/task_manager_test.cc
index 6f52579..28c0a8e 100644
--- a/platform/linux/tests/task_manager_test.cc
+++ b/platform/linux/tests/task_manager_test.cc
@@ -16,8 +16,8 @@
 
 #include <chrono>
 #include <cmath>
+#include <mutex>
 #include <optional>
-#include <thread>
 
 #include "gtest/gtest.h"
 
@@ -25,70 +25,110 @@
 
 namespace {
 
-uint32_t gVarTaskManager = 0;
-uint32_t gTask1Var = 0;
-uint32_t gTask2Var = 0;
-
-constexpr auto incrementGVar = []() { ++gVarTaskManager; };
-constexpr auto task1Func = []() { ++gTask1Var; };
-constexpr auto task2Func = []() { ++gTask2Var; };
-
-TEST(TaskManager, FlushTasks) {
+TEST(TaskManager, FlushTasksCanBeCalledMultipleTimes) {
   chre::TaskManager taskManager;
-  for (uint32_t i = 0; i < 50; ++i) {
+
+  constexpr uint32_t numCallsToFlush = 50;
+  for (uint32_t i = 0; i < numCallsToFlush; ++i) {
     taskManager.flushTasks();
   }
 }
 
-TEST(TaskManager, MultipleNonRepeatingTasks) {
+TEST(TaskManager, MultipleNonRepeatingTasksAreExecuted) {
+  uint32_t counter = 0;
+  std::mutex mutex;
+  std::condition_variable condVar;
   chre::TaskManager taskManager;
-  gVarTaskManager = 0;
+
   constexpr uint32_t numTasks = 50;
+  auto incrementFunc = [&mutex, &condVar, &counter]() {
+    {
+      std::unique_lock<std::mutex> lock(mutex);
+      ++counter;
+    }
+
+    condVar.notify_all();
+  };
   for (uint32_t i = 0; i < numTasks; ++i) {
-    taskManager.addTask(incrementGVar, std::chrono::milliseconds(0));
-    std::this_thread::sleep_for(std::chrono::milliseconds(50));
+    std::optional<uint32_t> taskId =
+        taskManager.addTask(incrementFunc,
+                            /* intervalOrDelay */ std::chrono::nanoseconds(0));
+    EXPECT_TRUE(taskId.has_value());
   }
+
+  std::unique_lock<std::mutex> lock(mutex);
+  condVar.wait(lock, [&counter]() { return counter >= numTasks; });
   taskManager.flushTasks();
-  EXPECT_TRUE(gVarTaskManager == numTasks);
+  EXPECT_EQ(counter, numTasks);
 }
 
-TEST(TaskManager, MultipleTypesOfTasks) {
+TEST(TaskManager, RepeatingAndOneShotTasksCanExecuteTogether) {
+  uint32_t counter = 0;
+  std::mutex mutex;
+  std::condition_variable condVar;
   chre::TaskManager taskManager;
-  gVarTaskManager = 0;
+
   constexpr uint32_t numTasks = 50;
+  auto incrementFunc = [&mutex, &condVar, &counter]() {
+    {
+      std::unique_lock<std::mutex> lock(mutex);
+      ++counter;
+    }
+
+    condVar.notify_all();
+  };
   for (uint32_t i = 0; i < numTasks; ++i) {
-    taskManager.addTask(incrementGVar, std::chrono::milliseconds(0));
-    std::this_thread::sleep_for(std::chrono::milliseconds(50));
+    std::optional<uint32_t> taskId =
+        taskManager.addTask(incrementFunc,
+                            /* intervalOrDelay */ std::chrono::nanoseconds(0));
+    EXPECT_TRUE(taskId.has_value());
   }
-  uint32_t millisecondsToRepeat = 100;
-  std::optional<uint32_t> taskId = taskManager.addTask(
-      incrementGVar, std::chrono::milliseconds(millisecondsToRepeat));
-  EXPECT_TRUE(taskId.has_value());
-  uint32_t taskRepeatTimesMax = 11;
-  std::this_thread::sleep_for(
-      std::chrono::milliseconds(millisecondsToRepeat * taskRepeatTimesMax));
+
+  constexpr std::chrono::nanoseconds interval(50);
+  std::optional<uint32_t> taskId = taskManager.addTask(incrementFunc, interval);
+  ASSERT_TRUE(taskId.has_value());
+
+  constexpr uint32_t taskRepeatTimesMax = 5;
+  std::unique_lock<std::mutex> lock(mutex);
+  condVar.wait(
+      lock, [&counter]() { return counter >= numTasks + taskRepeatTimesMax; });
   EXPECT_TRUE(taskManager.cancelTask(taskId.value()));
   taskManager.flushTasks();
-  EXPECT_TRUE(gVarTaskManager >= numTasks + taskRepeatTimesMax - 1);
+  EXPECT_GE(counter, numTasks + taskRepeatTimesMax);
 }
 
-TEST(TaskManager, FlushTasksWithoutCancel) {
+TEST(TaskManager, TasksCanBeFlushedEvenIfNotCancelled) {
+  uint32_t counter = 0;
+  std::mutex mutex;
+  std::condition_variable condVar;
   chre::TaskManager taskManager;
-  gVarTaskManager = 0;
+
   constexpr uint32_t numTasks = 50;
+  auto incrementFunc = [&mutex, &condVar, &counter]() {
+    {
+      std::unique_lock<std::mutex> lock(mutex);
+      ++counter;
+    }
+
+    condVar.notify_all();
+  };
   for (uint32_t i = 0; i < numTasks; ++i) {
-    taskManager.addTask(incrementGVar, std::chrono::milliseconds(0));
-    std::this_thread::sleep_for(std::chrono::milliseconds(50));
+    std::optional<uint32_t> taskId =
+        taskManager.addTask(incrementFunc,
+                            /* intervalOrDelay */ std::chrono::nanoseconds(0));
+    EXPECT_TRUE(taskId.has_value());
   }
-  uint32_t millisecondsToRepeat = 100;
-  std::optional<uint32_t> taskId = taskManager.addTask(
-      incrementGVar, std::chrono::milliseconds(millisecondsToRepeat));
-  EXPECT_TRUE(taskId.has_value());
-  uint32_t taskRepeatTimesMax = 11;
-  std::this_thread::sleep_for(
-      std::chrono::milliseconds(millisecondsToRepeat * taskRepeatTimesMax));
+
+  constexpr std::chrono::nanoseconds interval(50);
+  std::optional<uint32_t> taskId = taskManager.addTask(incrementFunc, interval);
+  ASSERT_TRUE(taskId.has_value());
+
+  constexpr uint32_t taskRepeatTimesMax = 5;
+  std::unique_lock<std::mutex> lock(mutex);
+  condVar.wait(
+      lock, [&counter]() { return counter >= numTasks + taskRepeatTimesMax; });
   taskManager.flushTasks();
-  EXPECT_TRUE(gVarTaskManager >= numTasks + taskRepeatTimesMax - 1);
+  EXPECT_GE(counter, numTasks + taskRepeatTimesMax);
 }
 
 }  // namespace
diff --git a/platform/linux/tests/task_test.cc b/platform/linux/tests/task_test.cc
index f20823d..ed4656a 100644
--- a/platform/linux/tests/task_test.cc
+++ b/platform/linux/tests/task_test.cc
@@ -31,7 +31,7 @@
 
 TEST(Task, Execute) {
   gVarTask = 0;
-  std::chrono::milliseconds waitTime(1000);
+  std::chrono::milliseconds waitTime(100);
   Task task(incrementGVar, waitTime, 0);
   EXPECT_FALSE(task.isReadyToExecute());
   std::this_thread::sleep_for(waitTime);
@@ -43,7 +43,7 @@
   auto timeDiff =
       std::chrono::steady_clock::now() - task.getExecutionTimestamp();
   EXPECT_TRUE(
-      std::chrono::duration_cast<std::chrono::milliseconds>(timeDiff).count() <=
+      std::chrono::duration_cast<std::chrono::nanoseconds>(timeDiff).count() <=
       waitTime.count());
   task.cancel();
   EXPECT_FALSE(task.isRepeating());
@@ -51,7 +51,7 @@
 
 TEST(Task, ExecuteNoRepeat) {
   gVarTask = 0;
-  std::chrono::milliseconds waitTime(0);
+  std::chrono::nanoseconds waitTime(0);
   Task task(incrementGVar, waitTime, 0);
   EXPECT_TRUE(task.isReadyToExecute());
   task.execute();
@@ -62,12 +62,12 @@
 
 TEST(Task, ComparisonOperators) {
   constexpr uint32_t numTasks = 6;
-  Task tasks[numTasks] = {Task(incrementGVar, std::chrono::milliseconds(0), 0),
-                          Task(incrementGVar, std::chrono::milliseconds(1), 1),
-                          Task(incrementGVar, std::chrono::milliseconds(2), 2),
-                          Task(incrementGVar, std::chrono::milliseconds(3), 3),
-                          Task(incrementGVar, std::chrono::milliseconds(4), 4),
-                          Task(incrementGVar, std::chrono::milliseconds(5), 5)};
+  Task tasks[numTasks] = {Task(incrementGVar, std::chrono::nanoseconds(0), 0),
+                          Task(incrementGVar, std::chrono::nanoseconds(10), 1),
+                          Task(incrementGVar, std::chrono::nanoseconds(20), 2),
+                          Task(incrementGVar, std::chrono::nanoseconds(30), 3),
+                          Task(incrementGVar, std::chrono::nanoseconds(40), 4),
+                          Task(incrementGVar, std::chrono::nanoseconds(50), 5)};
 
   for (uint32_t i = 0; i < numTasks; ++i) {
     if (i < numTasks - 1) {
diff --git a/platform/platform.mk b/platform/platform.mk
index 7e5046f..5212c6d 100644
--- a/platform/platform.mk
+++ b/platform/platform.mk
@@ -110,7 +110,6 @@
 SLPI_SRCS += platform/shared/pal_system_api.cc
 SLPI_SRCS += platform/shared/platform_debug_dump_manager.cc
 SLPI_SRCS += platform/shared/system_time.cc
-SLPI_SRCS += platform/shared/tracing.cc
 SLPI_SRCS += platform/shared/version.cc
 SLPI_SRCS += platform/slpi/chre_api_re.cc
 SLPI_SRCS += platform/slpi/fatal_error.cc
@@ -227,7 +226,6 @@
 SIM_SRCS += platform/shared/nanoapp/nanoapp_dso_util.cc
 SIM_SRCS += platform/shared/pal_system_api.cc
 SIM_SRCS += platform/shared/system_time.cc
-SIM_SRCS += platform/shared/tracing.cc
 SIM_SRCS += platform/shared/version.cc
 
 # Optional audio support.
@@ -336,11 +334,14 @@
 
 # GoogleTest Compiler Flags ####################################################
 
+GOOGLETEST_CFLAGS += $(FLATBUFFERS_CFLAGS)
+
 # The order here is important so that the googletest target prefers shared,
 # linux and then SLPI.
 GOOGLETEST_CFLAGS += -Iplatform/shared/include
 GOOGLETEST_CFLAGS += -Iplatform/linux/include
 GOOGLETEST_CFLAGS += -Iplatform/slpi/include
+GOOGLETEST_CFLAGS += -Iplatform/shared/pw_trace/include
 
 # GoogleTest Source Files ######################################################
 
@@ -350,6 +351,7 @@
 GOOGLETEST_COMMON_SRCS += platform/linux/tests/task_test.cc
 GOOGLETEST_COMMON_SRCS += platform/linux/tests/task_manager_test.cc
 GOOGLETEST_COMMON_SRCS += platform/tests/log_buffer_test.cc
+GOOGLETEST_COMMON_SRCS += platform/tests/trace_test.cc
 GOOGLETEST_COMMON_SRCS += platform/shared/log_buffer.cc
 ifeq ($(CHRE_WIFI_NAN_SUPPORT_ENABLED), true)
 GOOGLETEST_COMMON_SRCS += platform/linux/pal_nan.cc
@@ -391,7 +393,6 @@
 EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/pal_sensor_stub.cc
 EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/platform_debug_dump_manager.cc
 EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/system_time.cc
-EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/tracing.cc
 EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/version.cc
 EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/nanoapp/nanoapp_dso_util.cc
 EMBOS_SRCS += $(CHRE_PREFIX)/platform/shared/nanoapp_loader.cc
@@ -481,7 +482,6 @@
 TINYSYS_SRCS += $(CHRE_PREFIX)/platform/shared/pal_system_api.cc
 TINYSYS_SRCS += $(CHRE_PREFIX)/platform/shared/platform_debug_dump_manager.cc
 TINYSYS_SRCS += $(CHRE_PREFIX)/platform/shared/system_time.cc
-TINYSYS_SRCS += $(CHRE_PREFIX)/platform/shared/tracing.cc
 TINYSYS_SRCS += $(CHRE_PREFIX)/platform/shared/version.cc
 TINYSYS_SRCS += $(CHRE_PREFIX)/platform/shared/nanoapp/nanoapp_dso_util.cc
 TINYSYS_SRCS += $(MBEDTLS_SRCS)
diff --git a/platform/shared/audio_pal/platform_audio.cc b/platform/shared/audio_pal/platform_audio.cc
index 4662a93..e68ff36 100644
--- a/platform/shared/audio_pal/platform_audio.cc
+++ b/platform/shared/audio_pal/platform_audio.cc
@@ -48,6 +48,12 @@
   if (mApi != nullptr) {
     if (!mApi->open(&gChrePalSystemApi, &sCallbacks)) {
       LOGE("Audio PAL open returned false");
+
+#ifdef CHRE_TELEMETRY_SUPPORT_ENABLED
+      EventLoopManagerSingleton::get()->getTelemetryManager().onPalOpenFailure(
+          TelemetryManager::PalType::AUDIO);
+#endif  // CHRE_TELEMETRY_SUPPORT_ENABLED
+
       mApi = nullptr;
     } else {
       LOGD("Opened audio PAL version 0x%08" PRIx32, mApi->moduleVersion);
diff --git a/platform/shared/chre_api_ble.cc b/platform/shared/chre_api_ble.cc
index 5b725b1..1f63017 100644
--- a/platform/shared/chre_api_ble.cc
+++ b/platform/shared/chre_api_ble.cc
@@ -44,36 +44,65 @@
 #endif  // CHRE_BLE_SUPPORT_ENABLED
 }
 
-DLL_EXPORT bool chreBleFlushAsync(const void * /* cookie */) {
+DLL_EXPORT bool chreBleFlushAsync(const void *cookie) {
+#ifdef CHRE_BLE_SUPPORT_ENABLED
+  chre::Nanoapp *nanoapp = EventLoopManager::validateChreApiCall(__func__);
+  return nanoapp->permitPermissionUse(NanoappPermissions::CHRE_PERMS_BLE) &&
+         EventLoopManagerSingleton::get()->getBleRequestManager().flushAsync(
+             nanoapp, cookie);
+#else
+  UNUSED_VAR(cookie);
   return false;
+#endif  // CHRE_BLE_SUPPORT_ENABLED
 }
 
-DLL_EXPORT bool chreBleStartScanAsync(chreBleScanMode mode,
-                                      uint32_t reportDelayMs,
-                                      const struct chreBleScanFilter *filter) {
+DLL_EXPORT bool chreBleStartScanAsyncV1_9(
+    chreBleScanMode mode, uint32_t reportDelayMs,
+    const struct chreBleScanFilterV1_9 *filter, const void *cookie) {
 #ifdef CHRE_BLE_SUPPORT_ENABLED
   chre::Nanoapp *nanoapp = EventLoopManager::validateChreApiCall(__func__);
   return nanoapp->permitPermissionUse(NanoappPermissions::CHRE_PERMS_BLE) &&
          EventLoopManagerSingleton::get()
              ->getBleRequestManager()
-             .startScanAsync(nanoapp, mode, reportDelayMs, filter);
+             .startScanAsync(nanoapp, mode, reportDelayMs, filter, cookie);
 #else
   UNUSED_VAR(mode);
   UNUSED_VAR(reportDelayMs);
   UNUSED_VAR(filter);
+  UNUSED_VAR(cookie);
+  return false;
+#endif  // CHRE_BLE_SUPPORT_ENABLED
+}
+
+DLL_EXPORT bool chreBleStartScanAsync(chreBleScanMode mode,
+                                      uint32_t reportDelayMs,
+                                      const struct chreBleScanFilter *filter) {
+  if (filter == nullptr) {
+    return chreBleStartScanAsyncV1_9(mode, reportDelayMs, nullptr /* filter */,
+                                     nullptr /* cookie */);
+  }
+  chreBleScanFilterV1_9 filterV1_9 = {
+      filter->rssiThreshold, filter->scanFilterCount, filter->scanFilters,
+      0 /* broadcasterAddressFilterCount */,
+      nullptr /* broadcasterAddressFilters */};
+  return chreBleStartScanAsyncV1_9(mode, reportDelayMs, &filterV1_9,
+                                   nullptr /* cookie */);
+}
+
+DLL_EXPORT bool chreBleStopScanAsyncV1_9(const void *cookie) {
+#ifdef CHRE_BLE_SUPPORT_ENABLED
+  chre::Nanoapp *nanoapp = EventLoopManager::validateChreApiCall(__func__);
+  return nanoapp->permitPermissionUse(NanoappPermissions::CHRE_PERMS_BLE) &&
+         EventLoopManagerSingleton::get()->getBleRequestManager().stopScanAsync(
+             nanoapp, cookie);
+#else
+  UNUSED_VAR(cookie);
   return false;
 #endif  // CHRE_BLE_SUPPORT_ENABLED
 }
 
 DLL_EXPORT bool chreBleStopScanAsync() {
-#ifdef CHRE_BLE_SUPPORT_ENABLED
-  chre::Nanoapp *nanoapp = EventLoopManager::validateChreApiCall(__func__);
-  return nanoapp->permitPermissionUse(NanoappPermissions::CHRE_PERMS_BLE) &&
-         EventLoopManagerSingleton::get()->getBleRequestManager().stopScanAsync(
-             nanoapp);
-#else
-  return false;
-#endif  // CHRE_BLE_SUPPORT_ENABLED
+  return chreBleStopScanAsyncV1_9(nullptr /* cookie */);
 }
 
 DLL_EXPORT bool chreBleReadRssiAsync(uint16_t connectionHandle,
diff --git a/platform/shared/dram_vote_client.cc b/platform/shared/dram_vote_client.cc
index 6cc484c..165faf4 100644
--- a/platform/shared/dram_vote_client.cc
+++ b/platform/shared/dram_vote_client.cc
@@ -18,6 +18,7 @@
 
 #include <cinttypes>
 
+#include "chre/core/event_loop_manager.h"
 #include "chre/platform/assert.h"
 #include "chre/platform/fatal_error.h"
 #include "chre/platform/log.h"
@@ -77,7 +78,7 @@
   }
 }
 
-Milliseconds DramVoteClient::checkDramDuration() const {
+Milliseconds DramVoteClient::checkDramDuration() {
   Milliseconds duration(0);
   if (mDramVoteCount > 0) {
     duration = Milliseconds(SystemTime::getMonotonicTime()) - mVoteCountStart;
@@ -87,8 +88,18 @@
   // requests. If there's a prolonged period of memory fallback, this might
   // indicate a memory leak or inadequate SRAM heap size.
   if (duration > kMaxDramDuration) {
-    FATAL_ERROR("Forced into DRAM for %" PRIu64 " msec",
-                duration.getMilliseconds());
+    if (EventLoopManagerSingleton::isInitialized() &&
+        !EventLoopManagerSingleton::get()
+             ->getEventLoop()
+             .getPowerControlManager()
+             .hostIsAwake()) {
+      // AP is asleep
+      FATAL_ERROR("Forced into DRAM for %" PRIu64 " msec",
+                  duration.getMilliseconds());
+    } else {
+      // AP is awake, don't report error, just reset the starting time
+      mVoteCountStart = Milliseconds(SystemTime::getMonotonicTime());
+    }
   }
   return duration;
 }
diff --git a/platform/shared/host_link.cc b/platform/shared/host_link.cc
index a66b797..2542055 100644
--- a/platform/shared/host_link.cc
+++ b/platform/shared/host_link.cc
@@ -104,6 +104,15 @@
       cbData->nanoapp = getLoadManager().releaseNanoapp();
       cbData->sendFragmentResponse = !respondBeforeStart;
 
+      LOGD("Instance ID %" PRIu16 " assigned to app ID 0x%" PRIx64,
+           cbData->nanoapp->getInstanceId(), appId);
+
+      // This message must be sent to the host before the nanoapp is started so
+      // that the host can correctly map the nanoapp instance ID to the app ID
+      // before processing tokenized logs from the nanoapp.
+      sendNanoappInstanceIdInfo(hostClientId, cbData->nanoapp->getInstanceId(),
+                                appId);
+
       // Note that if this fails, we'll generate the error response in
       // the normal deferred callback
       EventLoopManagerSingleton::get()->deferCallback(
diff --git a/platform/shared/host_protocol_chre.cc b/platform/shared/host_protocol_chre.cc
index d7b7fea..627a5a6 100644
--- a/platform/shared/host_protocol_chre.cc
+++ b/platform/shared/host_protocol_chre.cc
@@ -186,6 +186,11 @@
         break;
       }
 
+      case fbs::ChreMessage::PulseRequest: {
+        HostMessageHandlers::handlePulseRequest();
+        break;
+      }
+
       default:
         LOGW("Got invalid/unexpected message type %" PRIu8,
              static_cast<uint8_t>(container->message_type()));
@@ -251,6 +256,11 @@
            hostClientId);
 }
 
+void HostProtocolChre::encodePulseResponse(ChreFlatBufferBuilder &builder) {
+  auto response = fbs::CreatePulseResponse(builder);
+  finalize(builder, fbs::ChreMessage::PulseResponse, response.Union());
+}
+
 void HostProtocolChre::encodeLoadNanoappResponse(ChreFlatBufferBuilder &builder,
                                                  uint16_t hostClientId,
                                                  uint32_t transactionId,
@@ -262,6 +272,14 @@
            hostClientId);
 }
 
+void HostProtocolChre::encodeNanoappInstanceIdInfo(
+    ChreFlatBufferBuilder &builder, uint16_t hostClientId, uint16_t instanceId,
+    uint64_t appId) {
+  auto response = fbs::CreateNanoappInstanceIdInfo(builder, instanceId, appId);
+  finalize(builder, fbs::ChreMessage::NanoappInstanceIdInfo, response.Union(),
+           hostClientId);
+}
+
 void HostProtocolChre::encodeUnloadNanoappResponse(
     ChreFlatBufferBuilder &builder, uint16_t hostClientId,
     uint32_t transactionId, bool success) {
diff --git a/platform/shared/idl/host_messages.fbs b/platform/shared/idl/host_messages.fbs
index ff0dcd8..57b0d7c 100644
--- a/platform/shared/idl/host_messages.fbs
+++ b/platform/shared/idl/host_messages.fbs
@@ -196,6 +196,11 @@
   // TODO: detailed error code?
 }
 
+table NanoappInstanceIdInfo {
+  instance_id: uint;
+  app_id:ulong;
+}
+
 table UnloadNanoappRequest {
   transaction_id:uint;
 
@@ -323,7 +328,8 @@
   /// uint8_t                 - Log metadata, encoded as follows:
   ///                           [EI(Upper nibble) | Level(Lower nibble)]
   ///                            * Log Type
-  ///                              (0 = No encoding, 1 = Tokenized log, 2 = BT snoop log)
+  ///                              (0 = No encoding, 1 = Tokenized log,
+  ///                               2 = BT snoop log, 3 = Nanoapp Tokenized log)
   ///                            * LogBuffer log level (1 = error, 2 = warn,
   ///                                                   3 = info,  4 = debug,
   ///                                                   5 = verbose)
@@ -335,7 +341,7 @@
   ///   terminated string (eg: pass to string manipulation functions, get its
   ///   size via strlen(), etc.).
   ///
-  /// * Encoded logs: The first byte of the log buffer indicates the size of
+  /// * Tokenized logs: The first byte of the log buffer indicates the size of
   ///   the actual encoded data to follow. For example, if a tokenized log of
   ///   size 24 bytes were to be represented, a buffer of size 25 bytes would
   ///   be needed to encode this as: [Size(1B) | Data(24B)]. A decoder would
@@ -349,6 +355,15 @@
   ///   size 24 bytes were to be represented, a buffer of size 26 bytes would
   ///   be needed to encode this as: [Direction(1B) | Size(1B) | Data(24B)].
   ///
+  /// * Tokenized nanoapp logs: This log type is specifically for nanoapps with
+  ///   tokenized logs enabled. Similar to tokenized logs, the first byte is the
+  ///   size of the tokenized log data at the end. The next two bytes is the instance
+  ///   ID of the nanoapp which sends this tokenized log message. This instance ID
+  ///   will be used to map to the corresponding detokenizer in the log message parser.
+  ///   For example, if a nanoapp tokenized log of size 24 bytes were to be sent,
+  ///   a buffer of size 27 bytes would be needed to encode this as:
+  ///   [Size(1B) | InstanceId (2B) | Data(24B)].
+  ///
   /// This pattern repeats until the end of the buffer for multiple log
   /// messages. The last byte will always be a null-terminator. There are no
   /// padding bytes between these fields. Treat this like a packed struct and be
@@ -428,6 +443,10 @@
   health_monitor_failure_crash:bool;
 }
 
+// Pulse messages are used to check if CHRE is up running.
+table PulseRequest {}
+table PulseResponse {}
+
 /// A union that joins together all possible messages. Note that in FlatBuffers,
 /// unions have an implicit type
 union ChreMessage {
@@ -475,6 +494,11 @@
   NanConfigurationUpdate,
 
   DebugConfiguration,
+
+  PulseRequest,
+  PulseResponse,
+
+  NanoappInstanceIdInfo,
 }
 
 struct HostAddress {
diff --git a/platform/shared/include/chre/platform/shared/assert_func.h b/platform/shared/include/chre/platform/shared/assert_func.h
new file mode 100644
index 0000000..55f1ab3
--- /dev/null
+++ b/platform/shared/include/chre/platform/shared/assert_func.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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 PLATFORM_SHARED_INCLUDE_CHRE_PLATFORM_SHARED_ASSERT_FUNC_H
+#define PLATFORM_SHARED_INCLUDE_CHRE_PLATFORM_SHARED_ASSERT_FUNC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Performs assertion while logging the filename and line provided.
+ *
+ * @param filename The filename containing the assertion being fired.
+ * @param line The line that contains the assertion being fired.
+ */
+void chreDoAssert(const char *filename, size_t line);
+
+#ifdef __cplusplus
+}
+#endif
+
+#define CHRE_ASSERT(condition)               \
+  do {                                       \
+    if (!(condition)) {                      \
+      chreDoAssert(CHRE_FILENAME, __LINE__); \
+    }                                        \
+  } while (0)
+
+#endif // PLATFORM_SHARED_INCLUDE_CHRE_PLATFORM_SHARED_ASSERT_FUNC_H
diff --git a/platform/shared/include/chre/platform/shared/bt_snoop_log.h b/platform/shared/include/chre/platform/shared/bt_snoop_log.h
index 35719b9..d217f4e 100644
--- a/platform/shared/include/chre/platform/shared/bt_snoop_log.h
+++ b/platform/shared/include/chre/platform/shared/bt_snoop_log.h
@@ -17,6 +17,9 @@
 #ifndef CHRE_PLATFORM_SHARED_BT_SNOOP_LOG_H_
 #define CHRE_PLATFORM_SHARED_BT_SNOOP_LOG_H_
 
+#include <cinttypes>
+#include <cstddef>
+
 //! Indicates direction of a BT snoop log.
 //! TODO(b/294884658): Make the fbs definition as the single source of truth.
 enum class BtSnoopDirection : uint8_t {
diff --git a/platform/shared/include/chre/platform/shared/dram_vote_client.h b/platform/shared/include/chre/platform/shared/dram_vote_client.h
index 00476a4..50d5186 100644
--- a/platform/shared/include/chre/platform/shared/dram_vote_client.h
+++ b/platform/shared/include/chre/platform/shared/dram_vote_client.h
@@ -95,7 +95,7 @@
    * @return the duration in milliseconds since the system has been voted into
    *         big image due to incrementDramVoteCount.
    */
-  Milliseconds checkDramDuration() const;
+  Milliseconds checkDramDuration();
 };
 
 //! Provides an alias to the DramVoteClient singleton
diff --git a/platform/shared/include/chre/platform/shared/generated/host_messages_generated.h b/platform/shared/include/chre/platform/shared/generated/host_messages_generated.h
index ec71ea1..dad3131 100644
--- a/platform/shared/include/chre/platform/shared/generated/host_messages_generated.h
+++ b/platform/shared/include/chre/platform/shared/generated/host_messages_generated.h
@@ -36,6 +36,9 @@
 struct LoadNanoappResponse;
 struct LoadNanoappResponseBuilder;
 
+struct NanoappInstanceIdInfo;
+struct NanoappInstanceIdInfoBuilder;
+
 struct UnloadNanoappRequest;
 struct UnloadNanoappRequestBuilder;
 
@@ -99,6 +102,12 @@
 struct DebugConfiguration;
 struct DebugConfigurationBuilder;
 
+struct PulseRequest;
+struct PulseRequestBuilder;
+
+struct PulseResponse;
+struct PulseResponseBuilder;
+
 struct HostAddress;
 
 struct MessageContainer;
@@ -309,11 +318,14 @@
   NanConfigurationRequest = 26,
   NanConfigurationUpdate = 27,
   DebugConfiguration = 28,
+  PulseRequest = 29,
+  PulseResponse = 30,
+  NanoappInstanceIdInfo = 31,
   MIN = NONE,
-  MAX = DebugConfiguration
+  MAX = NanoappInstanceIdInfo
 };
 
-inline const ChreMessage (&EnumValuesChreMessage())[29] {
+inline const ChreMessage (&EnumValuesChreMessage())[32] {
   static const ChreMessage values[] = {
     ChreMessage::NONE,
     ChreMessage::NanoappMessage,
@@ -343,13 +355,16 @@
     ChreMessage::BatchedMetricLog,
     ChreMessage::NanConfigurationRequest,
     ChreMessage::NanConfigurationUpdate,
-    ChreMessage::DebugConfiguration
+    ChreMessage::DebugConfiguration,
+    ChreMessage::PulseRequest,
+    ChreMessage::PulseResponse,
+    ChreMessage::NanoappInstanceIdInfo
   };
   return values;
 }
 
 inline const char * const *EnumNamesChreMessage() {
-  static const char * const names[30] = {
+  static const char * const names[33] = {
     "NONE",
     "NanoappMessage",
     "HubInfoRequest",
@@ -379,13 +394,16 @@
     "NanConfigurationRequest",
     "NanConfigurationUpdate",
     "DebugConfiguration",
+    "PulseRequest",
+    "PulseResponse",
+    "NanoappInstanceIdInfo",
     nullptr
   };
   return names;
 }
 
 inline const char *EnumNameChreMessage(ChreMessage e) {
-  if (flatbuffers::IsOutRange(e, ChreMessage::NONE, ChreMessage::DebugConfiguration)) return "";
+  if (flatbuffers::IsOutRange(e, ChreMessage::NONE, ChreMessage::NanoappInstanceIdInfo)) return "";
   const size_t index = static_cast<size_t>(e);
   return EnumNamesChreMessage()[index];
 }
@@ -506,6 +524,18 @@
   static const ChreMessage enum_value = ChreMessage::DebugConfiguration;
 };
 
+template<> struct ChreMessageTraits<chre::fbs::PulseRequest> {
+  static const ChreMessage enum_value = ChreMessage::PulseRequest;
+};
+
+template<> struct ChreMessageTraits<chre::fbs::PulseResponse> {
+  static const ChreMessage enum_value = ChreMessage::PulseResponse;
+};
+
+template<> struct ChreMessageTraits<chre::fbs::NanoappInstanceIdInfo> {
+  static const ChreMessage enum_value = ChreMessage::NanoappInstanceIdInfo;
+};
+
 bool VerifyChreMessage(flatbuffers::Verifier &verifier, const void *obj, ChreMessage type);
 bool VerifyChreMessageVector(flatbuffers::Verifier &verifier, const flatbuffers::Vector<flatbuffers::Offset<void>> *values, const flatbuffers::Vector<uint8_t> *types);
 
@@ -1429,6 +1459,58 @@
   return builder_.Finish();
 }
 
+struct NanoappInstanceIdInfo FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+  typedef NanoappInstanceIdInfoBuilder Builder;
+  enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
+    VT_INSTANCE_ID = 4,
+    VT_APP_ID = 6
+  };
+  uint32_t instance_id() const {
+    return GetField<uint32_t>(VT_INSTANCE_ID, 0);
+  }
+  uint64_t app_id() const {
+    return GetField<uint64_t>(VT_APP_ID, 0);
+  }
+  bool Verify(flatbuffers::Verifier &verifier) const {
+    return VerifyTableStart(verifier) &&
+           VerifyField<uint32_t>(verifier, VT_INSTANCE_ID) &&
+           VerifyField<uint64_t>(verifier, VT_APP_ID) &&
+           verifier.EndTable();
+  }
+};
+
+struct NanoappInstanceIdInfoBuilder {
+  typedef NanoappInstanceIdInfo Table;
+  flatbuffers::FlatBufferBuilder &fbb_;
+  flatbuffers::uoffset_t start_;
+  void add_instance_id(uint32_t instance_id) {
+    fbb_.AddElement<uint32_t>(NanoappInstanceIdInfo::VT_INSTANCE_ID, instance_id, 0);
+  }
+  void add_app_id(uint64_t app_id) {
+    fbb_.AddElement<uint64_t>(NanoappInstanceIdInfo::VT_APP_ID, app_id, 0);
+  }
+  explicit NanoappInstanceIdInfoBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+        : fbb_(_fbb) {
+    start_ = fbb_.StartTable();
+  }
+  NanoappInstanceIdInfoBuilder &operator=(const NanoappInstanceIdInfoBuilder &);
+  flatbuffers::Offset<NanoappInstanceIdInfo> Finish() {
+    const auto end = fbb_.EndTable(start_);
+    auto o = flatbuffers::Offset<NanoappInstanceIdInfo>(end);
+    return o;
+  }
+};
+
+inline flatbuffers::Offset<NanoappInstanceIdInfo> CreateNanoappInstanceIdInfo(
+    flatbuffers::FlatBufferBuilder &_fbb,
+    uint32_t instance_id = 0,
+    uint64_t app_id = 0) {
+  NanoappInstanceIdInfoBuilder builder_(_fbb);
+  builder_.add_app_id(app_id);
+  builder_.add_instance_id(instance_id);
+  return builder_.Finish();
+}
+
 struct UnloadNanoappRequest FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
   typedef UnloadNanoappRequestBuilder Builder;
   enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
@@ -1963,7 +2045,8 @@
   /// uint8_t                 - Log metadata, encoded as follows:
   ///                           [EI(Upper nibble) | Level(Lower nibble)]
   ///                            * Log Type
-  ///                              (0 = No encoding, 1 = Tokenized log, 2 = BT snoop log)
+  ///                              (0 = No encoding, 1 = Tokenized log,
+  ///                               2 = BT snoop log, 3 = Nanoapp Tokenized log)
   ///                            * LogBuffer log level (1 = error, 2 = warn,
   ///                                                   3 = info,  4 = debug,
   ///                                                   5 = verbose)
@@ -1975,7 +2058,7 @@
   ///   terminated string (eg: pass to string manipulation functions, get its
   ///   size via strlen(), etc.).
   ///
-  /// * Encoded logs: The first byte of the log buffer indicates the size of
+  /// * Tokenized logs: The first byte of the log buffer indicates the size of
   ///   the actual encoded data to follow. For example, if a tokenized log of
   ///   size 24 bytes were to be represented, a buffer of size 25 bytes would
   ///   be needed to encode this as: [Size(1B) | Data(24B)]. A decoder would
@@ -1989,6 +2072,15 @@
   ///   size 24 bytes were to be represented, a buffer of size 26 bytes would
   ///   be needed to encode this as: [Direction(1B) | Size(1B) | Data(24B)].
   ///
+  /// * Tokenized nanoapp logs: This log type is specifically for nanoapps with
+  ///   tokenized logs enabled. Similar to tokenized logs, the first byte is the
+  ///   size of the tokenized log data at the end. The next two bytes is the instance
+  ///   ID of the nanoapp which sends this tokenized log message. This instance ID
+  ///   will be used to map to the corresponding detokenizer in the log message parser.
+  ///   For example, if a nanoapp tokenized log of size 24 bytes were to be sent,
+  ///   a buffer of size 27 bytes would be needed to encode this as:
+  ///   [Size(1B) | InstanceId (2B) | Data(24B)].
+  ///
   /// This pattern repeats until the end of the buffer for multiple log
   /// messages. The last byte will always be a null-terminator. There are no
   /// padding bytes between these fields. Treat this like a packed struct and be
@@ -2507,6 +2599,66 @@
   return builder_.Finish();
 }
 
+struct PulseRequest FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+  typedef PulseRequestBuilder Builder;
+  bool Verify(flatbuffers::Verifier &verifier) const {
+    return VerifyTableStart(verifier) &&
+           verifier.EndTable();
+  }
+};
+
+struct PulseRequestBuilder {
+  typedef PulseRequest Table;
+  flatbuffers::FlatBufferBuilder &fbb_;
+  flatbuffers::uoffset_t start_;
+  explicit PulseRequestBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+        : fbb_(_fbb) {
+    start_ = fbb_.StartTable();
+  }
+  PulseRequestBuilder &operator=(const PulseRequestBuilder &);
+  flatbuffers::Offset<PulseRequest> Finish() {
+    const auto end = fbb_.EndTable(start_);
+    auto o = flatbuffers::Offset<PulseRequest>(end);
+    return o;
+  }
+};
+
+inline flatbuffers::Offset<PulseRequest> CreatePulseRequest(
+    flatbuffers::FlatBufferBuilder &_fbb) {
+  PulseRequestBuilder builder_(_fbb);
+  return builder_.Finish();
+}
+
+struct PulseResponse FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table {
+  typedef PulseResponseBuilder Builder;
+  bool Verify(flatbuffers::Verifier &verifier) const {
+    return VerifyTableStart(verifier) &&
+           verifier.EndTable();
+  }
+};
+
+struct PulseResponseBuilder {
+  typedef PulseResponse Table;
+  flatbuffers::FlatBufferBuilder &fbb_;
+  flatbuffers::uoffset_t start_;
+  explicit PulseResponseBuilder(flatbuffers::FlatBufferBuilder &_fbb)
+        : fbb_(_fbb) {
+    start_ = fbb_.StartTable();
+  }
+  PulseResponseBuilder &operator=(const PulseResponseBuilder &);
+  flatbuffers::Offset<PulseResponse> Finish() {
+    const auto end = fbb_.EndTable(start_);
+    auto o = flatbuffers::Offset<PulseResponse>(end);
+    return o;
+  }
+};
+
+inline flatbuffers::Offset<PulseResponse> CreatePulseResponse(
+    flatbuffers::FlatBufferBuilder &_fbb) {
+  PulseResponseBuilder builder_(_fbb);
+  return builder_.Finish();
+}
+
 /// The top-level container that encapsulates all possible messages. Note that
 /// per FlatBuffers requirements, we can't use a union as the top-level
 /// structure (root type), so we must wrap it in a table.
@@ -2608,6 +2760,15 @@
   const chre::fbs::DebugConfiguration *message_as_DebugConfiguration() const {
     return message_type() == chre::fbs::ChreMessage::DebugConfiguration ? static_cast<const chre::fbs::DebugConfiguration *>(message()) : nullptr;
   }
+  const chre::fbs::PulseRequest *message_as_PulseRequest() const {
+    return message_type() == chre::fbs::ChreMessage::PulseRequest ? static_cast<const chre::fbs::PulseRequest *>(message()) : nullptr;
+  }
+  const chre::fbs::PulseResponse *message_as_PulseResponse() const {
+    return message_type() == chre::fbs::ChreMessage::PulseResponse ? static_cast<const chre::fbs::PulseResponse *>(message()) : nullptr;
+  }
+  const chre::fbs::NanoappInstanceIdInfo *message_as_NanoappInstanceIdInfo() const {
+    return message_type() == chre::fbs::ChreMessage::NanoappInstanceIdInfo ? static_cast<const chre::fbs::NanoappInstanceIdInfo *>(message()) : nullptr;
+  }
   /// The originating or destination client ID on the host side, used to direct
   /// responses only to the client that sent the request. Although initially
   /// populated by the requesting client, this is enforced to be the correct
@@ -2739,6 +2900,18 @@
   return message_as_DebugConfiguration();
 }
 
+template<> inline const chre::fbs::PulseRequest *MessageContainer::message_as<chre::fbs::PulseRequest>() const {
+  return message_as_PulseRequest();
+}
+
+template<> inline const chre::fbs::PulseResponse *MessageContainer::message_as<chre::fbs::PulseResponse>() const {
+  return message_as_PulseResponse();
+}
+
+template<> inline const chre::fbs::NanoappInstanceIdInfo *MessageContainer::message_as<chre::fbs::NanoappInstanceIdInfo>() const {
+  return message_as_NanoappInstanceIdInfo();
+}
+
 struct MessageContainerBuilder {
   typedef MessageContainer Table;
   flatbuffers::FlatBufferBuilder &fbb_;
@@ -2895,6 +3068,18 @@
       auto ptr = reinterpret_cast<const chre::fbs::DebugConfiguration *>(obj);
       return verifier.VerifyTable(ptr);
     }
+    case ChreMessage::PulseRequest: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseRequest *>(obj);
+      return verifier.VerifyTable(ptr);
+    }
+    case ChreMessage::PulseResponse: {
+      auto ptr = reinterpret_cast<const chre::fbs::PulseResponse *>(obj);
+      return verifier.VerifyTable(ptr);
+    }
+    case ChreMessage::NanoappInstanceIdInfo: {
+      auto ptr = reinterpret_cast<const chre::fbs::NanoappInstanceIdInfo *>(obj);
+      return verifier.VerifyTable(ptr);
+    }
     default: return true;
   }
 }
diff --git a/platform/shared/include/chre/platform/shared/host_protocol_chre.h b/platform/shared/include/chre/platform/shared/host_protocol_chre.h
index 27bf37f..eab9e77 100644
--- a/platform/shared/include/chre/platform/shared/host_protocol_chre.h
+++ b/platform/shared/include/chre/platform/shared/host_protocol_chre.h
@@ -70,6 +70,8 @@
 
   static void handleNanoappListRequest(uint16_t hostClientId);
 
+  static void handlePulseRequest();
+
   static void handleDebugConfiguration(
       const fbs::DebugConfiguration *debugConfiguration);
 
@@ -99,6 +101,9 @@
                                    uint32_t transactionId, uint32_t fragmentId,
                                    bool success);
 
+  static void sendNanoappInstanceIdInfo(uint16_t hostClientId,
+                                        uint16_t instanceId, uint64_t appId);
+
   static void finishLoadingNanoappCallback(
       SystemCallbackType type, UniquePtr<LoadNanoappCallbackData> &&cbData);
 
@@ -200,6 +205,11 @@
       uint16_t hostClientId);
 
   /**
+   * Encodes a response to the host indicating CHRE is up running.
+   */
+  static void encodePulseResponse(ChreFlatBufferBuilder &builder);
+
+  /**
    * Encodes a response to the host communicating the result of dynamically
    * loading a nanoapp.
    */
@@ -217,6 +227,13 @@
                                           uint32_t transactionId, bool success);
 
   /**
+   * Encodes a nanoapp's instance ID and app ID to the host.
+   */
+  static void encodeNanoappInstanceIdInfo(ChreFlatBufferBuilder &builder,
+                                          uint16_t hostClientId,
+                                          uint16_t instanceId, uint64_t appId);
+
+  /**
    * Encodes a buffer of log messages to the host.
    */
   static void encodeLogMessages(ChreFlatBufferBuilder &builder,
diff --git a/platform/shared/include/chre/platform/shared/log_buffer.h b/platform/shared/include/chre/platform/shared/log_buffer.h
index 8deaded..fb27130 100644
--- a/platform/shared/include/chre/platform/shared/log_buffer.h
+++ b/platform/shared/include/chre/platform/shared/log_buffer.h
@@ -23,9 +23,12 @@
 
 #include "chre/platform/mutex.h"
 #include "chre/platform/shared/bt_snoop_log.h"
+#include "chre/platform/shared/generated/host_messages_generated.h"
 
 namespace chre {
 
+using LogType = fbs::LogType;
+
 /**
  * Values that represent a preferred setting for when the LogBuffer should
  * notify the platform that logs are ready to be copied.
@@ -86,6 +89,21 @@
   //! message.
   static constexpr size_t kLogDataOffset = 5;
 
+  //! The number of overhead bytes in a printf style string entry. This value
+  //! indicates the size of the null terminator appended to the end of each log.
+  static constexpr size_t kStringLogOverhead = 1;
+
+  //! The number of bytes in a tokenized log entry of the buffer after the
+  //! 'header' and before the tokenized log data is encountered. The value
+  //! indicate the size of the uint8_t logSize field.
+  static constexpr size_t kTokenizedLogOffset = 1;
+
+  //! The number of bytes in a bt snoop log entry of the buffer after the
+  //! 'header' and before the bt snoop log data is encountered. The value
+  //! indicate the size of the uint8_t size field and the BtSnoopDirection
+  //! field.
+  static constexpr size_t kBtSnoopLogOffset = 2;
+
   /**
    * @param callback The callback object that will receive notifications about
    *                 the state of the log buffer or nullptr if it is not needed.
@@ -169,7 +187,8 @@
 
   /**
    *
-   * @param logSize The size of the log text in bytes.
+   * @param logSize The size of the log payload, including overhead like
+   * metadata, null terminator, etc.
    * @return true if log would cause an overflow of the buffer and would
    * overwrite a log if it was pushed onto the buffer.
    */
@@ -233,6 +252,16 @@
    */
   size_t getNumLogsDropped();
 
+  /**
+   * @param startingIndex The index to start from.
+   * @param type. The type of the log. See host_message.fbs.
+   * @return The length of the data portion of a log along with the null
+   *         terminator. If a null terminator was not found at most
+   *         kLogMaxSize - kLogDataOffset bytes away from the startingIndex
+   *         then kLogMaxSize - kLogDataOffset + 1 is returned.
+   */
+  size_t getLogDataLength(size_t startingIndex, LogType type);
+
  private:
   /**
    * Increment the value and take the modulus of the max size of the buffer.
@@ -253,6 +282,11 @@
    */
   void copyToBuffer(size_t size, const void *source);
 
+  template <typename Type>
+  void copyVarToBuffer(const Type *var) {
+    copyToBuffer(sizeof(Type), var);
+  }
+
   /**
    * Copy from the buffer data to a destination memory location ensuring that
    * the copy wraps around the buffer data if needed.
@@ -284,15 +318,6 @@
   size_t getNextLogIndex(size_t startingIndex, size_t *logSize);
 
   /**
-   * @param startingIndex The index to start from.
-   * @return The length of the data portion of a log along with the null
-   *         terminator. If a null terminator was not found at most
-   *         kLogMaxSize - kLogDataOffset bytes away from the startingIndex
-   *         then kLogMaxSize - kLogDataOffset + 1 is returned.
-   */
-  size_t getLogDataLength(size_t startingIndex);
-
-  /**
    * Encode the received log message (if tokenization or similar encoding
    * is used) and dispatch it.
    */
@@ -311,7 +336,7 @@
    * than max size. This function must only be called with the log buffer mutex
    * locked.
    */
-  void discardExcessOldLogsLocked(bool encoded, uint8_t currentLogLen);
+  void discardExcessOldLogsLocked(uint8_t currentLogLen);
 
   /**
    * Add an encoding header to the log message if the encoding param is true.
@@ -328,6 +353,21 @@
   void dispatch();
 
   /**
+   * @param metadata The metadata of the log message.
+   * @return The log type of the log message.
+   */
+  LogType getLogTypeFromMetadata(uint8_t metadata);
+
+  /**
+   * Set the upper nibble of the log metadata based on log type and log level.
+   *
+   * @param type The log type of the log message.
+   * @param logLevel The log level of the log message.
+   * @return The metadata of the log message.
+   */
+  uint8_t setLogMetadata(LogType type, LogBufferLogLevel logLevel);
+
+  /**
    * The buffer data is stored in the format
    *
    * [ metadata (1B) , timestamp (4B), data (dataLenB) ]
diff --git a/platform/shared/include/chre/platform/shared/log_buffer_manager.h b/platform/shared/include/chre/platform/shared/log_buffer_manager.h
index cae854e..44eb939 100644
--- a/platform/shared/include/chre/platform/shared/log_buffer_manager.h
+++ b/platform/shared/include/chre/platform/shared/log_buffer_manager.h
@@ -21,6 +21,7 @@
 #include "chre/platform/condition_variable.h"
 #include "chre/platform/mutex.h"
 #include "chre/platform/shared/bt_snoop_log.h"
+#include "chre/platform/shared/generated/host_messages_generated.h"
 #include "chre/platform/shared/log_buffer.h"
 #include "chre/util/singleton.h"
 #include "chre_api/chre/re.h"
@@ -31,6 +32,8 @@
 
 namespace chre {
 
+using LogType = fbs::LogType;
+
 /**
  * A log buffer manager that platform code can use to buffer logs when the host
  * is not available and then send them off when the host becomes available. Uses
@@ -132,7 +135,7 @@
 
   uint32_t getTimestampMs();
 
-  void bufferOverflowGuard(size_t logSize);
+  void bufferOverflowGuard(size_t logSize, LogType type);
 
   LogBuffer mPrimaryLogBuffer;
   LogBuffer mSecondaryLogBuffer;
diff --git a/platform/shared/include/chre/target_platform/platform_ble_base.h b/platform/shared/include/chre/target_platform/platform_ble_base.h
index e37e88a..fe47ed4 100644
--- a/platform/shared/include/chre/target_platform/platform_ble_base.h
+++ b/platform/shared/include/chre/target_platform/platform_ble_base.h
@@ -40,6 +40,7 @@
   static void advertisingEventCallback(struct chreBleAdvertisementEvent *event);
   static void readRssiCallback(uint8_t errorCode, uint16_t connectionHandle,
                                int8_t rssi);
+  static void flushCallback(uint8_t errorCode);
   static void handleBtSnoopLog(bool isTxToBtController, const uint8_t *buffer,
                                size_t size);
 };
diff --git a/platform/shared/log_buffer.cc b/platform/shared/log_buffer.cc
index 358c175..8bfb5ad 100644
--- a/platform/shared/log_buffer.cc
+++ b/platform/shared/log_buffer.cc
@@ -44,9 +44,8 @@
 
 void LogBuffer::handleLogVa(LogBufferLogLevel logLevel, uint32_t timestampMs,
                             const char *logFormat, va_list args) {
-  constexpr size_t maxLogLen = kLogMaxSize - kLogDataOffset;
-  char tempBuffer[maxLogLen];
-  int logLenSigned = vsnprintf(tempBuffer, maxLogLen, logFormat, args);
+  char tempBuffer[kLogMaxSize];
+  int logLenSigned = vsnprintf(tempBuffer, kLogMaxSize, logFormat, args);
   processLog(logLevel, timestampMs, tempBuffer, logLenSigned,
              false /* encoded */);
 }
@@ -54,41 +53,44 @@
 #ifdef CHRE_BLE_SUPPORT_ENABLED
 void LogBuffer::handleBtLog(BtSnoopDirection direction, uint32_t timestampMs,
                             const uint8_t *buffer, size_t size) {
-  if (size > 0) {
-    auto logLen = static_cast<uint8_t>(size);
-
-    if (size < kLogMaxSize) {
-      LockGuard<Mutex> lockGuard(mLock);
-
-      // No additional terminator towards the end.
-      discardExcessOldLogsLocked(false, logLen);
-
-      uint8_t logType = static_cast<uint8_t>(LogType::BLUETOOTH);
-      uint8_t snoopLogDirection = static_cast<uint8_t>(direction);
-
-      // Set all BT logs to the CHRE_LOG_LEVEL_INFO.
-      uint8_t metadata =
-          (static_cast<uint8_t>(logType) << 4) | CHRE_LOG_LEVEL_INFO;
-      copyToBuffer(sizeof(metadata), &metadata);
-
-      copyToBuffer(sizeof(timestampMs), &timestampMs);
-      copyToBuffer(sizeof(direction), &snoopLogDirection);
-      copyToBuffer(sizeof(logLen), &logLen);
-
-      copyToBuffer(logLen, buffer);
-    } else {
-      // Cannot truncate a BT event. Log a failure message instead.
-      constexpr char kBtSnoopLogGenericErrorMsg[] =
-          "Bt Snoop log message too large";
-      static_assert(
-          sizeof(kBtSnoopLogGenericErrorMsg) <= kLogMaxSize,
-          "Error meessage size needs to be smaller than max log length");
-      logLen = static_cast<uint8_t>(sizeof(kBtSnoopLogGenericErrorMsg));
-      copyLogToBuffer(LogBufferLogLevel::INFO, timestampMs,
-                      kBtSnoopLogGenericErrorMsg, logLen, false /* encoded */);
-    }
-    dispatch();
+  if (size == 0) {
+    return;
   }
+  auto logLen = static_cast<uint8_t>(size);
+
+  if (size < kLogMaxSize) {
+    LockGuard<Mutex> lockGuard(mLock);
+
+    static_assert(sizeof(LogType) == sizeof(uint8_t),
+                  "LogType size is not equal to size of uint8_t");
+    static_assert(sizeof(direction) == sizeof(uint8_t),
+                  "BtSnoopDirection size is not equal to the size of uint8_t");
+    uint8_t snoopLogDirection = static_cast<uint8_t>(direction);
+
+    discardExcessOldLogsLocked(logLen + kBtSnoopLogOffset);
+
+    // Set all BT logs to the CHRE_LOG_LEVEL_INFO.
+    uint8_t metadata =
+        setLogMetadata(LogType::BLUETOOTH, LogBufferLogLevel::INFO);
+
+    copyVarToBuffer(&metadata);
+    copyVarToBuffer(&timestampMs);
+    copyVarToBuffer(&snoopLogDirection);
+    copyVarToBuffer(&logLen);
+
+    copyToBuffer(logLen, buffer);
+  } else {
+    // Cannot truncate a BT event. Log a failure message instead.
+    constexpr char kBtSnoopLogGenericErrorMsg[] =
+        "Bt Snoop log message too large";
+    static_assert(
+        sizeof(kBtSnoopLogGenericErrorMsg) <= kLogMaxSize,
+        "Error meessage size needs to be smaller than max log length");
+    logLen = static_cast<uint8_t>(sizeof(kBtSnoopLogGenericErrorMsg));
+    copyLogToBuffer(LogBufferLogLevel::INFO, timestampMs,
+                    kBtSnoopLogGenericErrorMsg, logLen, false /* encoded */);
+  }
+  dispatch();
 }
 #endif  // CHRE_BLE_SUPPORT_ENABLED
 
@@ -106,8 +108,7 @@
 
 bool LogBuffer::logWouldCauseOverflow(size_t logSize) {
   LockGuard<Mutex> lock(mLock);
-  return (mBufferDataSize + logSize + kLogDataOffset + 1 /* nullptr */ >
-          mBufferMaxSize);
+  return (mBufferDataSize + logSize + kLogDataOffset > mBufferMaxSize);
 }
 
 void LogBuffer::transferTo(LogBuffer &buffer) {
@@ -202,8 +203,6 @@
       copySize = mBufferDataSize;
     } else {
       size_t logSize;
-      // There is guaranteed to be a null terminator within the max log length
-      // number of bytes so logStartIndex will not be maxBytes + 1
       size_t logStartIndex = getNextLogIndex(mBufferDataHeadIndex, &logSize);
       while (copySize + logSize <= size &&
              copySize + logSize <= mBufferDataSize) {
@@ -229,69 +228,81 @@
 size_t LogBuffer::getNextLogIndex(size_t startingIndex, size_t *logSize) {
   size_t logDataStartIndex =
       incrementAndModByBufferMaxSize(startingIndex, kLogDataOffset);
-
-  size_t logDataSize = getLogDataLength(logDataStartIndex);
+  LogType type = getLogTypeFromMetadata(mBufferData[startingIndex]);
+  size_t logDataSize = getLogDataLength(logDataStartIndex, type);
   *logSize = kLogDataOffset + logDataSize;
   return incrementAndModByBufferMaxSize(startingIndex, *logSize);
 }
 
-size_t LogBuffer::getLogDataLength(size_t startingIndex) {
+size_t LogBuffer::getLogDataLength(size_t startingIndex, LogType type) {
   size_t currentIndex = startingIndex;
-  constexpr size_t maxBytes = kLogMaxSize - kLogDataOffset;
-  size_t numBytes = maxBytes + 1;
+  size_t numBytes = kLogMaxSize;
 
-  for (size_t i = 0; i < maxBytes; i++) {
-    if (mBufferData[currentIndex] == '\0') {
-      // +1 to include the null terminator
-      numBytes = i + 1;
-      break;
+  if (type == LogType::STRING) {
+    for (size_t i = 0; i < kLogMaxSize; i++) {
+      if (mBufferData[currentIndex] == '\0') {
+        // +1 to include the null terminator
+        numBytes = i + 1;
+        break;
+      }
+      currentIndex = incrementAndModByBufferMaxSize(currentIndex, 1);
     }
-    currentIndex = incrementAndModByBufferMaxSize(currentIndex, 1);
+  } else if (type == LogType::TOKENIZED) {
+    numBytes = mBufferData[startingIndex] + kTokenizedLogOffset;
+  } else if (type == LogType::BLUETOOTH) {
+    currentIndex = incrementAndModByBufferMaxSize(startingIndex, 1);
+    numBytes = mBufferData[currentIndex] + kBtSnoopLogOffset;
+  } else {
+    CHRE_ASSERT_LOG(false, "Received unexpected log message type");
   }
+
   return numBytes;
 }
 
 void LogBuffer::processLog(LogBufferLogLevel logLevel, uint32_t timestampMs,
                            const void *logBuffer, size_t size, bool encoded) {
-  if (size > 0) {
-    auto logLen = static_cast<uint8_t>(size);
-    if (size >= kLogMaxSize) {
-      if (!encoded) {
-        // Leave space for null terminator to be copied on end
-        logLen = static_cast<uint8_t>(kLogMaxSize - 1);
-      } else {
-        // There is no way of decoding an encoded message if we truncate it, so
-        // we do the next best thing and try to log a generic failure message
-        // reusing the logbuffer for as much as we can. Note that we also need
-        // flip the encoding flag for proper decoding by the host log message
-        // parser.
-        constexpr char kTokenizedLogGenericErrorMsg[] =
-            "Tokenized log message too large";
-        static_assert(
-            sizeof(kTokenizedLogGenericErrorMsg) <= kLogMaxSize,
-            "Error meessage size needs to be smaller than max log length");
-        logBuffer = kTokenizedLogGenericErrorMsg;
-        logLen = static_cast<uint8_t>(sizeof(kTokenizedLogGenericErrorMsg));
-        encoded = false;
-      }
-    }
-    copyLogToBuffer(logLevel, timestampMs, logBuffer, logLen, encoded);
-    dispatch();
+  if (size == 0) {
+    return;
   }
+  auto logLen = static_cast<uint8_t>(size);
+
+  constexpr char kTokenizedLogGenericErrorMsg[] =
+      "Tokenized log message too large";
+
+  // For tokenized logs, need to leave space for the message size offset. For
+  // string logs, need to leave 1 byte for the null terminator at the end.
+  if (!encoded && size >= kLogMaxSize - 1) {
+    // String logs longer than kLogMaxSize - 1 will be truncated.
+    logLen = static_cast<uint8_t>(kLogMaxSize - 1);
+  } else if (encoded && size >= kLogMaxSize - kTokenizedLogOffset) {
+    // There is no way of decoding an encoded message if we truncate it, so
+    // we do the next best thing and try to log a generic failure message
+    // reusing the logbuffer for as much as we can. Note that we also need
+    // flip the encoding flag for proper decoding by the host log message
+    // parser.
+    static_assert(
+        sizeof(kTokenizedLogGenericErrorMsg) <= kLogMaxSize - 1,
+        "Error meessage size needs to be smaller than max log length");
+    logBuffer = kTokenizedLogGenericErrorMsg;
+    logLen = static_cast<uint8_t>(sizeof(kTokenizedLogGenericErrorMsg));
+    encoded = false;
+  }
+  copyLogToBuffer(logLevel, timestampMs, logBuffer, logLen, encoded);
+  dispatch();
 }
 
 void LogBuffer::copyLogToBuffer(LogBufferLogLevel level, uint32_t timestampMs,
                                 const void *logBuffer, uint8_t logLen,
                                 bool encoded) {
   LockGuard<Mutex> lockGuard(mLock);
-  discardExcessOldLogsLocked(encoded, logLen);
+  // For STRING logs, add 1 byte for null terminator. For TOKENIZED logs, add 1
+  // byte for the size metadata added to the message.
+  discardExcessOldLogsLocked(logLen + 1);
   encodeAndCopyLogLocked(level, timestampMs, logBuffer, logLen, encoded);
 }
 
-void LogBuffer::discardExcessOldLogsLocked(bool encoded,
-                                           uint8_t currentLogLen) {
-  size_t totalLogSize =
-      kLogDataOffset + (encoded ? currentLogLen : currentLogLen + 1);
+void LogBuffer::discardExcessOldLogsLocked(uint8_t currentLogLen) {
+  size_t totalLogSize = kLogDataOffset + currentLogLen;
   while (mBufferDataSize + totalLogSize > mBufferMaxSize) {
     mNumLogsDropped++;
     size_t logSize;
@@ -305,13 +316,13 @@
                                        const void *logBuffer, uint8_t logLen,
                                        bool encoded) {
   uint8_t metadata =
-      (static_cast<uint8_t>(encoded) << 4) | static_cast<uint8_t>(level);
+      setLogMetadata(encoded ? LogType::TOKENIZED : LogType::STRING, level);
 
-  copyToBuffer(sizeof(uint8_t), &metadata);
-  copyToBuffer(sizeof(timestampMs), &timestampMs);
+  copyVarToBuffer(&metadata);
+  copyVarToBuffer(&timestampMs);
 
   if (encoded) {
-    copyToBuffer(sizeof(uint8_t), &logLen);
+    copyVarToBuffer(&logLen);
   }
   copyToBuffer(logLen, logBuffer);
   if (!encoded) {
@@ -339,4 +350,20 @@
   }
 }
 
+LogType LogBuffer::getLogTypeFromMetadata(uint8_t metadata) {
+  LogType type;
+  if ((metadata & 0x20) != 0) {
+    type = LogType::BLUETOOTH;
+  } else if ((metadata & 0x10) != 0) {
+    type = LogType::TOKENIZED;
+  } else {
+    type = LogType::STRING;
+  }
+  return type;
+}
+
+uint8_t LogBuffer::setLogMetadata(LogType type, LogBufferLogLevel logLevel) {
+  return static_cast<uint8_t>(type) << 4 | static_cast<uint8_t>(logLevel);
+}
+
 }  // namespace chre
diff --git a/platform/shared/log_buffer_manager.cc b/platform/shared/log_buffer_manager.cc
index 82abee6..96b4633 100644
--- a/platform/shared/log_buffer_manager.cc
+++ b/platform/shared/log_buffer_manager.cc
@@ -18,6 +18,7 @@
 
 #include "chre/core/event_loop_manager.h"
 #include "chre/platform/shared/bt_snoop_log.h"
+#include "chre/platform/shared/generated/host_messages_generated.h"
 #include "chre/util/lock_guard.h"
 
 void chrePlatformLogToBuffer(chreLogLevel chreLogLevel, const char *format,
@@ -44,6 +45,8 @@
 
 namespace chre {
 
+using LogType = fbs::LogType;
+
 void LogBufferManager::onLogsReady() {
   LockGuard<Mutex> lockGuard(mFlushLogsMutex);
   if (!mLogFlushToHostPending) {
@@ -124,7 +127,15 @@
   return static_cast<uint32_t>(timeNs / kOneMillisecondInNanoseconds);
 }
 
-void LogBufferManager::bufferOverflowGuard(size_t logSize) {
+void LogBufferManager::bufferOverflowGuard(size_t logSize, LogType type) {
+  if (type == LogType::STRING) {
+    // Add one byte because of the null terminator added at the end.
+    logSize = logSize + LogBuffer::kStringLogOverhead;
+  } else if (type == LogType::TOKENIZED) {
+    logSize = logSize + LogBuffer::kTokenizedLogOffset;
+  } else if (type == LogType::BLUETOOTH) {
+    logSize = logSize + LogBuffer::kBtSnoopLogOffset;
+  }
   if (mPrimaryLogBuffer.logWouldCauseOverflow(logSize)) {
     LockGuard<Mutex> lockGuard(mFlushLogsMutex);
     if (!mLogFlushToHostPending) {
@@ -142,7 +153,7 @@
   va_copy(getSizeArgs, args);
   size_t logSize = vsnprintf(nullptr, 0, formatStr, getSizeArgs);
   va_end(getSizeArgs);
-  bufferOverflowGuard(logSize);
+  bufferOverflowGuard(logSize, LogType::STRING);
   mPrimaryLogBuffer.handleLogVa(chreToLogBufferLogLevel(logLevel),
                                 getTimestampMs(), formatStr, args);
 }
@@ -150,7 +161,7 @@
 void LogBufferManager::logBtSnoop(BtSnoopDirection direction,
                                   const uint8_t *buffer, size_t size) {
 #ifdef CHRE_BLE_SUPPORT_ENABLED
-  bufferOverflowGuard(size);
+  bufferOverflowGuard(size, LogType::BLUETOOTH);
   mPrimaryLogBuffer.handleBtLog(direction, getTimestampMs(), buffer, size);
 #else
   UNUSED_VAR(direction);
@@ -162,7 +173,7 @@
 void LogBufferManager::logEncoded(chreLogLevel logLevel,
                                   const uint8_t *encodedLog,
                                   size_t encodedLogSize) {
-  bufferOverflowGuard(encodedLogSize);
+  bufferOverflowGuard(encodedLogSize, LogType::TOKENIZED);
   mPrimaryLogBuffer.handleEncodedLog(chreToLogBufferLogLevel(logLevel),
                                      getTimestampMs(), encodedLog,
                                      encodedLogSize);
diff --git a/platform/shared/nanoapp/nanoapp_stack_check.cc b/platform/shared/nanoapp/nanoapp_stack_check.cc
new file mode 100644
index 0000000..3f6b24b
--- /dev/null
+++ b/platform/shared/nanoapp/nanoapp_stack_check.cc
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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 "chre/util/nanoapp/log.h"
+#include "chre_api/chre/re.h"
+#include "chre_api/chre/toolchain.h"
+
+/**
+ * @file
+ * Stack check support.
+ *
+ * The symbols defined in this file are needed when the code is compiled with
+ * the -fstack-protector flag.
+ */
+
+#ifndef LOG_TAG
+#define LOG_TAG "[STACK CHECK]"
+#endif  // LOG_TAG
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+uintptr_t __stack_chk_guard = 0x56494342;
+
+// Terminate a function in case of stack overflow.
+void __stack_chk_fail(void) CHRE_NO_RETURN;
+void __stack_chk_fail(void) {
+  LOGE("Stack corruption detected");
+  chreAbort(/*abortCode=*/0);
+}
+
+#ifdef __cplusplus
+}
+#endif
\ No newline at end of file
diff --git a/platform/shared/nanoapp/nanoapp_support_lib_dso.cc b/platform/shared/nanoapp/nanoapp_support_lib_dso.cc
index 2f21df0..55b5840 100644
--- a/platform/shared/nanoapp/nanoapp_support_lib_dso.cc
+++ b/platform/shared/nanoapp/nanoapp_support_lib_dso.cc
@@ -304,12 +304,38 @@
 }
 
 WEAK_SYMBOL
+bool chreBleStartScanAsyncV1_9(chreBleScanMode mode, uint32_t reportDelayMs,
+                               const struct chreBleScanFilterV1_9 *filter,
+                               const void *cookie) {
+  if (chreGetApiVersion() < CHRE_API_VERSION_1_9) {
+    return false;
+  }
+  auto *fptr = CHRE_NSL_LAZY_LOOKUP(chreBleStartScanAsyncV1_9);
+  if (fptr == nullptr) {
+    return false;
+  }
+  return fptr(mode, reportDelayMs, filter, cookie);
+}
+
+WEAK_SYMBOL
 bool chreBleStopScanAsync() {
   auto *fptr = CHRE_NSL_LAZY_LOOKUP(chreBleStopScanAsync);
   return (fptr != nullptr) ? fptr() : false;
 }
 
 WEAK_SYMBOL
+bool chreBleStopScanAsyncV1_9(const void *cookie) {
+  if (chreGetApiVersion() < CHRE_API_VERSION_1_9) {
+    return false;
+  }
+  auto *fptr = CHRE_NSL_LAZY_LOOKUP(chreBleStopScanAsyncV1_9);
+  if (fptr == nullptr) {
+    return false;
+  }
+  return fptr(cookie);
+}
+
+WEAK_SYMBOL
 bool chreBleReadRssiAsync(uint16_t connectionHandle, const void *cookie) {
   auto *fptr = CHRE_NSL_LAZY_LOOKUP(chreBleReadRssiAsync);
   return (fptr != nullptr) ? fptr(connectionHandle, cookie) : false;
diff --git a/platform/shared/nanoapp_loader.cc b/platform/shared/nanoapp_loader.cc
index eb4ec31..377d8ce 100644
--- a/platform/shared/nanoapp_loader.cc
+++ b/platform/shared/nanoapp_loader.cc
@@ -55,6 +55,10 @@
 //! Indicates whether a failure occurred during static initialization.
 bool gStaticInitFailure = false;
 
+void deleteOpOverride(void* /* ptr */, unsigned int size) {
+  FATAL_ERROR("Nanoapp: delete(void *, unsigned int) override : sz = %u", size);
+}
+
 int atexitInternal(struct AtExitCallback &cb) {
   if (gCurrentlyLoadingNanoapp == nullptr) {
     CHRE_ASSERT_LOG(false,
@@ -179,6 +183,7 @@
     ADD_EXPORTED_C_SYMBOL(__cxa_pure_virtual),
     ADD_EXPORTED_SYMBOL(cxaAtexitOverride, "__cxa_atexit"),
     ADD_EXPORTED_SYMBOL(atexitOverride, "atexit"),
+    ADD_EXPORTED_SYMBOL(deleteOpOverride, "_ZdlPvj"),
     ADD_EXPORTED_C_SYMBOL(dlsym),
     ADD_EXPORTED_C_SYMBOL(isgraph),
     ADD_EXPORTED_C_SYMBOL(memcmp),
@@ -198,7 +203,9 @@
     ADD_EXPORTED_C_SYMBOL(chreBleGetFilterCapabilities),
     ADD_EXPORTED_C_SYMBOL(chreBleFlushAsync),
     ADD_EXPORTED_C_SYMBOL(chreBleStartScanAsync),
+    ADD_EXPORTED_C_SYMBOL(chreBleStartScanAsyncV1_9),
     ADD_EXPORTED_C_SYMBOL(chreBleStopScanAsync),
+    ADD_EXPORTED_C_SYMBOL(chreBleStopScanAsyncV1_9),
     ADD_EXPORTED_C_SYMBOL(chreBleReadRssiAsync),
     ADD_EXPORTED_C_SYMBOL(chreBleGetScanStatus),
     ADD_EXPORTED_C_SYMBOL(chreConfigureDebugDumpEvent),
diff --git a/platform/shared/platform_ble.cc b/platform/shared/platform_ble.cc
index 3039b13..9d0bf90 100644
--- a/platform/shared/platform_ble.cc
+++ b/platform/shared/platform_ble.cc
@@ -31,6 +31,7 @@
     PlatformBleBase::scanStatusChangeCallback,
     PlatformBleBase::advertisingEventCallback,
     PlatformBleBase::readRssiCallback,
+    PlatformBleBase::flushCallback,
     PlatformBleBase::handleBtSnoopLog,
 };
 
@@ -49,6 +50,12 @@
   if (mBleApi != nullptr) {
     if (!mBleApi->open(&gChrePalSystemApi, &sBleCallbacks)) {
       LOGE("BLE PAL open returned false");
+
+#ifdef CHRE_TELEMETRY_SUPPORT_ENABLED
+      EventLoopManagerSingleton::get()->getTelemetryManager().onPalOpenFailure(
+          TelemetryManager::PalType::BLE);
+#endif  // CHRE_TELEMETRY_SUPPORT_ENABLED
+
       mBleApi = nullptr;
     } else {
       LOGD("Opened BLE PAL version 0x%08" PRIx32, mBleApi->moduleVersion);
@@ -78,7 +85,7 @@
 }
 
 bool PlatformBle::startScanAsync(chreBleScanMode mode, uint32_t reportDelayMs,
-                                 const struct chreBleScanFilter *filter) {
+                                 const struct chreBleScanFilterV1_9 *filter) {
   if (mBleApi != nullptr) {
     prePalApiCall(PalType::BLE);
     return mBleApi->startScan(mode, reportDelayMs, filter);
@@ -142,6 +149,20 @@
 #endif
 }
 
+bool PlatformBle::flushAsync() {
+  if (mBleApi != nullptr) {
+    prePalApiCall(PalType::BLE);
+    return mBleApi->flush();
+  } else {
+    return false;
+  }
+}
+
+void PlatformBleBase::flushCallback(uint8_t errorCode) {
+  EventLoopManagerSingleton::get()->getBleRequestManager().handleFlushComplete(
+      errorCode);
+}
+
 void PlatformBleBase::handleBtSnoopLog(bool isTxToBtController,
                                        const uint8_t *buffer, size_t size) {
   BtSnoopDirection direction =
diff --git a/platform/shared/pw_trace/include/chre/target_platform/tracing.h b/platform/shared/pw_trace/include/chre/target_platform/tracing.h
new file mode 100644
index 0000000..a48c9ff
--- /dev/null
+++ b/platform/shared/pw_trace/include/chre/target_platform/tracing.h
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2023 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 CHRE_PLATFORM_SHARED_TRACING_H_
+#define CHRE_PLATFORM_SHARED_TRACING_H_
+
+#include "chre/target_platform/tracing_util.h"
+#include "pw_trace/trace.h"
+
+// Format strings gotten from https://pigweed.dev/pw_trace/#data.
+// Must be macros for string concatenation at preprocessor time.
+#define PW_MAP_PREFIX "@pw_py_map_fmt:{"
+#define PW_MAP_SUFFIX "}"
+
+/**
+ * Traces an instantaneous event.
+ *
+ * @param label A string literal which describes the trace.
+ * @param group <optional> A string literal to group traces together.
+ * @param trace_id <optional>  A uint32_t which groups this trace with others
+ *                             with the same group and trace_id.
+ *                             Every trace with a trace_id must also have a
+ *                             group.
+ * @see https://pigweed.dev/pw_trace/#trace-macros
+ */
+#define CHRE_TRACE_INSTANT(label, ...) PW_TRACE_INSTANT(label, ##__VA_ARGS__)
+
+/**
+ * Used to trace the start of a duration event. Should be paired with a
+ * CHRE_TRACE_END (or CHRE_TRACE_END_DATA) with the same
+ * module/label/group/trace_id.
+ *
+ * @param label A string literal which describes the trace.
+ * @param group <optional> A string literal to group traces together.
+ * @param trace_id <optional>  A uint32_t which groups this trace with others
+ *                             with the same group and trace_id.
+ *                             Every trace with a trace_id must also have a
+ *                             group.
+ * @see https://pigweed.dev/pw_trace/#trace-macros
+ */
+#define CHRE_TRACE_START(label, ...) PW_TRACE_START(label, ##__VA_ARGS__)
+
+/**
+ * Used to trace the end of a duration event. Should be paired with a
+ * CHRE_TRACE_START (or CHRE_TRACE_START_DATA) with the same
+ * module/label/group/trace_id.
+ *
+ * @param label A string literal which describes the trace.
+ * @param group <optional> A string literal to group traces together.
+ * @param trace_id <optional>  A uint32_t which groups this trace with others
+ *                             with the same group and trace_id.
+ *                             Every trace with a trace_id must also have a
+ *                             group.
+ * @see https://pigweed.dev/pw_trace/#trace-macros
+ */
+#define CHRE_TRACE_END(label, ...) PW_TRACE_END(label, ##__VA_ARGS__)
+
+/**
+ * For the group of CHRE_TRACE_INSTANT_DATA... macros.
+ * Use the appropriate macro which contains the parameters you wish to pass to
+ * the trace. If you wish to specify a group you must use either the
+ * CHRE_TRACE_INSTANT_DATA_GROUP macro or CHRE_TRACE_INSTANT_DATA_TRACE_ID macro
+ * with a trace_id.
+ *
+ * Traces an instantaneous event with data variables or literals passed to the
+ * macro, correlating to the dataFmtString.
+ *
+ * @param label A string literal which describes the trace.
+ * @param group A string literal to group traces together.
+ *              Must use CHRE_TRACE_INSTANT_DATA_GROUP or
+ *              CHRE_TRACE_INSTANT_DATA_TRACE_ID with a trace_id to use this
+ *              parameter.
+ * @param trace_id  A uint32_t which groups this trace with others with the same
+ *                  group and trace_id.
+ *                  Every trace with a trace_id must also have a group.
+ *                  Must use CHRE_TRACE_INSTANT_DATA_TRACE_ID to use this
+ *                  parameter.
+ * @param dataFmtString A string literal which is used to relate data to its
+ *                      size. Use the defined macros "T_X" for ease of use in
+ *                      the format specifier. The format string must follow the
+ *                      format "<field name>:<specifier>,..." (omitting the
+ *                      final comma)
+ *                      Ex. "data1:" T_U8 ",data2:" T_I32
+ * @param firstData First data variable. Used to enforce proper usage of this
+ *                  macro (with at least one data variable).
+ * @param VA_ARGS List of variables holding data in the order specified by the
+ *                dataFmtString.
+ *
+ * @see https://pigweed.dev/pw_trace/#trace-macros
+ */
+#define CHRE_TRACE_INSTANT_DATA(label, dataFmtString, firstData, ...)       \
+  do {                                                                      \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__); \
+    PW_TRACE_INSTANT_DATA(label, PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX, \
+                          chreTraceDataBuffer, chreTraceDataSize);          \
+  } while (0)
+
+#define CHRE_TRACE_INSTANT_DATA_GROUP(label, group, dataFmtString, firstData, \
+                                      ...)                                    \
+  do {                                                                        \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__);   \
+    PW_TRACE_INSTANT_DATA(label, group,                                       \
+                          PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,          \
+                          chreTraceDataBuffer, chreTraceDataSize);            \
+  } while (0)
+
+#define CHRE_TRACE_INSTANT_DATA_TRACE_ID(label, group, trace_id,            \
+                                         dataFmtString, firstData, ...)     \
+  do {                                                                      \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__); \
+    PW_TRACE_INSTANT_DATA(label, group, trace_id,                           \
+                          PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,        \
+                          chreTraceDataBuffer, chreTraceDataSize);          \
+  } while (0)
+
+/**
+ * For the group of CHRE_TRACE_START_DATA... macros.
+ * Use the appropriate macro which contains the parameters you wish to pass to
+ * the trace. If you wish to specify a group you must use either the
+ * CHRE_TRACE_START_DATA_GROUP macro or CHRE_TRACE_START_DATA_TRACE_ID macro
+ * with a trace_id.
+ *
+ * Used to trace the start of a duration event with data variables or literals
+ * passed to the macro, correlating to the dataFmtString. This should be paired
+ * with a CHRE_TRACE_END (or CHRE_TRACE_END_DATA) with the same
+ * module/label/group/trace_id to measure the duration of this event.
+ *
+ * @param label A string literal which describes the trace.
+ * @param group A string literal to group traces together.
+ *              Must use CHRE_TRACE_START_DATA_GROUP or
+ *              CHRE_TRACE_START_DATA_TRACE_ID with a trace_id to use this
+ *              parameter.
+ * @param trace_id  A uint32_t which groups this trace with others with the same
+ *                  group and trace_id.
+ *                  Every trace with a trace_id must also have a group.
+ *                  Must use CHRE_TRACE_START_DATA_TRACE_ID to use this
+ *                  parameter.
+ * @param dataFmtString A string literal which is used to relate data to its
+ *                      size. Use the defined macros "T_X" for ease of use in
+ *                      the format specifier. The format string must follow the
+ *                      format "<field name>:<specifier>,..." (omitting the
+ *                      final comma)
+ *                      Ex. "data1:" T_U8 ",data2:" T_I32
+ * @param firstData First data variable. Used to enforce proper usage of this
+ *                  macro (with at least one data variable).
+ * @param VA_ARGS List of variables holding data in the order specified by the
+ *                dataFmtString.
+ *
+ * @see https://pigweed.dev/pw_trace/#trace-macros
+ */
+#define CHRE_TRACE_START_DATA(label, dataFmtString, firstData, ...)         \
+  do {                                                                      \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__); \
+    PW_TRACE_START_DATA(label, PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,   \
+                        chreTraceDataBuffer, chreTraceDataSize);            \
+  } while (0)
+
+#define CHRE_TRACE_START_DATA_GROUP(label, group, dataFmtString, firstData, \
+                                    ...)                                    \
+  do {                                                                      \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__); \
+    PW_TRACE_START_DATA(label, group,                                       \
+                        PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,          \
+                        chreTraceDataBuffer, chreTraceDataSize);            \
+  } while (0)
+
+#define CHRE_TRACE_START_DATA_TRACE_ID(label, group, trace_id, dataFmtString, \
+                                       firstData, ...)                        \
+  do {                                                                        \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__);   \
+    PW_TRACE_START_DATA(label, group, trace_id,                               \
+                        PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,            \
+                        chreTraceDataBuffer, chreTraceDataSize);              \
+  } while (0)
+
+/**
+ * For the group of CHRE_TRACE_END_DATA... macros.
+ * Use the appropriate macro which contains the parameters you wish to pass to
+ * the trace. If you wish to specify a group you must use either the
+ * CHRE_TRACE_END_DATA_GROUP macro or CHRE_TRACE_END_DATA_TRACE_ID macro
+ * with a trace_id.
+ *
+ * Used to trace the end of a duration event with data variables or literals
+ * passed to the macro, correlating to the dataFmtString. This should be paired
+ * with a CHRE_TRACE_START (or CHRE_TRACE_START_DATA) with the same
+ * module/label/group/trace_id to measure the duration of this event.
+ *
+ * @param label A string literal which describes the trace.
+ * @param group A string literal to group traces together.
+ *              Must use CHRE_TRACE_END_DATA_GROUP or
+ *              CHRE_TRACE_END_DATA_TRACE_ID with a trace_id to use this
+ *              parameter.
+ * @param trace_id  A uint32_t which groups this trace with others with the same
+ *                  group and trace_id.
+ *                  Every trace with a trace_id must also have a group.
+ *                  Must use CHRE_TRACE_END_DATA_TRACE_ID to use this
+ *                  parameter.
+ * @param dataFmtString A string literal which is used to relate data to its
+ *                      size. Use the defined macros "T_X" for ease of use in
+ *                      the format specifier. The format string must follow the
+ *                      format "<field name>:<specifier>,..." (omitting the
+ *                      final comma)
+ *                      Ex. "data1:" T_U8 ",data2:" T_I32
+ * @param firstData First data variable. Used to enforce proper usage of this
+ *                  macro (with at least one data variable).
+ * @param VA_ARGS List of variables holding data in the order specified by the
+ *                dataFmtString.
+ *
+ * @see https://pigweed.dev/pw_trace/#trace-macros
+ */
+#define CHRE_TRACE_END_DATA(label, dataFmtString, firstData, ...)           \
+  do {                                                                      \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__); \
+    PW_TRACE_END_DATA(label, PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,     \
+                      chreTraceDataBuffer, chreTraceDataSize);              \
+  } while (0)
+
+#define CHRE_TRACE_END_DATA_GROUP(label, group, dataFmtString, firstData, ...) \
+  do {                                                                         \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__);    \
+    PW_TRACE_END_DATA(label, group, PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX, \
+                      chreTraceDataBuffer, chreTraceDataSize);                 \
+  } while (0)
+
+#define CHRE_TRACE_END_DATA_TRACE_ID(label, group, trace_id, dataFmtString, \
+                                     firstData, ...)                        \
+  do {                                                                      \
+    CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(firstData, ##__VA_ARGS__); \
+    PW_TRACE_END_DATA(label, group, trace_id,                               \
+                      PW_MAP_PREFIX dataFmtString PW_MAP_SUFFIX,            \
+                      chreTraceDataBuffer, chreTraceDataSize);              \
+  } while (0)
+
+#endif  // CHRE_PLATFORM_SHARED_TRACING_H_
diff --git a/platform/shared/pw_trace/include/chre/target_platform/tracing_util.h b/platform/shared/pw_trace/include/chre/target_platform/tracing_util.h
new file mode 100644
index 0000000..d68ecec
--- /dev/null
+++ b/platform/shared/pw_trace/include/chre/target_platform/tracing_util.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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 CHRE_PLATFORM_SHARED_TRACING_UTIL_H_
+#define CHRE_PLATFORM_SHARED_TRACING_UTIL_H_
+
+#include "chre/util/macros.h"
+
+namespace tracing_internal {
+
+// Check to make sure pointer size macro is defined.
+#ifndef __SIZEOF_POINTER__
+#error "__SIZEOF_POINTER__ macro not defined - unsupported toolchain being used"
+#endif
+
+constexpr size_t kMaxTraceDataSize = 256;
+
+template <typename T>
+inline constexpr std::size_t chreTraceGetSizeOf() {
+  if constexpr (std::is_pointer_v<T>) {
+    return __SIZEOF_POINTER__;
+  }
+
+  return sizeof(T);
+}
+
+/**
+ * Special case for strings.
+ *
+ * Due to how python struct unpacking works, reading strings requires the data
+ * format string to specify the length of buffer containing the string.
+ * PW_TRACE macros require the data format string to be a string literal, and we
+ * don't always know the str length at compile-time and thus opt to put all
+ * strings in a fixed size buffer of length CHRE_TRACE_MAX_STRING_SIZE. Using
+ * the pascal string option indicates the buffer's first byte contains the size
+ * of string, followed by the string characters.
+ */
+template <>
+inline constexpr std::size_t chreTraceGetSizeOf<const char *>() {
+  return CHRE_TRACE_STR_BUFFER_SIZE;
+}
+template <>
+inline constexpr std::size_t chreTraceGetSizeOf<char *>() {
+  return chreTraceGetSizeOf<const char *>();
+}
+
+template <typename... Types>
+constexpr std::size_t chreTraceGetSizeOfVarArgs() {
+  return (chreTraceGetSizeOf<std::decay_t<Types>>() + ...);
+}
+
+template <typename Data>
+inline void chreTraceInsertVar(uint8_t *buffer, Data data,
+                               std::size_t dataSize) {
+  static_assert(std::is_integral_v<Data> || std::is_pointer_v<Data>,
+                "Unsupported data type");
+  memcpy(buffer, &data, dataSize);
+}
+template <>
+inline void chreTraceInsertVar<const char *>(uint8_t *buffer, const char *data,
+                                             std::size_t) {
+  // Insert size byte metadata as the first byte of the pascal string.
+  *buffer = static_cast<uint8_t>(strnlen(data, CHRE_TRACE_MAX_STRING_SIZE));
+
+  // Insert string after size byte and zero out remainder of buffer.
+  strncpy(reinterpret_cast<char *>(buffer + 1), data,
+          CHRE_TRACE_MAX_STRING_SIZE);
+}
+template <>
+inline void chreTraceInsertVar<char *>(uint8_t *buffer, char *data,
+                                       std::size_t dataSize) {
+  chreTraceInsertVar<const char *>(buffer, data, dataSize);
+}
+
+/**
+ * Populate the pre-allocated buffer with data passed in.
+ * Should only be called in the CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER
+ * macro.
+ *
+ * Example Usage:
+ *   uint_16_t data1; char data2; uint32_t data3;
+ *   ... // define data
+ *   chreTracePopulateBufferWithArgs(buf, data1, data2, data3);
+ *
+ * @param buffer A buffer to insert data into.
+ *               Assumed to be large enough to hold all data since we use the
+ *               same size logic to allocate space for the buffer.
+ * @param data   Single piece of data to insert into the buffer. Assumed to
+ *               have >= 1 data element to insert into the buffer. Strings
+ *               assumed to be null-terminated or have length >=
+ *               CHRE_TRACE_MAX_STRING_SIZE.
+ * @param args   Variable length argument to hold the remainder of the data
+ *               to insert into the buffer.
+ */
+template <typename Data, typename... Types>
+void chreTracePopulateBufferWithArgs(uint8_t *buffer, Data data,
+                                     Types... args) {
+  constexpr std::size_t dataSize = chreTraceGetSizeOf<Data>();
+  tracing_internal::chreTraceInsertVar(buffer, data, dataSize);
+  buffer += dataSize;
+
+  if constexpr (sizeof...(args) > 0) {
+    chreTracePopulateBufferWithArgs(buffer, args...);
+  }
+}
+
+// Create and populate the chreTraceDataBuffer. We allocate only the amount of
+// space required to store all data.
+// Unscoped to export the variables holding the buffer and data size.
+#define CHRE_TRACE_ALLOCATE_AND_POPULATE_DATA_BUFFER(...)                    \
+  constexpr std::size_t chreTraceDataSize =                                  \
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(__VA_ARGS__)>(); \
+  static_assert(chreTraceDataSize <= tracing_internal::kMaxTraceDataSize,    \
+                "Trace data size too large");                                \
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];                            \
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer,     \
+                                                    __VA_ARGS__);
+
+}  // namespace tracing_internal
+
+#endif  // CHRE_PLATFORM_SHARED_TRACING_UTIL_H_
\ No newline at end of file
diff --git a/platform/shared/sensor_pal/platform_sensor_manager.cc b/platform/shared/sensor_pal/platform_sensor_manager.cc
index 28a2c59..3b5af49 100644
--- a/platform/shared/sensor_pal/platform_sensor_manager.cc
+++ b/platform/shared/sensor_pal/platform_sensor_manager.cc
@@ -44,6 +44,12 @@
   if (mSensorApi != nullptr) {
     if (!mSensorApi->open(&gChrePalSystemApi, &sSensorCallbacks)) {
       LOGE("Sensor PAL open returned false");
+
+#ifdef CHRE_TELEMETRY_SUPPORT_ENABLED
+      EventLoopManagerSingleton::get()->getTelemetryManager().onPalOpenFailure(
+          TelemetryManager::PalType::SENSOR);
+#endif  // CHRE_TELEMETRY_SUPPORT_ENABLED
+
       mSensorApi = nullptr;
     } else {
       LOGD("Opened Sensor PAL version 0x%08" PRIx32, mSensorApi->moduleVersion);
diff --git a/platform/shared/tracing.cc b/platform/shared/tracing.cc
deleted file mode 100644
index 876fe19..0000000
--- a/platform/shared/tracing.cc
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2022 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 "chre/platform/tracing.h"
-#include "chre/util/macros.h"
-
-namespace chre {
-
-void traceRegisterNanoapp(uint16_t instanceId, const char *name) {
-  UNUSED_VAR(instanceId);
-  UNUSED_VAR(name);
-}
-
-void traceNanoappHandleEventStart(uint16_t instanceId, uint16_t eventType) {
-  UNUSED_VAR(instanceId);
-  UNUSED_VAR(eventType);
-}
-
-void traceNanoappHandleEventEnd(uint16_t instanceId) {
-  UNUSED_VAR(instanceId);
-}
-
-}  // namespace chre
diff --git a/platform/slpi/host_link.cc b/platform/slpi/host_link.cc
index e1d6985..7b2e0cd 100644
--- a/platform/slpi/host_link.cc
+++ b/platform/slpi/host_link.cc
@@ -908,6 +908,8 @@
   sendSelfTestResponse(hostClientId, success);
 }
 
+void HostMessageHandlers::handlePulseRequest() {}
+
 void HostMessageHandlers::handleNanConfigurationUpdate(bool enabled) {
 #ifdef CHRE_WIFI_NAN_SUPPORT_ENABLED
   EventLoopManagerSingleton::get()
diff --git a/platform/slpi/include/chre/target_platform/assert.h b/platform/slpi/include/chre/target_platform/assert.h
new file mode 100644
index 0000000..7d00b51
--- /dev/null
+++ b/platform/slpi/include/chre/target_platform/assert.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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 PLATFORM_SLPI_INCLUDE_CHRE_TARGET_PLATFORM_ASSERT_H
+#define PLATFORM_SLPI_INCLUDE_CHRE_TARGET_PLATFORM_ASSERT_H
+
+#include "chre/platform/shared/assert_func.h"
+
+#endif // PLATFORM_SLPI_INCLUDE_CHRE_TARGET_PLATFORM_ASSERT_H
diff --git a/platform/tests/log_buffer_test.cc b/platform/tests/log_buffer_test.cc
index 674fe8f..9a71cbc 100644
--- a/platform/tests/log_buffer_test.cc
+++ b/platform/tests/log_buffer_test.cc
@@ -14,14 +14,18 @@
  * limitations under the License.
  */
 
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 #include <string>
 
 #include "chre/platform/atomic.h"
 #include "chre/platform/condition_variable.h"
 #include "chre/platform/mutex.h"
+#include "chre/platform/shared/bt_snoop_log.h"
 #include "chre/platform/shared/log_buffer.h"
 
+using testing::ContainerEq;
+
 namespace chre {
 
 // TODO(b/146164384): Test that the onLogsReady callback is called
@@ -111,7 +115,7 @@
   char outBuffer[kOutBufferSize];
   // Note the size of this log is too big to fit in the buffer that we are
   // using for the LogBuffer object
-  std::string testLogStrStr(1025, 'a');
+  std::string testLogStrStr(kDefaultBufferSize + 1, 'a');
   TestLogBufferCallback callback;
 
   LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
@@ -125,32 +129,141 @@
   EXPECT_EQ(bytesCopied, 0);
 }
 
-TEST(LogBuffer, LogOverwritten) {
+TEST(LogBuffer, StringLogOverwritten) {
   char buffer[kDefaultBufferSize];
   constexpr size_t kOutBufferSize = 200;
   char outBuffer[kOutBufferSize];
-  char testedBuffer[kOutBufferSize];
   TestLogBufferCallback callback;
   LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
 
-  // This for loop adds 1060 bytes of data through the buffer which is > than
-  // 1024
-  for (size_t i = 0; i < 10; i++) {
-    std::string testLogStrStr(100, 'a' + i);
+  constexpr size_t kLogPayloadSize = 100;
+  constexpr size_t kBufferUsePerLog = LogBuffer::kLogDataOffset +
+                                      LogBuffer::kStringLogOverhead +
+                                      kLogPayloadSize;
+  constexpr int kNumInsertions = 10;
+  constexpr int kNumLogDropsExpected =
+      kNumInsertions - kDefaultBufferSize / kBufferUsePerLog;
+  static_assert(kNumLogDropsExpected > 0);
+
+  // This for loop adds 1060 (kNumInsertions * kBufferUsePerLog) bytes of data
+  // through the buffer which is > than 1024.
+  for (size_t i = 0; i < kNumInsertions; i++) {
+    std::string testLogStrStr(kLogPayloadSize, 'a' + i);
     const char *testLogStr = testLogStrStr.c_str();
     logBuffer.handleLog(LogBufferLogLevel::INFO, 0, testLogStr);
   }
-  size_t numLogsDropped;
-  size_t bytesCopied =
-      logBuffer.copyLogs(outBuffer, kOutBufferSize, &numLogsDropped);
-  memcpy(testedBuffer, outBuffer + LogBuffer::kLogDataOffset, 101);
+  EXPECT_EQ(logBuffer.getBufferSize(),
+            (kNumInsertions - kNumLogDropsExpected) * kBufferUsePerLog);
+  EXPECT_EQ(logBuffer.getNumLogsDropped(), kNumLogDropsExpected);
 
-  // Should have read out the second from front test log string which is 'a' + 1
-  // = 'b'
-  EXPECT_TRUE(strcmp(testedBuffer, std::string(100, 'b').c_str()) == 0);
-  EXPECT_EQ(bytesCopied, LogBuffer::kLogDataOffset + 100 + 1);
-  // Should have dropped the first log
-  EXPECT_EQ(numLogsDropped, 1);
+  for (size_t i = logBuffer.getNumLogsDropped(); i < kNumInsertions; i++) {
+    // Should have read out the i-th from front test log string which a string
+    // log 'a' + i.
+    size_t numLogsDropped;
+    size_t bytesCopied =
+        logBuffer.copyLogs(outBuffer, kOutBufferSize, &numLogsDropped);
+    EXPECT_TRUE(strcmp(outBuffer + LogBuffer::kLogDataOffset,
+                       std::string(kLogPayloadSize, 'a' + i).c_str()) == 0);
+    EXPECT_EQ(bytesCopied, kBufferUsePerLog);
+  }
+}
+
+TEST(LogBuffer, TokenizedLogOverwritten) {
+  char buffer[kDefaultBufferSize];
+  TestLogBufferCallback callback;
+  LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
+
+  constexpr size_t kLogPayloadSize = 100;
+  constexpr size_t kBufferUsePerLog = LogBuffer::kLogDataOffset +
+                                      LogBuffer::kTokenizedLogOffset +
+                                      kLogPayloadSize;
+  constexpr int kNumInsertions = 10;
+  constexpr int kNumLogDropsExpected =
+      kNumInsertions - kDefaultBufferSize / kBufferUsePerLog;
+  static_assert(kNumLogDropsExpected > 0);
+
+  // This for loop adds 1060 (kNumInsertions * kBufferUsePerLog) bytes of data
+  // through the buffer which is > than 1024.
+  for (size_t i = 0; i < kNumInsertions; i++) {
+    std::vector<uint8_t> testData(kLogPayloadSize, i);
+    logBuffer.handleEncodedLog(LogBufferLogLevel::INFO, 0, testData.data(),
+                               testData.size());
+  }
+  EXPECT_EQ(logBuffer.getBufferSize(),
+            (kNumInsertions - kNumLogDropsExpected) * kBufferUsePerLog);
+  EXPECT_EQ(logBuffer.getNumLogsDropped(), kNumLogDropsExpected);
+
+  for (size_t i = logBuffer.getNumLogsDropped(); i < kNumInsertions; i++) {
+    // Should have read out the i-th from front test log data which is an
+    // integer i.
+    std::vector<uint8_t> outBuffer(kBufferUsePerLog, 0x77);
+    size_t numLogsDropped;
+    size_t bytesCopied =
+        logBuffer.copyLogs(outBuffer.data(), outBuffer.size(), &numLogsDropped);
+
+    // Validate that the log size in Tokenized log header matches the expected
+    // log size.
+    EXPECT_EQ(outBuffer[LogBuffer::kLogDataOffset], kLogPayloadSize);
+
+    outBuffer.erase(outBuffer.begin(), outBuffer.begin() +
+                                           LogBuffer::kLogDataOffset +
+                                           LogBuffer::kTokenizedLogOffset);
+    EXPECT_THAT(outBuffer,
+                ContainerEq(std::vector<uint8_t>(kLogPayloadSize, i)));
+    EXPECT_EQ(bytesCopied, kBufferUsePerLog);
+  }
+}
+
+TEST(LogBuffer, BtSnoopLogOverwritten) {
+  char buffer[kDefaultBufferSize];
+  TestLogBufferCallback callback;
+  LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
+
+  constexpr size_t kLogPayloadSize = 100;
+  constexpr size_t kBufferUsePerLog = LogBuffer::kLogDataOffset +
+                                      LogBuffer::kBtSnoopLogOffset +
+                                      kLogPayloadSize;
+  constexpr int kNumInsertions = 10;
+  constexpr int kNumLogDropsExpected =
+      kNumInsertions - kDefaultBufferSize / kBufferUsePerLog;
+  static_assert(kNumLogDropsExpected > 0);
+
+  // This for loop adds 1070 (kNumInsertions * kBufferUsePerLog) bytes of data
+  // through the buffer which is > than 1024.
+  for (size_t i = 0; i < kNumInsertions; i++) {
+    std::vector<uint8_t> testData(kLogPayloadSize, i);
+    logBuffer.handleBtLog(BtSnoopDirection::INCOMING_FROM_BT_CONTROLLER, 0,
+                          testData.data(), testData.size());
+  }
+  EXPECT_EQ(logBuffer.getBufferSize(),
+            (kNumInsertions - kNumLogDropsExpected) * kBufferUsePerLog);
+  EXPECT_EQ(logBuffer.getNumLogsDropped(), kNumLogDropsExpected);
+
+  for (size_t i = logBuffer.getNumLogsDropped(); i < kNumInsertions; i++) {
+    // Should have read out the i-th from front test log data which is an
+    // integer i.
+    std::vector<uint8_t> outBuffer(kBufferUsePerLog, 0x77);
+    size_t numLogsDropped;
+    size_t bytesCopied =
+        logBuffer.copyLogs(outBuffer.data(), outBuffer.size(), &numLogsDropped);
+
+    // Validate that the Bt Snoop log header matches the expected log direction
+    // and size.
+    constexpr int kBtSnoopLogHeaderSizeOffset = 1;
+    EXPECT_EQ(
+        static_cast<BtSnoopDirection>(outBuffer[LogBuffer::kLogDataOffset]),
+        BtSnoopDirection::INCOMING_FROM_BT_CONTROLLER);
+    EXPECT_EQ(
+        outBuffer[LogBuffer::kLogDataOffset + kBtSnoopLogHeaderSizeOffset],
+        kLogPayloadSize);
+
+    outBuffer.erase(outBuffer.begin(), outBuffer.begin() +
+                                           LogBuffer::kLogDataOffset +
+                                           LogBuffer::kBtSnoopLogOffset);
+    EXPECT_THAT(outBuffer,
+                ContainerEq(std::vector<uint8_t>(kLogPayloadSize, i)));
+    EXPECT_EQ(bytesCopied, kBufferUsePerLog);
+  }
 }
 
 TEST(LogBuffer, CopyIntoEmptyBuffer) {
@@ -204,16 +317,16 @@
   char outBuffer[kOutBufferSize];
   TestLogBufferCallback callback;
   LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
-  std::string testStr(256, 'a');
+  std::string testStr(LogBuffer::kLogMaxSize + 1, 'a');
 
   logBuffer.handleLog(LogBufferLogLevel::INFO, 0, testStr.c_str());
   size_t numLogsDropped;
   size_t bytesCopied =
       logBuffer.copyLogs(outBuffer, kOutBufferSize, &numLogsDropped);
 
-  // Should truncate the logs down to the kLogMaxSize value of 255 by the time
-  // it is copied out.
-  EXPECT_EQ(bytesCopied, 255);
+  // Should truncate the logs down to the kLogMaxSize + kLogDataOffset by the
+  // time it is copied out.
+  EXPECT_EQ(bytesCopied, LogBuffer::kLogMaxSize + LogBuffer::kLogDataOffset);
 }
 
 TEST(LogBuffer, WouldCauseOverflowTest) {
@@ -221,28 +334,33 @@
   TestLogBufferCallback callback;
   LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
 
-  // With an empty buffer inerting one character should not overflow
-  // ASSERT because if this fails the next ASSERT statement is undefined most
-  // likely.
+  // With an empty buffer inserting an empty string (only null terminator)
+  // should not overflow ASSERT because if this fails the next ASSERT statement
+  // is undefined most likely.
   ASSERT_FALSE(logBuffer.logWouldCauseOverflow(1));
 
-  // This for loop adds 1000 bytes of data. There is 24 bytes of space left in
-  // the buffer after this loop.
+  // This for loop adds 1000 bytes of data (kLogPayloadSize + kStringLogOverhead
+  // + kLogDataOffset). There is 24 bytes of space left in the buffer after this
+  // loop.
+  constexpr size_t kLogPayloadSize = 94;
   for (size_t i = 0; i < 10; i++) {
-    std::string testLogStrStr(94, 'a');
+    std::string testLogStrStr(kLogPayloadSize, 'a');
     const char *testLogStr = testLogStrStr.c_str();
     logBuffer.handleLog(LogBufferLogLevel::INFO, 0, testLogStr);
   }
 
-  std::string testLogStrStr(11, 'a');
+  // This adds 18 (kLogPayloadSize + kStringLogOverhead + kLogDataOffset) bytes
+  // of data. After this log entry there is room enough for a log of character
+  // size 1.
+  constexpr size_t kLastLogPayloadSize = 12;
+  std::string testLogStrStr(kLastLogPayloadSize, 'a');
   const char *testLogStr = testLogStrStr.c_str();
-  // After this log entry there is room enough for a log of character size 1.
   logBuffer.handleLog(LogBufferLogLevel::INFO, 0, testLogStr);
 
-  // There should be just enough space for this log
+  // There should be just enough space for this log.
   ASSERT_FALSE(logBuffer.logWouldCauseOverflow(1));
 
-  // Inserting any more than a one char log should cause overflow
+  // Inserting any more than a one char log should cause overflow.
   ASSERT_TRUE(logBuffer.logWouldCauseOverflow(2));
 }
 
@@ -280,6 +398,47 @@
   ASSERT_EQ(bytesCopied, 0);
 }
 
+TEST(LogBuffer, GetLogDataLengthTest) {
+  char buffer[kDefaultBufferSize];
+
+  TestLogBufferCallback callback;
+  LogBuffer logBuffer(&callback, buffer, kDefaultBufferSize);
+
+  constexpr size_t kLogPayloadSize = 10;
+  constexpr size_t kBufferUseStringLog = LogBuffer::kLogDataOffset +
+                                         LogBuffer::kStringLogOverhead +
+                                         kLogPayloadSize;
+  constexpr size_t kBufferUseTokenizedLog = LogBuffer::kLogDataOffset +
+                                            LogBuffer::kTokenizedLogOffset +
+                                            kLogPayloadSize;
+  uint8_t mCurrentLogStartingIndex = 0;
+
+  std::string testLogStr(kLogPayloadSize, 'a');
+  logBuffer.handleLog(LogBufferLogLevel::INFO, 0, testLogStr.c_str());
+  EXPECT_EQ(logBuffer.getLogDataLength(
+                mCurrentLogStartingIndex + LogBuffer::kLogDataOffset,
+                LogType::STRING),
+            LogBuffer::kStringLogOverhead + kLogPayloadSize);
+  mCurrentLogStartingIndex += kBufferUseStringLog;
+
+  std::vector<uint8_t> testLogTokenized(kLogPayloadSize, 0x77);
+  logBuffer.handleEncodedLog(LogBufferLogLevel::INFO, 0,
+                             testLogTokenized.data(), testLogTokenized.size());
+  EXPECT_EQ(logBuffer.getLogDataLength(
+                mCurrentLogStartingIndex + LogBuffer::kLogDataOffset,
+                LogType::TOKENIZED),
+            LogBuffer::kTokenizedLogOffset + kLogPayloadSize);
+  mCurrentLogStartingIndex += kBufferUseTokenizedLog;
+
+  std::vector<uint8_t> testLogBtSnoop(kLogPayloadSize, 0x77);
+  logBuffer.handleBtLog(BtSnoopDirection::INCOMING_FROM_BT_CONTROLLER, 0,
+                        testLogBtSnoop.data(), testLogBtSnoop.size());
+  EXPECT_EQ(logBuffer.getLogDataLength(
+                mCurrentLogStartingIndex + LogBuffer::kLogDataOffset,
+                LogType::BLUETOOTH),
+            LogBuffer::kBtSnoopLogOffset + kLogPayloadSize);
+}
+
 // TODO(srok): Add multithreaded tests
 
 }  // namespace chre
diff --git a/platform/tests/trace_test.cc b/platform/tests/trace_test.cc
new file mode 100644
index 0000000..ce162a5
--- /dev/null
+++ b/platform/tests/trace_test.cc
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// Ensuring this macro is not defined since we include the tracing macros and
+// tracing utilities separately and do not want to test or include the pw_trace
+// functions.
+#ifdef CHRE_TRACING_ENABLED
+#undef CHRE_TRACING_ENABLED
+#endif  // CHRE_TRACING_ENABLED
+
+#include <stdlib.h>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+
+#include "chre/platform/tracing.h"
+#include "chre/target_platform/tracing_util.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace tracing_internal {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
+
+TEST(Trace, PopulateBufferWithTracedPtr) {
+  const uint8_t var = 0x12;
+  const uint8_t *data = &var;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, __SIZEOF_POINTER__);
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+// Already verified in chre/platform/tracing.h this value is either 8 or 4.
+#if __SIZEOF_POINTER__ == 8
+  EXPECT_EQ(*((uint64_t *)chreTraceDataBuffer), (uint64_t)data);
+#elif __SIZEOF_POINTER__ == 4
+  EXPECT_EQ(*((uint32_t *)chreTraceDataBuffer), (uint32_t)data);
+#else
+#error "Pointer size is of unsupported size"
+#endif
+}
+
+TEST(Trace, PopulateBufferWithTracedBool) {
+  const bool data = true;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(bool));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(1));
+}
+
+TEST(Trace, PopulateBufferWithTracedUInt8) {
+  const uint8_t data = 0x12;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(uint8_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedUInt16) {
+  uint16_t data = 0x1234;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(uint16_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(0x34, 0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedUInt32) {
+  const uint32_t data = 0x12345678;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(uint32_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(0x78, 0x56, 0x34, 0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedUInt64) {
+  const uint64_t data = 0x1234567890123456;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(uint64_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer,
+              ElementsAre(0x56, 0x34, 0x12, 0x90, 0x78, 0x56, 0x34, 0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedInt8) {
+  const int8_t data = 0x12;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(int8_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedInt16) {
+  const int16_t data = 0x1234;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(int16_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(0x34, 0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedInt32) {
+  const int32_t data = 0x12345678;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(int32_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre(0x78, 0x56, 0x34, 0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedInt64) {
+  const int64_t data = 0x1234567890123456;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(int64_t));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer,
+              ElementsAre(0x56, 0x34, 0x12, 0x90, 0x78, 0x56, 0x34, 0x12));
+}
+
+TEST(Trace, PopulateBufferWithTracedChar) {
+  char data = 'a';
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, sizeof(char));
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAre('a'));
+}
+
+TEST(Trace, PopulateBufferWithTracedStrDoesNotOverflow) {
+  const char data[] = "test";
+  const size_t kBufferPadding = 5;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize + kBufferPadding];
+  memset(chreTraceDataBuffer, 0xFF, chreTraceDataSize + kBufferPadding);
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  for (std::size_t i = 0; i < kBufferPadding; i++) {
+    EXPECT_EQ(chreTraceDataBuffer[chreTraceDataSize + i], 0xFF);
+  }
+}
+
+TEST(Trace, PopulateBufferWithTracedShortStrAndNullBytePadding) {
+  // expected variable + length
+  const char data[] = "test";
+  uint8_t dataLen = static_cast<uint8_t>(strlen(data));
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(data)>();
+
+  EXPECT_EQ(chreTraceDataSize, CHRE_TRACE_STR_BUFFER_SIZE);
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer, data);
+
+  // Fully populated buffer with data len and null-byte padding at the end.
+  uint8_t expectedBuffer[CHRE_TRACE_STR_BUFFER_SIZE] = {dataLen, 't', 'e', 's',
+                                                        't'};
+  for (std::size_t i = dataLen + 1; i < CHRE_TRACE_STR_BUFFER_SIZE; i++) {
+    expectedBuffer[i] = '\0';
+  }
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAreArray(expectedBuffer));
+}
+
+TEST(Trace, PopulateBufferWithTracedMaxLenStrNoPadding) {
+  // String data buffer to hold max-len string.
+  char dataBuffer[CHRE_TRACE_MAX_STRING_SIZE + 1];
+  // Fully populated buffer with data len and no null-byte padding.
+  // In this case data len should equal CHRE_TRACE_MAX_STRING_SIZE.
+  uint8_t expectedBuffer[CHRE_TRACE_STR_BUFFER_SIZE] = {
+      CHRE_TRACE_MAX_STRING_SIZE};
+
+  // Populate the buffer with str "0123456789..." until we hit max size.
+  for (std::size_t i = 0; i < CHRE_TRACE_MAX_STRING_SIZE; i++) {
+    dataBuffer[i] = '0' + (i % 10);
+    expectedBuffer[i + 1] = '0' + (i % 10);
+  }
+  dataBuffer[CHRE_TRACE_MAX_STRING_SIZE] = '\0';
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(dataBuffer)>();
+
+  EXPECT_EQ(chreTraceDataSize, CHRE_TRACE_STR_BUFFER_SIZE);
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer,
+                                                    dataBuffer);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAreArray(expectedBuffer));
+}
+
+TEST(Trace, PopulateBufferWithTracedStringTuncateToMaxLength) {
+  const size_t kBufferPadding = 5;
+  const std::size_t kOversizeStrLen =
+      CHRE_TRACE_MAX_STRING_SIZE + kBufferPadding;
+  // String data buffer to hold oversized string.
+  char dataBuffer[kOversizeStrLen + 1];
+
+  // Populate the buffer with str "0123456789..." until we hit kOversizeStrLen.
+  for (std::size_t i = 0; i < kOversizeStrLen; i++) {
+    dataBuffer[i] = '0' + (i % 10);
+  }
+  dataBuffer[kOversizeStrLen] = '\0';
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(dataBuffer)>();
+
+  EXPECT_EQ(chreTraceDataSize, CHRE_TRACE_STR_BUFFER_SIZE);
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(chreTraceDataBuffer,
+                                                    dataBuffer);
+
+  // Fully populated buffer with data len and truncated string.
+  // In this case data len should equal CHRE_TRACE_MAX_STRING_SIZE, not
+  // kOversizeStrLen.
+  uint8_t expectedBuffer[CHRE_TRACE_STR_BUFFER_SIZE] = {
+      CHRE_TRACE_MAX_STRING_SIZE};
+
+  // Populate the buffer with str "0123456789..." until we hit
+  // CHRE_TRACE_MAX_STRING_SIZE.
+  for (std::size_t i = 0; i < CHRE_TRACE_MAX_STRING_SIZE; i++) {
+    expectedBuffer[i + 1] = '0' + (i % 10);
+  }
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAreArray(expectedBuffer));
+}
+
+TEST(Trace, PopulateBufferWithMultipleTracedData) {
+  uint8_t dataUint8 = 0x12;
+  char dataChar = 'a';
+  uint32_t dataUint32 = 0x12345678;
+  char dataStr[CHRE_TRACE_MAX_STRING_SIZE];
+  strncpy(dataStr, "test", CHRE_TRACE_MAX_STRING_SIZE);
+  uint8_t dataStrLen = static_cast<uint8_t>(strlen(dataStr));
+  std::size_t totalDataSize = sizeof(uint8_t) + sizeof(char) +
+                              sizeof(uint32_t) + CHRE_TRACE_STR_BUFFER_SIZE;
+
+  constexpr std::size_t chreTraceDataSize =
+      tracing_internal::chreTraceGetSizeOfVarArgs<TYPE_LIST(
+          dataUint8, dataChar, dataUint32, dataStr)>();
+
+  EXPECT_EQ(chreTraceDataSize, totalDataSize);
+
+  uint8_t expectedBuffer[chreTraceDataSize] = {0x12, 'a',  0x78,      0x56,
+                                               0x34, 0x12, dataStrLen};
+  strncpy((char *)(&expectedBuffer[7]), dataStr, CHRE_TRACE_MAX_STRING_SIZE);
+
+  uint8_t chreTraceDataBuffer[chreTraceDataSize];
+  tracing_internal::chreTracePopulateBufferWithArgs(
+      chreTraceDataBuffer, dataUint8, dataChar, dataUint32, dataStr);
+
+  EXPECT_THAT(chreTraceDataBuffer, ElementsAreArray(expectedBuffer));
+}
+
+// TODO(b/302232350): Add a death test for unsupported data types. Currently
+//                    testing unsupported types (structs) with manual testing.
+
+}  // namespace
+}  // namespace tracing_internal
\ No newline at end of file
diff --git a/platform/tinysys/authentication.cc b/platform/tinysys/authentication.cc
index e33555e..259ba67 100644
--- a/platform/tinysys/authentication.cc
+++ b/platform/tinysys/authentication.cc
@@ -24,9 +24,14 @@
 #include "mbedtls/pk.h"
 #include "mbedtls/sha256.h"
 
+#include "cpufreq_vote.h"
+
 namespace chre {
 namespace {
 
+// A data structure needed for SCP chip frequency change
+DECLARE_OPPDEV(gChreScpFreqVote);
+
 // All the size below are in bytes
 constexpr uint32_t kEcdsaP256SigSize = 64;
 constexpr uint32_t kEcdsaP256PublicKeySize = 64;
@@ -101,6 +106,7 @@
 class Authenticator {
  public:
   Authenticator() {
+    scp_vote_opp(&gChreScpFreqVote, CLK_OPP2);
     mbedtls_ecp_group_init(&mGroup);
     mbedtls_ecp_point_init(&mQ);
     mbedtls_mpi_init(&mR);
@@ -112,6 +118,7 @@
     mbedtls_mpi_free(&mR);
     mbedtls_ecp_point_free(&mQ);
     mbedtls_ecp_group_free(&mGroup);
+    scp_unvote_opp(&gChreScpFreqVote, CLK_OPP2);
   }
 
   bool loadEcpGroup() {
diff --git a/platform/tinysys/condition_variable_base.cc b/platform/tinysys/condition_variable_base.cc
index 715b46b..77b993d 100644
--- a/platform/tinysys/condition_variable_base.cc
+++ b/platform/tinysys/condition_variable_base.cc
@@ -32,8 +32,7 @@
 }
 
 ConditionVariable::ConditionVariable() {
-  // TODO(b/256870101): Use xSemaphoreCreateBinaryStatic when possible
-  semaphoreHandle = xSemaphoreCreateBinary();
+  semaphoreHandle = xSemaphoreCreateBinaryStatic(&mSemaphoreBuffer);
   if (semaphoreHandle == nullptr) {
     FATAL_ERROR("Failed to create cv semaphore");
   }
@@ -79,4 +78,4 @@
   taskEXIT_CRITICAL();
   return isTimedOut;
 }
-}  // namespace chre
\ No newline at end of file
+}  // namespace chre
diff --git a/platform/tinysys/host_link.cc b/platform/tinysys/host_link.cc
index 3df0517..5ef720a 100644
--- a/platform/tinysys/host_link.cc
+++ b/platform/tinysys/host_link.cc
@@ -15,6 +15,7 @@
  */
 
 #include "FreeRTOS.h"
+#include "encoding.h"
 #include "task.h"
 
 #include "chre/core/event_loop_manager.h"
@@ -83,10 +84,11 @@
 void *gChreSubregionSendAddr;
 size_t gChreSubregionSendSize;
 
-// TODO(b/263958729): move it to HostLinkBase, and revisit buffer size
+// TODO(b/277235389): move it to HostLinkBase, and revisit buffer size
 // payload buffers
 #define CHRE_IPI_RECV_BUFFER_SIZE (CHRE_MESSAGE_TO_HOST_MAX_SIZE + 128)
-uint32_t gChreRecvBuffer[CHRE_IPI_RECV_BUFFER_SIZE / sizeof(uint32_t)]
+DRAM_REGION_VARIABLE uint32_t
+    gChreRecvBuffer[CHRE_IPI_RECV_BUFFER_SIZE / sizeof(uint32_t)]
     __attribute__((aligned(CACHE_LINE_SIZE)));
 
 #ifdef SCP_CHRE_USE_DMA
@@ -123,6 +125,8 @@
   SelfTestResponse,
   MetricLog,
   NanConfigurationRequest,
+  PulseRequest,
+  PulseResponse,
 };
 
 struct PendingMessage {
@@ -151,7 +155,8 @@
 };
 
 constexpr size_t kOutboundQueueSize = 100;
-FixedSizeBlockingQueue<PendingMessage, kOutboundQueueSize> gOutboundQueue;
+DRAM_REGION_VARIABLE FixedSizeBlockingQueue<PendingMessage, kOutboundQueueSize>
+    gOutboundQueue;
 
 typedef void(MessageBuilderFunction)(ChreFlatBufferBuilder &builder,
                                      void *cookie);
@@ -160,7 +165,8 @@
   return EventLoopManagerSingleton::get()->getHostCommsManager();
 }
 
-bool generateMessageFromBuilder(ChreFlatBufferBuilder *builder) {
+DRAM_REGION_FUNCTION bool generateMessageFromBuilder(
+    ChreFlatBufferBuilder *builder) {
   CHRE_ASSERT(builder != nullptr);
   LOGV("%s: message size %d", __func__, builder->GetSize());
   bool result =
@@ -172,9 +178,9 @@
   return result;
 }
 
-bool generateMessageToHost(const HostMessage *message) {
+DRAM_REGION_FUNCTION bool generateMessageToHost(const HostMessage *message) {
   LOGV("%s: message size %zu", __func__, message->message.size());
-  // TODO(b/263958729): ideally we'd construct our flatbuffer directly in the
+  // TODO(b/285219398): ideally we'd construct our flatbuffer directly in the
   // host-supplied buffer
   constexpr size_t kFixedReserveSize = 80;
   ChreFlatBufferBuilder builder(message->message.size() + kFixedReserveSize);
@@ -191,7 +197,7 @@
   return result;
 }
 
-int generateHubInfoResponse(uint16_t hostClientId) {
+DRAM_REGION_FUNCTION int generateHubInfoResponse(uint16_t hostClientId) {
   constexpr size_t kInitialBufferSize = 192;
 
   constexpr char kHubName[] = "CHRE on Tinysys";
@@ -219,7 +225,7 @@
   return HostLinkBase::send(builder.GetBufferPointer(), builder.GetSize());
 }
 
-bool dequeueMessage(PendingMessage pendingMsg) {
+DRAM_REGION_FUNCTION bool dequeueMessage(PendingMessage pendingMsg) {
   LOGV("%s: message type %d", __func__, pendingMsg.type);
   bool result = false;
   switch (pendingMsg.type) {
@@ -243,6 +249,7 @@
     case PendingMessageType::SelfTestResponse:
     case PendingMessageType::MetricLog:
     case PendingMessageType::NanConfigurationRequest:
+    case PendingMessageType::PulseResponse:
       result = generateMessageFromBuilder(pendingMsg.data.builder);
       break;
 
@@ -260,7 +267,7 @@
  *
  * @return true if the message was successfully added to the queue.
  */
-bool enqueueMessage(PendingMessage pendingMsg) {
+DRAM_REGION_FUNCTION bool enqueueMessage(PendingMessage pendingMsg) {
   return gOutboundQueue.push(pendingMsg);
 }
 
@@ -278,9 +285,9 @@
  *
  * @return true if the message was successfully added to the queue
  */
-bool buildAndEnqueueMessage(PendingMessageType msgType,
-                            size_t initialBufferSize,
-                            MessageBuilderFunction *msgBuilder, void *cookie) {
+DRAM_REGION_FUNCTION bool buildAndEnqueueMessage(
+    PendingMessageType msgType, size_t initialBufferSize,
+    MessageBuilderFunction *msgBuilder, void *cookie) {
   LOGV("%s: message type %d, size %zu", __func__, msgType, initialBufferSize);
   bool pushed = false;
 
@@ -291,8 +298,6 @@
   } else {
     msgBuilder(*builder, cookie);
 
-    // TODO(b/263958729): if this fails, ideally we should block for some
-    // timeout until there's space in the queue
     if (!enqueueMessage(PendingMessage(msgType, builder.get()))) {
       LOGE("Couldn't push message type %d to outbound queue",
            static_cast<int>(msgType));
@@ -308,7 +313,16 @@
 /**
  * FlatBuffer message builder callback used with handleNanoappListRequest()
  */
-void buildNanoappListResponse(ChreFlatBufferBuilder &builder, void *cookie) {
+DRAM_REGION_FUNCTION void buildPulseResponse(ChreFlatBufferBuilder &builder,
+                                             void * /*cookie*/) {
+  HostProtocolChre::encodePulseResponse(builder);
+}
+
+/**
+ * FlatBuffer message builder callback used with handleNanoappListRequest()
+ */
+DRAM_REGION_FUNCTION void buildNanoappListResponse(
+    ChreFlatBufferBuilder &builder, void *cookie) {
   LOGV("%s", __func__);
   auto nanoappAdderCallback = [](const Nanoapp *nanoapp, void *data) {
     auto *cbData = static_cast<NanoappListData *>(data);
@@ -327,8 +341,9 @@
                                               cbData->hostClientId);
 }
 
-void handleUnloadNanoappCallback(uint16_t /*type*/, void *data,
-                                 void * /*extraData*/) {
+DRAM_REGION_FUNCTION void handleUnloadNanoappCallback(uint16_t /*type*/,
+                                                      void *data,
+                                                      void * /*extraData*/) {
   auto *cbData = static_cast<UnloadNanoappCallbackData *>(data);
   bool success = false;
   uint16_t instanceId;
@@ -356,25 +371,79 @@
   memoryFree(data);
 }
 
-}  // anonymous namespace
+DRAM_REGION_FUNCTION void sendDebugDumpData(uint16_t hostClientId,
+                                            const char *debugStr,
+                                            size_t debugStrSize) {
+  struct DebugDumpMessageData {
+    uint16_t hostClientId;
+    const char *debugStr;
+    size_t debugStrSize;
+  };
 
-void sendDebugDumpResultToHost(uint16_t hostClientId, const char * /*debugStr*/,
-                               size_t /*debugStrSize*/, bool /*complete*/,
-                               uint32_t /*dataCount*/) {
-  LOGV("%s: host client id %d", __func__, hostClientId);
-  // TODO(b/263958729): Implement this.
+  auto msgBuilder = [](ChreFlatBufferBuilder &builder, void *cookie) {
+    const auto *data = static_cast<const DebugDumpMessageData *>(cookie);
+    HostProtocolChre::encodeDebugDumpData(builder, data->hostClientId,
+                                          data->debugStr, data->debugStrSize);
+  };
+
+  constexpr size_t kFixedSizePortion = 52;
+  DebugDumpMessageData data;
+  data.hostClientId = hostClientId;
+  data.debugStr = debugStr;
+  data.debugStrSize = debugStrSize;
+  buildAndEnqueueMessage(PendingMessageType::DebugDumpData,
+                         kFixedSizePortion + debugStrSize, msgBuilder, &data);
 }
 
-HostLinkBase::HostLinkBase() {
+DRAM_REGION_FUNCTION void sendDebugDumpResponse(uint16_t hostClientId,
+                                                bool success,
+                                                uint32_t dataCount) {
+  struct DebugDumpResponseData {
+    uint16_t hostClientId;
+    bool success;
+    uint32_t dataCount;
+  };
+
+  auto msgBuilder = [](ChreFlatBufferBuilder &builder, void *cookie) {
+    const auto *data = static_cast<const DebugDumpResponseData *>(cookie);
+    HostProtocolChre::encodeDebugDumpResponse(builder, data->hostClientId,
+                                              data->success, data->dataCount);
+  };
+
+  constexpr size_t kInitialSize = 52;
+  DebugDumpResponseData data;
+  data.hostClientId = hostClientId;
+  data.success = success;
+  data.dataCount = dataCount;
+  buildAndEnqueueMessage(PendingMessageType::DebugDumpResponse, kInitialSize,
+                         msgBuilder, &data);
+}
+}  // anonymous namespace
+
+DRAM_REGION_FUNCTION void sendDebugDumpResultToHost(uint16_t hostClientId,
+                                                    const char *debugStr,
+                                                    size_t debugStrSize,
+                                                    bool complete,
+                                                    uint32_t dataCount) {
+  LOGV("%s: host client id %d", __func__, hostClientId);
+  if (debugStrSize > 0) {
+    sendDebugDumpData(hostClientId, debugStr, debugStrSize);
+  }
+  if (complete) {
+    sendDebugDumpResponse(hostClientId, /* success= */ true, dataCount);
+  }
+}
+
+DRAM_REGION_FUNCTION HostLinkBase::HostLinkBase() {
   LOGV("HostLinkBase::%s", __func__);
   initializeIpi();
 }
 
-HostLinkBase::~HostLinkBase() {
+DRAM_REGION_FUNCTION HostLinkBase::~HostLinkBase() {
   LOGV("HostLinkBase::%s", __func__);
 }
 
-void HostLinkBase::vChreReceiveTask(void *pvParameters) {
+DRAM_REGION_FUNCTION void HostLinkBase::vChreReceiveTask(void *pvParameters) {
   int i = 0;
   int ret = 0;
 
@@ -389,15 +458,16 @@
   }
 }
 
-void HostLinkBase::vChreSendTask(void *pvParameters) {
+DRAM_REGION_FUNCTION void HostLinkBase::vChreSendTask(void *pvParameters) {
   while (true) {
     auto msg = gOutboundQueue.pop();
     dequeueMessage(msg);
   }
 }
 
-void HostLinkBase::chreIpiHandler(unsigned int id, void *prdata, void *data,
-                                  unsigned int len) {
+DRAM_REGION_FUNCTION void HostLinkBase::chreIpiHandler(unsigned int id,
+                                                       void *prdata, void *data,
+                                                       unsigned int len) {
   /* receive magic and cmd */
   struct ScpChreIpiMsg msg = *(struct ScpChreIpiMsg *)data;
 
@@ -441,7 +511,7 @@
   gChreIpiAckToHost[1] = msg.size;
 }
 
-void HostLinkBase::initializeIpi(void) {
+DRAM_REGION_FUNCTION void HostLinkBase::initializeIpi(void) {
   LOGV("%s", __func__);
   bool success = false;
   int ret;
@@ -482,11 +552,11 @@
   }
 }
 
-void HostLinkBase::receive(HostLinkBase *instance, void *message,
-                           int messageLen) {
+DRAM_REGION_FUNCTION void HostLinkBase::receive(HostLinkBase *instance,
+                                                void *message, int messageLen) {
   LOGV("%s: message len %d", __func__, messageLen);
 
-  // TODO(b/263958729): A crude way to initially determine daemon's up - set
+  // TODO(b/277128368): A crude way to initially determine daemon's up - set
   // a flag on the first message received. This is temporary until a better
   // way to do this is available.
   instance->setInitialized(true);
@@ -496,7 +566,7 @@
   }
 }
 
-bool HostLinkBase::send(uint8_t *data, size_t dataLen) {
+DRAM_REGION_FUNCTION bool HostLinkBase::send(uint8_t *data, size_t dataLen) {
 #ifndef HOST_LINK_IPI_SEND_TIMEOUT_MS
 #define HOST_LINK_IPI_SEND_TIMEOUT_MS 100
 #endif
@@ -513,7 +583,7 @@
       ap_to_scp(reinterpret_cast<uint32_t>(gChreSubregionSendAddr)));
 
 #ifdef SCP_CHRE_USE_DMA
-  // TODO(b/263958729): use DMA for larger payload
+  // TODO(b/288415339): use DMA for larger payload
   // No need cache operation, because src_dst handled by SCP CPU and dstAddr is
   // non-cacheable
 #else
@@ -548,14 +618,10 @@
   return ret == IPI_ACTION_DONE;
 }
 
-void HostLinkBase::sendTimeSyncRequest() {
-  LOGV("%s", __func__);
-  // TODO(b/263958729): Implement this.
-}
+DRAM_REGION_FUNCTION void HostLinkBase::sendTimeSyncRequest() {}
 
-void HostLinkBase::sendLogMessageV2(const uint8_t *logMessage,
-                                    size_t logMessageSize,
-                                    uint32_t numLogsDropped) {
+DRAM_REGION_FUNCTION void HostLinkBase::sendLogMessageV2(
+    const uint8_t *logMessage, size_t logMessageSize, uint32_t numLogsDropped) {
   LOGV("%s: size %zu", __func__, logMessageSize);
   struct LogMessageData {
     const uint8_t *logMsg;
@@ -589,7 +655,7 @@
 #endif
 }
 
-bool HostLink::sendMessage(HostMessage const *message) {
+DRAM_REGION_FUNCTION bool HostLink::sendMessage(HostMessage const *message) {
   LOGV("HostLink::%s size(%zu)", __func__, message->message.size());
   bool success = false;
 
@@ -602,32 +668,31 @@
   return success;
 }
 
-// TODO(b/263958729): HostMessageHandlers member function implementations are
+// TODO(b/285219398): HostMessageHandlers member function implementations are
 // expected to be (mostly) identical for any platform that uses flatbuffers
 // to encode messages - refactor the host link to merge the multiple copies
 // we currently have.
-void HostMessageHandlers::handleNanoappMessage(uint64_t appId,
-                                               uint32_t messageType,
-                                               uint16_t hostEndpoint,
-                                               const void *messageData,
-                                               size_t messageDataLen) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleNanoappMessage(
+    uint64_t appId, uint32_t messageType, uint16_t hostEndpoint,
+    const void *messageData, size_t messageDataLen) {
   LOGV("Parsed nanoapp message from host: app ID 0x%016" PRIx64
        ", endpoint "
        "0x%" PRIx16 ", msgType %" PRIu32 ", payload size %zu",
        appId, hostEndpoint, messageType, messageDataLen);
 
-  // TODO(b/263958729): Implement this.
   getHostCommsManager().sendMessageToNanoappFromHost(
       appId, messageType, hostEndpoint, messageData, messageDataLen);
 }
 
-void HostMessageHandlers::handleHubInfoRequest(uint16_t hostClientId) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleHubInfoRequest(
+    uint16_t hostClientId) {
   LOGV("%s: host client id %d", __func__, hostClientId);
   enqueueMessage(
       PendingMessage(PendingMessageType::HubInfoResponse, hostClientId));
 }
 
-void HostMessageHandlers::handleNanoappListRequest(uint16_t hostClientId) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleNanoappListRequest(
+    uint16_t hostClientId) {
   auto callback = [](uint16_t /*type*/, void *data, void * /*extraData*/) {
     uint16_t cbHostClientId = NestedDataPtr<uint16_t>(data);
 
@@ -656,10 +721,9 @@
       NestedDataPtr<uint16_t>(hostClientId), callback);
 }
 
-void HostMessageHandlers::sendFragmentResponse(uint16_t hostClientId,
-                                               uint32_t transactionId,
-                                               uint32_t fragmentId,
-                                               bool success) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::sendFragmentResponse(
+    uint16_t hostClientId, uint32_t transactionId, uint32_t fragmentId,
+    bool success) {
   struct FragmentedLoadInfoResponse {
     uint16_t hostClientId;
     uint32_t transactionId;
@@ -685,7 +749,17 @@
                          kInitialBufferSize, msgBuilder, &response);
 }
 
-void HostMessageHandlers::handleLoadNanoappRequest(
+DRAM_REGION_FUNCTION void HostMessageHandlers::handlePulseRequest() {
+  auto callback = [](uint16_t /*type*/, void * /*data*/, void * /*extraData*/) {
+    buildAndEnqueueMessage(PendingMessageType::PulseResponse,
+                           /*initialBufferSize= */ 48, buildPulseResponse,
+                           /* cookie= */ nullptr);
+  };
+  EventLoopManagerSingleton::get()->deferCallback(
+      SystemCallbackType::PulseResponse, /* data= */ nullptr, callback);
+}
+
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleLoadNanoappRequest(
     uint16_t hostClientId, uint32_t transactionId, uint64_t appId,
     uint32_t appVersion, uint32_t appFlags, uint32_t targetApiVersion,
     const void *buffer, size_t bufferLen, const char *appFileName,
@@ -697,7 +771,7 @@
                   respondBeforeStart);
 }
 
-void HostMessageHandlers::handleUnloadNanoappRequest(
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleUnloadNanoappRequest(
     uint16_t hostClientId, uint32_t transactionId, uint64_t appId,
     bool allowSystemNanoappUnload) {
   LOGD("Unload nanoapp request from client %" PRIu16 " (txnID %" PRIu32
@@ -718,22 +792,31 @@
   }
 }
 
-void HostLink::flushMessagesSentByNanoapp(uint64_t /* appId */) {
+DRAM_REGION_FUNCTION void HostLink::flushMessagesSentByNanoapp(
+    uint64_t /* appId */) {
   // Not implemented
 }
 
-void HostMessageHandlers::handleTimeSyncMessage(int64_t offset) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleTimeSyncMessage(
+    int64_t offset) {
   LOGE("%s unsupported.", __func__);
 }
 
-void HostMessageHandlers::handleDebugDumpRequest(uint16_t hostClientId) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleDebugDumpRequest(
+    uint16_t hostClientId) {
   LOGV("%s: host client id %d", __func__, hostClientId);
-  // TODO(b/263958729): Implement this.
+  if (!EventLoopManagerSingleton::get()
+           ->getDebugDumpManager()
+           .onDebugDumpRequested(hostClientId)) {
+    LOGE("Couldn't trigger debug dump process");
+    sendDebugDumpResponse(hostClientId, /* success= */ false,
+                          /* dataCount= */ 0);
+  }
 }
 
-void HostMessageHandlers::handleSettingChangeMessage(fbs::Setting setting,
-                                                     fbs::SettingState state) {
-  // TODO(b/267207477): Refactor handleSettingChangeMessage to shared code
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleSettingChangeMessage(
+    fbs::Setting setting, fbs::SettingState state) {
+  // TODO(b/285219398): Refactor handleSettingChangeMessage to shared code
   Setting chreSetting;
   bool chreSettingEnabled;
   if (HostProtocolChre::getSettingFromFbs(setting, &chreSetting) &&
@@ -743,16 +826,17 @@
   }
 }
 
-void HostMessageHandlers::handleSelfTestRequest(uint16_t hostClientId) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleSelfTestRequest(
+    uint16_t hostClientId) {
   LOGV("%s: host client id %d", __func__, hostClientId);
-  // TODO(b/263958729): Implement this.
 }
 
-void HostMessageHandlers::handleNanConfigurationUpdate(bool /* enabled */) {
+DRAM_REGION_FUNCTION void HostMessageHandlers::handleNanConfigurationUpdate(
+    bool /* enabled */) {
   LOGE("%s NAN unsupported.", __func__);
 }
 
-void sendAudioRequest() {
+DRAM_REGION_FUNCTION void sendAudioRequest() {
   auto msgBuilder = [](ChreFlatBufferBuilder &builder, void * /*cookie*/) {
     HostProtocolChre::encodeLowPowerMicAccessRequest(builder);
   };
@@ -761,7 +845,7 @@
                          kInitialSize, msgBuilder, /* cookie= */ nullptr);
 }
 
-void sendAudioRelease() {
+DRAM_REGION_FUNCTION void sendAudioRelease() {
   auto msgBuilder = [](ChreFlatBufferBuilder &builder, void * /*cookie*/) {
     HostProtocolChre::encodeLowPowerMicAccessRelease(builder);
   };
diff --git a/platform/tinysys/include/chre/target_platform/condition_variable_base.h b/platform/tinysys/include/chre/target_platform/condition_variable_base.h
index ba03021..46f2f93 100644
--- a/platform/tinysys/include/chre/target_platform/condition_variable_base.h
+++ b/platform/tinysys/include/chre/target_platform/condition_variable_base.h
@@ -47,6 +47,9 @@
   /** semaphore implementing the condition variable */
   SemaphoreHandle_t semaphoreHandle;
 
+  /** Buffer used to store state used by the semaphore */
+  StaticSemaphore_t mSemaphoreBuffer;
+
   /** True if wait_for() times out before semaphoreHandle is given */
   bool isTimedOut = false;
 
diff --git a/run_pal_impl_tests.sh b/run_pal_impl_tests.sh
index 4b0daed..9e7426e 100755
--- a/run_pal_impl_tests.sh
+++ b/run_pal_impl_tests.sh
@@ -3,6 +3,15 @@
 # Quit if any command produces an error.
 set -e
 
+BUILD_ONLY="false"
+while getopts "b" opt; do
+  case ${opt} in
+    b)
+      BUILD_ONLY="true"
+      ;;
+  esac
+done
+
 # Build and run the CHRE unit test binary.
 JOB_COUNT=$((`grep -c ^processor /proc/cpuinfo`))
 
@@ -14,4 +23,13 @@
 
 make clean
 make google_x86_googletest_debug -j$JOB_COUNT
-./out/google_x86_googletest_debug/libchre $1
+
+if [ "$BUILD_ONLY" = "false" ]; then
+./out/google_x86_googletest_debug/libchre ${@:1}
+else
+    if [ ! -f ./out/google_x86_googletest_debug/libchre ]; then
+        echo  "./out/google_x86_googletest_debug/libchre does not exist."
+        echo  "run_pal_impl_test.sh failed to build the binary."
+        exit 1
+    fi
+fi
\ No newline at end of file
diff --git a/run_sim.sh b/run_sim.sh
index 680dadd..6b8ad8d 100755
--- a/run_sim.sh
+++ b/run_sim.sh
@@ -30,4 +30,9 @@
 
 if [ "$BUILD_ONLY" = "false" ]; then
 ./out/google_x86_linux_debug/libchre ${@:1}
+else
+    if [ ! -f ./out/google_x86_linux_debug/libchre ]; then
+        echo  "./out/google_x86_linux_debug/libchre does not exist."
+        exit 1
+    fi
 fi
diff --git a/run_tests.sh b/run_tests.sh
index b73c253..ddb2271 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -3,6 +3,15 @@
 # Quit if any command produces an error.
 set -e
 
+BUILD_ONLY="false"
+while getopts "b" opt; do
+  case ${opt} in
+    b)
+      BUILD_ONLY="true"
+      ;;
+  esac
+done
+
 # Build and run the CHRE unit test binary.
 JOB_COUNT=$((`grep -c ^processor /proc/cpuinfo`))
 
@@ -12,4 +21,12 @@
 
 make clean
 make google_x86_googletest_debug -j$JOB_COUNT
-./out/google_x86_googletest_debug/libchre "$@"
+
+if [ "$BUILD_ONLY" = "false" ]; then
+./out/google_x86_googletest_debug/libchre ${@:1}
+else
+    if [ ! -f ./out/google_x86_googletest_debug/libchre ]; then
+        echo  "./out/google_x86_googletest_debug/libchre does not exist."
+        exit 1
+    fi
+fi
\ No newline at end of file
diff --git a/test/simulation/README.md b/test/simulation/README.md
index db380ea..86938ad 100644
--- a/test/simulation/README.md
+++ b/test/simulation/README.md
@@ -31,9 +31,8 @@
   CREATE_CHRE_TEST_EVENT(MY_TEST_EVENT, 0);
 
   // 2. Create a test Nanpoapp by inheriting TestNanoapp.
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public TestNanoapp {
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) override {
       switch (eventType) {
         // 3. Handle system events.
         case CHRE_EVENT_WIFI_ASYNC_RESULT: {
@@ -54,36 +53,45 @@
           }
         }
       }
-    };
+    }
   };
 
   // 6. Load the app and add initial expectations.
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());;
   EXPECT_TRUE(...);
 
   // 7. Send test events to the Nanoapp to execute some actions and add
   //    expectations about the result.
-  sendEventToNanoapp(app, MY_TEST_EVENT);
+  sendEventToNanoapp(appId, MY_TEST_EVENT);
   waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT);
   EXPECT_TRUE(...);
 
   // 8. Optionally unload the Nanoapp
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
 }
 ```
 
 ##### Test app (#2, #6, #8)
 
-Inherit from `TestNanoapp` to create a test nanoapp. The following
-properties oif a nanoapp can be overridden `name`, `id`, `version`, `perms`,
-`start`, `handleEvent`, and `end`.
+Inherit from `TestNanoapp` to create a test nanoapp.
 
-Typical tests only override of few of the above properties:
+If you need to customize any of the nanoapp `name`, `id`, `version`, or `perms`,
+you will need to add a constructor calling the `TestNanoapp` constructor with that info, i.e.:
 
-* `perms` to set the permissions required for the test,
-* `start` to put the system in a known state before each test,
-* `handleEvent` is probably the most important function where system and test
-   events are handled. See the sections below for more details.
+```
+class App: public TestNanoapp {
+  public:
+    explicit App(TestNanoappInfo info): TestNanoapp(info) {}
+
+  // ...
+};
+```
+
+The nanoapp entry points are implemented as methods of the class:
+
+- `start`,
+- `handleEvent`,
+- `end`.
 
 ##### Test events (#1)
 
@@ -97,15 +105,14 @@
 the test:
 
 ```cpp
-decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                               const void *eventData) {
+void handleEvent(uint32_t, uint16_t eventType, const void *eventData) override {
   switch (eventType) {
     case CHRE_EVENT_WIFI_ASYNC_RESULT: {
       // ...
       break;
     }
   }
-};
+}
 ```
 
 The handler would typically send an event back to the nanoapp, see the next
@@ -137,20 +144,18 @@
 expectation. For example the status of an event:
 
 ```cpp
-  decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                 const void *eventData) {
-    switch (eventType) {
-      case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-        auto *event = static_cast<const chreAsyncResult *>(eventData);
-        if (event->success) {
-          TestEventQueueSingleton::get()->pushEvent(
-              CHRE_EVENT_WIFI_ASYNC_RESULT);
-        }
-        break;
+void handleEvent(uint32_t, uint16_t eventType, const void *eventData) override {
+  switch (eventType) {
+    case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+      auto *event = static_cast<const chreAsyncResult *>(eventData);
+      if (event->success) {
+        TestEventQueueSingleton::get()->pushEvent(
+            CHRE_EVENT_WIFI_ASYNC_RESULT);
       }
+      break;
     }
-  };
-};
+  }
+}
 ```
 
 With the above snippet `waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT)` will timeout
@@ -160,20 +165,19 @@
 the data as the second argument to pushEvent:
 
 ```cpp
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      switch (eventType) {
-        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-          auto *event = static_cast<const chreAsyncResult *>(eventData);
-          if (event->success) {
-            TestEventQueueSingleton::get()->pushEvent(
-                CHRE_EVENT_WIFI_ASYNC_RESULT,
-                *(static_cast<const uint32_t *>(event->cookie)));
-          }
-          break;
+  void handleEvent(uint32_t, uint16_t eventType, const void *eventData) override {
+    switch (eventType) {
+      case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+        auto *event = static_cast<const chreAsyncResult *>(eventData);
+        if (event->success) {
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_WIFI_ASYNC_RESULT,
+              *(static_cast<const uint32_t *>(event->cookie)));
         }
+        break;
       }
-    };
+    }
+  }
 ```
 
 The data must be trivially copyable (a scalar or a struct of scalar are safe).
@@ -197,15 +201,14 @@
 
 // ...
 
-sendEventToNanoapp(app, MY_TEST_EVENT);
+sendEventToNanoapp(appId, MY_TEST_EVENT);
 ```
 
 The code to be executed in the context of the nanoapp should be added to its
 `handleEvent` function:
 
 ```cpp
-decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                               const void *eventData) {
+void handleEvent(uint32_t, uint16_t eventType, const void *eventData) override {
   switch (eventType) {
     // Test event are received with a CHRE_EVENT_TEST_EVENT type.
     case CHRE_EVENT_TEST_EVENT: {
@@ -218,14 +221,14 @@
       }
     }
   }
-};
+}
 ```
 
 It is possible to send data alongside a test event:
 
 ```cpp
 bool enable = true;
-sendEventToNanoapp(app, MY_TEST_EVENT, &enable);
+sendEventToNanoapp(appId, MY_TEST_EVENT, &enable);
 ```
 
 The data should be a scalar type or a struct of scalars. Be careful not to send
@@ -236,8 +239,7 @@
 the `TestEvent`:
 
 ```cpp
-decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                               const void *eventData) {
+void handleEvent(uint32_t, uint16_t eventType, const void *eventData) override {
   switch (eventType) {
     // Test event are received with a CHRE_EVENT_TEST_EVENT type.
     case CHRE_EVENT_TEST_EVENT: {
@@ -250,5 +252,5 @@
       }
     }
   }
-};
+}
 ```
diff --git a/test/simulation/audio_test.cc b/test/simulation/audio_test.cc
index df03aaa..85ddc6d 100644
--- a/test/simulation/audio_test.cc
+++ b/test/simulation/audio_test.cc
@@ -36,31 +36,33 @@
 namespace chre {
 namespace {
 
-struct AudioNanoapp : public TestNanoapp {
-  uint32_t perms = NanoappPermissions::CHRE_PERMS_AUDIO;
+class AudioNanoapp : public TestNanoapp {
+ public:
+  AudioNanoapp()
+      : TestNanoapp(
+            TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_AUDIO}) {}
 
-  decltype(nanoappStart) *start = []() {
+  bool start() override {
     chreUserSettingConfigureEvents(CHRE_USER_SETTING_MICROPHONE,
                                    true /* enable */);
     return true;
-  };
+  }
 };
 
 TEST_F(TestBase, AudioCanSubscribeAndUnsubscribeToDataEvents) {
   CREATE_CHRE_TEST_EVENT(CONFIGURE, 0);
 
-  struct App : public AudioNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static int count = 0;
-
+  class App : public AudioNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_AUDIO_DATA: {
           auto event =
               static_cast<const struct chreAudioDataEvent *>(eventData);
           if (event->handle == 0) {
-            count++;
-            if (count == 3) {
+            mCount++;
+            if (mCount == 3) {
               TestEventQueueSingleton::get()->pushEvent(CHRE_EVENT_AUDIO_DATA);
             }
           }
@@ -91,15 +93,18 @@
           }
         }
       }
-    };
+    }
+
+   protected:
+    int mCount = 0;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
   EXPECT_FALSE(chrePalAudioIsHandle0Enabled());
 
   bool enable = true;
   bool success;
-  sendEventToNanoapp(app, CONFIGURE, enable);
+  sendEventToNanoapp(appId, CONFIGURE, enable);
   waitForEvent(CONFIGURE, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_AUDIO_SAMPLING_CHANGE);
@@ -108,7 +113,7 @@
   waitForEvent(CHRE_EVENT_AUDIO_DATA);
 
   enable = false;
-  sendEventToNanoapp(app, CONFIGURE, enable);
+  sendEventToNanoapp(appId, CONFIGURE, enable);
   waitForEvent(CONFIGURE, &success);
   EXPECT_TRUE(success);
   EXPECT_FALSE(chrePalAudioIsHandle0Enabled());
@@ -117,9 +122,9 @@
 TEST_F(TestBase, AudioUnsubscribeToDataEventsOnUnload) {
   CREATE_CHRE_TEST_EVENT(CONFIGURE, 0);
 
-  struct App : public AudioNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public AudioNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) {
       switch (eventType) {
         case CHRE_EVENT_AUDIO_SAMPLING_CHANGE: {
           auto event =
@@ -145,21 +150,21 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
   EXPECT_FALSE(chrePalAudioIsHandle0Enabled());
 
   bool enable = true;
   bool success;
-  sendEventToNanoapp(app, CONFIGURE, enable);
+  sendEventToNanoapp(appId, CONFIGURE, enable);
   waitForEvent(CONFIGURE, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_AUDIO_SAMPLING_CHANGE);
   EXPECT_TRUE(chrePalAudioIsHandle0Enabled());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalAudioIsHandle0Enabled());
 }
 
diff --git a/test/simulation/ble_test.cc b/test/simulation/ble_test.cc
index 24fe044..a071420 100644
--- a/test/simulation/ble_test.cc
+++ b/test/simulation/ble_test.cc
@@ -43,11 +43,14 @@
   CREATE_CHRE_TEST_EVENT(GET_CAPABILITIES, 0);
   CREATE_CHRE_TEST_EVENT(GET_FILTER_CAPABILITIES, 1);
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
@@ -66,37 +69,40 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   uint32_t capabilities;
-  sendEventToNanoapp(app, GET_CAPABILITIES);
+  sendEventToNanoapp(appId, GET_CAPABILITIES);
   waitForEvent(GET_CAPABILITIES, &capabilities);
   ASSERT_EQ(capabilities, CHRE_BLE_CAPABILITIES_SCAN |
                               CHRE_BLE_CAPABILITIES_SCAN_RESULT_BATCHING |
                               CHRE_BLE_CAPABILITIES_SCAN_FILTER_BEST_EFFORT);
 
-  sendEventToNanoapp(app, GET_FILTER_CAPABILITIES);
+  sendEventToNanoapp(appId, GET_FILTER_CAPABILITIES);
   waitForEvent(GET_FILTER_CAPABILITIES, &capabilities);
   ASSERT_EQ(capabilities, CHRE_BLE_FILTER_CAPABILITIES_RSSI |
                               CHRE_BLE_FILTER_CAPABILITIES_SERVICE_DATA);
 }
 
-struct BleTestNanoapp : public TestNanoapp {
-  uint32_t perms = NanoappPermissions::CHRE_PERMS_BLE;
+class BleTestNanoapp : public TestNanoapp {
+ public:
+  BleTestNanoapp()
+      : TestNanoapp(
+            TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_BLE}) {}
 
-  decltype(nanoappStart) *start = []() {
+  bool start() override {
     chreUserSettingConfigureEvents(CHRE_USER_SETTING_BLE_AVAILABLE,
                                    true /* enable */);
     return true;
-  };
+  }
 
-  decltype(nanoappEnd) *end = []() {
+  void end() override {
     chreUserSettingConfigureEvents(CHRE_USER_SETTING_BLE_AVAILABLE,
                                    false /* enable */);
-  };
+  }
 };
 
 /**
@@ -109,9 +115,10 @@
   CREATE_CHRE_TEST_EVENT(STOP_SCAN, 2);
   CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -150,20 +157,20 @@
           break;
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   bool success;
-  sendEventToNanoapp(app, START_SCAN);
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STARTED);
   ASSERT_TRUE(chrePalIsBleEnabled());
   waitForEvent(CHRE_EVENT_BLE_ADVERTISEMENT);
 
-  sendEventToNanoapp(app, STOP_SCAN);
+  sendEventToNanoapp(appId, STOP_SCAN);
   waitForEvent(STOP_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STOPPED);
@@ -174,9 +181,10 @@
   CREATE_CHRE_TEST_EVENT(START_SCAN, 0);
   CREATE_CHRE_TEST_EVENT(SCAN_STARTED, 1);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -200,19 +208,19 @@
           break;
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
-  bool success;
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
-  sendEventToNanoapp(app, START_SCAN);
+  bool success;
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STARTED);
   ASSERT_TRUE(chrePalIsBleEnabled());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   ASSERT_FALSE(chrePalIsBleEnabled());
 }
 
@@ -226,9 +234,9 @@
   CREATE_CHRE_TEST_EVENT(STOP_SCAN, 2);
   CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -266,24 +274,24 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
   bool success;
 
-  sendEventToNanoapp(app, START_SCAN);
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STARTED);
 
-  sendEventToNanoapp(app, START_SCAN);
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STARTED);
   waitForEvent(CHRE_EVENT_BLE_ADVERTISEMENT);
 
-  sendEventToNanoapp(app, STOP_SCAN);
+  sendEventToNanoapp(appId, STOP_SCAN);
   waitForEvent(STOP_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STOPPED);
@@ -299,9 +307,10 @@
   CREATE_CHRE_TEST_EVENT(STOP_SCAN, 2);
   CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -331,23 +340,23 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
-
-  sendEventToNanoapp(app, STOP_SCAN);
+  sendEventToNanoapp(appId, STOP_SCAN);
   waitForEvent(STOP_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STOPPED);
 
-  sendEventToNanoapp(app, STOP_SCAN);
+  sendEventToNanoapp(appId, STOP_SCAN);
   waitForEvent(STOP_SCAN, &success);
   EXPECT_TRUE(success);
 
   waitForEvent(SCAN_STOPPED);
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
 }
 
 /**
@@ -362,9 +371,10 @@
   CREATE_CHRE_TEST_EVENT(SCAN_STARTED, 1);
   CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -406,13 +416,13 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
-  bool success;
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
-  sendEventToNanoapp(app, START_SCAN);
+  bool success;
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
   EXPECT_TRUE(success);
 
@@ -448,9 +458,10 @@
 TEST_F(TestBase, BleSettingDisabledStartScanTest) {
   CREATE_CHRE_TEST_EVENT(START_SCAN, 0);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -483,10 +494,10 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::BLE_AVAILABLE, false /* enable */);
@@ -496,7 +507,7 @@
   EXPECT_FALSE(enabled);
 
   bool success;
-  sendEventToNanoapp(app, START_SCAN);
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_BLE_ASYNC_RESULT);
@@ -511,9 +522,10 @@
   CREATE_CHRE_TEST_EVENT(STOP_SCAN, 2);
   CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -548,10 +560,10 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::BLE_AVAILABLE, false /* enable */);
@@ -561,7 +573,7 @@
   EXPECT_FALSE(enabled);
 
   bool success;
-  sendEventToNanoapp(app, STOP_SCAN);
+  sendEventToNanoapp(appId, STOP_SCAN);
   waitForEvent(STOP_SCAN, &success);
   EXPECT_TRUE(success);
   waitForEvent(SCAN_STOPPED);
@@ -571,15 +583,14 @@
  * Test that a nanoapp can read RSSI successfully.
  */
 TEST_F(TestBase, BleReadRssi) {
-  constexpr auto kConnectionHandle = 6;
-  constexpr auto kCookie = 123;
+  constexpr uint16_t kConnectionHandle = 6;
+  constexpr uint32_t kCookie = 123;
 
   CREATE_CHRE_TEST_EVENT(RSSI_REQUEST, 1);
   CREATE_CHRE_TEST_EVENT(RSSI_REQUEST_SENT, 2);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public BleTestNanoapp {
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) {
       switch (eventType) {
         case CHRE_EVENT_BLE_RSSI_READ: {
           auto *event =
@@ -611,10 +622,10 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::BLE_AVAILABLE, true /* enabled */);
@@ -623,7 +634,7 @@
   EXPECT_TRUE(enabled);
 
   bool success;
-  sendEventToNanoapp(app, RSSI_REQUEST);
+  sendEventToNanoapp(appId, RSSI_REQUEST);
   waitForEvent(RSSI_REQUEST_SENT, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_BLE_RSSI_READ);
@@ -640,9 +651,12 @@
   CREATE_CHRE_TEST_EVENT(STOP_SCAN, 2);
   CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
 
-  struct App : public BleTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  struct testData {
+    void *cookie;
+  };
+
+  class App : public BleTestNanoapp {
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) {
       switch (eventType) {
         case CHRE_EVENT_BLE_ASYNC_RESULT: {
           auto *event = static_cast<const struct chreAsyncResult *>(eventData);
@@ -650,15 +664,117 @@
               (event->requestType == CHRE_BLE_REQUEST_TYPE_START_SCAN)
                   ? SCAN_STARTED
                   : SCAN_STOPPED;
-          TestEventQueueSingleton::get()->pushEvent(type, event->errorCode);
+          TestEventQueueSingleton::get()->pushEvent(type, *event);
           break;
         }
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
           switch (event->type) {
             case START_SCAN: {
+              auto data = static_cast<testData *>(event->data);
+              const bool success = chreBleStartScanAsyncV1_9(
+                  CHRE_BLE_SCAN_MODE_BACKGROUND, 0, nullptr, data->cookie);
+              TestEventQueueSingleton::get()->pushEvent(START_SCAN, success);
+              break;
+            }
+
+            case STOP_SCAN: {
+              auto data = static_cast<testData *>(event->data);
+              const bool success = chreBleStopScanAsyncV1_9(data->cookie);
+              TestEventQueueSingleton::get()->pushEvent(STOP_SCAN, success);
+              break;
+            }
+          }
+        }
+      }
+    }
+  };
+
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+  bool success;
+
+  delayBleScanStart(true /* delay */);
+
+  testData data;
+  uint32_t cookieOne = 1;
+  data.cookie = &cookieOne;
+  sendEventToNanoapp(appId, START_SCAN, data);
+  waitForEvent(START_SCAN, &success);
+  EXPECT_TRUE(success);
+
+  uint32_t cookieTwo = 2;
+  data.cookie = &cookieTwo;
+  sendEventToNanoapp(appId, START_SCAN, data);
+  waitForEvent(START_SCAN, &success);
+  EXPECT_TRUE(success);
+
+  chreAsyncResult result;
+  waitForEvent(SCAN_STARTED, &result);
+  EXPECT_EQ(result.errorCode, CHRE_ERROR_OBSOLETE_REQUEST);
+  EXPECT_EQ(result.cookie, &cookieOne);
+
+  // Respond to the first scan request. CHRE will then attempt the next scan
+  // request at which point the PAL should no longer delay the response.
+  delayBleScanStart(false /* delay */);
+  EXPECT_TRUE(startBleScan());
+
+  waitForEvent(SCAN_STARTED, &result);
+  EXPECT_EQ(result.errorCode, CHRE_ERROR_NONE);
+  EXPECT_EQ(result.cookie, &cookieTwo);
+
+  sendEventToNanoapp(appId, STOP_SCAN, data);
+  waitForEvent(STOP_SCAN, &success);
+  EXPECT_TRUE(success);
+  waitForEvent(SCAN_STOPPED);
+}
+
+/**
+ * This test validates that a nanoapp can call flush only when an existing scan
+ * is enabled for the nanoapp. This test validates that batching will hold the
+ * data and flush will send the batched data and then a flush complete event.
+ */
+TEST_F(TestBase, BleFlush) {
+  CREATE_CHRE_TEST_EVENT(START_SCAN, 0);
+  CREATE_CHRE_TEST_EVENT(SCAN_STARTED, 1);
+  CREATE_CHRE_TEST_EVENT(STOP_SCAN, 2);
+  CREATE_CHRE_TEST_EVENT(SCAN_STOPPED, 3);
+  CREATE_CHRE_TEST_EVENT(CALL_FLUSH, 4);
+  CREATE_CHRE_TEST_EVENT(SAW_BLE_AD_AND_FLUSH_COMPLETE, 5);
+
+  class App : public BleTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_BLE_ASYNC_RESULT: {
+          auto *event = static_cast<const struct chreAsyncResult *>(eventData);
+          if (event->errorCode == CHRE_ERROR_NONE) {
+            uint16_t type =
+                (event->requestType == CHRE_BLE_REQUEST_TYPE_START_SCAN)
+                    ? SCAN_STARTED
+                    : SCAN_STOPPED;
+            TestEventQueueSingleton::get()->pushEvent(type);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_BLE_ADVERTISEMENT: {
+          mSawBleAdvertisementEvent = true;
+          break;
+        }
+
+        case CHRE_EVENT_BLE_FLUSH_COMPLETE: {
+          auto *event = static_cast<const struct chreAsyncResult *>(eventData);
+          mSawFlushCompleteEvent = event->success && event->cookie == &mCookie;
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case START_SCAN: {
               const bool success = chreBleStartScanAsync(
-                  CHRE_BLE_SCAN_MODE_BACKGROUND, 0, nullptr);
+                  CHRE_BLE_SCAN_MODE_AGGRESSIVE, 60000, nullptr);
               TestEventQueueSingleton::get()->pushEvent(START_SCAN, success);
               break;
             }
@@ -668,41 +784,82 @@
               TestEventQueueSingleton::get()->pushEvent(STOP_SCAN, success);
               break;
             }
+
+            case CALL_FLUSH: {
+              const bool success = chreBleFlushAsync(&mCookie);
+              TestEventQueueSingleton::get()->pushEvent(CALL_FLUSH, success);
+              break;
+            }
           }
+          break;
         }
       }
-    };
+
+      if (mSawBleAdvertisementEvent && mSawFlushCompleteEvent) {
+        TestEventQueueSingleton::get()->pushEvent(
+            SAW_BLE_AD_AND_FLUSH_COMPLETE);
+        mSawBleAdvertisementEvent = false;
+        mSawFlushCompleteEvent = false;
+      }
+    }
+
+   private:
+    uint32_t mCookie;
+    bool mSawBleAdvertisementEvent = false;
+    bool mSawFlushCompleteEvent = false;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
+  // Flushing before a scan should fail.
   bool success;
+  sendEventToNanoapp(appId, CALL_FLUSH);
+  waitForEvent(CALL_FLUSH, &success);
+  ASSERT_FALSE(success);
 
-  delayBleScanStart(true /* delay */);
-
-  sendEventToNanoapp(app, START_SCAN);
+  // Start a scan with batching.
+  sendEventToNanoapp(appId, START_SCAN);
   waitForEvent(START_SCAN, &success);
-  EXPECT_TRUE(success);
+  ASSERT_TRUE(success);
+  waitForEvent(SCAN_STARTED);
+  ASSERT_TRUE(chrePalIsBleEnabled());
 
-  sendEventToNanoapp(app, START_SCAN);
-  waitForEvent(START_SCAN, &success);
-  EXPECT_TRUE(success);
+  // Call flush again multiple times and get the complete event.
+  // We should only receive data when flush is called as the batch
+  // delay is extremely large.
+  constexpr uint32_t kNumFlushCalls = 3;
+  for (uint32_t i = 0; i < kNumFlushCalls; ++i) {
+    std::this_thread::sleep_for(std::chrono::milliseconds(250));
 
-  uint8_t errorCode;
-  waitForEvent(SCAN_STARTED, &errorCode);
-  EXPECT_EQ(errorCode, CHRE_ERROR_OBSOLETE_REQUEST);
+    sendEventToNanoapp(appId, CALL_FLUSH);
+    waitForEvent(CALL_FLUSH, &success);
+    ASSERT_TRUE(success);
 
-  // Respond to the first scan request. CHRE will then attempt the next scan
-  // request at which point the PAL should no longer delay the response.
-  delayBleScanStart(false /* delay */);
-  EXPECT_TRUE(startBleScan());
+    // Wait for some data and a flush complete.
+    // This ensures we receive both advertisement events
+    // and a flush complete event. We are not guaranteed
+    // that the advertisement events will come after
+    // the CALL_FLUSH event or before. If they come
+    // before, then they will be ignored. This
+    // change allows the advertisement events to come
+    // after during the normal expiration of the
+    // batch timer, which is valid (call flush, get
+    // any advertisement events, flush complete event
+    // might get some advertisement events afterwards).
+    waitForEvent(SAW_BLE_AD_AND_FLUSH_COMPLETE);
+  }
 
-  waitForEvent(SCAN_STARTED, &errorCode);
-  EXPECT_EQ(errorCode, CHRE_ERROR_NONE);
-
-  sendEventToNanoapp(app, STOP_SCAN);
+  // Stop a scan.
+  sendEventToNanoapp(appId, STOP_SCAN);
   waitForEvent(STOP_SCAN, &success);
-  EXPECT_TRUE(success);
+  ASSERT_TRUE(success);
   waitForEvent(SCAN_STOPPED);
+  ASSERT_FALSE(chrePalIsBleEnabled());
+
+  // Flushing after a scan should fail.
+  sendEventToNanoapp(appId, CALL_FLUSH);
+  waitForEvent(CALL_FLUSH, &success);
+  ASSERT_FALSE(success);
 }
 
 }  // namespace
diff --git a/test/simulation/gnss_test.cc b/test/simulation/gnss_test.cc
index 5b2a6dc..f9e388b 100644
--- a/test/simulation/gnss_test.cc
+++ b/test/simulation/gnss_test.cc
@@ -61,73 +61,77 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappStart) *start = []() {
+    bool start() override {
       chreUserSettingConfigureEvents(CHRE_USER_SETTING_LOCATION,
                                      true /*enabled*/);
       return true;
-    };
+    }
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
-          switch (eventType) {
-            case CHRE_EVENT_GNSS_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_GNSS_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_GNSS_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_GNSS_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
+          }
+          break;
+        }
+
+        case CHRE_EVENT_SETTING_CHANGED_LOCATION: {
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_SETTING_CHANGED_LOCATION);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case LOCATION_REQUEST: {
+              auto request = static_cast<const LocationRequest *>(event->data);
+              bool success;
+              mCookie = request->cookie;
+              if (request->enable) {
+                success = chreGnssLocationSessionStartAsync(
+                    1000 /*minIntervalMs*/, 1000 /*minTimeToNextFixMs*/,
+                    &mCookie);
+              } else {
+                success = chreGnssLocationSessionStopAsync(&mCookie);
               }
+              TestEventQueueSingleton::get()->pushEvent(LOCATION_REQUEST,
+                                                        success);
               break;
             }
-
-            case CHRE_EVENT_SETTING_CHANGED_LOCATION: {
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_SETTING_CHANGED_LOCATION);
-              break;
-            }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case LOCATION_REQUEST: {
-                  auto request =
-                      static_cast<const LocationRequest *>(event->data);
-                  bool success;
-                  if (request->enable) {
-                    cookie = request->cookie;
-                    success = chreGnssLocationSessionStartAsync(
-                        1000 /*minIntervalMs*/, 1000 /*minTimeToNextFixMs*/,
-                        &cookie);
-                  } else {
-                    cookie = request->cookie;
-                    success = chreGnssLocationSessionStopAsync(&cookie);
-                  }
-                  TestEventQueueSingleton::get()->pushEvent(LOCATION_REQUEST,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
 
-    decltype(nanoappEnd) *end = []() {
+    void end() override {
       chreUserSettingConfigureEvents(CHRE_USER_SETTING_LOCATION,
                                      false /*enabled*/);
-    };
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
   EXPECT_FALSE(chrePalGnssIsLocationEnabled());
   chrePalGnssDelaySendingLocationEvents(true);
 
   LocationRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, LOCATION_REQUEST, request);
+  sendEventToNanoapp(appId, LOCATION_REQUEST, request);
   waitForEvent(LOCATION_REQUEST, &success);
   EXPECT_TRUE(success);
   chrePalGnssStartSendingLocationEvents();
@@ -155,7 +159,7 @@
                                std::chrono::milliseconds(1000)));
 
   request.enable = false;
-  sendEventToNanoapp(app, LOCATION_REQUEST, request);
+  sendEventToNanoapp(appId, LOCATION_REQUEST, request);
   waitForEvent(LOCATION_REQUEST, &success);
   EXPECT_TRUE(success);
   chrePalGnssStartSendingLocationEvents();
@@ -173,12 +177,14 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static uint32_t cookie;
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_GNSS_ASYNC_RESULT: {
           auto *event = static_cast<const chreAsyncResult *>(eventData);
@@ -196,14 +202,13 @@
             case LOCATION_REQUEST: {
               auto request = static_cast<const LocationRequest *>(event->data);
               bool success;
+              mCookie = request->cookie;
               if (request->enable) {
-                cookie = request->cookie;
                 success = chreGnssLocationSessionStartAsync(
                     1000 /*minIntervalMs*/, 1000 /*minTimeToNextFixMs*/,
-                    &cookie);
+                    &mCookie);
               } else {
-                cookie = request->cookie;
-                success = chreGnssLocationSessionStopAsync(&cookie);
+                success = chreGnssLocationSessionStopAsync(&mCookie);
               }
               TestEventQueueSingleton::get()->pushEvent(LOCATION_REQUEST,
                                                         success);
@@ -212,15 +217,19 @@
           }
         }
       }
-    };
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
   EXPECT_FALSE(chrePalGnssIsLocationEnabled());
 
   LocationRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, LOCATION_REQUEST, request);
+  sendEventToNanoapp(appId, LOCATION_REQUEST, request);
   waitForEvent(LOCATION_REQUEST, &success);
   EXPECT_TRUE(success);
   uint32_t cookie;
@@ -229,7 +238,7 @@
   EXPECT_TRUE(chrePalGnssIsLocationEnabled());
 
   request.enable = false;
-  sendEventToNanoapp(app, LOCATION_REQUEST, request);
+  sendEventToNanoapp(appId, LOCATION_REQUEST, request);
   waitForEvent(LOCATION_REQUEST, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_GNSS_ASYNC_RESULT, &cookie);
@@ -245,12 +254,14 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static uint32_t cookie;
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_GNSS_ASYNC_RESULT: {
           auto *event = static_cast<const chreAsyncResult *>(eventData);
@@ -268,10 +279,10 @@
             case LOCATION_REQUEST: {
               auto request = static_cast<const LocationRequest *>(event->data);
               if (request->enable) {
-                cookie = request->cookie;
+                mCookie = request->cookie;
                 const bool success = chreGnssLocationSessionStartAsync(
                     1000 /*minIntervalMs*/, 1000 /*minTimeToNextFixMs*/,
-                    &cookie);
+                    &mCookie);
                 TestEventQueueSingleton::get()->pushEvent(LOCATION_REQUEST,
                                                           success);
               }
@@ -280,14 +291,18 @@
           }
         }
       }
-    };
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalGnssIsLocationEnabled());
 
   LocationRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, LOCATION_REQUEST, request);
+  sendEventToNanoapp(appId, LOCATION_REQUEST, request);
   bool success;
   waitForEvent(LOCATION_REQUEST, &success);
   EXPECT_TRUE(success);
@@ -296,7 +311,7 @@
   EXPECT_EQ(cookie, request.cookie);
   EXPECT_TRUE(chrePalGnssIsLocationEnabled());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalGnssIsLocationEnabled());
 }
 
@@ -308,54 +323,61 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
-          switch (eventType) {
-            case CHRE_EVENT_GNSS_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_GNSS_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      static uint32_t cookie;
+      switch (eventType) {
+        case CHRE_EVENT_GNSS_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_GNSS_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case MEASUREMENT_REQUEST: {
+              auto request =
+                  static_cast<const MeasurementRequest *>(event->data);
+              bool success;
+              mCookie = request->cookie;
+              if (request->enable) {
+                success = chreGnssMeasurementSessionStartAsync(
+                    1000 /*minIntervalMs*/, &mCookie);
+              } else {
+                cookie = request->cookie;
+                success = chreGnssMeasurementSessionStopAsync(&mCookie);
               }
+              TestEventQueueSingleton::get()->pushEvent(MEASUREMENT_REQUEST,
+                                                        success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case MEASUREMENT_REQUEST: {
-                  auto request =
-                      static_cast<const MeasurementRequest *>(event->data);
-                  bool success;
-                  if (request->enable) {
-                    cookie = request->cookie;
-                    success = chreGnssMeasurementSessionStartAsync(
-                        1000 /*minIntervalMs*/, &cookie);
-                  } else {
-                    cookie = request->cookie;
-                    success = chreGnssMeasurementSessionStopAsync(&cookie);
-                  }
-                  TestEventQueueSingleton::get()->pushEvent(MEASUREMENT_REQUEST,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
   EXPECT_FALSE(chrePalGnssIsLocationEnabled());
 
   MeasurementRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, MEASUREMENT_REQUEST, request);
+  sendEventToNanoapp(appId, MEASUREMENT_REQUEST, request);
   waitForEvent(MEASUREMENT_REQUEST, &success);
   EXPECT_TRUE(success);
   uint32_t cookie;
@@ -364,7 +386,7 @@
   EXPECT_TRUE(chrePalGnssIsMeasurementEnabled());
 
   request.enable = false;
-  sendEventToNanoapp(app, MEASUREMENT_REQUEST, request);
+  sendEventToNanoapp(appId, MEASUREMENT_REQUEST, request);
   waitForEvent(MEASUREMENT_REQUEST, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_GNSS_ASYNC_RESULT, &cookie);
@@ -380,49 +402,55 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
-          switch (eventType) {
-            case CHRE_EVENT_GNSS_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_GNSS_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_GNSS_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_GNSS_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case MEASUREMENT_REQUEST: {
+              auto request =
+                  static_cast<const MeasurementRequest *>(event->data);
+              if (request->enable) {
+                mCookie = request->cookie;
+                const bool success = chreGnssMeasurementSessionStartAsync(
+                    1000 /*minIntervalMs*/, &mCookie);
+                TestEventQueueSingleton::get()->pushEvent(MEASUREMENT_REQUEST,
+                                                          success);
               }
               break;
             }
+          }
+        }
+      }
+    }
 
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case MEASUREMENT_REQUEST: {
-                  auto request =
-                      static_cast<const MeasurementRequest *>(event->data);
-                  if (request->enable) {
-                    cookie = request->cookie;
-                    const bool success = chreGnssMeasurementSessionStartAsync(
-                        1000 /*minIntervalMs*/, &cookie);
-                    TestEventQueueSingleton::get()->pushEvent(
-                        MEASUREMENT_REQUEST, success);
-                  }
-                  break;
-                }
-              }
-            }
-          };
-        };
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalGnssIsLocationEnabled());
 
   MeasurementRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, MEASUREMENT_REQUEST, request);
+  sendEventToNanoapp(appId, MEASUREMENT_REQUEST, request);
   bool success;
   waitForEvent(MEASUREMENT_REQUEST, &success);
   EXPECT_TRUE(success);
@@ -431,46 +459,50 @@
   EXPECT_EQ(cookie, request.cookie);
   EXPECT_TRUE(chrePalGnssIsMeasurementEnabled());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalGnssIsMeasurementEnabled());
 }
 
 TEST_F(TestBase, GnssCanSubscribeAndUnsubscribeToPassiveListener) {
   CREATE_CHRE_TEST_EVENT(LISTENER_REQUEST, 0);
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          switch (eventType) {
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case LISTENER_REQUEST: {
-                  auto enable = *(static_cast<const bool *>(event->data));
-                  const bool success =
-                      chreGnssConfigurePassiveLocationListener(enable);
-                  TestEventQueueSingleton::get()->pushEvent(LISTENER_REQUEST,
-                                                            success);
-                  break;
-                }
-              }
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case LISTENER_REQUEST: {
+              auto enable = *(static_cast<const bool *>(event->data));
+              const bool success =
+                  chreGnssConfigurePassiveLocationListener(enable);
+              TestEventQueueSingleton::get()->pushEvent(LISTENER_REQUEST,
+                                                        success);
+              break;
             }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
   EXPECT_FALSE(chrePalGnssIsPassiveLocationListenerEnabled());
 
-  sendEventToNanoapp(app, LISTENER_REQUEST, true);
+  sendEventToNanoapp(appId, LISTENER_REQUEST, true);
   waitForEvent(LISTENER_REQUEST, &success);
   EXPECT_TRUE(success);
   EXPECT_TRUE(chrePalGnssIsPassiveLocationListenerEnabled());
 
-  sendEventToNanoapp(app, LISTENER_REQUEST, false);
+  sendEventToNanoapp(appId, LISTENER_REQUEST, false);
   waitForEvent(LISTENER_REQUEST, &success);
   EXPECT_TRUE(success);
   EXPECT_FALSE(chrePalGnssIsPassiveLocationListenerEnabled());
@@ -479,38 +511,42 @@
 TEST_F(TestBase, GnssUnsubscribeToPassiveListenerOnUnload) {
   CREATE_CHRE_TEST_EVENT(LISTENER_REQUEST, 0);
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_GNSS;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_GNSS}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          switch (eventType) {
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case LISTENER_REQUEST: {
-                  auto enable = *(static_cast<const bool *>(event->data));
-                  const bool success =
-                      chreGnssConfigurePassiveLocationListener(enable);
-                  TestEventQueueSingleton::get()->pushEvent(LISTENER_REQUEST,
-                                                            success);
-                }
-              }
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case LISTENER_REQUEST: {
+              auto enable = *(static_cast<const bool *>(event->data));
+              const bool success =
+                  chreGnssConfigurePassiveLocationListener(enable);
+              TestEventQueueSingleton::get()->pushEvent(LISTENER_REQUEST,
+                                                        success);
             }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalGnssIsPassiveLocationListenerEnabled());
 
-  sendEventToNanoapp(app, LISTENER_REQUEST, true);
+  sendEventToNanoapp(appId, LISTENER_REQUEST, true);
   bool success;
   waitForEvent(LISTENER_REQUEST, &success);
   EXPECT_TRUE(success);
   EXPECT_TRUE(chrePalGnssIsPassiveLocationListenerEnabled());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalGnssIsPassiveLocationListenerEnabled());
 }
 
diff --git a/test/simulation/host_endpoint_notification_test.cc b/test/simulation/host_endpoint_notification_test.cc
index 67d3f98..75e3a7e 100644
--- a/test/simulation/host_endpoint_notification_test.cc
+++ b/test/simulation/host_endpoint_notification_test.cc
@@ -52,31 +52,31 @@
     uint16_t endpointId;
   };
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          switch (eventType) {
-            case CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION: {
-              auto notification =
-                  *(struct chreHostEndpointNotification *)eventData;
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION, notification);
-            } break;
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION: {
+          auto notification = *(struct chreHostEndpointNotification *)eventData;
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION, notification);
+        } break;
 
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case SETUP_NOTIFICATION: {
-                  auto config = static_cast<const Config *>(event->data);
-                  const bool success = chreConfigureHostEndpointNotifications(
-                      config->endpointId, config->enable);
-                  TestEventQueueSingleton::get()->pushEvent(SETUP_NOTIFICATION,
-                                                            success);
-                }
-              }
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case SETUP_NOTIFICATION: {
+              auto config = static_cast<const Config *>(event->data);
+              const bool success = chreConfigureHostEndpointNotifications(
+                  config->endpointId, config->enable);
+              TestEventQueueSingleton::get()->pushEvent(SETUP_NOTIFICATION,
+                                                        success);
             }
           }
-        };
+        }
+      }
+    }
   };
 
   struct chreHostEndpointInfo info;
@@ -88,10 +88,11 @@
   strcpy(&info.endpointTag[0], "Test tag");
   getHostEndpointManager().postHostEndpointConnected(info);
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   Config config = {.enable = true, .endpointId = kHostEndpointId};
 
-  sendEventToNanoapp(app, SETUP_NOTIFICATION, config);
+  sendEventToNanoapp(appId, SETUP_NOTIFICATION, config);
   bool success;
   waitForEvent(SETUP_NOTIFICATION, &success);
   EXPECT_TRUE(success);
diff --git a/test/simulation/inc/rpc_test.h b/test/simulation/inc/rpc_test.h
index ce71d4e..1fcc7df 100644
--- a/test/simulation/inc/rpc_test.h
+++ b/test/simulation/inc/rpc_test.h
@@ -42,12 +42,21 @@
                        chre_rpc_NumberMessage &response);
 };
 
-struct Env {
+class Env {
+ public:
   RpcTestService mRpcTestService;
   chre::RpcServer mServer;
   chre::RpcClient mClient{kPwRcpServerAppId};
   pw::rpc::NanopbUnaryReceiver<chre_rpc_NumberMessage> mIncrementCall;
   uint32_t mNumber;
+
+  void closeServer() {
+    mServer.close();
+  }
+
+  void closeClient() {
+    mClient.close();
+  }
 };
 
 typedef Singleton<Env> EnvSingleton;
diff --git a/test/simulation/inc/test_base.h b/test/simulation/inc/test_base.h
index 0b02741..95ac29c 100644
--- a/test/simulation/inc/test_base.h
+++ b/test/simulation/inc/test_base.h
@@ -21,6 +21,8 @@
 #include <cstdint>
 #include <thread>
 
+#include "chre/core/event_loop_manager.h"
+#include "chre/core/nanoapp.h"
 #include "chre/platform/system_timer.h"
 #include "chre/util/time.h"
 #include "test_event_queue.h"
@@ -74,6 +76,24 @@
     TestEventQueueSingleton::get()->waitForEvent(eventType, eventData);
   }
 
+  /**
+   * Retrieves the Nanoapp instance from its ID.
+   *
+   * @param id Nanoapp ID
+   * @return A pointer to the Nanoapp instance or nullptr if not found.
+   */
+  Nanoapp *getNanoappByAppId(uint64_t id) {
+    uint16_t instanceId;
+    EXPECT_TRUE(EventLoopManagerSingleton::get()
+                    ->getEventLoop()
+                    .findNanoappInstanceIdByAppId(id, &instanceId));
+    Nanoapp *nanoapp = EventLoopManagerSingleton::get()
+                           ->getEventLoop()
+                           .findNanoappByInstanceId(instanceId);
+    EXPECT_NE(nanoapp, nullptr);
+    return nanoapp;
+  }
+
   std::thread mChreThread;
   SystemTimer mSystemTimer;
 };
diff --git a/test/simulation/inc/test_util.h b/test/simulation/inc/test_util.h
index 0a12e4c..9e79601 100644
--- a/test/simulation/inc/test_util.h
+++ b/test/simulation/inc/test_util.h
@@ -28,7 +28,77 @@
 
 namespace chre {
 
-struct TestNanoapp;
+constexpr uint64_t kDefaultTestNanoappId = 0x0123456789abcdef;
+
+/**
+ * Unregister all nanoapps.
+ *
+ * This is called by the test framework to unregister all nanoapps after each
+ * test. The destructor is called when the nanoapp is unregistered.
+ */
+void unregisterAllTestNanoapps();
+
+/**
+ * Information about a test nanoapp.
+ */
+struct TestNanoappInfo {
+  const char *name = "Test";
+  uint64_t id = kDefaultTestNanoappId;
+  uint32_t version = 0;
+  uint32_t perms = NanoappPermissions::CHRE_PERMS_NONE;
+};
+
+/**
+ * Test nanoapp.
+ *
+ * Tests typically inherit this class and override the entry points to test the
+ * nanoapp behavior.
+ *
+ * The bulk of the code should be in the handleEvent method to respond to
+ * events sent to the nanoapp by the platform and by the sendEventToNanoapp
+ * function. start and end can be use to setup and cleanup the test environment
+ * around each test.
+ *
+ * Note: end is only executed when the nanoapp is explicitly unloaded.
+ */
+class TestNanoapp {
+ public:
+  TestNanoapp() = default;
+  explicit TestNanoapp(TestNanoappInfo info) : mTestNanoappInfo(info) {}
+  virtual ~TestNanoapp() {}
+
+  // NanoappStart Entrypoint.
+  virtual bool start() {
+    return true;
+  }
+
+  // nanoappHandleEvent Entrypoint.
+  virtual void handleEvent(uint32_t /*senderInstanceId*/,
+                           uint16_t /*eventType*/, const void * /*eventData*/) {
+  }
+
+  // nanoappEnd Entrypoint.
+  virtual void end() {}
+
+  const char *name() {
+    return mTestNanoappInfo.name;
+  }
+
+  uint64_t id() {
+    return mTestNanoappInfo.id;
+  }
+
+  uint32_t version() {
+    return mTestNanoappInfo.version;
+  }
+
+  uint32_t perms() {
+    return mTestNanoappInfo.perms;
+  }
+
+ private:
+  const TestNanoappInfo mTestNanoappInfo;
+};
 
 /**
  * @return the statically loaded nanoapp based on the arguments.
@@ -85,30 +155,9 @@
  *
  * This function returns after the nanoapp start has been executed.
  *
- * @return An instance of the TestNanoapp.
+ * @return The id of the nanoapp.
  */
-template <class Nanoapp>
-Nanoapp loadNanoapp() {
-  static_assert(std::is_base_of<TestNanoapp, Nanoapp>::value);
-  Nanoapp app;
-  loadNanoapp(app.name, app.id, app.version, app.perms, app.start,
-              app.handleEvent, app.end);
-
-  return app;
-}
-
-/**
- * Unload a test nanoapp.
- *
- * This function returns after the nanoapp end has been executed.
- *
- * @param app An instance of TestNanoapp.
- */
-template <class Nanoapp>
-void unloadNanoapp(Nanoapp app) {
-  static_assert(std::is_base_of<TestNanoapp, Nanoapp>::value);
-  unloadNanoapp(app.id);
-}
+uint64_t loadNanoapp(UniquePtr<TestNanoapp> app);
 
 /**
  * Unload nanoapp corresponding to appId.
@@ -117,8 +166,7 @@
  *
  * @param appId App Id of nanoapp to be unloaded.
  */
-template <>
-void unloadNanoapp<uint64_t>(uint64_t appId);
+void unloadNanoapp(uint64_t appId);
 
 /**
  * A convenience deferred callback function that can be used to start an already
@@ -141,31 +189,6 @@
                                         void *extraData);
 
 /**
- * Test nanoapp.
- *
- * Tests typically inherit this struct to test the nanoapp behavior.
- * The bulk of the code should be in the handleEvent closure to respond to
- * events sent to the nanoapp by the platform and by the sendEventToNanoapp
- * function. start and end can be use to setup and cleanup the test environment
- * around each test.
- *
- * Note: end is only executed when the nanoapp is explicitly unloaded.
- */
-struct TestNanoapp {
-  const char *name = "Test";
-  uint64_t id = 0x0123456789abcdef;
-  uint32_t version = 0;
-  uint32_t perms = NanoappPermissions::CHRE_PERMS_NONE;
-
-  decltype(nanoappStart) *start = []() { return true; };
-
-  decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t,
-                                                 const void *) {};
-
-  decltype(nanoappEnd) *end = []() {};
-};
-
-/**
  * Deallocate the memory allocated for a TestEvent.
  */
 void freeTestEventDataCallback(uint16_t /*eventType*/, void *eventData);
@@ -176,27 +199,10 @@
  * This function is typically used to execute code in the context of the
  * nanoapp in its handleEvent method.
  *
- * @param app An instance of TestNanoapp.
+ * @param appId ID of the nanoapp.
  * @param eventType The event to send.
  */
-template <class Nanoapp>
-void sendEventToNanoapp(const Nanoapp &app, uint16_t eventType) {
-  static_assert(std::is_base_of<TestNanoapp, Nanoapp>::value);
-  uint16_t instanceId;
-  if (EventLoopManagerSingleton::get()
-          ->getEventLoop()
-          .findNanoappInstanceIdByAppId(app.id, &instanceId)) {
-    auto event = memoryAlloc<TestEvent>();
-    ASSERT_NE(event, nullptr);
-    event->type = eventType;
-    EventLoopManagerSingleton::get()->getEventLoop().postEventOrDie(
-        CHRE_EVENT_TEST_EVENT, static_cast<void *>(event),
-        freeTestEventDataCallback, instanceId);
-
-  } else {
-    LOGE("No instance found for nanoapp id = 0x%016" PRIx64, app.id);
-  }
-}
+void sendEventToNanoapp(uint64_t appId, uint16_t eventType);
 
 /**
  * Sends a message to a nanoapp with data.
@@ -208,19 +214,18 @@
  * populated with the eventType and a pointer to as copy of the evenData as
  * a CHRE_EVENT_TEST_EVENT event.
  *
- * @param app An instance of TestNanoapp.
+ * @param appId ID of the nanoapp.
  * @param eventType The event to send.
  * @param eventData The data to send.
  */
-template <class Nanoapp, class T>
-void sendEventToNanoapp(const Nanoapp &app, uint16_t eventType,
+template <class T>
+void sendEventToNanoapp(uint64_t appId, uint16_t eventType,
                         const T &eventData) {
-  static_assert(std::is_base_of<TestNanoapp, Nanoapp>::value);
   static_assert(std::is_trivial<T>::value);
   uint16_t instanceId;
   if (EventLoopManagerSingleton::get()
           ->getEventLoop()
-          .findNanoappInstanceIdByAppId(app.id, &instanceId)) {
+          .findNanoappInstanceIdByAppId(appId, &instanceId)) {
     auto event = memoryAlloc<TestEvent>();
     ASSERT_NE(event, nullptr);
     event->type = eventType;
@@ -232,7 +237,7 @@
         CHRE_EVENT_TEST_EVENT, static_cast<void *>(event),
         freeTestEventDataCallback, instanceId);
   } else {
-    LOGE("No instance found for nanoapp id = 0x%016" PRIx64, app.id);
+    LOGE("No instance found for nanoapp id = 0x%016" PRIx64, appId);
   }
 }
 
diff --git a/test/simulation/memory_test.cc b/test/simulation/memory_test.cc
index 5acd2e9..a304734 100644
--- a/test/simulation/memory_test.cc
+++ b/test/simulation/memory_test.cc
@@ -33,25 +33,14 @@
 namespace chre {
 namespace {
 
-Nanoapp *getNanoappByAppId(uint64_t id) {
-  uint16_t instanceId;
-  EXPECT_TRUE(EventLoopManagerSingleton::get()
-                  ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(id, &instanceId));
-  Nanoapp *nanoapp =
-      EventLoopManagerSingleton::get()->getEventLoop().findNanoappByInstanceId(
-          instanceId);
-  EXPECT_NE(nanoapp, nullptr);
-  return nanoapp;
-}
-
 TEST_F(TestBase, MemoryAllocateAndFree) {
   CREATE_CHRE_TEST_EVENT(ALLOCATE, 0);
   CREATE_CHRE_TEST_EVENT(FREE, 1);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
@@ -71,21 +60,22 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   MemoryManager &memManager =
       EventLoopManagerSingleton::get()->getMemoryManager();
-  Nanoapp *nanoapp = getNanoappByAppId(app.id);
+  Nanoapp *nanoapp = getNanoappByAppId(appId);
+  ASSERT_NE(nanoapp, nullptr);
 
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
 
   void *ptr1;
-  sendEventToNanoapp(app, ALLOCATE, 100);
+  sendEventToNanoapp(appId, ALLOCATE, 100);
   waitForEvent(ALLOCATE, &ptr1);
   EXPECT_NE(ptr1, nullptr);
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 100);
@@ -93,20 +83,20 @@
   EXPECT_EQ(memManager.getAllocationCount(), 1);
 
   void *ptr2;
-  sendEventToNanoapp(app, ALLOCATE, 200);
+  sendEventToNanoapp(appId, ALLOCATE, 200);
   waitForEvent(ALLOCATE, &ptr2);
   EXPECT_NE(ptr2, nullptr);
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 100 + 200);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 100 + 200);
   EXPECT_EQ(memManager.getAllocationCount(), 2);
 
-  sendEventToNanoapp(app, FREE, ptr1);
+  sendEventToNanoapp(appId, FREE, ptr1);
   waitForEvent(FREE);
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 200);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 200);
   EXPECT_EQ(memManager.getAllocationCount(), 1);
 
-  sendEventToNanoapp(app, FREE, ptr2);
+  sendEventToNanoapp(appId, FREE, ptr2);
   waitForEvent(FREE);
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
@@ -116,9 +106,10 @@
 TEST_F(TestBase, MemoryFreeOnNanoappUnload) {
   CREATE_CHRE_TEST_EVENT(ALLOCATE, 0);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
@@ -132,21 +123,22 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   MemoryManager &memManager =
       EventLoopManagerSingleton::get()->getMemoryManager();
-  Nanoapp *nanoapp = getNanoappByAppId(app.id);
+  Nanoapp *nanoapp = getNanoappByAppId(appId);
+  ASSERT_NE(nanoapp, nullptr);
 
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
 
   void *ptr1;
-  sendEventToNanoapp(app, ALLOCATE, 100);
+  sendEventToNanoapp(appId, ALLOCATE, 100);
   waitForEvent(ALLOCATE, &ptr1);
   EXPECT_NE(ptr1, nullptr);
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 100);
@@ -154,14 +146,14 @@
   EXPECT_EQ(memManager.getAllocationCount(), 1);
 
   void *ptr2;
-  sendEventToNanoapp(app, ALLOCATE, 200);
+  sendEventToNanoapp(appId, ALLOCATE, 200);
   waitForEvent(ALLOCATE, &ptr2);
   EXPECT_NE(ptr2, nullptr);
   EXPECT_EQ(nanoapp->getTotalAllocatedBytes(), 100 + 200);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 100 + 200);
   EXPECT_EQ(memManager.getAllocationCount(), 2);
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
 }
@@ -170,9 +162,10 @@
   CREATE_CHRE_TEST_EVENT(ALLOCATE, 0);
   CREATE_CHRE_TEST_EVENT(FREE, 1);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
@@ -192,13 +185,13 @@
           }
         }
       }
-    };
+    }
   };
 
   MemoryManager &memManager =
       EventLoopManagerSingleton::get()->getMemoryManager();
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
@@ -207,55 +200,55 @@
   void *ptr2;
   void *ptr3;
 
-  sendEventToNanoapp(app, ALLOCATE, 100);
+  sendEventToNanoapp(appId, ALLOCATE, 100);
   waitForEvent(ALLOCATE, &ptr1);
-  sendEventToNanoapp(app, ALLOCATE, 200);
+  sendEventToNanoapp(appId, ALLOCATE, 200);
   waitForEvent(ALLOCATE, &ptr2);
-  sendEventToNanoapp(app, ALLOCATE, 300);
+  sendEventToNanoapp(appId, ALLOCATE, 300);
   waitForEvent(ALLOCATE, &ptr3);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 100 + 200 + 300);
   EXPECT_EQ(memManager.getAllocationCount(), 3);
 
   // Free middle, last, and first blocks.
-  sendEventToNanoapp(app, FREE, ptr2);
+  sendEventToNanoapp(appId, FREE, ptr2);
   waitForEvent(FREE);
-  sendEventToNanoapp(app, FREE, ptr3);
+  sendEventToNanoapp(appId, FREE, ptr3);
   waitForEvent(FREE);
-  sendEventToNanoapp(app, FREE, ptr1);
+  sendEventToNanoapp(appId, FREE, ptr1);
   waitForEvent(FREE);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
 
-  sendEventToNanoapp(app, ALLOCATE, 100);
+  sendEventToNanoapp(appId, ALLOCATE, 100);
   waitForEvent(ALLOCATE, &ptr1);
-  sendEventToNanoapp(app, ALLOCATE, 200);
+  sendEventToNanoapp(appId, ALLOCATE, 200);
   waitForEvent(ALLOCATE, &ptr2);
-  sendEventToNanoapp(app, ALLOCATE, 300);
+  sendEventToNanoapp(appId, ALLOCATE, 300);
   waitForEvent(ALLOCATE, &ptr3);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 100 + 200 + 300);
   EXPECT_EQ(memManager.getAllocationCount(), 3);
 
   // Free last, last and last blocks.
-  sendEventToNanoapp(app, FREE, ptr3);
+  sendEventToNanoapp(appId, FREE, ptr3);
   waitForEvent(FREE);
-  sendEventToNanoapp(app, FREE, ptr2);
+  sendEventToNanoapp(appId, FREE, ptr2);
   waitForEvent(FREE);
-  sendEventToNanoapp(app, FREE, ptr1);
+  sendEventToNanoapp(appId, FREE, ptr1);
   waitForEvent(FREE);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
 
-  sendEventToNanoapp(app, ALLOCATE, 100);
+  sendEventToNanoapp(appId, ALLOCATE, 100);
   waitForEvent(ALLOCATE, &ptr1);
-  sendEventToNanoapp(app, ALLOCATE, 200);
+  sendEventToNanoapp(appId, ALLOCATE, 200);
   waitForEvent(ALLOCATE, &ptr2);
-  sendEventToNanoapp(app, ALLOCATE, 300);
+  sendEventToNanoapp(appId, ALLOCATE, 300);
   waitForEvent(ALLOCATE, &ptr3);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 100 + 200 + 300);
   EXPECT_EQ(memManager.getAllocationCount(), 3);
 
   // Automatic cleanup.
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_EQ(memManager.getTotalAllocatedBytes(), 0);
   EXPECT_EQ(memManager.getAllocationCount(), 0);
 }
diff --git a/test/simulation/rpc_test.cc b/test/simulation/rpc_test.cc
index ffcd564..a023f64 100644
--- a/test/simulation/rpc_test.cc
+++ b/test/simulation/rpc_test.cc
@@ -46,8 +46,9 @@
 namespace {
 
 TEST_F(TestBase, PwRpcCanPublishServicesInNanoappStart) {
-  struct App : public TestNanoapp {
-    decltype(nanoappStart) *start = []() -> bool {
+  class App : public TestNanoapp {
+   public:
+    bool start() override {
       struct chreNanoappRpcService servicesA[] = {
           {.id = 1, .version = 0},
           {.id = 2, .version = 0},
@@ -60,21 +61,12 @@
 
       return chrePublishRpcServices(servicesA, 2 /* numServices */) &&
              chrePublishRpcServices(servicesB, 2 /* numServices */);
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
-
-  uint16_t instanceId;
-  EXPECT_TRUE(EventLoopManagerSingleton::get()
-                  ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app.id, &instanceId));
-
-  Nanoapp *napp =
-      EventLoopManagerSingleton::get()->getEventLoop().findNanoappByInstanceId(
-          instanceId);
-
-  ASSERT_FALSE(napp == nullptr);
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+  Nanoapp *napp = getNanoappByAppId(appId);
+  ASSERT_NE(napp, nullptr);
 
   EXPECT_EQ(napp->getRpcServices().size(), 4);
   EXPECT_EQ(napp->getRpcServices()[0].id, 1);
@@ -84,8 +76,8 @@
 }
 
 TEST_F(TestBase, PwRpcCanNotPublishDuplicateServices) {
-  struct App : public TestNanoapp {
-    decltype(nanoappStart) *start = []() -> bool {
+  class App : public TestNanoapp {
+    bool start() override {
       struct chreNanoappRpcService servicesA[] = {
           {.id = 1, .version = 0},
           {.id = 2, .version = 0},
@@ -103,21 +95,12 @@
       EXPECT_FALSE(chrePublishRpcServices(servicesB, 2 /* numServices */));
 
       return success;
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
-
-  uint16_t instanceId;
-  EXPECT_TRUE(EventLoopManagerSingleton::get()
-                  ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app.id, &instanceId));
-
-  Nanoapp *napp =
-      EventLoopManagerSingleton::get()->getEventLoop().findNanoappByInstanceId(
-          instanceId);
-
-  ASSERT_FALSE(napp == nullptr);
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+  Nanoapp *napp = getNanoappByAppId(appId);
+  ASSERT_NE(napp, nullptr);
 
   EXPECT_EQ(napp->getRpcServices().size(), 2);
   EXPECT_EQ(napp->getRpcServices()[0].id, 1);
@@ -125,51 +108,31 @@
 }
 
 TEST_F(TestBase, PwRpcDifferentAppCanPublishSameServices) {
-  struct App1 : public TestNanoapp {
-    uint64_t id = 0x01;
+  class App : public TestNanoapp {
+   public:
+    explicit App(uint64_t id) : TestNanoapp(TestNanoappInfo{.id = id}) {}
 
-    decltype(nanoappStart) *start = []() -> bool {
+    bool start() override {
       struct chreNanoappRpcService services[] = {
           {.id = 1, .version = 0},
           {.id = 2, .version = 0},
       };
 
       return chrePublishRpcServices(services, 2 /* numServices */);
-    };
+    }
   };
 
-  struct App2 : public App1 {
-    uint64_t id = 0x02;
-  };
-
-  auto app1 = loadNanoapp<App1>();
-  auto app2 = loadNanoapp<App2>();
-
-  uint16_t instanceId1;
-  EXPECT_TRUE(EventLoopManagerSingleton::get()
-                  ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app1.id, &instanceId1));
-
-  Nanoapp *napp1 =
-      EventLoopManagerSingleton::get()->getEventLoop().findNanoappByInstanceId(
-          instanceId1);
-
-  ASSERT_FALSE(napp1 == nullptr);
+  uint64_t app1Id = loadNanoapp(MakeUnique<App>(0x01));
+  uint64_t app2Id = loadNanoapp(MakeUnique<App>(0x02));
+  Nanoapp *napp1 = getNanoappByAppId(app1Id);
+  ASSERT_NE(napp1, nullptr);
 
   EXPECT_EQ(napp1->getRpcServices().size(), 2);
   EXPECT_EQ(napp1->getRpcServices()[0].id, 1);
   EXPECT_EQ(napp1->getRpcServices()[1].id, 2);
 
-  uint16_t instanceId2;
-  EXPECT_TRUE(EventLoopManagerSingleton::get()
-                  ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app2.id, &instanceId2));
-
-  Nanoapp *napp2 =
-      EventLoopManagerSingleton::get()->getEventLoop().findNanoappByInstanceId(
-          instanceId2);
-
-  ASSERT_FALSE(napp2 == nullptr);
+  Nanoapp *napp2 = getNanoappByAppId(app2Id);
+  ASSERT_NE(napp2, nullptr);
 
   EXPECT_EQ(napp2->getRpcServices().size(), 2);
   EXPECT_EQ(napp2->getRpcServices()[0].id, 1);
@@ -179,57 +142,51 @@
 TEST_F(TestBase, PwRpcCanNotPublishServicesOutsideOfNanoappStart) {
   CREATE_CHRE_TEST_EVENT(PUBLISH_SERVICES, 0);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          switch (eventType) {
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case PUBLISH_SERVICES: {
-                  struct chreNanoappRpcService services[] = {
-                      {.id = 1, .version = 0},
-                      {.id = 2, .version = 0},
-                  };
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case PUBLISH_SERVICES: {
+              struct chreNanoappRpcService services[] = {
+                  {.id = 1, .version = 0},
+                  {.id = 2, .version = 0},
+              };
 
-                  bool success =
-                      chrePublishRpcServices(services, 2 /* numServices */);
-                  TestEventQueueSingleton::get()->pushEvent(PUBLISH_SERVICES,
-                                                            success);
-                  break;
-                }
-              }
+              bool success =
+                  chrePublishRpcServices(services, 2 /* numServices */);
+              TestEventQueueSingleton::get()->pushEvent(PUBLISH_SERVICES,
+                                                        success);
+              break;
             }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   bool success = true;
-  sendEventToNanoapp(app, PUBLISH_SERVICES);
+  sendEventToNanoapp(appId, PUBLISH_SERVICES);
   waitForEvent(PUBLISH_SERVICES, &success);
   EXPECT_FALSE(success);
 
-  uint16_t instanceId;
-  EXPECT_TRUE(EventLoopManagerSingleton::get()
-                  ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app.id, &instanceId));
-
-  Nanoapp *napp =
-      EventLoopManagerSingleton::get()->getEventLoop().findNanoappByInstanceId(
-          instanceId);
-
-  ASSERT_FALSE(napp == nullptr);
+  Nanoapp *napp = getNanoappByAppId(appId);
+  ASSERT_NE(napp, nullptr);
 
   EXPECT_EQ(napp->getRpcServices().size(), 0);
 }
 
 TEST_F(TestBase, PwRpcRegisterServicesShouldGracefullyFailOnDuplicatedService) {
-  struct App : public TestNanoapp {
-    decltype(nanoappStart) *start = []() -> bool {
+  class App : public TestNanoapp {
+   public:
+    bool start() override {
       static RpcTestService testService;
-      EnvSingleton::init();
+
       chre::RpcServer::Service service = {.service = testService,
                                           .id = 0xca8f7150a3f05847,
                                           .version = 0x01020034};
@@ -243,49 +200,59 @@
       EXPECT_FALSE(server.registerServices(1, &service));
 
       return status;
-    };
+    }
+
+    void end() override {
+      EnvSingleton::get()->closeServer();
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  EnvSingleton::init();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+  unloadNanoapp(appId);
+  EnvSingleton::deinit();
 }
 
 TEST_F(TestBase, PwRpcGetNanoappInfoByAppIdReturnsServices) {
   CREATE_CHRE_TEST_EVENT(QUERY_INFO, 0);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappStart) *start = []() -> bool {
+  class App : public TestNanoapp {
+   public:
+    bool start() override {
       struct chreNanoappRpcService services[] = {
           {.id = 1, .version = 2},
           {.id = 2, .version = 3},
       };
 
       return chrePublishRpcServices(services, 2 /* numServices */);
-    };
+    }
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static struct chreNanoappInfo info;
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
           switch (event->type) {
             case QUERY_INFO: {
               auto id = static_cast<uint64_t *>(event->data);
-              bool success = chreGetNanoappInfoByAppId(*id, &info);
-              const struct chreNanoappInfo *pInfo = success ? &info : nullptr;
+              bool success = chreGetNanoappInfoByAppId(*id, &mInfo);
+              const struct chreNanoappInfo *pInfo = success ? &mInfo : nullptr;
               TestEventQueueSingleton::get()->pushEvent(QUERY_INFO, pInfo);
               break;
             }
           }
         }
       }
-    };
+    }
+
+   protected:
+    struct chreNanoappInfo mInfo;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   struct chreNanoappInfo *pInfo = nullptr;
-  sendEventToNanoapp(app, QUERY_INFO, app.id);
+  sendEventToNanoapp(appId, QUERY_INFO, appId);
   waitForEvent(QUERY_INFO, &pInfo);
   EXPECT_TRUE(pInfo != nullptr);
   EXPECT_EQ(pInfo->rpcServiceCount, 2);
@@ -301,12 +268,12 @@
 TEST_F(TestBase, PwRpcClientNanoappCanRequestServerNanoapp) {
   CREATE_CHRE_TEST_EVENT(INCREMENT_REQUEST, 0);
 
-  struct ClientApp : public TestNanoapp {
-    uint64_t id = kPwRcpClientAppId;
+  class ClientApp : public TestNanoapp {
+   public:
+    ClientApp() : TestNanoapp(TestNanoappInfo{.id = kPwRcpClientAppId}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t senderInstanceId,
-                                                   uint16_t eventType,
-                                                   const void *eventData) {
+    void handleEvent(uint32_t senderInstanceId, uint16_t eventType,
+                     const void *eventData) override {
       Env *env = EnvSingleton::get();
 
       env->mClient.handleEvent(senderInstanceId, eventType, eventData);
@@ -341,37 +308,49 @@
           }
         }
       }
-    };
+    }
+
+    void end() {
+      EnvSingleton::get()->closeClient();
+    }
   };
 
-  struct ServerApp : public TestNanoapp {
-    uint64_t id = kPwRcpServerAppId;
-    decltype(nanoappStart) *start = []() {
-      EnvSingleton::init();
+  class ServerApp : public TestNanoapp {
+   public:
+    ServerApp() : TestNanoapp(TestNanoappInfo{.id = kPwRcpServerAppId}) {}
+
+    bool start() override {
       chre::RpcServer::Service service = {
           .service = EnvSingleton::get()->mRpcTestService,
           .id = 0xca8f7150a3f05847,
           .version = 0x01020034};
       return EnvSingleton::get()->mServer.registerServices(1, &service);
-    };
+    }
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t senderInstanceId,
-                                                   uint16_t eventType,
-                                                   const void *eventData) {
+    void handleEvent(uint32_t senderInstanceId, uint16_t eventType,
+                     const void *eventData) override {
       EnvSingleton::get()->mServer.handleEvent(senderInstanceId, eventType,
                                                eventData);
-    };
+    }
+
+    void end() {
+      EnvSingleton::get()->closeServer();
+    }
   };
 
-  auto server = loadNanoapp<ServerApp>();
-  auto client = loadNanoapp<ClientApp>();
+  EnvSingleton::init();
+  uint64_t serverId = loadNanoapp(MakeUnique<ServerApp>());
+  uint64_t clientId = loadNanoapp(MakeUnique<ClientApp>());
   bool status;
   constexpr uint32_t kNumber = 101;
 
-  sendEventToNanoapp(client, INCREMENT_REQUEST, kNumber);
+  sendEventToNanoapp(clientId, INCREMENT_REQUEST, kNumber);
   waitForEvent(INCREMENT_REQUEST, &status);
   EXPECT_TRUE(status);
   EXPECT_EQ(EnvSingleton::get()->mNumber, kNumber + 1);
+  unloadNanoapp(serverId);
+  unloadNanoapp(clientId);
+  EnvSingleton::deinit();
 }
 
 TEST_F(TestBase, PwRpcRpcClientHasServiceCheckForAMatchingService) {
@@ -383,16 +362,15 @@
     uint64_t appId;
   };
 
-  struct App : public TestNanoapp {
-    decltype(nanoappStart) *start = []() -> bool {
+  class App : public TestNanoapp {
+   public:
+    bool start() override {
       struct chreNanoappRpcService services[] = {{.id = 1, .version = 2}};
 
       return chrePublishRpcServices(services, 1 /* numServices */);
-    };
+    }
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static struct chreNanoappInfo info;
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) {
       switch (eventType) {
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
@@ -411,20 +389,20 @@
           break;
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   ServiceInfo service;
   bool hasService = false;
 
-  service = {.id = 1, .version = 2, .appId = app.id};
-  sendEventToNanoapp(app, QUERY_HAS_SERVICE, service);
+  service = {.id = 1, .version = 2, .appId = appId};
+  sendEventToNanoapp(appId, QUERY_HAS_SERVICE, service);
   waitForEvent(QUERY_HAS_SERVICE, &hasService);
   EXPECT_TRUE(hasService);
-  service = {.id = 10, .version = 2, .appId = app.id};
-  sendEventToNanoapp(app, QUERY_HAS_SERVICE, service);
+  service = {.id = 10, .version = 2, .appId = appId};
+  sendEventToNanoapp(appId, QUERY_HAS_SERVICE, service);
   waitForEvent(QUERY_HAS_SERVICE, &hasService);
   EXPECT_FALSE(hasService);
 }
diff --git a/test/simulation/sensor_test.cc b/test/simulation/sensor_test.cc
index f75a2c1..91d9ad1 100644
--- a/test/simulation/sensor_test.cc
+++ b/test/simulation/sensor_test.cc
@@ -44,36 +44,38 @@
     enum chreSensorConfigureMode mode;
   };
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          switch (eventType) {
-            case CHRE_EVENT_SENSOR_SAMPLING_CHANGE: {
-              auto *event =
-                  static_cast<const struct chreSensorSamplingStatusEvent *>(
-                      eventData);
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_SENSOR_SAMPLING_CHANGE, *event);
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_SENSOR_SAMPLING_CHANGE: {
+          auto *event =
+              static_cast<const struct chreSensorSamplingStatusEvent *>(
+                  eventData);
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_SENSOR_SAMPLING_CHANGE, *event);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case CONFIGURE: {
+              auto config = static_cast<const Configuration *>(event->data);
+              const bool success = chreSensorConfigure(
+                  config->sensorHandle, config->mode, config->interval, 0);
+              TestEventQueueSingleton::get()->pushEvent(CONFIGURE, success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case CONFIGURE: {
-                  auto config = static_cast<const Configuration *>(event->data);
-                  const bool success = chreSensorConfigure(
-                      config->sensorHandle, config->mode, config->interval, 0);
-                  TestEventQueueSingleton::get()->pushEvent(CONFIGURE, success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
 
   EXPECT_FALSE(chrePalSensorIsSensor0Enabled());
@@ -81,7 +83,7 @@
   Configuration config{.sensorHandle = 0,
                        .interval = 100,
                        .mode = CHRE_SENSOR_CONFIGURE_MODE_CONTINUOUS};
-  sendEventToNanoapp(app, CONFIGURE, config);
+  sendEventToNanoapp(appId, CONFIGURE, config);
   waitForEvent(CONFIGURE, &success);
   EXPECT_TRUE(success);
   struct chreSensorSamplingStatusEvent event;
@@ -94,7 +96,7 @@
   config = {.sensorHandle = 0,
             .interval = 50,
             .mode = CHRE_SENSOR_CONFIGURE_MODE_DONE};
-  sendEventToNanoapp(app, CONFIGURE, config);
+  sendEventToNanoapp(appId, CONFIGURE, config);
   waitForEvent(CONFIGURE, &success);
   EXPECT_TRUE(success);
   EXPECT_FALSE(chrePalSensorIsSensor0Enabled());
@@ -109,42 +111,44 @@
     enum chreSensorConfigureMode mode;
   };
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          switch (eventType) {
-            case CHRE_EVENT_SENSOR_SAMPLING_CHANGE: {
-              auto *event =
-                  static_cast<const struct chreSensorSamplingStatusEvent *>(
-                      eventData);
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_SENSOR_SAMPLING_CHANGE, *event);
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_SENSOR_SAMPLING_CHANGE: {
+          auto *event =
+              static_cast<const struct chreSensorSamplingStatusEvent *>(
+                  eventData);
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_SENSOR_SAMPLING_CHANGE, *event);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case CONFIGURE: {
+              auto config = static_cast<const Configuration *>(event->data);
+              const bool success = chreSensorConfigure(
+                  config->sensorHandle, config->mode, config->interval, 0);
+              TestEventQueueSingleton::get()->pushEvent(CONFIGURE, success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case CONFIGURE: {
-                  auto config = static_cast<const Configuration *>(event->data);
-                  const bool success = chreSensorConfigure(
-                      config->sensorHandle, config->mode, config->interval, 0);
-                  TestEventQueueSingleton::get()->pushEvent(CONFIGURE, success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalSensorIsSensor0Enabled());
 
   Configuration config{.sensorHandle = 0,
                        .interval = 100,
                        .mode = CHRE_SENSOR_CONFIGURE_MODE_CONTINUOUS};
-  sendEventToNanoapp(app, CONFIGURE, config);
+  sendEventToNanoapp(appId, CONFIGURE, config);
   bool success;
   waitForEvent(CONFIGURE, &success);
   EXPECT_TRUE(success);
@@ -155,7 +159,7 @@
   EXPECT_TRUE(event.status.enabled);
   EXPECT_TRUE(chrePalSensorIsSensor0Enabled());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalSensorIsSensor0Enabled());
 }
 
diff --git a/test/simulation/settings_test.cc b/test/simulation/settings_test.cc
index c11731e..8dd5314 100644
--- a/test/simulation/settings_test.cc
+++ b/test/simulation/settings_test.cc
@@ -35,8 +35,8 @@
 
 namespace {
 
-int8_t gExpectedLocationSettingState = CHRE_USER_SETTING_STATE_DISABLED;
-int8_t gExpectedWifiSettingState = CHRE_USER_SETTING_STATE_DISABLED;
+int8_t gExpectedLocationSettingState;
+int8_t gExpectedWifiSettingState;
 
 bool start() {
   bool success = chreGnssLocationSessionStartAsync(50, 50, nullptr);
@@ -120,6 +120,9 @@
   constexpr uint32_t kAppPerms =
       NanoappPermissions::CHRE_PERMS_GNSS | NanoappPermissions::CHRE_PERMS_WIFI;
 
+  gExpectedLocationSettingState = CHRE_USER_SETTING_STATE_DISABLED;
+  gExpectedWifiSettingState = CHRE_USER_SETTING_STATE_DISABLED;
+
   UniquePtr<Nanoapp> nanoapp = createStaticNanoapp(
       "Test nanoapp", kAppId, kAppVersion, kAppPerms, start, handleEvent, end);
   EventLoopManagerSingleton::get()->deferCallback(
diff --git a/test/simulation/test_base.cc b/test/simulation/test_base.cc
index 622b84c..bdec520 100644
--- a/test/simulation/test_base.cc
+++ b/test/simulation/test_base.cc
@@ -76,6 +76,7 @@
   TestEventQueueSingleton::deinit();
   TaskManagerSingleton::deinit();
   deleteNanoappInfos();
+  unregisterAllTestNanoapps();
 }
 
 TEST_F(TestBase, CanLoadAndStartSingleNanoapp) {
@@ -118,6 +119,36 @@
   EXPECT_NE(id1, id2);
 }
 
+TEST_F(TestBase, methods) {
+  CREATE_CHRE_TEST_EVENT(SOME_EVENT, 0);
+
+  class App : public TestNanoapp {
+   public:
+    explicit App(TestNanoappInfo info) : TestNanoapp(info) {}
+    bool start() override {
+      LOGE("start");
+      mTest = 0xc0ffee;
+      return true;
+    }
+
+    void handleEvent(uint32_t /*senderInstanceId*/, uint16_t /*eventType*/,
+                     const void * /**eventData*/) override {
+      LOGE("handleEvent %" PRIx16, mTest);
+    }
+
+    void end() override {
+      LOGE("end");
+    }
+
+   protected:
+    uint32_t mTest = 0;
+  };
+
+  uint64_t appId = loadNanoapp(MakeUnique<App>(TestNanoappInfo{.id = 0x456}));
+
+  sendEventToNanoapp(appId, SOME_EVENT);
+}
+
 // Explicitly instantiate the TestEventQueueSingleton to reduce codesize.
 template class Singleton<TestEventQueue>;
 
diff --git a/test/simulation/test_util.cc b/test/simulation/test_util.cc
index acc17fb..44398ab 100644
--- a/test/simulation/test_util.cc
+++ b/test/simulation/test_util.cc
@@ -17,6 +17,7 @@
 #include "test_util.h"
 
 #include <gtest/gtest.h>
+#include <unordered_map>
 
 #include "chre/core/event_loop_manager.h"
 #include "chre/core/nanoapp.h"
@@ -29,13 +30,102 @@
 namespace chre {
 
 namespace {
-
-// Keep the chreNslNanoappInfo instances alive for the lifetime of the
-// test nanoapps.
+/**
+ * List of chreNslNanoappInfo.
+ *
+ * Keep the chreNslNanoappInfo instances alive for the lifetime of the test
+ * nanoapps.
+ */
 DynamicVector<UniquePtr<chreNslNanoappInfo>> gNanoappInfos;
 
+/** Registry of nanoapp by ID. */
+std::unordered_map<uint64_t, UniquePtr<TestNanoapp>> nanoapps;
+
+/**
+ * @return a pointer to a registered nanoapp or nullptr if the appId is not
+ *         registered.
+ */
+TestNanoapp *queryNanoapp(uint64_t appId) {
+  return nanoapps.count(appId) == 0 ? nullptr : nanoapps[appId].get();
+}
+
+/**
+ * Nanoapp start.
+ *
+ * Invokes the start method of a registered TestNanoapp.
+ */
+bool start() {
+  uint64_t id = chreGetAppId();
+  TestNanoapp *app = queryNanoapp(id);
+  if (app == nullptr) {
+    LOGE("[start] unregistered nanoapp 0x%016" PRIx64, id);
+    return false;
+  }
+  return app->start();
+}
+
+/**
+ * Nanoapp handleEvent.
+ *
+ * Invokes the handleMethod method of a registered TestNanoapp.
+ */
+void handleEvent(uint32_t senderInstanceId, uint16_t eventType,
+                 const void *eventData) {
+  uint64_t id = chreGetAppId();
+  TestNanoapp *app = queryNanoapp(id);
+  if (app == nullptr) {
+    LOGE("[handleEvent] unregistered nanoapp 0x%016" PRIx64, id);
+  } else {
+    app->handleEvent(senderInstanceId, eventType, eventData);
+  }
+}
+
+/**
+ * Nanoapp end.
+ *
+ * Invokes the end method of a registered TestNanoapp.
+ */
+void end() {
+  uint64_t id = chreGetAppId();
+  TestNanoapp *app = queryNanoapp(id);
+  if (app == nullptr) {
+    LOGE("[end] unregistered nanoapp 0x%016" PRIx64, id);
+  } else {
+    app->end();
+  }
+}
+
+/**
+ * Registers a test nanoapp.
+ *
+ * TestNanoapps are registered when they are loaded so that their entry point
+ * methods can be called.
+ */
+void registerNanoapp(UniquePtr<TestNanoapp> app) {
+  if (nanoapps.count(app->id()) != 0) {
+    LOGE("A nanoapp with the same id is already registered");
+  } else {
+    nanoapps[app->id()] = std::move(app);
+  }
+}
+
+/**
+ * Unregisters a nanoapp.
+ *
+ * Calls the TestNanoapp destructor.
+ */
+void unregisterNanoapp(uint64_t appId) {
+  if (nanoapps.erase(appId) == 0) {
+    LOGE("The nanoapp is not registered");
+  }
+}
+
 }  // namespace
 
+void unregisterAllTestNanoapps() {
+  nanoapps.clear();
+}
+
 UniquePtr<Nanoapp> createStaticNanoapp(
     const char *name, uint64_t appId, uint32_t appVersion, uint32_t appPerms,
     decltype(nanoappStart) *startFunc,
@@ -83,14 +173,11 @@
   return true;
 }
 
-void defaultNanoappHandleEvent(uint32_t senderInstanceId, uint16_t eventType,
-                               const void *eventData) {
-  UNUSED_VAR(senderInstanceId);
-  UNUSED_VAR(eventType);
-  UNUSED_VAR(eventData);
-}
+void defaultNanoappHandleEvent(uint32_t /*senderInstanceId*/,
+                               uint16_t /*eventType*/,
+                               const void * /*eventData*/) {}
 
-void defaultNanoappEnd(){};
+void defaultNanoappEnd() {}
 
 void loadNanoapp(const char *name, uint64_t appId, uint32_t appVersion,
                  uint32_t appPerms, decltype(nanoappStart) *startFunc,
@@ -107,8 +194,33 @@
       CHRE_EVENT_SIMULATION_TEST_NANOAPP_LOADED);
 }
 
-template <>
-void unloadNanoapp<uint64_t>(uint64_t appId) {
+uint64_t loadNanoapp(UniquePtr<TestNanoapp> app) {
+  TestNanoapp *pApp = app.get();
+  registerNanoapp(std::move(app));
+  loadNanoapp(pApp->name(), pApp->id(), pApp->version(), pApp->perms(), &start,
+              &handleEvent, &end);
+
+  return pApp->id();
+}
+
+void sendEventToNanoapp(uint64_t appId, uint16_t eventType) {
+  uint16_t instanceId;
+  if (EventLoopManagerSingleton::get()
+          ->getEventLoop()
+          .findNanoappInstanceIdByAppId(appId, &instanceId)) {
+    auto event = memoryAlloc<TestEvent>();
+    ASSERT_NE(event, nullptr);
+    event->type = eventType;
+    EventLoopManagerSingleton::get()->getEventLoop().postEventOrDie(
+        CHRE_EVENT_TEST_EVENT, static_cast<void *>(event),
+        freeTestEventDataCallback, instanceId);
+
+  } else {
+    LOGE("No instance found for nanoapp id = 0x%016" PRIx64, appId);
+  }
+}
+
+void unloadNanoapp(uint64_t appId) {
   uint64_t *ptr = memoryAlloc<uint64_t>();
   ASSERT_NE(ptr, nullptr);
   *ptr = appId;
@@ -118,6 +230,8 @@
 
   TestEventQueueSingleton::get()->waitForEvent(
       CHRE_EVENT_SIMULATION_TEST_NANOAPP_UNLOADED);
+
+  unregisterNanoapp(appId);
 }
 
 void testFinishLoadingNanoappCallback(SystemCallbackType /* type */,
diff --git a/test/simulation/timer_test.cc b/test/simulation/timer_test.cc
index 1e81da0..ff76ddb 100644
--- a/test/simulation/timer_test.cc
+++ b/test/simulation/timer_test.cc
@@ -46,18 +46,16 @@
   CREATE_CHRE_TEST_EVENT(START_TIMER, 0);
   CREATE_CHRE_TEST_EVENT(STOP_TIMER, 1);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static const uint32_t cookie = 123;
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
-        static int count = 0;
-
         case CHRE_EVENT_TIMER: {
           auto data = static_cast<const uint32_t *>(eventData);
-          if (*data == cookie) {
-            count++;
-            if (count == 3) {
+          if (*data == mCookie) {
+            mCount++;
+            if (mCount == 3) {
               TestEventQueueSingleton::get()->pushEvent(CHRE_EVENT_TIMER);
             }
           }
@@ -68,8 +66,8 @@
           auto event = static_cast<const TestEvent *>(eventData);
           switch (event->type) {
             case START_TIMER: {
-              uint32_t handle = chreTimerSet(kOneMillisecondInNanoseconds,
-                                             &cookie, false /*oneShot*/);
+              uint32_t handle = chreTimerSet(10 * kOneMillisecondInNanoseconds,
+                                             &mCookie, false /*oneShot*/);
               TestEventQueueSingleton::get()->pushEvent(START_TIMER, handle);
               break;
             }
@@ -82,10 +80,14 @@
           }
         }
       }
-    };
+    }
+
+   protected:
+    const uint32_t mCookie = 123;
+    int mCount = 0;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   TimerPool &timerPool =
       EventLoopManagerSingleton::get()->getEventLoop().getTimerPool();
@@ -93,10 +95,10 @@
   uint16_t instanceId;
   EXPECT_TRUE(EventLoopManagerSingleton::get()
                   ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app.id, &instanceId));
+                  .findNanoappInstanceIdByAppId(appId, &instanceId));
 
   uint32_t handle;
-  sendEventToNanoapp(app, START_TIMER);
+  sendEventToNanoapp(appId, START_TIMER);
   waitForEvent(START_TIMER, &handle);
   EXPECT_NE(handle, CHRE_TIMER_INVALID);
   EXPECT_TRUE(hasNanoappTimers(timerPool, instanceId));
@@ -106,13 +108,13 @@
   bool success;
 
   // Cancelling an active timer should be successful.
-  sendEventToNanoapp(app, STOP_TIMER, handle);
+  sendEventToNanoapp(appId, STOP_TIMER, handle);
   waitForEvent(STOP_TIMER, &success);
   EXPECT_TRUE(success);
   EXPECT_FALSE(hasNanoappTimers(timerPool, instanceId));
 
   // Cancelling an inactive time should return false.
-  sendEventToNanoapp(app, STOP_TIMER, handle);
+  sendEventToNanoapp(appId, STOP_TIMER, handle);
   waitForEvent(STOP_TIMER, &success);
   EXPECT_FALSE(success);
 }
@@ -120,18 +122,15 @@
 TEST_F(TestTimer, CancelPeriodicTimerOnUnload) {
   CREATE_CHRE_TEST_EVENT(START_TIMER, 0);
 
-  struct App : public TestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static const uint32_t cookie = 123;
+  class App : public TestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType, const void *eventData) {
       switch (eventType) {
-        static int count = 0;
-
         case CHRE_EVENT_TIMER: {
           auto data = static_cast<const uint32_t *>(eventData);
-          if (*data == cookie) {
-            count++;
-            if (count == 3) {
+          if (*data == mCookie) {
+            mCount++;
+            if (mCount == 3) {
               TestEventQueueSingleton::get()->pushEvent(CHRE_EVENT_TIMER);
             }
           }
@@ -142,18 +141,22 @@
           auto event = static_cast<const TestEvent *>(eventData);
           switch (event->type) {
             case START_TIMER: {
-              uint32_t handle = chreTimerSet(kOneMillisecondInNanoseconds,
-                                             &cookie, false /*oneShot*/);
+              uint32_t handle = chreTimerSet(10 * kOneMillisecondInNanoseconds,
+                                             &mCookie, false /*oneShot*/);
               TestEventQueueSingleton::get()->pushEvent(START_TIMER, handle);
               break;
             }
           }
         }
       }
-    };
+    }
+
+   protected:
+    const uint32_t mCookie = 123;
+    int mCount = 0;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   TimerPool &timerPool =
       EventLoopManagerSingleton::get()->getEventLoop().getTimerPool();
@@ -161,17 +164,17 @@
   uint16_t instanceId;
   EXPECT_TRUE(EventLoopManagerSingleton::get()
                   ->getEventLoop()
-                  .findNanoappInstanceIdByAppId(app.id, &instanceId));
+                  .findNanoappInstanceIdByAppId(appId, &instanceId));
 
   uint32_t handle;
-  sendEventToNanoapp(app, START_TIMER);
+  sendEventToNanoapp(appId, START_TIMER);
   waitForEvent(START_TIMER, &handle);
   EXPECT_NE(handle, CHRE_TIMER_INVALID);
   EXPECT_TRUE(hasNanoappTimers(timerPool, instanceId));
 
   waitForEvent(CHRE_EVENT_TIMER);
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(hasNanoappTimers(timerPool, instanceId));
 }
 
diff --git a/test/simulation/wifi_nan_test.cc b/test/simulation/wifi_nan_test.cc
index 560e97a..bab012e 100644
--- a/test/simulation/wifi_nan_test.cc
+++ b/test/simulation/wifi_nan_test.cc
@@ -53,15 +53,18 @@
  * - Grant WiFi permissions,
  * - Initialize the WiFi state in start.
  */
-struct NanTestNanoapp : public TestNanoapp {
-  uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+class NanTestNanoapp : public TestNanoapp {
+ public:
+  NanTestNanoapp()
+      : TestNanoapp(
+            TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-  decltype(nanoappStart) *start = []() {
+  bool start() override {
     EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
         Setting::WIFI_AVAILABLE, true /* enabled */);
     PalNanEngineSingleton::get()->setFlags(PalNanEngine::Flags::NONE);
     return true;
-  };
+  }
 };
 
 /**
@@ -71,37 +74,38 @@
 TEST_F(TestBase, WifiNanDisabledViaSettings) {
   CREATE_CHRE_TEST_EVENT(NAN_SUBSCRIBE, 0);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          constexpr uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      constexpr uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->requestType == CHRE_WIFI_REQUEST_TYPE_NAN_SUBSCRIBE) {
-                ASSERT_EQ(event->errorCode, CHRE_ERROR_FUNCTION_DISABLED);
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_ASYNC_RESULT);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->requestType == CHRE_WIFI_REQUEST_TYPE_NAN_SUBSCRIBE) {
+            ASSERT_EQ(event->errorCode, CHRE_ERROR_FUNCTION_DISABLED);
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              chreWifiNanSubscribe(config, &kSubscribeCookie);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::WIFI_AVAILABLE, false /* enabled */);
@@ -110,7 +114,7 @@
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT);
 }
 
@@ -122,54 +126,54 @@
 TEST_F(TestBase, WifiNanSuccessfulSubscribe) {
   CREATE_CHRE_TEST_EVENT(NAN_SUBSCRIBE, 0);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          const uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      const uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanIdentifierEvent *>(eventData);
-              if (event->result.errorCode == CHRE_ERROR_NONE) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanIdentifierEvent *>(eventData);
+          if (event->result.errorCode == CHRE_ERROR_NONE) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanDiscoveryEvent *>(eventData);
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT, event->subscribeId);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              const bool success =
+                  chreWifiNanSubscribe(config, &kSubscribeCookie);
+              TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE, success);
               break;
             }
-
-            case CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanDiscoveryEvent *>(eventData);
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT, event->subscribeId);
-              break;
-            }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  const bool success =
-                      chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   chreWifiNanSubscribeConfig config = {
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   bool success;
   waitForEvent(NAN_SUBSCRIBE, &success);
   EXPECT_TRUE(success);
@@ -188,46 +192,46 @@
 TEST_F(TestBase, WifiNanUnsSubscribeOnNanoappUnload) {
   CREATE_CHRE_TEST_EVENT(NAN_SUBSCRIBE, 0);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          const uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      const uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanIdentifierEvent *>(eventData);
-              if (event->result.errorCode == CHRE_ERROR_NONE) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanIdentifierEvent *>(eventData);
+          if (event->result.errorCode == CHRE_ERROR_NONE) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              const bool success =
+                  chreWifiNanSubscribe(config, &kSubscribeCookie);
+              TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE, success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  const bool success =
-                      chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   chreWifiNanSubscribeConfig config = {
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   bool success;
   waitForEvent(NAN_SUBSCRIBE, &success);
   EXPECT_TRUE(success);
@@ -236,7 +240,7 @@
   waitForEvent(CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, &id);
   EXPECT_TRUE(PalNanEngineSingleton::get()->isSubscriptionActive(id));
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(PalNanEngineSingleton::get()->isSubscriptionActive(id));
 }
 
@@ -249,40 +253,40 @@
 TEST_F(TestBase, WifiNanUnuccessfulSubscribeTest) {
   CREATE_CHRE_TEST_EVENT(NAN_SUBSCRIBE, 0);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          const uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      const uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanIdentifierEvent *>(eventData);
-              if (event->result.errorCode != CHRE_ERROR_NONE) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanIdentifierEvent *>(eventData);
+          if (event->result.errorCode != CHRE_ERROR_NONE) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              const bool success =
+                  chreWifiNanSubscribe(config, &kSubscribeCookie);
+              TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE, success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  const bool success =
-                      chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   PalNanEngineSingleton::get()->setFlags(PalNanEngine::Flags::FAIL_SUBSCRIBE);
 
@@ -290,7 +294,7 @@
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   bool success;
   waitForEvent(NAN_SUBSCRIBE, &success);
   EXPECT_TRUE(success);
@@ -305,9 +309,10 @@
 TEST_F(TestBase, WifiNanServiceTerminatedTest) {
   CREATE_CHRE_TEST_EVENT(NAN_SUBSCRIBE, 0);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       const uint32_t kSubscribeCookie = 0x10aded;
 
       switch (eventType) {
@@ -350,16 +355,16 @@
           }
         }
       }
-    };
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   chreWifiNanSubscribeConfig config = {
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   bool success;
   waitForEvent(NAN_SUBSCRIBE, &success);
   EXPECT_TRUE(success);
@@ -390,63 +395,63 @@
     uint32_t publish;
   };
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          const uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      const uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanIdentifierEvent *>(eventData);
-              if (event->result.errorCode == CHRE_ERROR_NONE) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanIdentifierEvent *>(eventData);
+          if (event->result.errorCode == CHRE_ERROR_NONE) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanDiscoveryEvent *>(eventData);
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT, event->subscribeId);
+          break;
+        }
+
+        case CHRE_EVENT_WIFI_NAN_SESSION_LOST: {
+          auto event =
+              static_cast<const chreWifiNanSessionLostEvent *>(eventData);
+          Ids ids = {.subscribe = event->id, .publish = event->peerId};
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_WIFI_NAN_SESSION_LOST, ids);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              const bool success =
+                  chreWifiNanSubscribe(config, &kSubscribeCookie);
+              TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE, success);
               break;
             }
-
-            case CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanDiscoveryEvent *>(eventData);
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_WIFI_NAN_DISCOVERY_RESULT, event->subscribeId);
-              break;
-            }
-
-            case CHRE_EVENT_WIFI_NAN_SESSION_LOST: {
-              auto event =
-                  static_cast<const chreWifiNanSessionLostEvent *>(eventData);
-              Ids ids = {.subscribe = event->id, .publish = event->peerId};
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_WIFI_NAN_SESSION_LOST, ids);
-              break;
-            }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  const bool success =
-                      chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   chreWifiNanSubscribeConfig config = {
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   bool success;
   waitForEvent(NAN_SUBSCRIBE, &success);
   EXPECT_TRUE(success);
@@ -474,70 +479,71 @@
   CREATE_CHRE_TEST_EVENT(NAN_SUBSCRIBE, 0);
   CREATE_CHRE_TEST_EVENT(REQUEST_RANGING, 1);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          const uint32_t kRangingCookie = 0xfa11;
-          const uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      const uint32_t kRangingCookie = 0xfa11;
+      const uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->requestType == CHRE_WIFI_REQUEST_TYPE_RANGING) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_ASYNC_RESULT);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->requestType == CHRE_WIFI_REQUEST_TYPE_RANGING) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_WIFI_RANGING_RESULT: {
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_WIFI_RANGING_RESULT);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              const bool success =
+                  chreWifiNanSubscribe(config, &kSubscribeCookie);
+              TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE, success);
               break;
             }
 
-            case CHRE_EVENT_WIFI_RANGING_RESULT: {
-              TestEventQueueSingleton::get()->pushEvent(
-                  CHRE_EVENT_WIFI_RANGING_RESULT);
+            case REQUEST_RANGING: {
+              uint8_t fakeMacAddress[CHRE_WIFI_BSSID_LEN] = {0x1, 0x2, 0x3,
+                                                             0x4, 0x5, 0x6};
+              struct chreWifiNanRangingParams fakeRangingParams;
+              std::memcpy(fakeRangingParams.macAddress, fakeMacAddress,
+                          CHRE_WIFI_BSSID_LEN);
+              const bool success = chreWifiNanRequestRangingAsync(
+                  &fakeRangingParams, &kRangingCookie);
+              TestEventQueueSingleton::get()->pushEvent(REQUEST_RANGING,
+                                                        success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  const bool success =
-                      chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE,
-                                                            success);
-                  break;
-                }
-
-                case REQUEST_RANGING: {
-                  uint8_t fakeMacAddress[CHRE_WIFI_BSSID_LEN] = {0x1, 0x2, 0x3,
-                                                                 0x4, 0x5, 0x6};
-                  struct chreWifiNanRangingParams fakeRangingParams;
-                  std::memcpy(fakeRangingParams.macAddress, fakeMacAddress,
-                              CHRE_WIFI_BSSID_LEN);
-                  const bool success = chreWifiNanRequestRangingAsync(
-                      &fakeRangingParams, &kRangingCookie);
-                  TestEventQueueSingleton::get()->pushEvent(REQUEST_RANGING,
-                                                            success);
-                  break;
-                }
-              }
-            }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   bool success;
 
   chreWifiNanSubscribeConfig config = {
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
       .service = "SomeServiceName",
   };
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   waitForEvent(NAN_SUBSCRIBE, &success);
   EXPECT_TRUE(success);
 
-  sendEventToNanoapp(app, REQUEST_RANGING, config);
+  sendEventToNanoapp(appId, REQUEST_RANGING, config);
   waitForEvent(REQUEST_RANGING, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT);
@@ -550,52 +556,52 @@
   CREATE_CHRE_TEST_EVENT(NAN_UNSUBSCRIBE, 2);
   CREATE_CHRE_TEST_EVENT(NAN_UNSUBSCRIBE_DONE, 3);
 
-  struct App : public NanTestNanoapp {
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          const uint32_t kSubscribeCookie = 0x10aded;
+  class App : public NanTestNanoapp {
+   public:
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      const uint32_t kSubscribeCookie = 0x10aded;
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
-              auto event =
-                  static_cast<const chreWifiNanIdentifierEvent *>(eventData);
-              if (event->result.errorCode == CHRE_ERROR_NONE) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
-              }
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT: {
+          auto event =
+              static_cast<const chreWifiNanIdentifierEvent *>(eventData);
+          if (event->result.errorCode == CHRE_ERROR_NONE) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_NAN_IDENTIFIER_RESULT, event->id);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case NAN_SUBSCRIBE: {
+              auto config = (chreWifiNanSubscribeConfig *)(event->data);
+              bool success = chreWifiNanSubscribe(config, &kSubscribeCookie);
+              TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE_DONE,
+                                                        success);
               break;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case NAN_SUBSCRIBE: {
-                  auto config = (chreWifiNanSubscribeConfig *)(event->data);
-                  bool success =
-                      chreWifiNanSubscribe(config, &kSubscribeCookie);
-                  TestEventQueueSingleton::get()->pushEvent(NAN_SUBSCRIBE_DONE,
-                                                            success);
-                  break;
-                }
-                case NAN_UNSUBSCRIBE: {
-                  auto *id = static_cast<uint32_t *>(event->data);
-                  bool success = chreWifiNanSubscribeCancel(*id);
-                  // Note that since we're 'simulating' NAN functionality here,
-                  // the async subscribe cancel event will be handled before
-                  // the return event below is posted. For a real on-device (or
-                  // non-simulated) test, this won't be the case, and care must
-                  // be taken to handle the asynchronicity appropriately.
-                  TestEventQueueSingleton::get()->pushEvent(
-                      NAN_UNSUBSCRIBE_DONE, success);
-                  break;
-                }
-              }
+            case NAN_UNSUBSCRIBE: {
+              auto *id = static_cast<uint32_t *>(event->data);
+              bool success = chreWifiNanSubscribeCancel(*id);
+              // Note that since we're 'simulating' NAN functionality here,
+              // the async subscribe cancel event will be handled before
+              // the return event below is posted. For a real on-device (or
+              // non-simulated) test, this won't be the case, and care must
+              // be taken to handle the asynchronicity appropriately.
+              TestEventQueueSingleton::get()->pushEvent(NAN_UNSUBSCRIBE_DONE,
+                                                        success);
+              break;
             }
           }
-        };
+        }
+      }
+    }
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   chreWifiNanSubscribeConfig config = {
       .subscribeType = CHRE_WIFI_NAN_SUBSCRIBE_TYPE_PASSIVE,
@@ -603,7 +609,7 @@
   };
 
   bool success = false;
-  sendEventToNanoapp(app, NAN_SUBSCRIBE, config);
+  sendEventToNanoapp(appId, NAN_SUBSCRIBE, config);
   waitForEvent(NAN_SUBSCRIBE_DONE, &success);
   ASSERT_TRUE(success);
 
@@ -615,7 +621,7 @@
   EXPECT_EQ(wifiRequestManager.getNumNanSubscriptions(), 1);
 
   success = false;
-  sendEventToNanoapp(app, NAN_UNSUBSCRIBE, id);
+  sendEventToNanoapp(appId, NAN_UNSUBSCRIBE, id);
   waitForEvent(NAN_UNSUBSCRIBE_DONE, &success);
   ASSERT_TRUE(success);
   // TODO(b/272351526): consider adding an async result event to catch when the
diff --git a/test/simulation/wifi_scan_test.cc b/test/simulation/wifi_scan_test.cc
index 4f8d80d..70bfa7c 100644
--- a/test/simulation/wifi_scan_test.cc
+++ b/test/simulation/wifi_scan_test.cc
@@ -40,6 +40,9 @@
   chreError errorCode;
 };
 
+constexpr uint64_t kAppOneId = 0x0123456789000001;
+constexpr uint64_t kAppTwoId = 0x0123456789000002;
+
 class WifiScanRequestQueueTestBase : public TestBase {
  public:
   void SetUp() {
@@ -56,15 +59,18 @@
   }
 };
 
-struct WifiScanTestNanoapp : public TestNanoapp {
-  uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+class WifiScanTestNanoapp : public TestNanoapp {
+ public:
+  WifiScanTestNanoapp()
+      : TestNanoapp(
+            TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-  decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                 const void *eventData) {
-    constexpr uint8_t kMaxPendingCookie = 10;
-    static uint32_t cookies[kMaxPendingCookie];
-    static uint8_t nextFreeCookieIndex = 0;
+  explicit WifiScanTestNanoapp(uint64_t id)
+      : TestNanoapp(TestNanoappInfo{
+            .id = id, .perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
+  void handleEvent(uint32_t, uint16_t eventType,
+                   const void *eventData) override {
     switch (eventType) {
       case CHRE_EVENT_WIFI_ASYNC_RESULT: {
         auto *event = static_cast<const chreAsyncResult *>(eventData);
@@ -86,12 +92,12 @@
         switch (event->type) {
           case SCAN_REQUEST:
             bool success = false;
-            if (nextFreeCookieIndex < kMaxPendingCookie) {
-              cookies[nextFreeCookieIndex] =
+            if (mNextFreeCookieIndex < kMaxPendingCookie) {
+              mCookies[mNextFreeCookieIndex] =
                   *static_cast<uint32_t *>(event->data);
               success = chreWifiRequestScanAsyncDefault(
-                  &cookies[nextFreeCookieIndex]);
-              nextFreeCookieIndex++;
+                  &mCookies[mNextFreeCookieIndex]);
+              mNextFreeCookieIndex++;
             } else {
               LOGE("Too many cookies passed from test body!");
             }
@@ -99,11 +105,16 @@
         }
       }
     }
-  };
+  }
+
+ protected:
+  static constexpr uint8_t kMaxPendingCookie = 10;
+  uint32_t mCookies[kMaxPendingCookie];
+  uint8_t mNextFreeCookieIndex = 0;
 };
 
 TEST_F(TestBase, WifiScanBasicSettingTest) {
-  auto app = loadNanoapp<WifiScanTestNanoapp>();
+  uint64_t appId = loadNanoapp(MakeUnique<WifiScanTestNanoapp>());
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::WIFI_AVAILABLE, true /* enabled */);
@@ -112,7 +123,7 @@
   bool success;
   WifiAsyncData wifiAsyncData;
 
-  sendEventToNanoapp(app, SCAN_REQUEST, firstCookie);
+  sendEventToNanoapp(appId, SCAN_REQUEST, firstCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
 
@@ -125,7 +136,7 @@
       Setting::WIFI_AVAILABLE, false /* enabled */);
 
   constexpr uint32_t secondCookie = 0x2020;
-  sendEventToNanoapp(app, SCAN_REQUEST, secondCookie);
+  sendEventToNanoapp(appId, SCAN_REQUEST, secondCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
 
@@ -135,67 +146,197 @@
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::WIFI_AVAILABLE, true /* enabled */);
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
 }
 
 TEST_F(WifiScanRequestQueueTestBase, WifiQueuedScanSettingChangeTest) {
-  struct WifiScanTestNanoappTwo : public WifiScanTestNanoapp {
-    uint64_t id = 0x1123456789abcdef;
+  CREATE_CHRE_TEST_EVENT(CONCURRENT_NANOAPP_RECEIVED_EXPECTED_ASYNC_EVENT_COUNT,
+                         1);
+  CREATE_CHRE_TEST_EVENT(CONCURRENT_NANOAPP_READ_ASYNC_EVENT, 2);
+  // Expecting to receive two event, one from each nanoapp.
+  constexpr uint8_t kExpectedReceiveAsyncResultCount = 2;
+  // receivedAsyncEventCount is shared across apps and must be static.
+  // But we want it initialized each time the test is executed.
+  static uint8_t receivedAsyncEventCount;
+  receivedAsyncEventCount = 0;
+
+  class WifiScanTestConcurrentNanoapp : public TestNanoapp {
+   public:
+    explicit WifiScanTestConcurrentNanoapp(uint64_t id)
+        : TestNanoapp(TestNanoappInfo{
+              .id = id, .perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
+
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          mReceivedAsyncResult = WifiAsyncData{
+              .cookie = static_cast<const uint32_t *>(event->cookie),
+              .errorCode = static_cast<chreError>(event->errorCode)};
+          ++receivedAsyncEventCount;
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          bool success = false;
+          switch (event->type) {
+            case SCAN_REQUEST:
+              mSentCookie = *static_cast<uint32_t *>(event->data);
+              success = chreWifiRequestScanAsyncDefault(&(mSentCookie));
+              TestEventQueueSingleton::get()->pushEvent(SCAN_REQUEST, success);
+              break;
+            case CONCURRENT_NANOAPP_READ_ASYNC_EVENT:
+              TestEventQueueSingleton::get()->pushEvent(
+                  CONCURRENT_NANOAPP_READ_ASYNC_EVENT, mReceivedAsyncResult);
+              break;
+          }
+        }
+      }
+
+      if (receivedAsyncEventCount == kExpectedReceiveAsyncResultCount) {
+        TestEventQueueSingleton::get()->pushEvent(
+            CONCURRENT_NANOAPP_RECEIVED_EXPECTED_ASYNC_EVENT_COUNT);
+      }
+    }
+
+   protected:
+    uint32_t mSentCookie;
+    WifiAsyncData mReceivedAsyncResult;
   };
 
-  auto firstApp = loadNanoapp<WifiScanTestNanoapp>();
-  auto secondApp = loadNanoapp<WifiScanTestNanoappTwo>();
+  uint64_t appOneId =
+      loadNanoapp(MakeUnique<WifiScanTestConcurrentNanoapp>(kAppOneId));
+  uint64_t appTwoId =
+      loadNanoapp(MakeUnique<WifiScanTestConcurrentNanoapp>(kAppTwoId));
 
-  constexpr uint32_t firstRequestCookie = 0x1010;
-  constexpr uint32_t secondRequestCookie = 0x2020;
+  constexpr uint32_t appOneRequestCookie = 0x1010;
+  constexpr uint32_t appTwoRequestCookie = 0x2020;
   bool success;
-  sendEventToNanoapp(firstApp, SCAN_REQUEST, firstRequestCookie);
+  sendEventToNanoapp(appOneId, SCAN_REQUEST, appOneRequestCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
-  sendEventToNanoapp(secondApp, SCAN_REQUEST, secondRequestCookie);
+  sendEventToNanoapp(appTwoId, SCAN_REQUEST, appTwoRequestCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::WIFI_AVAILABLE, false /* enabled */);
 
-  WifiAsyncData wifiAsyncData;
-  waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT, &wifiAsyncData);
-  EXPECT_EQ(wifiAsyncData.errorCode, CHRE_ERROR_NONE);
-  EXPECT_EQ(*wifiAsyncData.cookie, firstRequestCookie);
-  waitForEvent(CHRE_EVENT_WIFI_SCAN_RESULT);
+  // We need to make sure that each nanoapp has received one async result before
+  // further analysis.
+  waitForEvent(CONCURRENT_NANOAPP_RECEIVED_EXPECTED_ASYNC_EVENT_COUNT);
 
-  waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT, &wifiAsyncData);
+  WifiAsyncData wifiAsyncData;
+  sendEventToNanoapp(appOneId, CONCURRENT_NANOAPP_READ_ASYNC_EVENT);
+  waitForEvent(CONCURRENT_NANOAPP_READ_ASYNC_EVENT, &wifiAsyncData);
+  EXPECT_EQ(wifiAsyncData.errorCode, CHRE_ERROR_NONE);
+  EXPECT_EQ(*wifiAsyncData.cookie, appOneRequestCookie);
+
+  sendEventToNanoapp(appTwoId, CONCURRENT_NANOAPP_READ_ASYNC_EVENT);
+  waitForEvent(CONCURRENT_NANOAPP_READ_ASYNC_EVENT, &wifiAsyncData);
   EXPECT_EQ(wifiAsyncData.errorCode, CHRE_ERROR_FUNCTION_DISABLED);
-  EXPECT_EQ(*wifiAsyncData.cookie, secondRequestCookie);
+  EXPECT_EQ(*wifiAsyncData.cookie, appTwoRequestCookie);
 
   EventLoopManagerSingleton::get()->getSettingManager().postSettingChange(
       Setting::WIFI_AVAILABLE, true /* enabled */);
 
-  unloadNanoapp(firstApp);
-  unloadNanoapp(secondApp);
+  unloadNanoapp(appOneId);
+  unloadNanoapp(appTwoId);
 }
 
 TEST_F(WifiScanRequestQueueTestBase, WifiScanRejectRequestFromSameNanoapp) {
-  auto app = loadNanoapp<WifiScanTestNanoapp>();
+  CREATE_CHRE_TEST_EVENT(RECEIVED_ALL_EXPECTED_EVENTS, 1);
+  CREATE_CHRE_TEST_EVENT(READ_ASYNC_EVENT, 2);
 
-  constexpr uint32_t firstRequestCookie = 0x1010;
-  constexpr uint32_t secondRequestCookie = 0x2020;
+  static constexpr uint8_t kExpectedReceivedScanRequestCount = 2;
+
+  class WifiScanTestBufferedAsyncResultNanoapp : public TestNanoapp {
+   public:
+    WifiScanTestBufferedAsyncResultNanoapp()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
+
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          mReceivedAsyncResult = WifiAsyncData{
+              .cookie = static_cast<const uint32_t *>(event->cookie),
+              .errorCode = static_cast<chreError>(event->errorCode)};
+          ++mReceivedAsyncEventCount;
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          bool success = false;
+          switch (event->type) {
+            case SCAN_REQUEST:
+              if (mReceivedScanRequestCount >=
+                  kExpectedReceivedScanRequestCount) {
+                LOGE("Asking too many scan request");
+              } else {
+                mReceivedCookies[mReceivedScanRequestCount] =
+                    *static_cast<uint32_t *>(event->data);
+                success = chreWifiRequestScanAsyncDefault(
+                    &(mReceivedCookies[mReceivedScanRequestCount]));
+                ++mReceivedScanRequestCount;
+                TestEventQueueSingleton::get()->pushEvent(SCAN_REQUEST,
+                                                          success);
+              }
+              break;
+            case READ_ASYNC_EVENT:
+              TestEventQueueSingleton::get()->pushEvent(READ_ASYNC_EVENT,
+                                                        mReceivedAsyncResult);
+              break;
+          }
+        }
+      }
+      if (mReceivedAsyncEventCount == kExpectedReceivedAsyncResultCount &&
+          mReceivedScanRequestCount == kExpectedReceivedScanRequestCount) {
+        TestEventQueueSingleton::get()->pushEvent(RECEIVED_ALL_EXPECTED_EVENTS);
+      }
+    }
+
+   protected:
+    // We are only expecting to receive one async result since the second
+    // request is expected to fail.
+    const uint8_t kExpectedReceivedAsyncResultCount = 1;
+    uint8_t mReceivedAsyncEventCount = 0;
+    uint8_t mReceivedScanRequestCount = 0;
+
+    // We need to have two cookie storage to separate the two scan request.
+    uint32_t mReceivedCookies[kExpectedReceivedScanRequestCount];
+    WifiAsyncData mReceivedAsyncResult;
+  };
+
+  uint64_t appId =
+      loadNanoapp(MakeUnique<WifiScanTestBufferedAsyncResultNanoapp>());
+
+  constexpr uint32_t kFirstRequestCookie = 0x1010;
+  constexpr uint32_t kSecondRequestCookie = 0x2020;
   bool success;
-  sendEventToNanoapp(app, SCAN_REQUEST, firstRequestCookie);
+  sendEventToNanoapp(appId, SCAN_REQUEST, kFirstRequestCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
-  sendEventToNanoapp(app, SCAN_REQUEST, secondRequestCookie);
+  sendEventToNanoapp(appId, SCAN_REQUEST, kSecondRequestCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_FALSE(success);
 
-  WifiAsyncData wifiAsyncData;
-  waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT, &wifiAsyncData);
-  EXPECT_EQ(wifiAsyncData.errorCode, CHRE_ERROR_NONE);
-  EXPECT_EQ(*wifiAsyncData.cookie, firstRequestCookie);
-  waitForEvent(CHRE_EVENT_WIFI_SCAN_RESULT);
+  // We need to make sure that the nanoapp has received one async result and did
+  // two scan requests before further analysis.
+  waitForEvent(RECEIVED_ALL_EXPECTED_EVENTS);
 
-  unloadNanoapp(app);
+  WifiAsyncData wifiAsyncData;
+  sendEventToNanoapp(appId, READ_ASYNC_EVENT);
+  waitForEvent(READ_ASYNC_EVENT, &wifiAsyncData);
+  EXPECT_EQ(wifiAsyncData.errorCode, CHRE_ERROR_NONE);
+  EXPECT_EQ(*wifiAsyncData.cookie, kFirstRequestCookie);
+
+  unloadNanoapp(appId);
 }
 
 TEST_F(WifiScanRequestQueueTestBase, WifiScanActiveScanFromDistinctNanoapps) {
@@ -203,36 +344,25 @@
                          1);
   CREATE_CHRE_TEST_EVENT(CONCURRENT_NANOAPP_READ_COOKIE, 2);
 
-  struct AppCookies {
-    uint32_t sent = 0;
-    uint32_t received = 0;
-  };
+  constexpr uint8_t kExpectedReceiveAsyncResultCount = 2;
+  // receivedCookieCount is shared across apps and must be static.
+  // But we want it initialized each time the test is executed.
+  static uint8_t receivedCookieCount;
+  receivedCookieCount = 0;
 
-  constexpr uint64_t kAppOneId = 0x0123456789000001;
-  constexpr uint64_t kAppTwoId = 0x0123456789000002;
+  class WifiScanTestConcurrentNanoapp : public TestNanoapp {
+   public:
+    explicit WifiScanTestConcurrentNanoapp(uint64_t id)
+        : TestNanoapp(TestNanoappInfo{
+              .id = id, .perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-  struct WifiScanTestConcurrentNanoappOne : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
-    uint64_t id = kAppOneId;
-
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      constexpr uint8_t kExpectedReceiveAsyncResultCount = 2;
-      static uint8_t receivedCookieCount = 0;
-      static AppCookies appOneCookies;
-      static AppCookies appTwoCookies;
-
-      // Retrieve cookies from different apps that have the same access to
-      // static storage due to inheritance.
-      AppCookies *appCookies =
-          chreGetAppId() == kAppTwoId ? &appTwoCookies : &appOneCookies;
-
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_WIFI_ASYNC_RESULT: {
           auto *event = static_cast<const chreAsyncResult *>(eventData);
           if (event->errorCode == CHRE_ERROR_NONE) {
-            appCookies->received =
-                *static_cast<const uint32_t *>(event->cookie);
+            mReceivedCookie = *static_cast<const uint32_t *>(event->cookie);
             ++receivedCookieCount;
           } else {
             LOGE("Received failed async result");
@@ -248,54 +378,54 @@
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
           bool success = false;
-          uint32_t expectedCookie;
           switch (event->type) {
             case SCAN_REQUEST:
-              appCookies->sent = *static_cast<uint32_t *>(event->data);
-              success = chreWifiRequestScanAsyncDefault(&(appCookies->sent));
+              mSentCookie = *static_cast<uint32_t *>(event->data);
+              success = chreWifiRequestScanAsyncDefault(&(mSentCookie));
               TestEventQueueSingleton::get()->pushEvent(SCAN_REQUEST, success);
               break;
             case CONCURRENT_NANOAPP_READ_COOKIE:
               TestEventQueueSingleton::get()->pushEvent(
-                  CONCURRENT_NANOAPP_READ_COOKIE, appCookies->received);
+                  CONCURRENT_NANOAPP_READ_COOKIE, mReceivedCookie);
               break;
           }
         }
-      };
-    };
+      }
+    }
+
+   protected:
+    uint32_t mSentCookie;
+    uint32_t mReceivedCookie;
   };
 
-  struct WifiScanTestConcurrentNanoappTwo
-      : public WifiScanTestConcurrentNanoappOne {
-    uint64_t id = kAppTwoId;
-  };
-
-  auto appOne = loadNanoapp<WifiScanTestConcurrentNanoappOne>();
-  auto appTwo = loadNanoapp<WifiScanTestConcurrentNanoappTwo>();
+  uint64_t appOneId =
+      loadNanoapp(MakeUnique<WifiScanTestConcurrentNanoapp>(kAppOneId));
+  uint64_t appTwoId =
+      loadNanoapp(MakeUnique<WifiScanTestConcurrentNanoapp>(kAppTwoId));
 
   constexpr uint32_t kAppOneRequestCookie = 0x1010;
   constexpr uint32_t kAppTwoRequestCookie = 0x2020;
   bool success;
-  sendEventToNanoapp(appOne, SCAN_REQUEST, kAppOneRequestCookie);
+  sendEventToNanoapp(appOneId, SCAN_REQUEST, kAppOneRequestCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
-  sendEventToNanoapp(appTwo, SCAN_REQUEST, kAppTwoRequestCookie);
+  sendEventToNanoapp(appTwoId, SCAN_REQUEST, kAppTwoRequestCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
 
   waitForEvent(CONCURRENT_NANOAPP_RECEIVED_EXPECTED_ASYNC_EVENT_COUNT);
 
   uint32_t receivedCookie;
-  sendEventToNanoapp(appOne, CONCURRENT_NANOAPP_READ_COOKIE);
+  sendEventToNanoapp(appOneId, CONCURRENT_NANOAPP_READ_COOKIE);
   waitForEvent(CONCURRENT_NANOAPP_READ_COOKIE, &receivedCookie);
   EXPECT_EQ(kAppOneRequestCookie, receivedCookie);
 
-  sendEventToNanoapp(appTwo, CONCURRENT_NANOAPP_READ_COOKIE);
+  sendEventToNanoapp(appTwoId, CONCURRENT_NANOAPP_READ_COOKIE);
   waitForEvent(CONCURRENT_NANOAPP_READ_COOKIE, &receivedCookie);
   EXPECT_EQ(kAppTwoRequestCookie, receivedCookie);
 
-  unloadNanoapp(appOne);
-  unloadNanoapp(appTwo);
+  unloadNanoapp(appOneId);
+  unloadNanoapp(appTwoId);
 }
 
 }  // namespace
diff --git a/test/simulation/wifi_test.cc b/test/simulation/wifi_test.cc
index 43c9384..deca31c 100644
--- a/test/simulation/wifi_test.cc
+++ b/test/simulation/wifi_test.cc
@@ -41,53 +41,58 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
-
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
-              }
-              break;
-            }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case MONITORING_REQUEST:
-                  auto request =
-                      static_cast<const MonitoringRequest *>(event->data);
-                  cookie = request->cookie;
-                  bool success = chreWifiConfigureScanMonitorAsync(
-                      request->enable, &cookie);
-                  TestEventQueueSingleton::get()->pushEvent(MONITORING_REQUEST,
-                                                            success);
-              }
-            }
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
           }
-        };
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case MONITORING_REQUEST:
+              auto request =
+                  static_cast<const MonitoringRequest *>(event->data);
+              mCookie = request->cookie;
+              bool success =
+                  chreWifiConfigureScanMonitorAsync(request->enable, &mCookie);
+              TestEventQueueSingleton::get()->pushEvent(MONITORING_REQUEST,
+                                                        success);
+          }
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 
   MonitoringRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, MONITORING_REQUEST, request);
+  sendEventToNanoapp(appId, MONITORING_REQUEST, request);
   uint32_t cookie;
   waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT, &cookie);
   EXPECT_EQ(cookie, request.cookie);
   EXPECT_TRUE(chrePalWifiIsScanMonitoringActive());
 
   request = {.enable = false, .cookie = 0x456};
-  sendEventToNanoapp(app, MONITORING_REQUEST, request);
+  sendEventToNanoapp(appId, MONITORING_REQUEST, request);
   bool success;
   waitForEvent(MONITORING_REQUEST, &success);
   EXPECT_TRUE(success);
@@ -104,46 +109,51 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
-
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
-              }
-              break;
-            }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case MONITORING_REQUEST:
-                  auto request =
-                      static_cast<const MonitoringRequest *>(event->data);
-                  cookie = request->cookie;
-                  bool success = chreWifiConfigureScanMonitorAsync(
-                      request->enable, &cookie);
-                  TestEventQueueSingleton::get()->pushEvent(MONITORING_REQUEST,
-                                                            success);
-              }
-            }
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
           }
-        };
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case MONITORING_REQUEST:
+              auto request =
+                  static_cast<const MonitoringRequest *>(event->data);
+              mCookie = request->cookie;
+              bool success =
+                  chreWifiConfigureScanMonitorAsync(request->enable, &mCookie);
+              TestEventQueueSingleton::get()->pushEvent(MONITORING_REQUEST,
+                                                        success);
+          }
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 
   MonitoringRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, MONITORING_REQUEST, request);
+  sendEventToNanoapp(appId, MONITORING_REQUEST, request);
   bool success;
   waitForEvent(MONITORING_REQUEST, &success);
   EXPECT_TRUE(success);
@@ -152,7 +162,7 @@
   EXPECT_EQ(cookie, request.cookie);
   EXPECT_TRUE(chrePalWifiIsScanMonitoringActive());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 }
 
@@ -164,46 +174,51 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
-
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
-              }
-              break;
-            }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case MONITORING_REQUEST:
-                  auto request =
-                      static_cast<const MonitoringRequest *>(event->data);
-                  cookie = request->cookie;
-                  bool success = chreWifiConfigureScanMonitorAsync(
-                      request->enable, &cookie);
-                  TestEventQueueSingleton::get()->pushEvent(MONITORING_REQUEST,
-                                                            success);
-              }
-            }
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
           }
-        };
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case MONITORING_REQUEST:
+              auto request =
+                  static_cast<const MonitoringRequest *>(event->data);
+              mCookie = request->cookie;
+              bool success =
+                  chreWifiConfigureScanMonitorAsync(request->enable, &mCookie);
+              TestEventQueueSingleton::get()->pushEvent(MONITORING_REQUEST,
+                                                        success);
+          }
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 
   MonitoringRequest request{.enable = true, .cookie = 0x123};
-  sendEventToNanoapp(app, MONITORING_REQUEST, request);
+  sendEventToNanoapp(appId, MONITORING_REQUEST, request);
   bool success;
   waitForEvent(MONITORING_REQUEST, &success);
   EXPECT_TRUE(success);
@@ -212,14 +227,14 @@
   EXPECT_EQ(cookie, request.cookie);
   EXPECT_TRUE(chrePalWifiIsScanMonitoringActive());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 
-  app = loadNanoapp<App>();
+  appId = loadNanoapp(MakeUnique<App>());
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 
   request = {.enable = true, .cookie = 0x456};
-  sendEventToNanoapp(app, MONITORING_REQUEST, request);
+  sendEventToNanoapp(appId, MONITORING_REQUEST, request);
   waitForEvent(MONITORING_REQUEST, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT, &cookie);
diff --git a/test/simulation/wifi_timeout_test.cc b/test/simulation/wifi_timeout_test.cc
index 6e448b0..7c310fd 100644
--- a/test/simulation/wifi_timeout_test.cc
+++ b/test/simulation/wifi_timeout_test.cc
@@ -20,8 +20,10 @@
 #include "chre/core/settings.h"
 #include "chre/platform/linux/pal_wifi.h"
 #include "chre/platform/log.h"
+#include "chre/util/nanoapp/app_id.h"
 #include "chre/util/system/napp_permissions.h"
 #include "chre_api/chre/event.h"
+#include "chre_api/chre/re.h"
 #include "chre_api/chre/wifi.h"
 #include "gtest/gtest.h"
 #include "test_base.h"
@@ -31,29 +33,42 @@
 
 namespace chre {
 namespace {
-// WifiTimeoutTestBase needs to set timeout more than max wifi async timeout
-// time. If not, waitForEvent will timeout before actual timeout happens in
-// CHRE, making us unable to observe how system handles timeout.
+// WifiTimeoutTestBase needs to set timeout more than the max waitForEvent()
+// should process (Currently it is
+// WifiCanDispatchSecondScanRequestInQueueAfterFirstTimeout). If not,
+// waitForEvent will timeout before actual timeout happens in CHRE, making us
+// unable to observe how system handles timeout.
 class WifiTimeoutTestBase : public TestBase {
  protected:
   uint64_t getTimeoutNs() const override {
-    return 2 * CHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS;
+    return 3 * CHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS;
   }
 };
 
+CREATE_CHRE_TEST_EVENT(SCAN_REQUEST, 20);
+CREATE_CHRE_TEST_EVENT(REQUEST_TIMED_OUT, 21);
+
 TEST_F(WifiTimeoutTestBase, WifiScanRequestTimeoutTest) {
-  CREATE_CHRE_TEST_EVENT(SCAN_REQUEST, 1);
+  class ScanTestNanoapp : public TestNanoapp {
+   public:
+    explicit ScanTestNanoapp()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+    bool start() override {
+      mRequestTimer = CHRE_TIMER_INVALID;
+      return true;
+    }
 
-    decltype(nanoappHandleEvent) *handleEvent = [](uint32_t, uint16_t eventType,
-                                                   const void *eventData) {
-      static uint32_t cookie;
-
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
       switch (eventType) {
         case CHRE_EVENT_WIFI_ASYNC_RESULT: {
           auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (mRequestTimer != CHRE_TIMER_INVALID) {
+            chreTimerCancel(mRequestTimer);
+            mRequestTimer = CHRE_TIMER_INVALID;
+          }
           if (event->success) {
             TestEventQueueSingleton::get()->pushEvent(
                 CHRE_EVENT_WIFI_ASYNC_RESULT,
@@ -68,43 +83,176 @@
           break;
         }
 
+        case CHRE_EVENT_TIMER: {
+          TestEventQueueSingleton::get()->pushEvent(REQUEST_TIMED_OUT);
+          mRequestTimer = CHRE_TIMER_INVALID;
+          break;
+        }
+
         case CHRE_EVENT_TEST_EVENT: {
           auto event = static_cast<const TestEvent *>(eventData);
           switch (event->type) {
             case SCAN_REQUEST:
-              cookie = *static_cast<uint32_t *>(event->data);
-              bool success = chreWifiRequestScanAsyncDefault(&cookie);
+              bool success = false;
+              mCookie = *static_cast<uint32_t *>(event->data);
+              if (chreWifiRequestScanAsyncDefault(&mCookie)) {
+                mRequestTimer =
+                    chreTimerSet(CHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS, nullptr,
+                                 true /* oneShot */);
+                success = mRequestTimer != CHRE_TIMER_INVALID;
+              }
               TestEventQueueSingleton::get()->pushEvent(SCAN_REQUEST, success);
+              break;
           }
+          break;
         }
       }
-    };
+    }
+
+   protected:
+    uint32_t mCookie;
+    uint32_t mRequestTimer;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<ScanTestNanoapp>());
 
   constexpr uint32_t timeOutCookie = 0xdead;
-  chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN, false);
-  sendEventToNanoapp(app, SCAN_REQUEST, timeOutCookie);
+  chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN,
+                            false /* enableResponse */);
+  sendEventToNanoapp(appId, SCAN_REQUEST, timeOutCookie);
   bool success;
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
 
-  // Add 1 second to prevent race condition.
-  constexpr uint8_t kWifiScanRequestTimeoutSec =
-      (CHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS / CHRE_NSEC_PER_SEC) + 1;
-  std::this_thread::sleep_for(std::chrono::seconds(kWifiScanRequestTimeoutSec));
+  waitForEvent(REQUEST_TIMED_OUT);
 
   // Make sure that we can still request scan after a timedout
   // request.
   constexpr uint32_t successCookie = 0x0101;
-  chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN, true);
-  sendEventToNanoapp(app, SCAN_REQUEST, successCookie);
+  chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN,
+                            true /* enableResponse */);
+  sendEventToNanoapp(appId, SCAN_REQUEST, successCookie);
   waitForEvent(SCAN_REQUEST, &success);
   EXPECT_TRUE(success);
   waitForEvent(CHRE_EVENT_WIFI_SCAN_RESULT);
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
+}
+
+TEST_F(WifiTimeoutTestBase, WifiCanDispatchQueuedRequestAfterOneTimeout) {
+  constexpr uint8_t kNanoappNum = 2;
+  // receivedTimeout is shared across apps and must be static.
+  // But we want it initialized each time the test is executed.
+  static uint8_t receivedTimeout;
+  receivedTimeout = 0;
+
+  class ScanTestNanoapp : public TestNanoapp {
+   public:
+    explicit ScanTestNanoapp(uint64_t id = kDefaultTestNanoappId)
+        : TestNanoapp(TestNanoappInfo{
+              .id = id, .perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
+
+    bool start() override {
+      for (uint8_t i = 0; i < kNanoappNum; ++i) {
+        mRequestTimers[i] = CHRE_TIMER_INVALID;
+      }
+      return true;
+    }
+
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      size_t index = id() - CHRE_VENDOR_ID_EXAMPLE - 1;
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (mRequestTimers[index] != CHRE_TIMER_INVALID) {
+            chreTimerCancel(mRequestTimers[index]);
+            mRequestTimers[index] = CHRE_TIMER_INVALID;
+          }
+          if (event->success) {
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
+          }
+          break;
+        }
+
+        case CHRE_EVENT_WIFI_SCAN_RESULT: {
+          TestEventQueueSingleton::get()->pushEvent(
+              CHRE_EVENT_WIFI_SCAN_RESULT);
+          break;
+        }
+
+        case CHRE_EVENT_TIMER: {
+          if (eventData == &mCookie[index]) {
+            receivedTimeout++;
+            mRequestTimers[index] = CHRE_TIMER_INVALID;
+          }
+          if (receivedTimeout == 2) {
+            TestEventQueueSingleton::get()->pushEvent(REQUEST_TIMED_OUT);
+          }
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case SCAN_REQUEST:
+              bool success = false;
+              mCookie[index] = *static_cast<uint32_t *>(event->data);
+              if (chreWifiRequestScanAsyncDefault(&mCookie[index])) {
+                mRequestTimers[index] =
+                    chreTimerSet(CHRE_TEST_WIFI_SCAN_RESULT_TIMEOUT_NS,
+                                 &mCookie[index], true /* oneShot */);
+                success = mRequestTimers[index] != CHRE_TIMER_INVALID;
+              }
+              TestEventQueueSingleton::get()->pushEvent(SCAN_REQUEST, success);
+              break;
+          }
+          break;
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie[kNanoappNum];
+    uint32_t mRequestTimers[kNanoappNum];
+  };
+  constexpr uint64_t kAppOneId = makeExampleNanoappId(1);
+  constexpr uint64_t kAppTwoId = makeExampleNanoappId(2);
+
+  uint64_t firstAppId = loadNanoapp(MakeUnique<ScanTestNanoapp>(kAppOneId));
+  uint64_t secondAppId = loadNanoapp(MakeUnique<ScanTestNanoapp>(kAppTwoId));
+
+  constexpr uint32_t timeOutCookie = 0xdead;
+  chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN,
+                            false /* enableResponse */);
+  bool success;
+  sendEventToNanoapp(firstAppId, SCAN_REQUEST, timeOutCookie);
+  waitForEvent(SCAN_REQUEST, &success);
+  EXPECT_TRUE(success);
+  sendEventToNanoapp(secondAppId, SCAN_REQUEST, timeOutCookie);
+  waitForEvent(SCAN_REQUEST, &success);
+  EXPECT_TRUE(success);
+
+  waitForEvent(REQUEST_TIMED_OUT);
+
+  // Make sure that we can still request scan for both nanoapps after a timedout
+  // request.
+  constexpr uint32_t successCookie = 0x0101;
+  chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN,
+                            true /* enableResponse */);
+  sendEventToNanoapp(firstAppId, SCAN_REQUEST, successCookie);
+  waitForEvent(SCAN_REQUEST, &success);
+  EXPECT_TRUE(success);
+  waitForEvent(CHRE_EVENT_WIFI_SCAN_RESULT);
+  sendEventToNanoapp(secondAppId, SCAN_REQUEST, successCookie);
+  waitForEvent(SCAN_REQUEST, &success);
+  EXPECT_TRUE(success);
+  waitForEvent(CHRE_EVENT_WIFI_SCAN_RESULT);
+
+  unloadNanoapp(firstAppId);
+  unloadNanoapp(secondAppId);
 }
 
 TEST_F(WifiTimeoutTestBase, WifiScanMonitorTimeoutTest) {
@@ -115,61 +263,83 @@
     uint32_t cookie;
   };
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
+    bool start() override {
+      mRequestTimer = CHRE_TIMER_INVALID;
+      return true;
+    }
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    CHRE_EVENT_WIFI_ASYNC_RESULT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
-              }
-              break;
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            if (mRequestTimer != CHRE_TIMER_INVALID) {
+              chreTimerCancel(mRequestTimer);
+              mRequestTimer = CHRE_TIMER_INVALID;
             }
-
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case SCAN_MONITOR_REQUEST:
-                  auto request =
-                      static_cast<const MonitoringRequest *>(event->data);
-                  cookie = request->cookie;
-                  bool success = chreWifiConfigureScanMonitorAsync(
-                      request->enable, &cookie);
-                  TestEventQueueSingleton::get()->pushEvent(
-                      SCAN_MONITOR_REQUEST, success);
-              }
-            }
+            TestEventQueueSingleton::get()->pushEvent(
+                CHRE_EVENT_WIFI_ASYNC_RESULT,
+                *(static_cast<const uint32_t *>(event->cookie)));
           }
-        };
+          break;
+        }
+
+        case CHRE_EVENT_TIMER: {
+          mRequestTimer = CHRE_TIMER_INVALID;
+          TestEventQueueSingleton::get()->pushEvent(REQUEST_TIMED_OUT);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case SCAN_MONITOR_REQUEST:
+              bool success = false;
+              auto request =
+                  static_cast<const MonitoringRequest *>(event->data);
+              if (chreWifiConfigureScanMonitorAsync(request->enable,
+                                                    &mCookie)) {
+                mCookie = request->cookie;
+                mRequestTimer = chreTimerSet(CHRE_TEST_ASYNC_RESULT_TIMEOUT_NS,
+                                             nullptr, true /* oneShot */);
+                success = mRequestTimer != CHRE_TIMER_INVALID;
+              }
+
+              TestEventQueueSingleton::get()->pushEvent(SCAN_MONITOR_REQUEST,
+                                                        success);
+          }
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie;
+    uint32_t mRequestTimer;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
 
   MonitoringRequest timeoutRequest{.enable = true, .cookie = 0xdead};
   chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN_MONITORING, false);
-  sendEventToNanoapp(app, SCAN_MONITOR_REQUEST, timeoutRequest);
+  sendEventToNanoapp(appId, SCAN_MONITOR_REQUEST, timeoutRequest);
   bool success;
   waitForEvent(SCAN_MONITOR_REQUEST, &success);
   EXPECT_TRUE(success);
 
-  // Add 1 second to prevent race condition.
-  constexpr uint8_t kWifiConfigureScanMonitorTimeoutSec =
-      (CHRE_TEST_ASYNC_RESULT_TIMEOUT_NS / CHRE_NSEC_PER_SEC) + 1;
-  std::this_thread::sleep_for(
-      std::chrono::seconds(kWifiConfigureScanMonitorTimeoutSec));
+  waitForEvent(REQUEST_TIMED_OUT);
 
   // Make sure that we can still request to change scan monitor after a timedout
   // request.
   MonitoringRequest enableRequest{.enable = true, .cookie = 0x1010};
   chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::SCAN_MONITORING, true);
-  sendEventToNanoapp(app, SCAN_MONITOR_REQUEST, enableRequest);
+  sendEventToNanoapp(appId, SCAN_MONITOR_REQUEST, enableRequest);
   waitForEvent(SCAN_MONITOR_REQUEST, &success);
   EXPECT_TRUE(success);
 
@@ -179,7 +349,7 @@
   EXPECT_TRUE(chrePalWifiIsScanMonitoringActive());
 
   MonitoringRequest disableRequest{.enable = false, .cookie = 0x0101};
-  sendEventToNanoapp(app, SCAN_MONITOR_REQUEST, disableRequest);
+  sendEventToNanoapp(appId, SCAN_MONITOR_REQUEST, disableRequest);
   waitForEvent(SCAN_MONITOR_REQUEST, &success);
   EXPECT_TRUE(success);
 
@@ -187,87 +357,105 @@
   EXPECT_EQ(cookie, disableRequest.cookie);
   EXPECT_FALSE(chrePalWifiIsScanMonitoringActive());
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
 }
 
 TEST_F(WifiTimeoutTestBase, WifiRequestRangingTimeoutTest) {
   CREATE_CHRE_TEST_EVENT(RANGING_REQUEST, 0);
-  CREATE_CHRE_TEST_EVENT(RANGING_RESULT_TIMEOUT, 1);
 
-  struct App : public TestNanoapp {
-    uint32_t perms = NanoappPermissions::CHRE_PERMS_WIFI;
+  class App : public TestNanoapp {
+   public:
+    App()
+        : TestNanoapp(
+              TestNanoappInfo{.perms = NanoappPermissions::CHRE_PERMS_WIFI}) {}
 
-    decltype(nanoappHandleEvent) *handleEvent =
-        [](uint32_t, uint16_t eventType, const void *eventData) {
-          static uint32_t cookie;
+    bool start() override {
+      mRequestTimer = CHRE_TIMER_INVALID;
+      return true;
+    }
 
-          switch (eventType) {
-            case CHRE_EVENT_WIFI_ASYNC_RESULT: {
-              auto *event = static_cast<const chreAsyncResult *>(eventData);
-              if (event->success) {
-                if (event->errorCode == 0) {
-                  TestEventQueueSingleton::get()->pushEvent(
-                      CHRE_EVENT_WIFI_ASYNC_RESULT,
-                      *(static_cast<const uint32_t *>(event->cookie)));
-                }
-              } else if (event->errorCode == CHRE_ERROR_TIMEOUT) {
-                TestEventQueueSingleton::get()->pushEvent(
-                    RANGING_RESULT_TIMEOUT,
-                    *(static_cast<const uint32_t *>(event->cookie)));
-              }
-              break;
-            }
+    void handleEvent(uint32_t, uint16_t eventType,
+                     const void *eventData) override {
+      switch (eventType) {
+        case CHRE_EVENT_WIFI_ASYNC_RESULT: {
+          if (mRequestTimer != CHRE_TIMER_INVALID) {
+            chreTimerCancel(mRequestTimer);
+            mRequestTimer = CHRE_TIMER_INVALID;
+          }
 
-            case CHRE_EVENT_TEST_EVENT: {
-              auto event = static_cast<const TestEvent *>(eventData);
-              switch (event->type) {
-                case RANGING_REQUEST:
-                  cookie = *static_cast<uint32_t *>(event->data);
-
-                  // Placeholder parameters since linux PAL does not use this to
-                  // generate response
-                  struct chreWifiRangingTarget dummyRangingTarget = {
-                      .macAddress = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc},
-                      .primaryChannel = 0xdef02468,
-                      .centerFreqPrimary = 0xace13579,
-                      .centerFreqSecondary = 0xbdf369cf,
-                      .channelWidth = 0x48,
-                  };
-
-                  struct chreWifiRangingParams dummyRangingParams = {
-                      .targetListLen = 1,
-                      .targetList = &dummyRangingTarget,
-                  };
-
-                  bool success =
-                      chreWifiRequestRangingAsync(&dummyRangingParams, &cookie);
-                  TestEventQueueSingleton::get()->pushEvent(RANGING_REQUEST,
-                                                            success);
-              }
+          auto *event = static_cast<const chreAsyncResult *>(eventData);
+          if (event->success) {
+            if (event->errorCode == 0) {
+              TestEventQueueSingleton::get()->pushEvent(
+                  CHRE_EVENT_WIFI_ASYNC_RESULT,
+                  *(static_cast<const uint32_t *>(event->cookie)));
             }
           }
-        };
+          break;
+        }
+
+        case CHRE_EVENT_TIMER: {
+          mRequestTimer = CHRE_TIMER_INVALID;
+          TestEventQueueSingleton::get()->pushEvent(REQUEST_TIMED_OUT);
+          break;
+        }
+
+        case CHRE_EVENT_TEST_EVENT: {
+          auto event = static_cast<const TestEvent *>(eventData);
+          switch (event->type) {
+            case RANGING_REQUEST:
+              bool success = false;
+              mCookie = *static_cast<uint32_t *>(event->data);
+
+              // Placeholder parameters since linux PAL does not use this to
+              // generate response
+              struct chreWifiRangingTarget dummyRangingTarget = {
+                  .macAddress = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc},
+                  .primaryChannel = 0xdef02468,
+                  .centerFreqPrimary = 0xace13579,
+                  .centerFreqSecondary = 0xbdf369cf,
+                  .channelWidth = 0x48,
+              };
+
+              struct chreWifiRangingParams dummyRangingParams = {
+                  .targetListLen = 1,
+                  .targetList = &dummyRangingTarget,
+              };
+
+              if (chreWifiRequestRangingAsync(&dummyRangingParams, &mCookie)) {
+                mRequestTimer =
+                    chreTimerSet(CHRE_TEST_WIFI_RANGING_RESULT_TIMEOUT_NS,
+                                 nullptr, true /* oneShot */);
+                success = mRequestTimer != CHRE_TIMER_INVALID;
+              }
+              TestEventQueueSingleton::get()->pushEvent(RANGING_REQUEST,
+                                                        success);
+          }
+        }
+      }
+    }
+
+   protected:
+    uint32_t mCookie;
+    uint32_t mRequestTimer;
   };
 
-  auto app = loadNanoapp<App>();
+  uint64_t appId = loadNanoapp(MakeUnique<App>());
+
   uint32_t timeOutCookie = 0xdead;
 
   chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::RANGING, false);
-  sendEventToNanoapp(app, RANGING_REQUEST, timeOutCookie);
+  sendEventToNanoapp(appId, RANGING_REQUEST, timeOutCookie);
   bool success;
   waitForEvent(RANGING_REQUEST, &success);
   EXPECT_TRUE(success);
 
-  // Add 1 second to prevent race condition
-  constexpr uint8_t kWifiRequestRangingTimeoutSec =
-      (CHRE_TEST_WIFI_RANGING_RESULT_TIMEOUT_NS / CHRE_NSEC_PER_SEC) + 1;
-  std::this_thread::sleep_for(
-      std::chrono::seconds(kWifiRequestRangingTimeoutSec));
+  waitForEvent(REQUEST_TIMED_OUT);
 
   // Make sure that we can still request ranging after a timedout request
   uint32_t successCookie = 0x0101;
   chrePalWifiEnableResponse(PalWifiAsyncRequestTypes::RANGING, true);
-  sendEventToNanoapp(app, RANGING_REQUEST, successCookie);
+  sendEventToNanoapp(appId, RANGING_REQUEST, successCookie);
   waitForEvent(RANGING_REQUEST, &success);
   EXPECT_TRUE(success);
 
@@ -275,7 +463,7 @@
   waitForEvent(CHRE_EVENT_WIFI_ASYNC_RESULT, &cookie);
   EXPECT_EQ(cookie, successCookie);
 
-  unloadNanoapp(app);
+  unloadNanoapp(appId);
 }
 
 }  // namespace
diff --git a/util/dynamic_vector_base.cc b/util/dynamic_vector_base.cc
index ee7570b..23cc120 100644
--- a/util/dynamic_vector_base.cc
+++ b/util/dynamic_vector_base.cc
@@ -35,8 +35,10 @@
   if (!success) {
     void *newData = memoryAlloc(newCapacity * elementSize);
     if (newData != nullptr) {
-      memcpy(newData, mData, mSize * elementSize);
-      memoryFree(mData);
+      if (mData != nullptr) {
+        memcpy(newData, mData, mSize * elementSize);
+        memoryFree(mData);
+      }
       mData = newData;
       mCapacity = newCapacity;
       success = true;
diff --git a/util/include/chre/util/array_queue_impl.h b/util/include/chre/util/array_queue_impl.h
index c2982f4..0172c0e 100644
--- a/util/include/chre/util/array_queue_impl.h
+++ b/util/include/chre/util/array_queue_impl.h
@@ -132,8 +132,7 @@
 template <typename ElementType, typename StorageType>
 void ArrayQueueCore<ElementType, StorageType>::pop_back() {
   if (mSize > 0) {
-    size_t absoluteIndex = relativeIndexToAbsolute(mSize - 1);
-    StorageType::data()[absoluteIndex].~ElementType();
+    StorageType::data()[mTail].~ElementType();
     pullTail();
   }
 }
diff --git a/util/include/chre/util/dynamic_vector_impl.h b/util/include/chre/util/dynamic_vector_impl.h
index b322198..4ce6e7d 100644
--- a/util/include/chre/util/dynamic_vector_impl.h
+++ b/util/include/chre/util/dynamic_vector_impl.h
@@ -189,9 +189,11 @@
     ElementType *newData = static_cast<ElementType *>(
         memoryAlloc(newCapacity * sizeof(ElementType)));
     if (newData != nullptr) {
-      uninitializedMoveOrCopy(data(), mSize, newData);
-      destroy(data(), mSize);
-      memoryFree(data());
+      if (data() != nullptr) {
+        uninitializedMoveOrCopy(data(), mSize, newData);
+        destroy(data(), mSize);
+        memoryFree(data());
+      }
       mData = newData;
       mCapacity = newCapacity;
       success = true;
diff --git a/util/include/chre/util/intrusive_list_base.h b/util/include/chre/util/intrusive_list_base.h
index 212b524..76c8909 100644
--- a/util/include/chre/util/intrusive_list_base.h
+++ b/util/include/chre/util/intrusive_list_base.h
@@ -53,7 +53,7 @@
   IntrusiveListBase() {
     mSentinelNode.next = &mSentinelNode;
     mSentinelNode.prev = &mSentinelNode;
-  };
+  }
 
   /**
    * Link a new node to the end of the linked list.
diff --git a/util/include/chre/util/macros.h b/util/include/chre/util/macros.h
index 4f4877c..f6de181 100644
--- a/util/include/chre/util/macros.h
+++ b/util/include/chre/util/macros.h
@@ -63,6 +63,50 @@
 #define MAX(a, b) ((a) > (b) ? (a) : (b))
 #endif
 
+/**
+ * Obtain the number of arguments passed into a macro up till 20 args
+ */
+#define VA_NUM_ARGS(...)                                                       \
+  VA_NUM_ARGS_IMPL(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, \
+                   8, 7, 6, 5, 4, 3, 2, 1)
+#define VA_NUM_ARGS_IMPL(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, \
+                         _13, _14_, _15, _16, _17, _18, _19, _20, N, ...)   \
+  N
+
+/**
+ * Concats two preprocessor tokens together.
+ * If passed in args are macros, they are resolved first, then concat'd
+ */
+#define MACRO_CONCAT2(x, y) x##y
+#define MACRO_CONCAT(x, y) MACRO_CONCAT2(x, y)
+
+/**
+ * Get a list of types of the passed in parameters to TYPE_LIST(...)
+ */
+#define TYPE_LIST_1(a) decltype(a)
+#define TYPE_LIST_2(a, ...) decltype(a), TYPE_LIST_1(__VA_ARGS__)
+#define TYPE_LIST_3(a, ...) decltype(a), TYPE_LIST_2(__VA_ARGS__)
+#define TYPE_LIST_4(a, ...) decltype(a), TYPE_LIST_3(__VA_ARGS__)
+#define TYPE_LIST_5(a, ...) decltype(a), TYPE_LIST_4(__VA_ARGS__)
+#define TYPE_LIST_6(a, ...) decltype(a), TYPE_LIST_5(__VA_ARGS__)
+#define TYPE_LIST_7(a, ...) decltype(a), TYPE_LIST_6(__VA_ARGS__)
+#define TYPE_LIST_8(a, ...) decltype(a), TYPE_LIST_7(__VA_ARGS__)
+#define TYPE_LIST_9(a, ...) decltype(a), TYPE_LIST_8(__VA_ARGS__)
+#define TYPE_LIST_10(a, ...) decltype(a), TYPE_LIST_9(__VA_ARGS__)
+#define TYPE_LIST_11(a, ...) decltype(a), TYPE_LIST_10(__VA_ARGS__)
+#define TYPE_LIST_12(a, ...) decltype(a), TYPE_LIST_11(__VA_ARGS__)
+#define TYPE_LIST_13(a, ...) decltype(a), TYPE_LIST_12(__VA_ARGS__)
+#define TYPE_LIST_14(a, ...) decltype(a), TYPE_LIST_13(__VA_ARGS__)
+#define TYPE_LIST_15(a, ...) decltype(a), TYPE_LIST_14(__VA_ARGS__)
+#define TYPE_LIST_16(a, ...) decltype(a), TYPE_LIST_15(__VA_ARGS__)
+#define TYPE_LIST_17(a, ...) decltype(a), TYPE_LIST_16(__VA_ARGS__)
+#define TYPE_LIST_18(a, ...) decltype(a), TYPE_LIST_17(__VA_ARGS__)
+#define TYPE_LIST_19(a, ...) decltype(a), TYPE_LIST_18(__VA_ARGS__)
+#define TYPE_LIST_20(a, ...) decltype(a), TYPE_LIST_19(__VA_ARGS__)
+
+#define TYPE_LIST(...) \
+  MACRO_CONCAT(TYPE_LIST_, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)
+
 // Compiler-specific functionality
 #if defined(__clang__) || defined(__GNUC__)
 
diff --git a/util/include/chre/util/nanoapp/app_id.h b/util/include/chre/util/nanoapp/app_id.h
index edc9803..2ec40c7 100644
--- a/util/include/chre/util/nanoapp/app_id.h
+++ b/util/include/chre/util/nanoapp/app_id.h
@@ -50,7 +50,7 @@
 }
 
 /**
- * @return App ID combining the given 3-byte app number and Google's 5-byte
+ * @return App ID combining the given 3-byte app number and the example 5-byte
  *         vendor ID
  */
 constexpr uint64_t makeExampleNanoappId(uint32_t appNumber) {
@@ -67,8 +67,8 @@
 
 // clang-format off
 constexpr uint64_t kHelloWorldAppId       = makeExampleNanoappId(1);
-constexpr uint64_t kMessageWorldAppId     = makeExampleNanoappId(2);
-constexpr uint64_t kTimerWorldAppId       = makeExampleNanoappId(3);
+constexpr uint64_t kTimerWorldAppId       = makeExampleNanoappId(2);
+constexpr uint64_t kMessageWorldAppId     = makeExampleNanoappId(3);
 constexpr uint64_t kSensorWorldAppId      = makeExampleNanoappId(4);
 constexpr uint64_t kGnssWorldAppId        = makeExampleNanoappId(5);
 constexpr uint64_t kWifiWorldAppId        = makeExampleNanoappId(6);
diff --git a/util/include/chre/util/nanoapp/ble.h b/util/include/chre/util/nanoapp/ble.h
index c00ce30..ba9e444 100644
--- a/util/include/chre/util/nanoapp/ble.h
+++ b/util/include/chre/util/nanoapp/ble.h
@@ -87,6 +87,15 @@
                                         chreBleGenericFilter *genericFilters,
                                         uint8_t numGenericFilters);
 
+/**
+ * Similar to createBleScanFilterForKnownBeacons but creates a
+ * chreBleScanFilterV1_9 instead of a chreBleScanFilter. The
+ * broadcasterAddressFilters are set to empty.
+ */
+bool createBleScanFilterForKnownBeaconsV1_9(
+    struct chreBleScanFilterV1_9 &filter, chreBleGenericFilter *genericFilters,
+    uint8_t numGenericFilters);
+
 }  // namespace chre
 
 #endif  // CHRE_UTIL_NANOAPP_BLE_H_
diff --git a/util/include/chre/util/nanoapp/math.h b/util/include/chre/util/nanoapp/math.h
index 8346bb8..bfcdb9e 100644
--- a/util/include/chre/util/nanoapp/math.h
+++ b/util/include/chre/util/nanoapp/math.h
@@ -26,4 +26,7 @@
 //! The constant pi in single-precision floating point format.
 #define CHRE_PI_F (3.14159265358979f)
 
+//! The constant pi in double-precision floating point format.
+#define CHRE_PI_D (3.1415926535897932)
+
 #endif  // CHRE_UTIL_NANOAPP_MATH_H_
diff --git a/util/include/chre/util/nanoapp/string.h b/util/include/chre/util/nanoapp/string.h
new file mode 100644
index 0000000..4b277e9
--- /dev/null
+++ b/util/include/chre/util/nanoapp/string.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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 CHRE_UTIL_NANOAPP_STRING_H_
+#define CHRE_UTIL_NANOAPP_STRING_H_
+
+#include <cstdint>
+
+#include "chre_api/chre.h"
+
+namespace chre {
+
+/**
+ * Copies a null-terminated string from source to destination up to the length
+ * of the source string or the destinationBufferSize - 1. Pads with null
+ * characters.
+ *
+ * @param destination               the destination null-terminated string.
+ * @param source                    the source null-terminated string.
+ * @param destinationBufferSize     the size of the destination buffer.
+ */
+void copyString(char *destination, const char *source,
+                size_t destinationBufferSize);
+
+}  // namespace chre
+
+#endif  // CHRE_UTIL_NANOAPP_STRING_H_
diff --git a/util/include/chre/util/pigweed/chre_channel_output.h b/util/include/chre/util/pigweed/chre_channel_output.h
index 9f815b8..69c7f60 100644
--- a/util/include/chre/util/pigweed/chre_channel_output.h
+++ b/util/include/chre/util/pigweed/chre_channel_output.h
@@ -32,47 +32,26 @@
  */
 struct ChrePigweedNanoappMessage {
   size_t msgSize;
-  void *msg;
-};
-
-/**
- * ChannelOutput that can be used for nanoapps wishing to utilize
- * pw::rpc::Server and pw::rpc::Client for RPC communication between other
- * nanoapps and Android app host clients.
- */
-class ChreChannelOutputBase : public pw::rpc::ChannelOutput {
- public:
-  // Random value chosen that matches Java client util, but is random enough
-  // to not conflict with other CHRE messages the nanoapp and client may send.
-  static constexpr uint32_t PW_RPC_CHRE_HOST_MESSAGE_TYPE = INT32_MAX - 10;
-
-  // Random values chosen to be towards the end of the nanoapp event type region
-  // so it doesn't conflict with existing nanoapp messages that can be sent.
-  static constexpr uint16_t PW_RPC_CHRE_NAPP_REQUEST_EVENT_TYPE =
-      UINT16_MAX - 10;
-  static constexpr uint16_t PW_RPC_CHRE_NAPP_RESPONSE_EVENT_TYPE =
-      UINT16_MAX - 9;
-
-  size_t MaximumTransmissionUnit() override;
-
- protected:
-  ChreChannelOutputBase();
+  uint8_t msg[];
 };
 
 /**
  * Channel output that must be used on the server side of the channel between
  * two nanoapps.
  */
-class ChreServerNanoappChannelOutput : public ChreChannelOutputBase {
+class ChreServerNanoappChannelOutput : public pw::rpc::ChannelOutput {
  public:
   explicit ChreServerNanoappChannelOutput(RpcPermission &permission)
-      : mPermission(permission) {}
+      : ChannelOutput("CHRE"), mPermission(permission) {}
+
   /**
    * Sets the nanoapp instance ID that is being communicated with over this
    * channel output.
    */
   void setClient(uint32_t nanoappInstanceId);
 
+  size_t MaximumTransmissionUnit() override;
+
   pw::Status Send(pw::span<const std::byte> buffer) override;
 
  private:
@@ -84,8 +63,10 @@
  * Channel output that must be used on the client side of the channel between
  * two nanoapps.
  */
-class ChreClientNanoappChannelOutput : public ChreChannelOutputBase {
+class ChreClientNanoappChannelOutput : public pw::rpc::ChannelOutput {
  public:
+  ChreClientNanoappChannelOutput() : ChannelOutput("CHRE") {}
+
   /**
    * Sets the server instance ID.
    *
@@ -95,6 +76,8 @@
    */
   void setServer(uint32_t instanceId);
 
+  size_t MaximumTransmissionUnit() override;
+
   pw::Status Send(pw::span<const std::byte> buffer) override;
 
  private:
@@ -105,15 +88,18 @@
  * Channel output that must be used if the channel is between a nanoapp and
  * host client.
  */
-class ChreServerHostChannelOutput : public ChreChannelOutputBase {
+class ChreServerHostChannelOutput : public pw::rpc::ChannelOutput {
  public:
   explicit ChreServerHostChannelOutput(RpcPermission &permission)
-      : mPermission(permission) {}
+      : ChannelOutput("CHRE"), mPermission(permission) {}
+
   /**
    * Sets the host endpoint being communicated with.
    */
   void setHostEndpoint(uint16_t hostEndpoint);
 
+  size_t MaximumTransmissionUnit() override;
+
   pw::Status Send(pw::span<const std::byte> buffer) override;
 
  private:
diff --git a/util/include/chre/util/pigweed/rpc_client.h b/util/include/chre/util/pigweed/rpc_client.h
index 17756dc..4920b64 100644
--- a/util/include/chre/util/pigweed/rpc_client.h
+++ b/util/include/chre/util/pigweed/rpc_client.h
@@ -46,15 +46,11 @@
   explicit RpcClient(uint64_t serverNanoappId)
       : mServerNanoappId((serverNanoappId)) {}
 
-  ~RpcClient() {
-    chreConfigureNanoappInfoEvents(false);
-  }
-
   /**
    * Handles events related to RPC services.
    *
    * Handles the following events:
-   * - PW_RPC_CHRE_NAPP_RESPONSE_EVENT_TYPE: handle the server responses,
+   * - CHRE_EVENT_RPC_RESPONSE: handle the server responses,
    * - CHRE_EVENT_NANOAPP_STOPPED: close the channel when the server nanoapp
    *   terminates.
    *
@@ -87,12 +83,17 @@
    */
   bool hasService(uint64_t id, uint32_t version);
 
+  /**
+   * Must be called from nanoapp end.
+   */
+  void close();
+
  private:
   /**
    * Handles responses from the server.
    *
-   * This method must be called when nanoapps receive a
-   * PW_RPC_CHRE_NAPP_RESPONSE_EVENT_TYPE event.
+   * This method must be called when nanoapps receive a CHRE_EVENT_RPC_RESPONSE
+   * event.
    *
    * @param senderInstanceId The Instance ID for the source of this event.
    * @param eventData  The associated data, if any.
diff --git a/util/include/chre/util/pigweed/rpc_common.h b/util/include/chre/util/pigweed/rpc_common.h
new file mode 100644
index 0000000..ee9096d
--- /dev/null
+++ b/util/include/chre/util/pigweed/rpc_common.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 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 CHRE_UTIL_PIGWEED_RPC_COMMON_H_
+#define CHRE_UTIL_PIGWEED_RPC_COMMON_H_
+
+#include <cstdint>
+
+/**
+ * @file
+ * Common definitions across nanoapp and host sides.
+ */
+
+namespace chre {
+/** The upper 16b of a channel ID are set to 1 for host clients. */
+constexpr uint32_t kChannelIdHostClient = 1 << 16;
+
+/** Mask to extract the host ID / nanoapp ID from a channel ID. */
+constexpr uint32_t kRpcClientIdMask = 0xffff;
+
+/** Maximum ID for a nanoapp as the value is encoded on 16b. */
+constexpr uint32_t kRpcNanoappMaxId = 0xffff;
+
+}  // namespace chre
+
+#endif  // CHRE_UTIL_PIGWEED_RPC_COMMON_H_
\ No newline at end of file
diff --git a/util/include/chre/util/pigweed/rpc_helper.h b/util/include/chre/util/pigweed/rpc_helper.h
index 032693a..08bd2bf 100644
--- a/util/include/chre/util/pigweed/rpc_helper.h
+++ b/util/include/chre/util/pigweed/rpc_helper.h
@@ -19,19 +19,11 @@
 
 #include <cstdint>
 
+#include "chre/util/pigweed/rpc_common.h"
 #include "chre_api/chre.h"
 
 namespace chre {
 
-/** The upper 16b of a channel ID are set to 1 for host clients. */
-constexpr uint32_t kChannelIdHostClient = 1 << 16;
-
-/** Mask to extract the host ID / nanoapp ID from a channel ID. */
-constexpr uint32_t kRpcClientIdMask = 0xffff;
-
-/** Maximum ID for a nanoapp as the value is encoded on 16b. */
-constexpr uint32_t kRpcNanoappMaxId = 0xffff;
-
 /**
  * Returns whether the endpoint matches.
  *
diff --git a/util/include/chre/util/pigweed/rpc_server.h b/util/include/chre/util/pigweed/rpc_server.h
index df02621..ec0f86d 100644
--- a/util/include/chre/util/pigweed/rpc_server.h
+++ b/util/include/chre/util/pigweed/rpc_server.h
@@ -58,7 +58,6 @@
   };
 
   RpcServer() : mHostOutput(mPermission), mNanoappOutput(mPermission) {}
-  ~RpcServer();
 
   /**
    * Registers services to the server and to CHRE.
@@ -101,7 +100,7 @@
    *
    * Handles the following events:
    * - CHRE_EVENT_MESSAGE_FROM_HOST: respond to host RPC requests,
-   * - PW_RPC_CHRE_NAPP_REQUEST_EVENT_TYPE: respond to nanoapp RPC requests,
+   * - CHRE_EVENT_RPC_REQUEST: respond to nanoapp RPC requests,
    * - CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION: close the channel when the host
    *   terminates,
    * - CHRE_EVENT_NANOAPP_STOPPED: close the channel when a nanoapp
@@ -116,6 +115,13 @@
   bool handleEvent(uint32_t senderInstanceId, uint16_t eventType,
                    const void *eventData);
 
+  /**
+   * Close all connections to the server.
+   *
+   * Must be called from the nanoapp end.
+   */
+  void close();
+
  private:
   /**
    * Handles messages from host clients.
@@ -131,8 +137,8 @@
   /**
    * Handles messages from nanoapp clients.
    *
-   * This method must be called when nanoapps receive a
-   * PW_RPC_CHRE_NAPP_REQUEST_EVENT_TYPE event.
+   * This method must be called when nanoapps receive a CHRE_EVENT_RPC_REQUEST
+   * event.
    *
    * @param eventData  The associated data, if any.
    * @return whether the RPC was handled successfully.
diff --git a/util/include/chre/util/segmented_queue.h b/util/include/chre/util/segmented_queue.h
index d3d7fb5..025d2f0 100644
--- a/util/include/chre/util/segmented_queue.h
+++ b/util/include/chre/util/segmented_queue.h
@@ -92,7 +92,7 @@
    */
   bool empty() const {
     return mSize == 0;
-  };
+  }
 
   /**
    * Push a element to the end of the segmented queue.
@@ -403,4 +403,4 @@
 
 #include "chre/util/segmented_queue_impl.h"
 
-#endif  // CHRE_UTIL_SEGMENTED_QUEUE_H_
\ No newline at end of file
+#endif  // CHRE_UTIL_SEGMENTED_QUEUE_H_
diff --git a/util/include/chre/util/segmented_queue_impl.h b/util/include/chre/util/segmented_queue_impl.h
index 8bd1645..049ab61 100644
--- a/util/include/chre/util/segmented_queue_impl.h
+++ b/util/include/chre/util/segmented_queue_impl.h
@@ -20,9 +20,8 @@
 #include <type_traits>
 #include <utility>
 
-#include "chre/util/segmented_queue.h"
-
 #include "chre/util/container_support.h"
+#include "chre/util/segmented_queue.h"
 
 namespace chre {
 
@@ -205,11 +204,29 @@
     return;
   }
 
-  // TODO(b/264326627): Check if gapIndices is reverse order.
-  // TODO(b/264326627): Give a detailed explanation (example)\.
   // Move the elements between each gap indices section by section from the
   // section that is closest to the head. The destination index = the gap index
   // - how many gaps has been filled.
+  //
+  // For instance, assuming we have elements that we want to remove (gaps) at
+  // these indices = [8, 7, 5, 2] and the last element is at index 10.
+  //
+  // The first iteration will move the items at index 3, 4, which is the first
+  // section, to index 2, 3 and overwrite the original item at index 2, making
+  // the queue: [0, 1, 3, 4, x, 5, 6, ...., 10] where x means empty slot.
+  //
+  // The second iteration will do a similar thing, move item 6 to the empty
+  // slot, which could be calculated by using the index of the last gap and how
+  // many gaps has been filled. So the queue turns into:
+  // [0, 1, 3, 4, 6, x, x, 7, 8, 9, 10], note that there are now two empty slots
+  // since there are two gaps filled.
+  //
+  // The third iteration does not move anything since there are no items between
+  // 7 and 8.
+  //
+  // The final iteration is a special case to close the final gap. After the
+  // final iteration, the queue will become: [1, 3, 4, 6, 9, 10].
+
   for (size_t i = gapCount - 1; i > 0; --i) {
     moveElements(advanceOrWrapAround(gapIndices[i]),
                  subtractOrWrapAround(gapIndices[i], gapCount - 1 - i),
diff --git a/util/include/chre/util/synchronized_expandable_memory_pool_impl.h b/util/include/chre/util/synchronized_expandable_memory_pool_impl.h
index 8b58ce7..8e20913 100644
--- a/util/include/chre/util/synchronized_expandable_memory_pool_impl.h
+++ b/util/include/chre/util/synchronized_expandable_memory_pool_impl.h
@@ -17,6 +17,8 @@
 #ifndef CHRE_UTIL_SYNCHRONIZED_EXPANDABLE_MEMORY_POOL_IMPL_H_
 #define CHRE_UTIL_SYNCHRONIZED_EXPANDABLE_MEMORY_POOL_IMPL_H_
 
+#include <algorithm>
+
 #include "chre/util/lock_guard.h"
 #include "chre/util/memory_pool.h"
 #include "chre/util/synchronized_expandable_memory_pool.h"
diff --git a/util/include/chre/util/synchronized_memory_pool.h b/util/include/chre/util/synchronized_memory_pool.h
index bce3886..2c6245b 100644
--- a/util/include/chre/util/synchronized_memory_pool.h
+++ b/util/include/chre/util/synchronized_memory_pool.h
@@ -38,7 +38,7 @@
    *         fails.
    */
   template <typename... Args>
-  ElementType *allocate(Args &&... args);
+  ElementType *allocate(Args &&...args);
 
   /**
    * Releases the memory of a previously allocated element. The pointer provided
diff --git a/util/include/chre/util/synchronized_memory_pool_impl.h b/util/include/chre/util/synchronized_memory_pool_impl.h
index 9d11718..526c3f9 100644
--- a/util/include/chre/util/synchronized_memory_pool_impl.h
+++ b/util/include/chre/util/synchronized_memory_pool_impl.h
@@ -25,7 +25,7 @@
 template <typename ElementType, size_t kSize>
 template <typename... Args>
 ElementType *SynchronizedMemoryPool<ElementType, kSize>::allocate(
-    Args &&... args) {
+    Args &&...args) {
   LockGuard<Mutex> lock(mMutex);
   return mMemoryPool.allocate(args...);
 }
diff --git a/util/include/chre/util/system/ble_util.h b/util/include/chre/util/system/ble_util.h
new file mode 100644
index 0000000..5072008
--- /dev/null
+++ b/util/include/chre/util/system/ble_util.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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 CHRE_UTIL_SYSTEM_BLE_UTIL_H_
+#define CHRE_UTIL_SYSTEM_BLE_UTIL_H_
+
+#include "chre_api/chre.h"
+
+namespace chre {
+
+/**
+ * Populates a legacy chreBleAdvertisingReport's fields with values from the
+ * payload. The chreBleAdvertisingReport is based on the LE Extended Advertising
+ * Report Event defined in the BT Core Spec v5.3, Vol 4, Part E,
+ * Section 7.7.65.13. But for legacy LE Advertising Report Events (BT Core Spec
+ * v5.3, Vol 4, Part E, Section 7.7.65.2), some of these fields are only
+ * included in the payload. We parse out these fields to make it easier for the
+ * nanoapp to access this data.
+ *
+ * @param report CHRE BLE Advertising Report
+ */
+void populateLegacyAdvertisingReportFields(chreBleAdvertisingReport &report);
+
+}  // namespace chre
+
+#endif  // CHRE_UTIL_SYSTEM_BLE_UTIL_H_
diff --git a/util/include/chre/util/system/stats_container.h b/util/include/chre/util/system/stats_container.h
index d2e18d4..4a5e404 100644
--- a/util/include/chre/util/system/stats_container.h
+++ b/util/include/chre/util/system/stats_container.h
@@ -41,7 +41,7 @@
    * it should not be bigger than the default value to prevent rounding to 0
    */
   StatsContainer(uint32_t averageWindow_ = 512)
-      : mAverageWindow(averageWindow_){};
+      : mAverageWindow(averageWindow_) {}
 
   /**
    * Add a new value to the metric collection and update mean/max value
@@ -68,14 +68,14 @@
    */
   T getMean() const {
     return mMean;
-  };
+  }
 
   /**
    * @return the max value
    */
   T getMax() const {
     return mMax;
-  };
+  }
 
   /**
    * @return the average window
@@ -97,4 +97,4 @@
 
 }  // namespace chre
 
-#endif  // CHRE_UTIL_SYSTEM_STATS_CONTAINER_H_
\ No newline at end of file
+#endif  // CHRE_UTIL_SYSTEM_STATS_CONTAINER_H_
diff --git a/util/include/chre/util/time.h b/util/include/chre/util/time.h
index b3fef77..a0e210b 100644
--- a/util/include/chre/util/time.h
+++ b/util/include/chre/util/time.h
@@ -21,6 +21,9 @@
 
 namespace chre {
 
+//! The number of seconds in one day.
+constexpr uint64_t kOneDayInSeconds(60 * 60 * 24);
+
 //! The number of milliseconds in one min.
 constexpr uint64_t kOneMinuteInMilliseconds(60000);
 
diff --git a/util/nanoapp/ble.cc b/util/nanoapp/ble.cc
index 09de5c4..50e0fb7 100644
--- a/util/nanoapp/ble.cc
+++ b/util/nanoapp/ble.cc
@@ -57,4 +57,28 @@
   return true;
 }
 
+bool createBleScanFilterForKnownBeaconsV1_9(
+    struct chreBleScanFilterV1_9 &filter, chreBleGenericFilter *genericFilters,
+    uint8_t numGenericFilters) {
+  if (numGenericFilters < kNumScanFilters) {
+    return false;
+  }
+  memset(&filter, 0, sizeof(filter));
+
+  genericFilters[0] = createBleGenericFilter(
+      CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE, kNumScanFilters,
+      kGoogleEddystoneUuid, kGoogleUuidMask);
+  genericFilters[1] = createBleGenericFilter(
+      CHRE_BLE_AD_TYPE_SERVICE_DATA_WITH_UUID_16_LE, kNumScanFilters,
+      kGoogleNearbyFastpairUuid, kGoogleUuidMask);
+
+  filter.rssiThreshold = kRssiThreshold;
+  filter.genericFilterCount = kNumScanFilters;
+  filter.genericFilters = genericFilters;
+
+  filter.broadcasterAddressFilterCount = 0;
+  filter.broadcasterAddressFilters = nullptr;
+  return true;
+}
+
 }  // namespace chre
diff --git a/util/nanoapp/string.cc b/util/nanoapp/string.cc
new file mode 100644
index 0000000..3d160ad
--- /dev/null
+++ b/util/nanoapp/string.cc
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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 "chre/util/nanoapp/string.h"
+#include "chre/util/nanoapp/assert.h"
+
+namespace chre {
+
+void copyString(char *destination, const char *source,
+                size_t destinationBufferSize) {
+  CHRE_ASSERT_NOT_NULL(destination);
+  CHRE_ASSERT_NOT_NULL(source);
+
+  if (destinationBufferSize == 0) {
+    return;
+  }
+
+  uint32_t i;
+  for (i = 0; i < destinationBufferSize - 1 && source[i] != '\0'; ++i) {
+    destination[i] = source[i];
+  }
+
+  memset(&destination[i], 0, destinationBufferSize - i);
+}
+
+}  // namespace chre
diff --git a/util/pigweed/chre_channel_output.cc b/util/pigweed/chre_channel_output.cc
index f88e520..9fdce47 100644
--- a/util/pigweed/chre_channel_output.cc
+++ b/util/pigweed/chre_channel_output.cc
@@ -50,7 +50,6 @@
     }
 
     data->msgSize = buffer.size();
-    data->msg = &data[1];
     memcpy(data->msg, buffer.data(), buffer.size());
 
     if (!chreSendEvent(eventType, data, nappMessageFreeCb, targetInstanceId)) {
@@ -63,12 +62,6 @@
 
 }  // namespace
 
-ChreChannelOutputBase::ChreChannelOutputBase() : ChannelOutput("CHRE") {}
-
-size_t ChreChannelOutputBase::MaximumTransmissionUnit() {
-  return CHRE_MESSAGE_TO_HOST_MAX_SIZE - sizeof(ChrePigweedNanoappMessage);
-}
-
 void ChreServerNanoappChannelOutput::setClient(uint32_t nanoappInstanceId) {
   CHRE_ASSERT(nanoappInstanceId <= kRpcNanoappMaxId);
   if (nanoappInstanceId <= kRpcNanoappMaxId) {
@@ -78,14 +71,17 @@
   }
 }
 
+size_t ChreServerNanoappChannelOutput::MaximumTransmissionUnit() {
+  return CHRE_MESSAGE_TO_HOST_MAX_SIZE - sizeof(ChrePigweedNanoappMessage);
+}
+
 pw::Status ChreServerNanoappChannelOutput::Send(
     pw::span<const std::byte> buffer) {
   // The permission is not enforced across nanoapps but we still need to
   // reset the value as it is only applicable to the next message.
   mPermission.getAndReset();
 
-  return sendToNanoapp(mClientInstanceId, PW_RPC_CHRE_NAPP_RESPONSE_EVENT_TYPE,
-                       buffer);
+  return sendToNanoapp(mClientInstanceId, CHRE_EVENT_RPC_RESPONSE, buffer);
 }
 
 void ChreClientNanoappChannelOutput::setServer(uint32_t instanceId) {
@@ -97,16 +93,23 @@
   }
 }
 
+size_t ChreClientNanoappChannelOutput::MaximumTransmissionUnit() {
+  return CHRE_MESSAGE_TO_HOST_MAX_SIZE - sizeof(ChrePigweedNanoappMessage);
+}
+
 pw::Status ChreClientNanoappChannelOutput::Send(
     pw::span<const std::byte> buffer) {
-  return sendToNanoapp(mServerInstanceId, PW_RPC_CHRE_NAPP_REQUEST_EVENT_TYPE,
-                       buffer);
+  return sendToNanoapp(mServerInstanceId, CHRE_EVENT_RPC_REQUEST, buffer);
 }
 
 void ChreServerHostChannelOutput::setHostEndpoint(uint16_t hostEndpoint) {
   mEndpointId = hostEndpoint;
 }
 
+size_t ChreServerHostChannelOutput::MaximumTransmissionUnit() {
+  return CHRE_MESSAGE_TO_HOST_MAX_SIZE - sizeof(ChrePigweedNanoappMessage);
+}
+
 pw::Status ChreServerHostChannelOutput::Send(pw::span<const std::byte> buffer) {
   CHRE_ASSERT(mEndpointId != CHRE_HOST_ENDPOINT_UNSPECIFIED);
   pw::Status returnCode = PW_STATUS_OK;
@@ -119,7 +122,7 @@
     } else {
       memcpy(data, buffer.data(), buffer.size());
       if (!chreSendMessageWithPermissions(
-              data, buffer.size(), PW_RPC_CHRE_HOST_MESSAGE_TYPE, mEndpointId,
+              data, buffer.size(), CHRE_MESSAGE_TYPE_RPC, mEndpointId,
               permission, heapFreeMessageCallback)) {
         returnCode = PW_STATUS_INVALID_ARGUMENT;
       }
diff --git a/util/pigweed/rpc_client.cc b/util/pigweed/rpc_client.cc
index 72c034a..6cf8462 100644
--- a/util/pigweed/rpc_client.cc
+++ b/util/pigweed/rpc_client.cc
@@ -33,7 +33,7 @@
 bool RpcClient::handleEvent(uint32_t senderInstanceId, uint16_t eventType,
                             const void *eventData) {
   switch (eventType) {
-    case chre::ChreChannelOutputBase::PW_RPC_CHRE_NAPP_RESPONSE_EVENT_TYPE:
+    case CHRE_EVENT_RPC_RESPONSE:
       return handleMessageFromServer(senderInstanceId, eventData);
     case CHRE_EVENT_NANOAPP_STOPPED:
       handleNanoappStopped(eventData);
@@ -58,10 +58,15 @@
   return false;
 }
 
+void RpcClient::close() {
+  chreConfigureNanoappInfoEvents(false);
+}
+
 bool RpcClient::handleMessageFromServer(uint32_t senderInstanceId,
                                         const void *eventData) {
   auto data = static_cast<const chre::ChrePigweedNanoappMessage *>(eventData);
-  pw::span packet(static_cast<const std::byte *>(data->msg), data->msgSize);
+  pw::span packet(reinterpret_cast<const std::byte *>(data->msg),
+                  data->msgSize);
   struct chreNanoappInfo info;
 
   if (!chreGetNanoappInfoByAppId(mServerNanoappId, &info) ||
diff --git a/util/pigweed/rpc_server.cc b/util/pigweed/rpc_server.cc
index 1ee225a..0bf7d67 100644
--- a/util/pigweed/rpc_server.cc
+++ b/util/pigweed/rpc_server.cc
@@ -29,15 +29,6 @@
 
 namespace chre {
 
-RpcServer::~RpcServer() {
-  chreConfigureNanoappInfoEvents(false);
-  // TODO(b/251257328): Disable all notifications at once.
-  while (!mConnectedHosts.empty()) {
-    chreConfigureHostEndpointNotifications(mConnectedHosts[0], false);
-    mConnectedHosts.erase(0);
-  }
-}
-
 bool RpcServer::registerServices(size_t numServices,
                                  RpcServer::Service *services) {
   // Avoid blowing up the stack with chreServices.
@@ -77,7 +68,7 @@
   switch (eventType) {
     case CHRE_EVENT_MESSAGE_FROM_HOST:
       return handleMessageFromHost(eventData);
-    case ChreChannelOutputBase::PW_RPC_CHRE_NAPP_REQUEST_EVENT_TYPE:
+    case CHRE_EVENT_RPC_REQUEST:
       return handleMessageFromNanoapp(senderInstanceId, eventData);
     case CHRE_EVENT_HOST_ENDPOINT_NOTIFICATION:
       handleHostClientNotification(eventData);
@@ -90,11 +81,19 @@
   }
 }
 
+void RpcServer::close() {
+  chreConfigureNanoappInfoEvents(false);
+  // TODO(b/251257328): Disable all notifications at once.
+  while (!mConnectedHosts.empty()) {
+    chreConfigureHostEndpointNotifications(mConnectedHosts[0], false);
+    mConnectedHosts.erase(0);
+  }
+}
+
 bool RpcServer::handleMessageFromHost(const void *eventData) {
   auto *hostMessage = static_cast<const chreMessageFromHostData *>(eventData);
 
-  if (hostMessage->messageType !=
-      ChreChannelOutputBase::PW_RPC_CHRE_HOST_MESSAGE_TYPE) {
+  if (hostMessage->messageType != CHRE_MESSAGE_TYPE_RPC) {
     return false;
   }
 
@@ -138,7 +137,8 @@
 bool RpcServer::handleMessageFromNanoapp(uint32_t senderInstanceId,
                                          const void *eventData) {
   const auto data = static_cast<const ChrePigweedNanoappMessage *>(eventData);
-  pw::span packet(static_cast<const std::byte *>(data->msg), data->msgSize);
+  pw::span packet(reinterpret_cast<const std::byte *>(data->msg),
+                  data->msgSize);
 
   pw::Result<uint32_t> result = pw::rpc::ExtractChannelId(packet);
   if (result.status() != PW_STATUS_OK) {
diff --git a/util/system/ble_util.cc b/util/system/ble_util.cc
new file mode 100644
index 0000000..eb61426
--- /dev/null
+++ b/util/system/ble_util.cc
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 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 "chre/util/system/ble_util.h"
+
+#include <cinttypes>
+
+#include "chre/platform/log.h"
+
+namespace chre {
+
+namespace {
+
+// Tx Power Level AD Type defined in the BT Core Spec v5.3 Assigned Numbers,
+// Generic Access Profile (ref:
+// https://www.bluetooth.com/specifications/assigned-numbers/).
+constexpr uint8_t kTxPowerLevelAdType = 0x0A;
+constexpr uint8_t kAdTypeOffset = 1;
+constexpr uint8_t kExpectedAdDataLength = 2;
+
+/**
+ * Gets the TX Power from the data field of legacy AD reports. This function
+ * parses the advertising data which is defined in the BT Core Spec v5.3, Vol 3,
+ * Part C, Section 11, Advertising and Scan Response Data Format, and checks for
+ * the presence of the Tx Power Level AD Type.
+ *
+ * @param data Advertising data.
+ * @param dataLength Length of advertising data.
+ * @return int8_t Tx Power value.
+ */
+int8_t getTxPowerFromLegacyReport(const uint8_t *data, size_t dataLength) {
+  size_t i = 0;
+  while (i < dataLength) {
+    uint8_t adDataLength = data[i];
+    if (adDataLength == 0 || (adDataLength >= dataLength - i)) {
+      break;
+    }
+    if (data[i + kAdTypeOffset] == kTxPowerLevelAdType &&
+        adDataLength == kExpectedAdDataLength) {
+      return static_cast<int8_t>(data[i + kExpectedAdDataLength]);
+    }
+    i += kAdTypeOffset + adDataLength;
+  }
+  return CHRE_BLE_TX_POWER_NONE;
+}
+
+}  // namespace
+
+void populateLegacyAdvertisingReportFields(chreBleAdvertisingReport &report) {
+  if ((report.eventTypeAndDataStatus & CHRE_BLE_EVENT_TYPE_FLAG_LEGACY) != 0 &&
+      report.txPower == CHRE_BLE_TX_POWER_NONE) {
+    report.txPower = getTxPowerFromLegacyReport(report.data, report.dataLength);
+  }
+}
+
+}  // namespace chre
diff --git a/util/tests/array_queue_test.cc b/util/tests/array_queue_test.cc
index fbacaed..d1e0d6d 100644
--- a/util/tests/array_queue_test.cc
+++ b/util/tests/array_queue_test.cc
@@ -103,6 +103,15 @@
   EXPECT_EQ(5, q[0]);
   EXPECT_EQ(6, q[1]);
   EXPECT_EQ(7, q[2]);
+
+  q.pop_back();
+
+  EXPECT_EQ(5, q[0]);
+  EXPECT_EQ(6, q[1]);
+
+  q.pop();
+
+  EXPECT_EQ(6, q[0]);
 }
 
 TEST(ArrayQueueTest, TestSize) {
@@ -192,12 +201,19 @@
 TEST(ArrayQueueTest, TestBack) {
   ArrayQueue<int, 3> q;
   q.push(1);
-  EXPECT_EQ(1, q.back());
-  q.pop();
+  EXPECT_EQ(1, q.back());  // 1 x x
   q.push(2);
-  EXPECT_EQ(2, q.back());
+  EXPECT_EQ(2, q.back());  // 1 2 x
+  q.pop();
+  EXPECT_EQ(2, q.back());  // x 2 x
   q.push(3);
-  EXPECT_EQ(3, q.back());
+  EXPECT_EQ(3, q.back());  // x 2 3
+  q.push(4);
+  EXPECT_EQ(4, q.back());  // 4 2 3 (forward wrap-around)
+  q.pop_back();
+  EXPECT_EQ(3, q.back());  // x 2 3 (backwards wrap-around)
+  q.pop();
+  EXPECT_EQ(3, q.back());  // x x 3
 }
 
 TEST(ArrayQueueDeathTest, InvalidSubscript) {
diff --git a/util/tests/ble_util_test.cc b/util/tests/ble_util_test.cc
new file mode 100644
index 0000000..762a6aa
--- /dev/null
+++ b/util/tests/ble_util_test.cc
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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 "gtest/gtest.h"
+
+#include <string.h>
+
+#include "chre/util/system/ble_util.h"
+
+TEST(BleUtil, PopulateTxPower) {
+  chreBleAdvertisingReport report;
+  memset(&report, 0, sizeof(report));
+  report.txPower = CHRE_BLE_TX_POWER_NONE;
+  report.eventTypeAndDataStatus = CHRE_BLE_EVENT_TYPE_FLAG_LEGACY;
+  int8_t txPower = -11;
+  uint8_t data[3] = {0x02, 0x0A, static_cast<uint8_t>(txPower)};
+  report.data = data;
+  report.dataLength = 3;
+  chre::populateLegacyAdvertisingReportFields(report);
+  EXPECT_EQ(report.txPower, txPower);
+}
diff --git a/util/tests/string_test.cc b/util/tests/string_test.cc
new file mode 100644
index 0000000..74c2df7
--- /dev/null
+++ b/util/tests/string_test.cc
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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 "gtest/gtest.h"
+
+#include "chre/util/nanoapp/string.h"
+
+using ::chre::copyString;
+
+TEST(StringDeathTest, InvalidInput) {
+  char destination[100];
+  ASSERT_DEATH(copyString(nullptr, nullptr, 0), ".*");
+  ASSERT_DEATH(copyString(nullptr, destination, 1), ".*");
+  ASSERT_DEATH(copyString(destination, nullptr, 2), ".*");
+}
+
+TEST(String, ZeroCharsToCopy) {
+  const char *source = "hello world";
+  constexpr size_t destinationLength = 100;
+  char destination[destinationLength];
+  char fillValue = 123;
+
+  memset(destination, fillValue, destinationLength);
+
+  copyString(destination, source, 0);
+  for (size_t i = 0; i < destinationLength; ++i) {
+    ASSERT_EQ(destination[i], fillValue);
+  }
+}
+
+TEST(String, EmptyStringPadsWithZeroes) {
+  const char *source = "";
+  constexpr size_t destinationLength = 100;
+  char destination[destinationLength];
+  char fillValue = 123;
+
+  memset(destination, fillValue, destinationLength);
+
+  copyString(destination, source, destinationLength);
+  for (size_t i = 0; i < destinationLength; ++i) {
+    ASSERT_EQ(destination[i], 0);
+  }
+}
+
+TEST(String, NormalCopyOneChar) {
+  const char *source = "hello world";
+  constexpr size_t destinationLength = 100;
+  char destination[destinationLength];
+  char fillValue = 123;
+
+  memset(destination, fillValue, destinationLength);
+
+  copyString(destination, source, 2);  // one char + '\0'
+  ASSERT_EQ(destination[0], source[0]);
+  ASSERT_EQ(destination[1], 0);
+  for (size_t i = 2; i < destinationLength; ++i) {
+    ASSERT_EQ(destination[i], fillValue);
+  }
+}
+
+TEST(String, NormalCopyAllChars) {
+  const char *source = "hello world";
+  constexpr size_t sourceLength = 11;
+  constexpr size_t destinationLength = 100;
+  char destination[destinationLength];
+  char fillValue = 123;
+
+  memset(destination, fillValue, destinationLength);
+
+  copyString(destination, source, sourceLength + 1);  // account for '\0'
+  size_t i = 0;
+  for (; i < sourceLength; ++i) {
+    ASSERT_EQ(destination[i], source[i]);
+  }
+
+  ASSERT_EQ(destination[i], 0);
+  ++i;
+
+  for (; i < destinationLength; ++i) {
+    ASSERT_EQ(destination[i], fillValue);
+  }
+}
+
+TEST(String, NormalCopyGreaterThanSourceLength) {
+  const char *source = "hello world";
+  constexpr size_t sourceLength = 11;
+  constexpr size_t destinationLength = 100;
+  char destination[destinationLength];
+  char fillValue = 123;
+
+  memset(destination, fillValue, destinationLength);
+
+  copyString(destination, source, destinationLength);
+  size_t i = 0;
+  for (; i < sourceLength; ++i) {
+    ASSERT_EQ(destination[i], source[i]);
+  }
+
+  for (; i < destinationLength; ++i) {
+    ASSERT_EQ(destination[i], 0);
+  }
+}
+
+TEST(String, NormalCopyLessThanSourceLength) {
+  const char *source = "hello world";
+  constexpr size_t sourceLength = 11;
+  constexpr size_t destinationLength = 5;
+  char destination[destinationLength];
+  char fillValue = 123;
+
+  memset(destination, fillValue, destinationLength);
+
+  copyString(destination, source, destinationLength);
+  size_t i = 0;
+  for (; i < destinationLength - 1; ++i) {
+    ASSERT_EQ(destination[i], source[i]);
+  }
+  ASSERT_EQ(destination[i], 0);
+}
diff --git a/util/tests/synchronized_expandable_memory_pool_test.cc b/util/tests/synchronized_expandable_memory_pool_test.cc
index 3b88e59..f5957a9 100644
--- a/util/tests/synchronized_expandable_memory_pool_test.cc
+++ b/util/tests/synchronized_expandable_memory_pool_test.cc
@@ -16,7 +16,6 @@
 
 #include "chre/util/synchronized_expandable_memory_pool.h"
 
-#include "chre/util/synchronized_memory_pool.h"
 #include "gtest/gtest.h"
 
 using chre::SynchronizedExpandableMemoryPool;
@@ -102,4 +101,4 @@
   // Once it is empty, it should not still hold maxBlockCount as before.
   EXPECT_EQ(testMemoryPool.getFreeSpaceCount(), blockSize * maxBlockCount);
   EXPECT_EQ(testMemoryPool.getBlockCount(), staticBlockCount);
-}
\ No newline at end of file
+}
diff --git a/util/tests/synchronized_memory_pool_test.cc b/util/tests/synchronized_memory_pool_test.cc
new file mode 100644
index 0000000..cecff88
--- /dev/null
+++ b/util/tests/synchronized_memory_pool_test.cc
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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 "chre/util/synchronized_memory_pool.h"
+
+#include "gtest/gtest.h"
+
+using chre::SynchronizedMemoryPool;
+
+namespace {
+
+class ConstructorCount {
+ public:
+  ConstructorCount(int value_) : value(value_) {
+    sConstructedCounter++;
+  }
+  ~ConstructorCount() {
+    sConstructedCounter--;
+  }
+  int getValue() {
+    return value;
+  }
+
+  static ssize_t sConstructedCounter;
+
+ private:
+  const int value;
+};
+
+ssize_t ConstructorCount::sConstructedCounter = 0;
+
+}  // namespace
+
+TEST(SynchronizedMemoryPool, FreeBlockCheck) {
+  constexpr uint8_t maxSize = 12;
+  constexpr uint8_t blankSpace = 2;
+
+  SynchronizedMemoryPool<int, maxSize> testMemoryPool;
+  int *tempDataPtrs[maxSize];
+
+  for (int i = 0; i < maxSize - blankSpace; ++i) {
+    tempDataPtrs[i] = testMemoryPool.allocate(i);
+  }
+
+  EXPECT_EQ(testMemoryPool.getFreeBlockCount(), blankSpace);
+
+  for (int i = 0; i < maxSize - blankSpace; ++i) {
+    testMemoryPool.deallocate(tempDataPtrs[i]);
+  }
+}
diff --git a/util/util.mk b/util/util.mk
index 7c6337e..1a7ae3e 100644
--- a/util/util.mk
+++ b/util/util.mk
@@ -16,7 +16,9 @@
 COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/ble.cc
 COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/callbacks.cc
 COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/debug.cc
+COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/string.cc
 COMMON_SRCS += $(CHRE_PREFIX)/util/nanoapp/wifi.cc
+COMMON_SRCS += $(CHRE_PREFIX)/util/system/ble_util.cc
 COMMON_SRCS += $(CHRE_PREFIX)/util/system/event_callbacks.cc
 COMMON_SRCS += $(CHRE_PREFIX)/util/system/debug_dump.cc
 
@@ -43,6 +45,7 @@
 GOOGLETEST_SRCS += $(CHRE_PREFIX)/util/tests/singleton_test.cc
 GOOGLETEST_SRCS += $(CHRE_PREFIX)/util/tests/stats_container_test.cc
 GOOGLETEST_SRCS += $(CHRE_PREFIX)/util/tests/synchronized_expandable_memory_pool_test.cc
+GOOGLETEST_SRCS += $(CHRE_PREFIX)/util/tests/synchronized_memory_pool_test.cc
 GOOGLETEST_SRCS += $(CHRE_PREFIX)/util/tests/time_test.cc
 GOOGLETEST_SRCS += $(CHRE_PREFIX)/util/tests/unique_ptr_test.cc
 
diff --git a/variant/exynos-embos/variant.mk b/variant/exynos-embos/variant.mk
index 03868f4..a35e368 100644
--- a/variant/exynos-embos/variant.mk
+++ b/variant/exynos-embos/variant.mk
@@ -27,9 +27,6 @@
 EMBOS_CFLAGS += -DCHRE_EVENT_PER_BLOCK=32
 EMBOS_CFLAGS += -DCHRE_MAX_EVENT_BLOCKS=4
 
-EMBOS_CFLAGS += -DCHRE_UNSCHEDULED_EVENT_PER_BLOCK=32
-EMBOS_CFLAGS += -DCHRE_MAX_UNSCHEDULED_EVENT_BLOCKS=4
-
 # Optional Features ############################################################
 
 CHRE_AUDIO_SUPPORT_ENABLED = true
diff --git a/variant/tinysys/variant.mk b/variant/tinysys/variant.mk
index 15cf183..69ab3f9 100644
--- a/variant/tinysys/variant.mk
+++ b/variant/tinysys/variant.mk
@@ -60,9 +60,6 @@
 TINYSYS_CFLAGS += -DCHRE_EVENT_PER_BLOCK=32
 TINYSYS_CFLAGS += -DCHRE_MAX_EVENT_BLOCKS=4
 
-TINYSYS_CFLAGS += -DCHRE_UNSCHEDULED_EVENT_PER_BLOCK=32
-TINYSYS_CFLAGS += -DCHRE_MAX_UNSCHEDULED_EVENT_BLOCKS=4
-
 # Optional Features ############################################################
 
 CHRE_AUDIO_SUPPORT_ENABLED = true